001package com.nimbusds.oauth2.sdk;
002
003
004import java.net.URI;
005import java.net.URISyntaxException;
006import java.net.URL;
007import java.util.Collections;
008import java.util.HashMap;
009import java.util.HashSet;
010import java.util.Map;
011import java.util.Set;
012
013import net.jcip.annotations.Immutable;
014
015import org.apache.commons.lang3.StringUtils;
016
017import com.nimbusds.oauth2.sdk.id.State;
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 * </ul>
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<>();
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 URI 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 URI 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 URI 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<>();
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 URI 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
202                sb.append(URLUtils.serializeParameters(toParameters()));
203                
204                try {
205                        return new URI(sb.toString());
206                        
207                } catch (URISyntaxException e) {
208                
209                        throw new SerializeException("Couldn't serialize redirection URI: " + e.getMessage(), e);
210                }
211        }
212
213
214        /**
215         * Parses an authorisation error response from the specified redirect
216         * URI and parameters.
217         *
218         * @param redirectURI The base redirection URI. Must not be
219         *                    {@code null}.
220         * @param params      The response parameters to parse. Must not be 
221         *                    {@code null}.
222         *
223         * @return The authorisation error response.
224         *
225         * @throws ParseException If the parameters couldn't be parsed to an
226         *                        authorisation error response.
227         */
228        public static AuthorizationErrorResponse parse(final URI redirectURI,
229                                                       final Map<String,String> params)
230                throws ParseException {
231
232                // Parse the error
233                if (StringUtils.isBlank(params.get("error")))
234                        throw new ParseException("Missing error code");
235
236                // Parse error code
237                String errorCode = params.get("error");
238
239                String errorDescription = params.get("error_description");
240
241                String errorURIString = params.get("error_uri");
242
243                URI errorURI = null;
244
245                if (errorURIString != null) {
246                        
247                        try {
248                                errorURI = new URI(errorURIString);
249                                
250                        } catch (URISyntaxException e) {
251                
252                                throw new ParseException("Invalid error URI: " + errorURIString, e);
253                        }
254                }
255
256
257                ErrorObject error = new ErrorObject(errorCode, errorDescription, HTTPResponse.SC_FOUND, errorURI);
258                
259                
260                // State
261                State state = State.parse(params.get("state"));
262                
263                return new AuthorizationErrorResponse(redirectURI, error, null, state);
264        }
265        
266        
267        /**
268         * Parses an authorisation error response from the specified URI.
269         *
270         * <p>Example URI:
271         *
272         * <pre>
273         * https://client.example.com/cb?
274         * error=invalid_request
275         * &amp;error_description=the%20request%20is%20not%20valid%20or%20malformed
276         * &amp;state=af0ifjsldkj
277         * </pre>
278         *
279         * @param uri The URI to parse. Can be absolute or relative. Must not 
280         *            be {@code null}.
281         *
282         * @return The authorisation error response.
283         *
284         * @throws ParseException If the URI couldn't be parsed to an
285         *                        authorisation error response.
286         */
287        public static AuthorizationErrorResponse parse(final URI uri)
288                throws ParseException {
289                
290                Map<String,String> params;
291                
292                if (uri.getRawFragment() != null)
293                        params = URLUtils.parseParameters(uri.getRawFragment());
294
295                else if (uri.getRawQuery() != null)
296                        params = URLUtils.parseParameters(uri.getRawQuery());
297
298                else
299                        throw new ParseException("Missing URI fragment or query string");
300
301                
302                return parse(URIUtils.getBaseURI(uri), params);
303        }
304        
305        
306        /**
307         * Parses an authorisation error response from the specified HTTP
308         * response.
309         *
310         * <p>Example HTTP response:
311         *
312         * <pre>
313         * HTTP/1.1 302 Found
314         * Location: https://client.example.com/cb?
315         * error=invalid_request
316         * &amp;error_description=the%20request%20is%20not%20valid%20or%20malformed
317         * &amp;state=af0ifjsldkj
318         * </pre>
319         *
320         * @param httpResponse The HTTP response to parse. Must not be 
321         *                     {@code null}.
322         *
323         * @return The authorisation error response.
324         *
325         * @throws ParseException If the HTTP response couldn't be parsed to an 
326         *                        authorisation error response.
327         */
328        public static AuthorizationErrorResponse parse(final HTTPResponse httpResponse)
329                throws ParseException {
330                
331                if (httpResponse.getStatusCode() != HTTPResponse.SC_FOUND)
332                        throw new ParseException("Unexpected HTTP status code, must be 302 (Found): " + 
333                                                 httpResponse.getStatusCode());
334                
335                URL location = httpResponse.getLocation();
336                
337                if (location == null)
338                        throw new ParseException("Missing redirection URI / HTTP Location header");
339
340                try {
341                        return parse(location.toURI());
342
343                } catch (URISyntaxException e) {
344
345                        throw new ParseException(e.getMessage(), e);
346                }
347        }
348}