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