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. 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
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<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 redirection 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 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 URL 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 URL 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<String,String>();
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 URL 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                sb.append(URLUtils.serializeParameters(toParameters()));
202                
203                try {
204                        return new URL(sb.toString());
205                        
206                } catch (MalformedURLException e) {
207                
208                        throw new SerializeException("Couldn't serialize redirect URL: " + e.getMessage(), e);
209                }
210        }
211
212
213        /**
214         * Parses an authorisation error response from the specified redirect
215         * URI and parameters.
216         *
217         * @param redirectURI The base redirect URI. Must not be {@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 = null;
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 URL 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 redirect URL / HTTP Location header");
337                
338                return parse(location);
339        }
340}