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