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.
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@Immutable
053public class AuthorizationErrorResponse
054        extends AuthorizationResponse
055        implements ErrorResponse {
056
057
058        /**
059         * The standard OAuth 2.0 errors for an Authorisation error response.
060         */
061        private static Set<ErrorObject> stdErrors = new HashSet<ErrorObject>();
062        
063        
064        static {
065                stdErrors.add(OAuth2Error.INVALID_REQUEST);
066                stdErrors.add(OAuth2Error.UNAUTHORIZED_CLIENT);
067                stdErrors.add(OAuth2Error.ACCESS_DENIED);
068                stdErrors.add(OAuth2Error.UNSUPPORTED_RESPONSE_TYPE);
069                stdErrors.add(OAuth2Error.INVALID_SCOPE);
070                stdErrors.add(OAuth2Error.SERVER_ERROR);
071                stdErrors.add(OAuth2Error.TEMPORARILY_UNAVAILABLE);
072        }
073        
074        
075        /**
076         * Gets the standard OAuth 2.0 errors for an Authorisation error 
077         * response.
078         *
079         * @return The standard errors, as a read-only set.
080         */
081        public static Set<ErrorObject> getStandardErrors() {
082        
083                return Collections.unmodifiableSet(stdErrors);
084        }
085        
086        
087        /**
088         * The error.
089         */
090        private final ErrorObject error;
091        
092        
093        /**
094         * The response type, used to determine redirection URL composition. If
095         * unknown {@code null}.
096         */
097        private final ResponseType rt;
098
099
100        /**
101         * Creates a new authorisation error response.
102         *
103         * @param redirectURI The base redirection URI. Must not be
104         *                    {@code null}.
105         * @param error       The error. Should match one of the
106         *                    {@link #getStandardErrors standard errors} for an
107         *                    authorisation error response. Must not be
108         *                    {@code null}.
109         * @param rt          The response type, used to determine the
110         *                    redirection URI composition. If unknown
111         *                    {@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        /**
131         * Creates a new authorisation error response, with no specified
132         * response type to determine the redirection URI composition.
133         *
134         * @param redirectURI The base redirection URI. Must not be
135         *                    {@code null}.
136         * @param error       The error. Should match one of the 
137         *                    {@link #getStandardErrors standard errors} for an 
138         *                    authorisation error response. Must not be 
139         *                    {@code null}.
140         * @param state       The state, {@code null} if not requested.
141         */
142        public AuthorizationErrorResponse(final URL redirectURI,
143                                          final ErrorObject error,
144                                          final State state) {
145                                          
146                this(redirectURI, error, null, state);
147        }
148        
149
150        @Override
151        public ErrorObject getErrorObject() {
152        
153                return error;
154        }
155        
156        
157        /**
158         * Gets the response type.
159         *
160         * @return The response type, {@code null} if not specified.
161         */
162        public ResponseType getResponseType() {
163        
164                return rt;
165        }
166
167
168        @Override
169        public Map<String,String> toParameters() {
170
171                Map<String,String> params = new HashMap<String,String>();
172
173                params.put("error", error.getCode());
174
175                if (error.getDescription() != null)
176                        params.put("error_description", error.getDescription());
177
178                if (error.getURI() != null)
179                        params.put("error_uri", error.getURI().toString());
180
181                if (getState() != null)
182                        params.put("state", getState().getValue());
183
184                return params;
185        }
186        
187        
188        @Override
189        public URL toURI()
190                throws SerializeException {
191                
192                StringBuilder sb = new StringBuilder(getRedirectionURI().toString());
193                
194                if (rt == null || rt.contains(ResponseType.Value.TOKEN)) {
195                        sb.append("#");
196                } else {
197                        sb.append("?");
198                }
199
200                sb.append(URLUtils.serializeParameters(toParameters()));
201                
202                try {
203                        return new URL(sb.toString());
204                        
205                } catch (MalformedURLException e) {
206                
207                        throw new SerializeException("Couldn't serialize redirection URI: " + e.getMessage(), e);
208                }
209        }
210
211
212        /**
213         * Parses an authorisation error response from the specified redirect
214         * URI and parameters.
215         *
216         * @param redirectURI The base redirection URI. Must not be
217         *                    {@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         * &amp;error_description=the%20request%20is%20not%20valid%20or%20malformed
274         * &amp;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;
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 URI 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         * &amp;error_description=the%20request%20is%20not%20valid%20or%20malformed
315         * &amp;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 redirection URI / HTTP Location header");
337                
338                return parse(location);
339        }
340}