001/*
002 * oauth2-oidc-sdk
003 *
004 * Copyright 2012-2016, Connect2id Ltd and contributors.
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use
007 * this file except in compliance with the License. You may obtain a copy of the
008 * License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software distributed
013 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
014 * CONDITIONS OF ANY KIND, either express or implied. See the License for the
015 * specific language governing permissions and limitations under the License.
016 */
017
018package com.nimbusds.oauth2.sdk;
019
020
021import com.nimbusds.common.contenttype.ContentType;
022import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
023import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
024import com.nimbusds.oauth2.sdk.http.HTTPRequest;
025import com.nimbusds.oauth2.sdk.util.MultivaluedMapUtils;
026import com.nimbusds.oauth2.sdk.util.StringUtils;
027import com.nimbusds.oauth2.sdk.util.URLUtils;
028import com.nimbusds.openid.connect.sdk.AuthenticationRequest;
029import com.nimbusds.openid.connect.sdk.op.AuthenticationRequestDetector;
030import net.jcip.annotations.Immutable;
031
032import java.net.URI;
033import java.util.*;
034
035
036/**
037 * Pushed authorisation request (PAR).
038 *
039 * <p>Example HTTP request:
040 *
041 * <pre>
042 * POST /as/par HTTP/1.1
043 * Host: as.example.com
044 * Content-Type: application/x-www-form-urlencoded
045 * Authorization: Basic czZCaGRSa3F0Mzo3RmpmcDBaQnIxS3REUmJuZlZkbUl3
046 *
047 * response_type=code
048 * &amp;client_id=s6BhdRkqt3
049 * &amp;state=af0ifjsldkj
050 * &amp;redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb
051 * </pre>
052 *
053 * <p>Related specifications:
054 *
055 * <ul>
056 *     <li>OAuth 2.0 Pushed Authorization Requests (RFC 9126)
057 * </ul>
058 */
059@Immutable
060public class PushedAuthorizationRequest extends AbstractOptionallyAuthenticatedRequest {
061        
062        
063        /**
064         * The pushed authorisation request.
065         */
066        private final AuthorizationRequest authzRequest;
067        
068        
069        /**
070         * Creates a new authenticated pushed authorisation request for a
071         * confidential client.
072         *
073         * @param endpoint     The URI of the PAR endpoint. May be
074         *                     {@code null} if the {@link #toHTTPRequest}
075         *                     method is not going to be used.
076         * @param clientAuth   The client authentication. Must not be
077         *                     {@code null}.
078         * @param authzRequest The authorisation request. Must not be
079         *                     {@code null}.
080         */
081        public PushedAuthorizationRequest(final URI endpoint,
082                                          final ClientAuthentication clientAuth,
083                                          final AuthorizationRequest authzRequest) {
084                super(endpoint, Objects.requireNonNull(clientAuth));
085                
086                if (authzRequest.getRequestURI() != null) {
087                        throw new IllegalArgumentException("Authorization request_uri parameter not allowed");
088                }
089                this.authzRequest = authzRequest;
090        }
091        
092        
093        /**
094         * Creates a new pushed authorisation request for a public client.
095         *
096         * @param endpoint     The URI of the PAR endpoint. May be
097         *                     {@code null} if the {@link #toHTTPRequest}
098         *                     method is not going to be used.
099         * @param authzRequest The authorisation request. Must not be
100         *                     {@code null}.
101         */
102        public PushedAuthorizationRequest(final URI endpoint,
103                                          final AuthorizationRequest authzRequest) {
104                
105                super(endpoint, null);
106                if (authzRequest.getRequestURI() != null) {
107                        throw new IllegalArgumentException("Authorization request_uri parameter not allowed");
108                }
109                this.authzRequest = authzRequest;
110        }
111        
112        
113        /**
114         * Returns the pushed authorisation request.
115         *
116         * @return The pushed authorisation request.
117         */
118        public AuthorizationRequest getAuthorizationRequest() {
119                return authzRequest;
120        }
121        
122        
123        @Override
124        public HTTPRequest toHTTPRequest() {
125                
126                if (getEndpointURI() == null)
127                        throw new SerializeException("The endpoint URI is not specified");
128                
129                HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, getEndpointURI());
130                httpRequest.setEntityContentType(ContentType.APPLICATION_URLENCODED);
131                
132                if (getClientAuthentication() != null) {
133                        getClientAuthentication().applyTo(httpRequest);
134                }
135                
136                Map<String, List<String>> params;
137                try {
138                        params = new LinkedHashMap<>(httpRequest.getBodyAsFormParameters());
139                } catch (ParseException e) {
140                        throw new SerializeException(e.getMessage(), e);
141                }
142                params.putAll(authzRequest.toParameters());
143                httpRequest.setBody(URLUtils.serializeParameters(params));
144                
145                return httpRequest;
146        }
147        
148        
149        /**
150         * Parses a pushed authorisation request from the specified HTTP
151         * request.
152         *
153         * @param httpRequest The HTTP request. Must not be {@code null}.
154         *
155         * @return The pushed authorisation request.
156         *
157         * @throws ParseException If the HTTP request couldn't be parsed to a
158         *                        pushed authorisation request.
159         */
160        public static PushedAuthorizationRequest parse(final HTTPRequest httpRequest)
161                throws ParseException {
162                
163                // Only HTTP POST accepted
164                URI uri = httpRequest.getURI();
165                httpRequest.ensureMethod(HTTPRequest.Method.POST);
166                httpRequest.ensureEntityContentType(ContentType.APPLICATION_URLENCODED);
167                
168                // Parse client authentication, if any
169                ClientAuthentication clientAuth;
170                try {
171                        clientAuth = ClientAuthentication.parse(httpRequest);
172                } catch (ParseException e) {
173                        throw new ParseException(e.getMessage(), OAuth2Error.INVALID_REQUEST.appendDescription(": " + e.getMessage()));
174                }
175                
176                // No fragment! May use query component!
177                Map<String,List<String>> params = httpRequest.getBodyAsFormParameters();
178                
179                // Multiple conflicting client auth methods (issue #203)?
180                if (clientAuth instanceof ClientSecretBasic) {
181                        if (StringUtils.isNotBlank(MultivaluedMapUtils.getFirstValue(params, "client_assertion")) || StringUtils.isNotBlank(MultivaluedMapUtils.getFirstValue(params, "client_assertion_type"))) {
182                                String msg = "Multiple conflicting client authentication methods found: Basic and JWT assertion";
183                                throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg));
184                        }
185                }
186                
187                // client_id not required in authZ params if auth is present
188                if (! params.containsKey("client_id") && clientAuth != null) {
189                        params.put("client_id", Collections.singletonList(clientAuth.getClientID().getValue()));
190                }
191                
192                // Parse the authZ request, allow for OpenID
193                AuthorizationRequest authzRequest;
194                if (AuthenticationRequestDetector.isLikelyOpenID(params)) {
195                        authzRequest = AuthenticationRequest.parse(params);
196                } else {
197                        authzRequest = AuthorizationRequest.parse(params);
198                }
199                
200                if (authzRequest.getRequestURI() != null) {
201                        String msg = "Authorization request_uri parameter not allowed";
202                        throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg));
203                }
204                
205                if (clientAuth != null) {
206                        return new PushedAuthorizationRequest(uri, clientAuth, authzRequest);
207                } else {
208                        return new PushedAuthorizationRequest(uri, authzRequest);
209                }
210        }
211}