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.Collections;
034import java.util.LinkedHashMap;
035import java.util.List;
036import java.util.Map;
037
038
039/**
040 * Pushed authorisation request.
041 *
042 * <p>Example HTTP request:
043 *
044 * <pre>
045 * POST /as/par HTTP/1.1
046 * Host: as.example.com
047 * Content-Type: application/x-www-form-urlencoded
048 * Authorization: Basic czZCaGRSa3F0Mzo3RmpmcDBaQnIxS3REUmJuZlZkbUl3
049 *
050 * response_type=code
051 * &amp;client_id=s6BhdRkqt3
052 * &amp;state=af0ifjsldkj
053 * &amp;redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb
054 * </pre>
055 *
056 * <p>Related specifications:
057 *
058 * <ul>
059 *     <li>OAuth 2.0 Pushed Authorization Requests (RFC 9126)
060 * </ul>
061 */
062@Immutable
063public class PushedAuthorizationRequest extends AbstractOptionallyAuthenticatedRequest {
064        
065        
066        /**
067         * The pushed authorisation request.
068         */
069        private final AuthorizationRequest authzRequest;
070        
071        
072        /**
073         * Creates a new authenticated pushed authorisation request for a
074         * confidential client.
075         *
076         * @param uri          The URI of the token endpoint. May be
077         *                     {@code null} if the {@link #toHTTPRequest}
078         *                     method will not be used.
079         * @param clientAuth   The client authentication. Must not be
080         *                     {@code null}.
081         * @param authzRequest The authorisation request. Must not be
082         *                     {@code null}.
083         */
084        public PushedAuthorizationRequest(final URI uri,
085                                          final ClientAuthentication clientAuth,
086                                          final AuthorizationRequest authzRequest) {
087                super(uri, clientAuth);
088                
089                if (clientAuth == null)
090                        throw new IllegalArgumentException("The client authentication must not be null");
091                
092                if (authzRequest == null) {
093                        throw new IllegalArgumentException("The authorization request must not be null");
094                }
095                if (authzRequest.getRequestURI() != null) {
096                        throw new IllegalArgumentException("Authorization request_uri parameter not allowed");
097                }
098                this.authzRequest = authzRequest;
099        }
100        
101        
102        /**
103         * Creates a new pushed authorisation request for a public client.
104         *
105         * @param uri          The URI of the token endpoint. May be
106         *                     {@code null} if the {@link #toHTTPRequest}
107         *                     method will not be used.
108         * @param authzRequest The authorisation request. Must not be
109         *                     {@code null}.
110         */
111        public PushedAuthorizationRequest(final URI uri,
112                                          final AuthorizationRequest authzRequest) {
113                
114                super(uri, null);
115                if (authzRequest == null) {
116                        throw new IllegalArgumentException("The authorization request must not be null");
117                }
118                if (authzRequest.getRequestURI() != null) {
119                        throw new IllegalArgumentException("Authorization request_uri parameter not allowed");
120                }
121                this.authzRequest = authzRequest;
122        }
123        
124        
125        /**
126         * Returns the pushed authorisation request.
127         *
128         * @return The pushed authorisation request.
129         */
130        public AuthorizationRequest getAuthorizationRequest() {
131                return authzRequest;
132        }
133        
134        
135        @Override
136        public HTTPRequest toHTTPRequest() {
137                
138                if (getEndpointURI() == null)
139                        throw new SerializeException("The endpoint URI is not specified");
140                
141                HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, getEndpointURI());
142                httpRequest.setEntityContentType(ContentType.APPLICATION_URLENCODED);
143                
144                if (getClientAuthentication() != null) {
145                        getClientAuthentication().applyTo(httpRequest);
146                }
147                
148                Map<String, List<String>> params;
149                try {
150                        params = new LinkedHashMap<>(httpRequest.getBodyAsFormParameters());
151                } catch (ParseException e) {
152                        throw new SerializeException(e.getMessage(), e);
153                }
154                params.putAll(authzRequest.toParameters());
155                httpRequest.setBody(URLUtils.serializeParameters(params));
156                
157                return httpRequest;
158        }
159        
160        
161        /**
162         * Parses a pushed authorisation request from the specified HTTP
163         * request.
164         *
165         * @param httpRequest The HTTP request. Must not be {@code null}.
166         *
167         * @return The pushed authorisation request.
168         *
169         * @throws ParseException If the HTTP request couldn't be parsed to a
170         *                        pushed authorisation request.
171         */
172        public static PushedAuthorizationRequest parse(final HTTPRequest httpRequest)
173                throws ParseException {
174                
175                // Only HTTP POST accepted
176                URI uri = httpRequest.getURI();
177                httpRequest.ensureMethod(HTTPRequest.Method.POST);
178                httpRequest.ensureEntityContentType(ContentType.APPLICATION_URLENCODED);
179                
180                // Parse client authentication, if any
181                ClientAuthentication clientAuth;
182                try {
183                        clientAuth = ClientAuthentication.parse(httpRequest);
184                } catch (ParseException e) {
185                        throw new ParseException(e.getMessage(), OAuth2Error.INVALID_REQUEST.appendDescription(": " + e.getMessage()));
186                }
187                
188                // No fragment! May use query component!
189                Map<String,List<String>> params = httpRequest.getBodyAsFormParameters();
190                
191                // Multiple conflicting client auth methods (issue #203)?
192                if (clientAuth instanceof ClientSecretBasic) {
193                        if (StringUtils.isNotBlank(MultivaluedMapUtils.getFirstValue(params, "client_assertion")) || StringUtils.isNotBlank(MultivaluedMapUtils.getFirstValue(params, "client_assertion_type"))) {
194                                String msg = "Multiple conflicting client authentication methods found: Basic and JWT assertion";
195                                throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg));
196                        }
197                }
198                
199                // client_id not required in authZ params if auth is present
200                if (! params.containsKey("client_id") && clientAuth != null) {
201                        params.put("client_id", Collections.singletonList(clientAuth.getClientID().getValue()));
202                }
203                
204                // Parse the authZ request, allow for OpenID
205                AuthorizationRequest authzRequest;
206                if (AuthenticationRequestDetector.isLikelyOpenID(params)) {
207                        authzRequest = AuthenticationRequest.parse(params);
208                } else {
209                        authzRequest = AuthorizationRequest.parse(params);
210                }
211                
212                if (authzRequest.getRequestURI() != null) {
213                        String msg = "Authorization request_uri parameter not allowed";
214                        throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg));
215                }
216                
217                if (clientAuth != null) {
218                        return new PushedAuthorizationRequest(uri, clientAuth, authzRequest);
219                } else {
220                        return new PushedAuthorizationRequest(uri, authzRequest);
221                }
222        }
223}