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