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