001/*
002 * oauth2-oidc-sdk
003 *
004 * Copyright 2012-2026, 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.JWTAuthentication;
024import com.nimbusds.oauth2.sdk.http.HTTPRequest;
025import com.nimbusds.oauth2.sdk.id.ClientID;
026import com.nimbusds.oauth2.sdk.util.CollectionUtils;
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 pushed authorisation request for a confidential
071         * 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                this(endpoint, Collections.singletonList(clientAuth), authzRequest);
085        }
086
087
088        /**
089         * Creates a new pushed authorisation request for a confidential
090         * client.
091         *
092         * @param endpoint             The URI of the PAR endpoint. May be
093         *                             {@code null} if the
094         *                             {@link #toHTTPRequest} method is not
095         *                             going to be used.
096         * @param clientAuthCandidates The client authentication candidates.
097         *                             Must not be {@code null}.
098         * @param authzRequest         The authorisation request. Must not be
099         *                             {@code null}.
100         */
101        public PushedAuthorizationRequest(final URI endpoint,
102                                          final List<ClientAuthentication> clientAuthCandidates,
103                                          final AuthorizationRequest authzRequest) {
104                super(endpoint, Objects.requireNonNull(clientAuthCandidates));
105                for (ClientAuthentication ca: clientAuthCandidates) {
106                        Objects.requireNonNull(ca);
107                }
108
109                if (authzRequest.getRequestURI() != null) {
110                        throw new IllegalArgumentException("The request_uri parameter is prohibited");
111                }
112                this.authzRequest = authzRequest;
113        }
114        
115        
116        /**
117         * Creates a new pushed authorisation request for a public client.
118         *
119         * @param endpoint     The URI of the PAR endpoint. May be
120         *                     {@code null} if the {@link #toHTTPRequest}
121         *                     method is not going to be used.
122         * @param authzRequest The authorisation request. Must not be
123         *                     {@code null}.
124         */
125        public PushedAuthorizationRequest(final URI endpoint,
126                                          final AuthorizationRequest authzRequest) {
127                
128                super(endpoint, (ClientAuthentication) null);
129                if (authzRequest.getRequestURI() != null) {
130                        throw new IllegalArgumentException("The request_uri parameter is prohibited");
131                }
132                this.authzRequest = authzRequest;
133        }
134        
135        
136        /**
137         * Returns the pushed authorisation request.
138         *
139         * @return The pushed authorisation request.
140         */
141        public AuthorizationRequest getAuthorizationRequest() {
142                return authzRequest;
143        }
144        
145        
146        @Override
147        public HTTPRequest toHTTPRequest() {
148                
149                if (getEndpointURI() == null)
150                        throw new SerializeException("The endpoint URI is not specified");
151                
152                HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, getEndpointURI());
153                httpRequest.setEntityContentType(ContentType.APPLICATION_URLENCODED);
154
155                if (getClientAuthentication() != null) {
156                        for (ClientAuthentication ca: getClientAuthenticationCandidates()) {
157                                ca.applyTo(httpRequest);
158                        }
159                }
160                
161                Map<String, List<String>> params;
162                try {
163                        params = new LinkedHashMap<>(httpRequest.getBodyAsFormParameters());
164                } catch (ParseException e) {
165                        throw new SerializeException(e.getMessage(), e);
166                }
167                params.putAll(authzRequest.toParameters());
168                httpRequest.setBody(URLUtils.serializeParameters(params));
169                
170                return httpRequest;
171        }
172        
173        
174        /**
175         * Parses a pushed authorisation request from the specified HTTP
176         * request.
177         *
178         * @param httpRequest The HTTP request. Must not be {@code null}.
179         *
180         * @return The pushed authorisation request.
181         *
182         * @throws ParseException If the HTTP request couldn't be parsed to a
183         *                        pushed authorisation request.
184         */
185        public static PushedAuthorizationRequest parse(final HTTPRequest httpRequest)
186                throws ParseException {
187                
188                // Only HTTP POST accepted
189                URI uri = httpRequest.getURI();
190                httpRequest.ensureMethod(HTTPRequest.Method.POST);
191                httpRequest.ensureEntityContentType(ContentType.APPLICATION_URLENCODED);
192                
193                // Parse client authentication candidates, if any
194                List<ClientAuthentication> clientAuthCandidates;
195                try {
196                        clientAuthCandidates = ClientAuthentication.parseCandidates(httpRequest);
197                } catch (ParseException e) {
198                        throw new ParseException(e.getMessage(), OAuth2Error.INVALID_REQUEST.appendDescription(": " + e.getMessage()));
199                }
200                
201                // No fragment! May use query component!
202                Map<String,List<String>> params = httpRequest.getBodyAsFormParameters();
203
204                if (CollectionUtils.isNotEmpty(clientAuthCandidates)) {
205
206                        ClientID clientID = null;
207                        for (ClientAuthentication ca: clientAuthCandidates) {
208                                if (ca ==  null) {
209                                        continue;
210                                }
211                                clientID = ca.getClientID();
212                                for (String paramName: ca.getFormParameterNames()) {
213                                        // strip form params for client auth
214                                        params.remove(paramName);
215                                }
216                        }
217
218                        // client_id not required in authZ params if auth is present
219                        if (! params.containsKey("client_id") && clientID != null) {
220                                params.put("client_id", Collections.singletonList(clientID.getValue()));
221                        }
222                }
223                
224                // Parse the authZ request, allow for OpenID
225                AuthorizationRequest authzRequest;
226                if (AuthenticationRequestDetector.isLikelyOpenID(params)) {
227                        authzRequest = AuthenticationRequest.parse(params);
228                } else {
229                        authzRequest = AuthorizationRequest.parse(params);
230                }
231                
232                if (authzRequest.getRequestURI() != null) {
233                        String msg = "The request_uri parameter is prohibited";
234                        throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg));
235                }
236                
237                if (CollectionUtils.isNotEmpty(clientAuthCandidates)) {
238                        return new PushedAuthorizationRequest(uri, clientAuthCandidates, authzRequest);
239                } else {
240                        return new PushedAuthorizationRequest(uri, authzRequest);
241                }
242        }
243}