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.util.*; 023 024import net.jcip.annotations.Immutable; 025 026import com.nimbusds.jwt.JWT; 027import com.nimbusds.jwt.JWTParser; 028import com.nimbusds.oauth2.sdk.http.HTTPRequest; 029import com.nimbusds.oauth2.sdk.http.HTTPResponse; 030import com.nimbusds.oauth2.sdk.id.State; 031import com.nimbusds.oauth2.sdk.util.MultivaluedMapUtils; 032import com.nimbusds.oauth2.sdk.util.StringUtils; 033import com.nimbusds.oauth2.sdk.util.URIUtils; 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.putAll(getErrorObject().toParameters()); 203 204 if (getState() != null) 205 params.put("state", Collections.singletonList(getState().getValue())); 206 207 return params; 208 } 209 210 211 /** 212 * Parses an authorisation error response. 213 * 214 * @param redirectURI The base redirection URI. Must not be 215 * {@code null}. 216 * @param params The response parameters to parse. Must not be 217 * {@code null}. 218 * 219 * @return The authorisation error response. 220 * 221 * @throws ParseException If the parameters couldn't be parsed to an 222 * authorisation error response. 223 */ 224 public static AuthorizationErrorResponse parse(final URI redirectURI, 225 final Map<String,List<String>> params) 226 throws ParseException { 227 228 // JARM, ignore other top level params 229 if (params.get("response") != null) { 230 JWT jwtResponse; 231 try { 232 jwtResponse = JWTParser.parse(MultivaluedMapUtils.getFirstValue(params, "response")); 233 } catch (java.text.ParseException e) { 234 throw new ParseException("Invalid JWT response: " + e.getMessage(), e); 235 } 236 237 return new AuthorizationErrorResponse(redirectURI, jwtResponse, ResponseMode.JWT); 238 } 239 240 // Parse the error 241 ErrorObject error = ErrorObject.parse(params); 242 243 if (StringUtils.isBlank(error.getCode())) { 244 throw new ParseException("Missing error code"); 245 } 246 error = error.setHTTPStatusCode(HTTPResponse.SC_FOUND); // need a status code 247 248 // State 249 State state = State.parse(MultivaluedMapUtils.getFirstValue(params, "state")); 250 251 return new AuthorizationErrorResponse(redirectURI, error, state, null); 252 } 253 254 255 /** 256 * Parses an authorisation error response. 257 * 258 * <p>Use a relative URI if the host, port and path details are not 259 * known: 260 * 261 * <pre> 262 * URI relUrl = new URI("https:///?error=invalid_request"); 263 * </pre> 264 * 265 * <p>Example URI: 266 * 267 * <pre> 268 * https://client.example.com/cb? 269 * error=invalid_request 270 * &error_description=the%20request%20is%20not%20valid%20or%20malformed 271 * &state=af0ifjsldkj 272 * </pre> 273 * 274 * @param uri The URI to parse. Can be absolute or relative, with a 275 * fragment or query string containing the authorisation 276 * response parameters. Must not be {@code null}. 277 * 278 * @return The authorisation error response. 279 * 280 * @throws ParseException If the URI couldn't be parsed to an 281 * authorisation error response. 282 */ 283 public static AuthorizationErrorResponse parse(final URI uri) 284 throws ParseException { 285 286 return parse(URIUtils.getBaseURI(uri), parseResponseParameters(uri)); 287 } 288 289 290 /** 291 * Parses an authorisation error response from the specified initial 292 * HTTP 302 redirect response generated at the authorisation endpoint. 293 * 294 * <p>Example HTTP response: 295 * 296 * <pre> 297 * HTTP/1.1 302 Found 298 * Location: https://client.example.com/cb?error=invalid_request&state=af0ifjsldkj 299 * </pre> 300 * 301 * @see #parse(HTTPRequest) 302 * 303 * @param httpResponse The HTTP response to parse. Must not be 304 * {@code null}. 305 * 306 * @return The authorisation error response. 307 * 308 * @throws ParseException If the HTTP response couldn't be parsed to an 309 * authorisation error response. 310 */ 311 public static AuthorizationErrorResponse parse(final HTTPResponse httpResponse) 312 throws ParseException { 313 314 URI location = httpResponse.getLocation(); 315 316 if (location == null) { 317 throw new ParseException("Missing redirection URL / HTTP Location header"); 318 } 319 320 return parse(location); 321 } 322 323 324 /** 325 * Parses an authorisation error response from the specified HTTP 326 * request at the client redirection (callback) URI. Applies to 327 * {@code query}, {@code fragment} and {@code form_post} response 328 * modes. 329 * 330 * <p>Example HTTP request (authorisation success): 331 * 332 * <pre> 333 * GET /cb?error=invalid_request&state=af0ifjsldkj HTTP/1.1 334 * Host: client.example.com 335 * </pre> 336 * 337 * @see #parse(HTTPResponse) 338 * 339 * @param httpRequest The HTTP request to parse. Must not be 340 * {@code null}. 341 * 342 * @return The authorisation error response. 343 * 344 * @throws ParseException If the HTTP request couldn't be parsed to an 345 * authorisation error response. 346 */ 347 public static AuthorizationErrorResponse parse(final HTTPRequest httpRequest) 348 throws ParseException { 349 350 return parse(httpRequest.getURI(), parseResponseParameters(httpRequest)); 351 } 352}