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.jwt.JWT; 022import com.nimbusds.jwt.JWTParser; 023import com.nimbusds.oauth2.sdk.http.HTTPRequest; 024import com.nimbusds.oauth2.sdk.http.HTTPResponse; 025import com.nimbusds.oauth2.sdk.id.Issuer; 026import com.nimbusds.oauth2.sdk.id.State; 027import com.nimbusds.oauth2.sdk.util.MultivaluedMapUtils; 028import com.nimbusds.oauth2.sdk.util.StringUtils; 029import com.nimbusds.oauth2.sdk.util.URIUtils; 030import net.jcip.annotations.Immutable; 031 032import java.net.URI; 033import java.util.*; 034 035 036/** 037 * Authorisation error response. Intended only for errors which are allowed to 038 * be communicated back to the requesting OAuth 2.0 client, such as 039 * {@code access_denied}. 040 * 041 * <p>If the authorisation request fails due to a missing, invalid, or 042 * mismatching {@code redirect_uri}, or if the {@code client_id} is missing or 043 * invalid, a response <strong>must not</strong> be sent back to the requesting 044 * client. Instead, the authorisation server should simply display the error 045 * to the resource owner. 046 * 047 * <p>Standard authorisation errors: 048 * 049 * <ul> 050 * <li>{@link OAuth2Error#INVALID_REQUEST} 051 * <li>{@link OAuth2Error#UNAUTHORIZED_CLIENT} 052 * <li>{@link OAuth2Error#ACCESS_DENIED} 053 * <li>{@link OAuth2Error#UNSUPPORTED_RESPONSE_TYPE} 054 * <li>{@link OAuth2Error#INVALID_SCOPE} 055 * <li>{@link OAuth2Error#SERVER_ERROR} 056 * <li>{@link OAuth2Error#TEMPORARILY_UNAVAILABLE} 057 * </ul> 058 * 059 * <p>Example HTTP response: 060 * 061 * <pre> 062 * HTTP/1.1 302 Found 063 * Location: https://client.example.com/cb? 064 * error=invalid_request 065 * &error_description=the%20request%20is%20not%20valid%20or%20malformed 066 * &state=af0ifjsldkj 067 * </pre> 068 * 069 * <p>Related specifications: 070 * 071 * <ul> 072 * <li>OAuth 2.0 (RFC 6749) 073 * <li>OAuth 2.0 Multiple Response Type Encoding Practices 1.0 074 * <li>OAuth 2.0 Form Post Response Mode 1.0 075 * <li>Financial-grade API: JWT Secured Authorization Response Mode for 076 * OAuth 2.0 (JARM) 077 * <li>OAuth 2.0 Authorization Server Issuer Identification (RFC 9207) 078 * </ul> 079 */ 080@Immutable 081public class AuthorizationErrorResponse 082 extends AuthorizationResponse 083 implements ErrorResponse { 084 085 086 /** 087 * The standard OAuth 2.0 errors for an Authorisation error response. 088 */ 089 private static final Set<ErrorObject> stdErrors = new HashSet<>(); 090 091 092 static { 093 stdErrors.add(OAuth2Error.INVALID_REQUEST); 094 stdErrors.add(OAuth2Error.UNAUTHORIZED_CLIENT); 095 stdErrors.add(OAuth2Error.ACCESS_DENIED); 096 stdErrors.add(OAuth2Error.UNSUPPORTED_RESPONSE_TYPE); 097 stdErrors.add(OAuth2Error.INVALID_SCOPE); 098 stdErrors.add(OAuth2Error.SERVER_ERROR); 099 stdErrors.add(OAuth2Error.TEMPORARILY_UNAVAILABLE); 100 } 101 102 103 /** 104 * Gets the standard OAuth 2.0 errors for an Authorisation error 105 * response. 106 * 107 * @return The standard errors, as a read-only set. 108 */ 109 public static Set<ErrorObject> getStandardErrors() { 110 111 return Collections.unmodifiableSet(stdErrors); 112 } 113 114 115 /** 116 * The error. 117 */ 118 private final ErrorObject error; 119 120 121 /** 122 * Creates a new authorisation error response. 123 * 124 * @param redirectURI The base redirection URI. Must not be 125 * {@code null}. 126 * @param error The error. Should match one of the 127 * {@link #getStandardErrors standard errors} for an 128 * authorisation error response. Must not be 129 * {@code null}. 130 * @param state The state, {@code null} if not requested. 131 * @param rm The implied response mode, {@code null} if 132 * unknown. 133 */ 134 public AuthorizationErrorResponse(final URI redirectURI, 135 final ErrorObject error, 136 final State state, 137 final ResponseMode rm) { 138 139 this(redirectURI, error, state, null, rm); 140 } 141 142 143 /** 144 * Creates a new authorisation error response. 145 * 146 * @param redirectURI The base redirection URI. Must not be 147 * {@code null}. 148 * @param error The error. Should match one of the 149 * {@link #getStandardErrors standard errors} for an 150 * authorisation error response. Must not be 151 * {@code null}. 152 * @param state The state, {@code null} if not requested. 153 * @param issuer The issuer, {@code null} if not specified. 154 * @param rm The implied response mode, {@code null} if 155 * unknown. 156 */ 157 public AuthorizationErrorResponse(final URI redirectURI, 158 final ErrorObject error, 159 final State state, 160 final Issuer issuer, 161 final ResponseMode rm) { 162 163 super(redirectURI, state, issuer, rm); 164 165 if (error == null) 166 throw new IllegalArgumentException("The error must not be null"); 167 168 this.error = error; 169 } 170 171 172 /** 173 * Creates a new JSON Web Token (JWT) secured authorisation error 174 * response. 175 * 176 * @param redirectURI The base redirection URI. Must not be 177 * {@code null}. 178 * @param jwtResponse The JWT-secured response. Must not be 179 * {@code null}. 180 * @param rm The implied response mode, {@code null} if 181 * unknown. 182 */ 183 public AuthorizationErrorResponse(final URI redirectURI, 184 final JWT jwtResponse, 185 final ResponseMode rm) { 186 187 super(redirectURI, jwtResponse, rm); 188 189 error = null; 190 } 191 192 193 @Override 194 public boolean indicatesSuccess() { 195 196 return false; 197 } 198 199 200 @Override 201 public ErrorObject getErrorObject() { 202 203 return error; 204 } 205 206 207 @Override 208 public ResponseMode impliedResponseMode() { 209 210 // Return "query" if not known, assumed the most frequent case 211 return getResponseMode() != null ? getResponseMode() : ResponseMode.QUERY; 212 } 213 214 215 @Override 216 public Map<String,List<String>> toParameters() { 217 218 Map<String,List<String>> params = new HashMap<>(); 219 220 if (getJWTResponse() != null) { 221 // JARM, no other top-level parameters 222 params.put("response", Collections.singletonList(getJWTResponse().serialize())); 223 return params; 224 } 225 226 params.putAll(getErrorObject().toParameters()); 227 228 if (getState() != null) 229 params.put("state", Collections.singletonList(getState().getValue())); 230 231 if (getIssuer() != null) 232 params.put("iss", Collections.singletonList(getIssuer().getValue())); 233 234 return params; 235 } 236 237 238 /** 239 * Parses an authorisation error response. 240 * 241 * @param redirectURI The base redirection URI. Must not be 242 * {@code null}. 243 * @param params The response parameters to parse. Must not be 244 * {@code null}. 245 * 246 * @return The authorisation error response. 247 * 248 * @throws ParseException If the parameters couldn't be parsed to an 249 * authorisation error response. 250 */ 251 public static AuthorizationErrorResponse parse(final URI redirectURI, 252 final Map<String,List<String>> params) 253 throws ParseException { 254 255 // JARM, ignore other top level params 256 String responseString = MultivaluedMapUtils.getFirstValue(params, "response"); 257 if (responseString != null) { 258 JWT jwtResponse; 259 try { 260 jwtResponse = JWTParser.parse(responseString); 261 } catch (java.text.ParseException e) { 262 throw new ParseException("Invalid JWT response: " + e.getMessage(), e); 263 } 264 265 return new AuthorizationErrorResponse(redirectURI, jwtResponse, ResponseMode.JWT); 266 } 267 268 // Parse the error 269 ErrorObject error = ErrorObject.parse(params); 270 271 if (StringUtils.isBlank(error.getCode())) { 272 throw new ParseException("Missing error code"); 273 } 274 error = error.setHTTPStatusCode(HTTPResponse.SC_FOUND); // need a status code 275 276 // State 277 State state = State.parse(MultivaluedMapUtils.getFirstValue(params, "state")); 278 279 // Parse optional issuer, OAuth 2.0 Authorization Server Issuer Identification (RFC 9207) 280 Issuer issuer = Issuer.parse(MultivaluedMapUtils.getFirstValue(params, "iss")); 281 282 return new AuthorizationErrorResponse(redirectURI, error, state, issuer, null); 283 } 284 285 286 /** 287 * Parses an authorisation error response. 288 * 289 * <p>Use a relative URI if the host, port and path details are not 290 * known: 291 * 292 * <pre> 293 * URI relUrl = new URI("https:///?error=invalid_request"); 294 * </pre> 295 * 296 * <p>Example URI: 297 * 298 * <pre> 299 * https://client.example.com/cb? 300 * error=invalid_request 301 * &error_description=the%20request%20is%20not%20valid%20or%20malformed 302 * &state=af0ifjsldkj 303 * </pre> 304 * 305 * @param uri The URI to parse. Can be absolute or relative, with a 306 * fragment or query string containing the authorisation 307 * response parameters. Must not be {@code null}. 308 * 309 * @return The authorisation error response. 310 * 311 * @throws ParseException If the URI couldn't be parsed to an 312 * authorisation error response. 313 */ 314 public static AuthorizationErrorResponse parse(final URI uri) 315 throws ParseException { 316 317 return parse(URIUtils.getBaseURI(uri), parseResponseParameters(uri)); 318 } 319 320 321 /** 322 * Parses an authorisation error response from the specified initial 323 * HTTP 302 redirect response generated at the authorisation endpoint. 324 * 325 * <p>Example HTTP response: 326 * 327 * <pre> 328 * HTTP/1.1 302 Found 329 * Location: https://client.example.com/cb?error=invalid_request&state=af0ifjsldkj 330 * </pre> 331 * 332 * @see #parse(HTTPRequest) 333 * 334 * @param httpResponse The HTTP response to parse. Must not be 335 * {@code null}. 336 * 337 * @return The authorisation error response. 338 * 339 * @throws ParseException If the HTTP response couldn't be parsed to an 340 * authorisation error response. 341 */ 342 public static AuthorizationErrorResponse parse(final HTTPResponse httpResponse) 343 throws ParseException { 344 345 URI location = httpResponse.getLocation(); 346 347 if (location == null) { 348 throw new ParseException("Missing redirection URL / HTTP Location header"); 349 } 350 351 return parse(location); 352 } 353 354 355 /** 356 * Parses an authorisation error response from the specified HTTP 357 * request at the client redirection (callback) URI. Applies to 358 * {@code query}, {@code fragment} and {@code form_post} response 359 * modes. 360 * 361 * <p>Example HTTP request (authorisation success): 362 * 363 * <pre> 364 * GET /cb?error=invalid_request&state=af0ifjsldkj HTTP/1.1 365 * Host: client.example.com 366 * </pre> 367 * 368 * @see #parse(HTTPResponse) 369 * 370 * @param httpRequest The HTTP request to parse. Must not be 371 * {@code null}. 372 * 373 * @return The authorisation error response. 374 * 375 * @throws ParseException If the HTTP request couldn't be parsed to an 376 * authorisation error response. 377 */ 378 public static AuthorizationErrorResponse parse(final HTTPRequest httpRequest) 379 throws ParseException { 380 381 return parse(URIUtils.getBaseURI(httpRequest.getURI()), parseResponseParameters(httpRequest)); 382 } 383}