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 * &client_id=s6BhdRkqt3 049 * &state=af0ifjsldkj 050 * &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}