001    package com.nimbusds.oauth2.sdk;
002    
003    
004    import java.net.MalformedURLException;
005    import java.net.URL;
006    import java.util.Collections;
007    import java.util.HashMap;
008    import java.util.HashSet;
009    import java.util.Map;
010    import java.util.Set;
011    
012    import net.jcip.annotations.Immutable;
013    
014    import org.apache.commons.lang3.StringUtils;
015    
016    import com.nimbusds.oauth2.sdk.id.State;
017    import com.nimbusds.oauth2.sdk.http.HTTPResponse;
018    import 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     * &amp;error_description=the%20request%20is%20not%20valid%20or%20malformed
043     * &amp;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
055    public 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 redirect 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 redirect URI. Must not be {@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 redirect
111             *                    URI composition. If unknown {@code null}.
112             * @param state       The state, {@code null} if not requested.
113             */
114            public AuthorizationErrorResponse(final URL redirectURI,
115                                              final ErrorObject error,
116                                              final ResponseType rt,
117                                              final State state) {
118                                              
119                    super(redirectURI, state);
120                    
121                    if (error == null)
122                            throw new IllegalArgumentException("The error must not be null");
123                            
124                    this.error = error;
125                    
126                    this.rt = rt;
127            }
128            
129    
130            @Override
131            public ErrorObject getErrorObject() {
132            
133                    return error;
134            }
135            
136            
137            /**
138             * Gets the response type.
139             *
140             * @return The response type, {@code null} if not specified.
141             */
142            public ResponseType getResponseType() {
143            
144                    return rt;
145            }
146    
147    
148            @Override
149            public Map<String,String> toParameters() {
150    
151                    Map<String,String> params = new HashMap<String,String>();
152    
153                    params.put("error", error.getCode());
154    
155                    if (error.getDescription() != null)
156                            params.put("error_description", error.getDescription());
157    
158                    if (error.getURI() != null)
159                            params.put("error_uri", error.getURI().toString());
160    
161                    if (getState() != null)
162                            params.put("state", getState().getValue());
163    
164                    return params;
165            }
166            
167            
168            @Override
169            public URL toURI()
170                    throws SerializeException {
171                    
172                    StringBuilder sb = new StringBuilder(getRedirectURI().toString());
173                    
174                    if (rt == null || rt.contains(ResponseType.Value.TOKEN))
175                            sb.append("#");
176                    else
177                            sb.append("?");
178    
179                    sb.append(URLUtils.serializeParameters(toParameters()));
180                    
181                    try {
182                            return new URL(sb.toString());
183                            
184                    } catch (MalformedURLException e) {
185                    
186                            throw new SerializeException("Couldn't serialize redirect URL: " + e.getMessage(), e);
187                    }
188            }
189    
190    
191            /**
192             * Parses an authorisation error response from the specified redirect
193             * URI and parameters.
194             *
195             * @param redirectURI The base redirect URI. Must not be {@code null}.
196             * @param params      The response parameters to parse. Must not be 
197             *                    {@code null}.
198             *
199             * @return The authorisation error response.
200             *
201             * @throws ParseException If the parameters couldn't be parsed to an
202             *                        authorisation error response.
203             */
204            public static AuthorizationErrorResponse parse(final URL redirectURI, 
205                                                           final Map<String,String> params)
206                    throws ParseException {
207    
208                    // Parse the error
209                    if (StringUtils.isBlank(params.get("error")))
210                            throw new ParseException("Missing error code");
211    
212                    // Parse error code
213                    String errorCode = params.get("error");
214    
215                    String errorDescription = params.get("error_description");
216    
217                    String errorURIString = params.get("error_uri");
218    
219                    URL errorURI = null;
220    
221                    if (errorURIString != null) {
222                            
223                            try {
224                                    errorURI = new URL(errorURIString);
225                                    
226                            } catch (MalformedURLException e) {
227                    
228                                    throw new ParseException("Invalid error URI: " + errorURIString, e);
229                            }
230                    }
231    
232    
233                    ErrorObject error = new ErrorObject(errorCode, errorDescription, HTTPResponse.SC_FOUND, errorURI);
234                    
235                    
236                    // State
237                    State state = State.parse(params.get("state"));
238                    
239                    return new AuthorizationErrorResponse(redirectURI, error, null, state);
240            }
241            
242            
243            /**
244             * Parses an authorisation error response from the specified URI.
245             *
246             * <p>Example URI:
247             *
248             * <pre>
249             * https://client.example.com/cb?
250             * error=invalid_request
251             * &amp;error_description=the%20request%20is%20not%20valid%20or%20malformed
252             * &amp;state=af0ifjsldkj
253             * </pre>
254             *
255             * @param uri The URI to parse. Can be absolute or relative. Must not 
256             *            be {@code null}.
257             *
258             * @return The authorisation error response.
259             *
260             * @throws ParseException If the URI couldn't be parsed to an
261             *                        authorisation error response.
262             */
263            public static AuthorizationErrorResponse parse(final URL uri)
264                    throws ParseException {
265                    
266                    Map<String,String> params = null;
267                    
268                    if (uri.getRef() != null)
269                            params = URLUtils.parseParameters(uri.getRef());
270    
271                    else if (uri.getQuery() != null)
272                            params = URLUtils.parseParameters(uri.getQuery());
273    
274                    else
275                            throw new ParseException("Missing URL fragment or query string");
276    
277                    
278                    return parse(URLUtils.getBaseURL(uri), params);
279            }
280            
281            
282            /**
283             * Parses an authorisation error response from the specified HTTP
284             * response.
285             *
286             * <p>Example HTTP response:
287             *
288             * <pre>
289             * HTTP/1.1 302 Found
290             * Location: https://client.example.com/cb?
291             * error=invalid_request
292             * &amp;error_description=the%20request%20is%20not%20valid%20or%20malformed
293             * &amp;state=af0ifjsldkj
294             * </pre>
295             *
296             * @param httpResponse The HTTP response to parse. Must not be 
297             *                     {@code null}.
298             *
299             * @return The authorisation error response.
300             *
301             * @throws ParseException If the HTTP response couldn't be parsed to an 
302             *                        authorisation error response.
303             */
304            public static AuthorizationErrorResponse parse(final HTTPResponse httpResponse)
305                    throws ParseException {
306                    
307                    if (httpResponse.getStatusCode() != HTTPResponse.SC_FOUND)
308                            throw new ParseException("Unexpected HTTP status code, must be 302 (Found): " + 
309                                                     httpResponse.getStatusCode());
310                    
311                    URL location = httpResponse.getLocation();
312                    
313                    if (location == null)
314                            throw new ParseException("Missing redirect URL / HTTP Location header");
315                    
316                    return parse(location);
317            }
318    }