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