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