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.HTTPRequest;
018import com.nimbusds.oauth2.sdk.http.HTTPResponse;
019import com.nimbusds.oauth2.sdk.util.URIUtils;
020import com.nimbusds.oauth2.sdk.util.URLUtils;
021
022
023/**
024 * Authorisation error response.
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 * &amp;error_description=the%20request%20is%20not%20valid%20or%20malformed
045 * &amp;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 *     <li>OAuth 2.0 Multiple Response Type Encoding Practices 1.0.
053 *     <li>OAuth 2.0 Form Post Response Mode 1.0.
054 * </ul>
055 */
056@Immutable
057public class AuthorizationErrorResponse
058        extends AuthorizationResponse
059        implements ErrorResponse {
060
061
062        /**
063         * The standard OAuth 2.0 errors for an Authorisation error response.
064         */
065        private static final Set<ErrorObject> stdErrors = new HashSet<>();
066        
067        
068        static {
069                stdErrors.add(OAuth2Error.INVALID_REQUEST);
070                stdErrors.add(OAuth2Error.UNAUTHORIZED_CLIENT);
071                stdErrors.add(OAuth2Error.ACCESS_DENIED);
072                stdErrors.add(OAuth2Error.UNSUPPORTED_RESPONSE_TYPE);
073                stdErrors.add(OAuth2Error.INVALID_SCOPE);
074                stdErrors.add(OAuth2Error.SERVER_ERROR);
075                stdErrors.add(OAuth2Error.TEMPORARILY_UNAVAILABLE);
076        }
077        
078        
079        /**
080         * Gets the standard OAuth 2.0 errors for an Authorisation error 
081         * response.
082         *
083         * @return The standard errors, as a read-only set.
084         */
085        public static Set<ErrorObject> getStandardErrors() {
086        
087                return Collections.unmodifiableSet(stdErrors);
088        }
089        
090        
091        /**
092         * The error.
093         */
094        private final ErrorObject error;
095
096
097        /**
098         * Creates a new authorisation error response.
099         *
100         * @param redirectURI The base redirection URI. Must not be
101         *                    {@code null}.
102         * @param error       The error. Should match one of the
103         *                    {@link #getStandardErrors standard errors} for an
104         *                    authorisation error response. Must not be
105         *                    {@code null}.
106         * @param state       The state, {@code null} if not requested.
107         * @param rm          The implied response mode, {@code null} if
108         *                    unknown.
109         */
110        public AuthorizationErrorResponse(final URI redirectURI,
111                                          final ErrorObject error,
112                                          final State state,
113                                          final ResponseMode rm) {
114
115                super(redirectURI, state, rm);
116
117                if (error == null)
118                        throw new IllegalArgumentException("The error must not be null");
119
120                this.error = error;
121        }
122
123
124        @Override
125        public boolean indicatesSuccess() {
126
127                return false;
128        }
129        
130
131        @Override
132        public ErrorObject getErrorObject() {
133        
134                return error;
135        }
136
137
138        @Override
139        public ResponseMode impliedResponseMode() {
140
141                // Return "query" if not known, assumed the most frequent case
142                return getResponseMode() != null ? getResponseMode() : ResponseMode.QUERY;
143        }
144
145
146        @Override
147        public Map<String,String> toParameters() {
148
149                Map<String,String> params = new HashMap<>();
150
151                params.put("error", error.getCode());
152
153                if (error.getDescription() != null)
154                        params.put("error_description", error.getDescription());
155
156                if (error.getURI() != null)
157                        params.put("error_uri", error.getURI().toString());
158
159                if (getState() != null)
160                        params.put("state", getState().getValue());
161
162                return params;
163        }
164
165
166        /**
167         * Parses an authorisation error response.
168         *
169         * @param redirectURI The base redirection URI. Must not be
170         *                    {@code null}.
171         * @param params      The response parameters to parse. Must not be 
172         *                    {@code null}.
173         *
174         * @return The authorisation error response.
175         *
176         * @throws ParseException If the parameters couldn't be parsed to an
177         *                        authorisation error response.
178         */
179        public static AuthorizationErrorResponse parse(final URI redirectURI,
180                                                       final Map<String,String> params)
181                throws ParseException {
182
183                // Parse the error
184                if (StringUtils.isBlank(params.get("error")))
185                        throw new ParseException("Missing error code");
186
187                // Parse error code
188                String errorCode = params.get("error");
189
190                String errorDescription = params.get("error_description");
191
192                String errorURIString = params.get("error_uri");
193
194                URI errorURI = null;
195
196                if (errorURIString != null) {
197                        
198                        try {
199                                errorURI = new URI(errorURIString);
200                                
201                        } catch (URISyntaxException e) {
202                
203                                throw new ParseException("Invalid error URI: " + errorURIString, e);
204                        }
205                }
206
207                ErrorObject error = new ErrorObject(errorCode, errorDescription, HTTPResponse.SC_FOUND, errorURI);
208                
209                
210                // State
211                State state = State.parse(params.get("state"));
212                
213                return new AuthorizationErrorResponse(redirectURI, error, state, null);
214        }
215        
216        
217        /**
218         * Parses an authorisation error response.
219         *
220         * <p>Use a relative URI if the host, port and path details are not
221         * known:
222         *
223         * <pre>
224         * URI relUrl = new URI("http://?error=invalid_request");
225         * </pre>
226         *
227         * <p>Example URI:
228         *
229         * <pre>
230         * https://client.example.com/cb?
231         * error=invalid_request
232         * &amp;error_description=the%20request%20is%20not%20valid%20or%20malformed
233         * &amp;state=af0ifjsldkj
234         * </pre>
235         *
236         * @param uri The URI to parse. Can be absolute or relative, with a
237         *            fragment or query string containing the authorisation
238         *            response parameters. Must not be {@code null}.
239         *
240         * @return The authorisation error response.
241         *
242         * @throws ParseException If the URI couldn't be parsed to an
243         *                        authorisation error response.
244         */
245        public static AuthorizationErrorResponse parse(final URI uri)
246                throws ParseException {
247                
248                Map<String,String> params;
249
250                if (uri.getRawFragment() != null) {
251
252                        params = URLUtils.parseParameters(uri.getRawFragment());
253
254                } else if (uri.getRawQuery() != null) {
255
256                        params = URLUtils.parseParameters(uri.getRawQuery());
257
258                } else {
259
260                        throw new ParseException("Missing URI fragment or query string");
261                }
262
263                
264                return parse(URIUtils.getBaseURI(uri), params);
265        }
266        
267        
268        /**
269         * Parses an authorisation error response from the specified initial
270         * HTTP 302 redirect response generated at the authorisation endpoint.
271         *
272         * <p>Example HTTP response:
273         *
274         * <pre>
275         * HTTP/1.1 302 Found
276         * Location: https://client.example.com/cb?error=invalid_request&amp;state=af0ifjsldkj
277         * </pre>
278         *
279         * @see #parse(HTTPRequest)
280         *
281         * @param httpResponse The HTTP response to parse. Must not be 
282         *                     {@code null}.
283         *
284         * @return The authorisation error response.
285         *
286         * @throws ParseException If the HTTP response couldn't be parsed to an 
287         *                        authorisation error response.
288         */
289        public static AuthorizationErrorResponse parse(final HTTPResponse httpResponse)
290                throws ParseException {
291
292                URI location = httpResponse.getLocation();
293
294                if (location == null) {
295                        throw new ParseException("Missing redirection URL / HTTP Location header");
296                }
297
298                return parse(location);
299        }
300
301
302        /**
303         * Parses an authorisation error response from the specified HTTP
304         * request at the client redirection (callback) URI. Applies to
305         * {@code query}, {@code fragment} and {@code form_post} response
306         * modes.
307         *
308         * <p>Example HTTP request (authorisation success):
309         *
310         * <pre>
311         * GET /cb?error=invalid_request&amp;state=af0ifjsldkj HTTP/1.1
312         * Host: client.example.com
313         * </pre>
314         *
315         * @see #parse(HTTPResponse)
316         *
317         * @param httpRequest The HTTP request to parse. Must not be
318         *                    {@code null}.
319         *
320         * @throws ParseException If the HTTP request couldn't be parsed to an
321         *                        authorisation error response.
322         */
323        public static AuthorizationErrorResponse parse(final HTTPRequest httpRequest)
324                throws ParseException {
325
326                final URI baseURI;
327
328                try {
329                        baseURI = httpRequest.getURL().toURI();
330
331                } catch (URISyntaxException e) {
332                        throw new ParseException(e.getMessage(), e);
333                }
334
335                if (httpRequest.getQuery() != null) {
336                        // For query string and form_post response mode
337                        return parse(baseURI, URLUtils.parseParameters(httpRequest.getQuery()));
338                } else if (httpRequest.getFragment() != null) {
339                        // For fragment response mode (never available in actual HTTP request from browser)
340                        return parse(baseURI, URLUtils.parseParameters(httpRequest.getFragment()));
341                } else {
342                        throw new ParseException("Missing URI fragment, query string or post body");
343                }
344        }
345}