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