001package com.nimbusds.oauth2.sdk; 002 003 004import java.net.URI; 005import java.net.URISyntaxException; 006import java.net.URL; 007import java.util.Collections; 008import java.util.HashMap; 009import java.util.HashSet; 010import java.util.Map; 011import java.util.Set; 012 013import net.jcip.annotations.Immutable; 014 015import org.apache.commons.lang3.StringUtils; 016 017import com.nimbusds.oauth2.sdk.id.State; 018import com.nimbusds.oauth2.sdk.http.HTTPResponse; 019import com.nimbusds.oauth2.sdk.util.URIUtils; 020import com.nimbusds.oauth2.sdk.util.URLUtils; 021 022 023/** 024 * Authorisation error response. 025 * 026 * <p>Standard authorisation errors: 027 * 028 * <ul> 029 * <li>{@link OAuth2Error#INVALID_REQUEST} 030 * <li>{@link OAuth2Error#UNAUTHORIZED_CLIENT} 031 * <li>{@link OAuth2Error#ACCESS_DENIED} 032 * <li>{@link OAuth2Error#UNSUPPORTED_RESPONSE_TYPE} 033 * <li>{@link OAuth2Error#INVALID_SCOPE} 034 * <li>{@link OAuth2Error#SERVER_ERROR} 035 * <li>{@link OAuth2Error#TEMPORARILY_UNAVAILABLE} 036 * </ul> 037 * 038 * <p>Example HTTP response: 039 * 040 * <pre> 041 * HTTP/1.1 302 Found 042 * Location: https://client.example.com/cb? 043 * error=invalid_request 044 * &error_description=the%20request%20is%20not%20valid%20or%20malformed 045 * &state=af0ifjsldkj 046 * </pre> 047 * 048 * <p>Related specifications: 049 * 050 * <ul> 051 * <li>OAuth 2.0 (RFC 6749), sections 4.1.2.1 and 4.2.2.1. 052 * </ul> 053 */ 054@Immutable 055public class AuthorizationErrorResponse 056 extends AuthorizationResponse 057 implements ErrorResponse { 058 059 060 /** 061 * The standard OAuth 2.0 errors for an Authorisation error response. 062 */ 063 private static Set<ErrorObject> stdErrors = new HashSet<>(); 064 065 066 static { 067 stdErrors.add(OAuth2Error.INVALID_REQUEST); 068 stdErrors.add(OAuth2Error.UNAUTHORIZED_CLIENT); 069 stdErrors.add(OAuth2Error.ACCESS_DENIED); 070 stdErrors.add(OAuth2Error.UNSUPPORTED_RESPONSE_TYPE); 071 stdErrors.add(OAuth2Error.INVALID_SCOPE); 072 stdErrors.add(OAuth2Error.SERVER_ERROR); 073 stdErrors.add(OAuth2Error.TEMPORARILY_UNAVAILABLE); 074 } 075 076 077 /** 078 * Gets the standard OAuth 2.0 errors for an Authorisation error 079 * response. 080 * 081 * @return The standard errors, as a read-only set. 082 */ 083 public static Set<ErrorObject> getStandardErrors() { 084 085 return Collections.unmodifiableSet(stdErrors); 086 } 087 088 089 /** 090 * The error. 091 */ 092 private final ErrorObject error; 093 094 095 /** 096 * The response type, used to determine redirection URI composition. If 097 * unknown {@code null}. 098 */ 099 private final ResponseType rt; 100 101 102 /** 103 * Creates a new authorisation error response. 104 * 105 * @param redirectURI The base redirection URI. Must not be 106 * {@code null}. 107 * @param error The error. Should match one of the 108 * {@link #getStandardErrors standard errors} for an 109 * authorisation error response. Must not be 110 * {@code null}. 111 * @param rt The response type, used to determine the 112 * redirection URI composition. If unknown 113 * {@code null}. 114 * @param state The state, {@code null} if not requested. 115 */ 116 public AuthorizationErrorResponse(final URI redirectURI, 117 final ErrorObject error, 118 final ResponseType rt, 119 final State state) { 120 121 super(redirectURI, state); 122 123 if (error == null) 124 throw new IllegalArgumentException("The error must not be null"); 125 126 this.error = error; 127 128 this.rt = rt; 129 } 130 131 132 /** 133 * Creates a new authorisation error response, with no specified 134 * response type to determine the redirection URI composition. 135 * 136 * @param redirectURI The base redirection URI. Must not be 137 * {@code null}. 138 * @param error The error. Should match one of the 139 * {@link #getStandardErrors standard errors} for an 140 * authorisation error response. Must not be 141 * {@code null}. 142 * @param state The state, {@code null} if not requested. 143 */ 144 public AuthorizationErrorResponse(final URI redirectURI, 145 final ErrorObject error, 146 final State state) { 147 148 this(redirectURI, error, null, state); 149 } 150 151 152 @Override 153 public ErrorObject getErrorObject() { 154 155 return error; 156 } 157 158 159 /** 160 * Gets the response type. 161 * 162 * @return The response type, {@code null} if not specified. 163 */ 164 public ResponseType getResponseType() { 165 166 return rt; 167 } 168 169 170 @Override 171 public Map<String,String> toParameters() { 172 173 Map<String,String> params = new HashMap<>(); 174 175 params.put("error", error.getCode()); 176 177 if (error.getDescription() != null) 178 params.put("error_description", error.getDescription()); 179 180 if (error.getURI() != null) 181 params.put("error_uri", error.getURI().toString()); 182 183 if (getState() != null) 184 params.put("state", getState().getValue()); 185 186 return params; 187 } 188 189 190 @Override 191 public URI toURI() 192 throws SerializeException { 193 194 StringBuilder sb = new StringBuilder(getRedirectionURI().toString()); 195 196 if (rt == null || rt.contains(ResponseType.Value.TOKEN)) { 197 sb.append("#"); 198 } else { 199 sb.append("?"); 200 } 201 202 sb.append(URLUtils.serializeParameters(toParameters())); 203 204 try { 205 return new URI(sb.toString()); 206 207 } catch (URISyntaxException e) { 208 209 throw new SerializeException("Couldn't serialize redirection URI: " + e.getMessage(), e); 210 } 211 } 212 213 214 /** 215 * Parses an authorisation error response from the specified redirect 216 * URI and parameters. 217 * 218 * @param redirectURI The base redirection URI. Must not be 219 * {@code null}. 220 * @param params The response parameters to parse. Must not be 221 * {@code null}. 222 * 223 * @return The authorisation error response. 224 * 225 * @throws ParseException If the parameters couldn't be parsed to an 226 * authorisation error response. 227 */ 228 public static AuthorizationErrorResponse parse(final URI redirectURI, 229 final Map<String,String> params) 230 throws ParseException { 231 232 // Parse the error 233 if (StringUtils.isBlank(params.get("error"))) 234 throw new ParseException("Missing error code"); 235 236 // Parse error code 237 String errorCode = params.get("error"); 238 239 String errorDescription = params.get("error_description"); 240 241 String errorURIString = params.get("error_uri"); 242 243 URI errorURI = null; 244 245 if (errorURIString != null) { 246 247 try { 248 errorURI = new URI(errorURIString); 249 250 } catch (URISyntaxException e) { 251 252 throw new ParseException("Invalid error URI: " + errorURIString, e); 253 } 254 } 255 256 257 ErrorObject error = new ErrorObject(errorCode, errorDescription, HTTPResponse.SC_FOUND, errorURI); 258 259 260 // State 261 State state = State.parse(params.get("state")); 262 263 return new AuthorizationErrorResponse(redirectURI, error, null, state); 264 } 265 266 267 /** 268 * Parses an authorisation error response from the specified URI. 269 * 270 * <p>Example URI: 271 * 272 * <pre> 273 * https://client.example.com/cb? 274 * error=invalid_request 275 * &error_description=the%20request%20is%20not%20valid%20or%20malformed 276 * &state=af0ifjsldkj 277 * </pre> 278 * 279 * @param uri The URI to parse. Can be absolute or relative. Must not 280 * be {@code null}. 281 * 282 * @return The authorisation error response. 283 * 284 * @throws ParseException If the URI couldn't be parsed to an 285 * authorisation error response. 286 */ 287 public static AuthorizationErrorResponse parse(final URI uri) 288 throws ParseException { 289 290 Map<String,String> params; 291 292 if (uri.getRawFragment() != null) 293 params = URLUtils.parseParameters(uri.getRawFragment()); 294 295 else if (uri.getRawQuery() != null) 296 params = URLUtils.parseParameters(uri.getRawQuery()); 297 298 else 299 throw new ParseException("Missing URI fragment or query string"); 300 301 302 return parse(URIUtils.getBaseURI(uri), params); 303 } 304 305 306 /** 307 * Parses an authorisation error response from the specified HTTP 308 * response. 309 * 310 * <p>Example HTTP response: 311 * 312 * <pre> 313 * HTTP/1.1 302 Found 314 * Location: https://client.example.com/cb? 315 * error=invalid_request 316 * &error_description=the%20request%20is%20not%20valid%20or%20malformed 317 * &state=af0ifjsldkj 318 * </pre> 319 * 320 * @param httpResponse The HTTP response to parse. Must not be 321 * {@code null}. 322 * 323 * @return The authorisation error response. 324 * 325 * @throws ParseException If the HTTP response couldn't be parsed to an 326 * authorisation error response. 327 */ 328 public static AuthorizationErrorResponse parse(final HTTPResponse httpResponse) 329 throws ParseException { 330 331 if (httpResponse.getStatusCode() != HTTPResponse.SC_FOUND) 332 throw new ParseException("Unexpected HTTP status code, must be 302 (Found): " + 333 httpResponse.getStatusCode()); 334 335 URL location = httpResponse.getLocation(); 336 337 if (location == null) 338 throw new ParseException("Missing redirection URI / HTTP Location header"); 339 340 try { 341 return parse(location.toURI()); 342 343 } catch (URISyntaxException e) { 344 345 throw new ParseException(e.getMessage(), e); 346 } 347 } 348}