001package com.nimbusds.oauth2.sdk; 002 003 004import java.net.MalformedURLException; 005import java.net.URL; 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.URLUtils; 019 020 021/** 022 * Authorisation error response. This class is immutable. 023 * 024 * <p>Standard authorisation errors: 025 * 026 * <ul> 027 * <li>{@link OAuth2Error#INVALID_REQUEST} 028 * <li>{@link OAuth2Error#UNAUTHORIZED_CLIENT} 029 * <li>{@link OAuth2Error#ACCESS_DENIED} 030 * <li>{@link OAuth2Error#UNSUPPORTED_RESPONSE_TYPE} 031 * <li>{@link OAuth2Error#INVALID_SCOPE} 032 * <li>{@link OAuth2Error#SERVER_ERROR} 033 * <li>{@link OAuth2Error#TEMPORARILY_UNAVAILABLE} 034 * </ul> 035 * 036 * <p>Example HTTP response: 037 * 038 * <pre> 039 * HTTP/1.1 302 Found 040 * Location: https://client.example.com/cb? 041 * error=invalid_request 042 * &error_description=the%20request%20is%20not%20valid%20or%20malformed 043 * &state=af0ifjsldkj 044 * </pre> 045 * 046 * <p>Related specifications: 047 * 048 * <ul> 049 * <li>OAuth 2.0 (RFC 6749), sections 4.1.2.1 and 4.2.2.1. 050 * </ul> 051 * 052 * @author Vladimir Dzhuvinov 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<ErrorObject>(); 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 URL 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 URL 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 URL 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<String,String>(); 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 URL 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 sb.append(URLUtils.serializeParameters(toParameters())); 202 203 try { 204 return new URL(sb.toString()); 205 206 } catch (MalformedURLException e) { 207 208 throw new SerializeException("Couldn't serialize redirect URL: " + e.getMessage(), e); 209 } 210 } 211 212 213 /** 214 * Parses an authorisation error response from the specified redirect 215 * URI and parameters. 216 * 217 * @param redirectURI The base redirect URI. Must not be {@code null}. 218 * @param params The response parameters to parse. Must not be 219 * {@code null}. 220 * 221 * @return The authorisation error response. 222 * 223 * @throws ParseException If the parameters couldn't be parsed to an 224 * authorisation error response. 225 */ 226 public static AuthorizationErrorResponse parse(final URL redirectURI, 227 final Map<String,String> params) 228 throws ParseException { 229 230 // Parse the error 231 if (StringUtils.isBlank(params.get("error"))) 232 throw new ParseException("Missing error code"); 233 234 // Parse error code 235 String errorCode = params.get("error"); 236 237 String errorDescription = params.get("error_description"); 238 239 String errorURIString = params.get("error_uri"); 240 241 URL errorURI = null; 242 243 if (errorURIString != null) { 244 245 try { 246 errorURI = new URL(errorURIString); 247 248 } catch (MalformedURLException e) { 249 250 throw new ParseException("Invalid error URI: " + errorURIString, e); 251 } 252 } 253 254 255 ErrorObject error = new ErrorObject(errorCode, errorDescription, HTTPResponse.SC_FOUND, errorURI); 256 257 258 // State 259 State state = State.parse(params.get("state")); 260 261 return new AuthorizationErrorResponse(redirectURI, error, null, state); 262 } 263 264 265 /** 266 * Parses an authorisation error response from the specified URI. 267 * 268 * <p>Example URI: 269 * 270 * <pre> 271 * https://client.example.com/cb? 272 * error=invalid_request 273 * &error_description=the%20request%20is%20not%20valid%20or%20malformed 274 * &state=af0ifjsldkj 275 * </pre> 276 * 277 * @param uri The URI to parse. Can be absolute or relative. Must not 278 * be {@code null}. 279 * 280 * @return The authorisation error response. 281 * 282 * @throws ParseException If the URI couldn't be parsed to an 283 * authorisation error response. 284 */ 285 public static AuthorizationErrorResponse parse(final URL uri) 286 throws ParseException { 287 288 Map<String,String> params = null; 289 290 if (uri.getRef() != null) 291 params = URLUtils.parseParameters(uri.getRef()); 292 293 else if (uri.getQuery() != null) 294 params = URLUtils.parseParameters(uri.getQuery()); 295 296 else 297 throw new ParseException("Missing URL fragment or query string"); 298 299 300 return parse(URLUtils.getBaseURL(uri), params); 301 } 302 303 304 /** 305 * Parses an authorisation error response from the specified HTTP 306 * response. 307 * 308 * <p>Example HTTP response: 309 * 310 * <pre> 311 * HTTP/1.1 302 Found 312 * Location: https://client.example.com/cb? 313 * error=invalid_request 314 * &error_description=the%20request%20is%20not%20valid%20or%20malformed 315 * &state=af0ifjsldkj 316 * </pre> 317 * 318 * @param httpResponse The HTTP response to parse. Must not be 319 * {@code null}. 320 * 321 * @return The authorisation error response. 322 * 323 * @throws ParseException If the HTTP response couldn't be parsed to an 324 * authorisation error response. 325 */ 326 public static AuthorizationErrorResponse parse(final HTTPResponse httpResponse) 327 throws ParseException { 328 329 if (httpResponse.getStatusCode() != HTTPResponse.SC_FOUND) 330 throw new ParseException("Unexpected HTTP status code, must be 302 (Found): " + 331 httpResponse.getStatusCode()); 332 333 URL location = httpResponse.getLocation(); 334 335 if (location == null) 336 throw new ParseException("Missing redirect URL / HTTP Location header"); 337 338 return parse(location); 339 } 340}