001/*
002 * oauth2-oidc-sdk
003 *
004 * Copyright 2012-2016, Connect2id Ltd and contributors.
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use
007 * this file except in compliance with the License. You may obtain a copy of the
008 * License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software distributed
013 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
014 * CONDITIONS OF ANY KIND, either express or implied. See the License for the
015 * specific language governing permissions and limitations under the License.
016 */
017
018package com.nimbusds.oauth2.sdk;
019
020
021import java.net.URI;
022import java.net.URISyntaxException;
023import java.util.Collections;
024import java.util.HashMap;
025import java.util.HashSet;
026import java.util.Map;
027import java.util.Set;
028
029import net.jcip.annotations.Immutable;
030
031import org.apache.commons.lang3.StringUtils;
032
033import com.nimbusds.oauth2.sdk.id.State;
034import com.nimbusds.oauth2.sdk.http.HTTPRequest;
035import com.nimbusds.oauth2.sdk.http.HTTPResponse;
036import com.nimbusds.oauth2.sdk.util.URIUtils;
037import com.nimbusds.oauth2.sdk.util.URLUtils;
038
039
040/**
041 * Authorisation error response. Intended only for errors which are allowed to
042 * be communicated back to the requesting OAuth 2.0 client, such as
043 * {@code access_denied}. For a complete list see OAuth 2.0 (RFC 6749),
044 * sections 4.1.2.1 and 4.2.2.1.
045 *
046 * <p>If the authorisation request fails due to a missing, invalid, or
047 * mismatching {@code redirect_uri}, or if the {@code client_id} is missing or
048 * invalid, a response <strong>must not</strong> be sent back to the requesting
049 * client. Instead, the authorisation server should simply display the error
050 * to the resource owner.
051 *
052 * <p>Standard authorisation errors:
053 *
054 * <ul>
055 *     <li>{@link OAuth2Error#INVALID_REQUEST}
056 *     <li>{@link OAuth2Error#UNAUTHORIZED_CLIENT}
057 *     <li>{@link OAuth2Error#ACCESS_DENIED}
058 *     <li>{@link OAuth2Error#UNSUPPORTED_RESPONSE_TYPE}
059 *     <li>{@link OAuth2Error#INVALID_SCOPE}
060 *     <li>{@link OAuth2Error#SERVER_ERROR}
061 *     <li>{@link OAuth2Error#TEMPORARILY_UNAVAILABLE}
062 * </ul>
063 *
064 * <p>Example HTTP response:
065 *
066 * <pre>
067 * HTTP/1.1 302 Found
068 * Location: https://client.example.com/cb?
069 * error=invalid_request
070 * &amp;error_description=the%20request%20is%20not%20valid%20or%20malformed
071 * &amp;state=af0ifjsldkj
072 * </pre>
073 *
074 * <p>Related specifications:
075 *
076 * <ul>
077 *     <li>OAuth 2.0 (RFC 6749), sections 4.1.2.1 and 4.2.2.1.
078 *     <li>OAuth 2.0 Multiple Response Type Encoding Practices 1.0.
079 *     <li>OAuth 2.0 Form Post Response Mode 1.0.
080 * </ul>
081 */
082@Immutable
083public class AuthorizationErrorResponse
084        extends AuthorizationResponse
085        implements ErrorResponse {
086
087
088        /**
089         * The standard OAuth 2.0 errors for an Authorisation error response.
090         */
091        private static final Set<ErrorObject> stdErrors = new HashSet<>();
092        
093        
094        static {
095                stdErrors.add(OAuth2Error.INVALID_REQUEST);
096                stdErrors.add(OAuth2Error.UNAUTHORIZED_CLIENT);
097                stdErrors.add(OAuth2Error.ACCESS_DENIED);
098                stdErrors.add(OAuth2Error.UNSUPPORTED_RESPONSE_TYPE);
099                stdErrors.add(OAuth2Error.INVALID_SCOPE);
100                stdErrors.add(OAuth2Error.SERVER_ERROR);
101                stdErrors.add(OAuth2Error.TEMPORARILY_UNAVAILABLE);
102        }
103        
104        
105        /**
106         * Gets the standard OAuth 2.0 errors for an Authorisation error 
107         * response.
108         *
109         * @return The standard errors, as a read-only set.
110         */
111        public static Set<ErrorObject> getStandardErrors() {
112        
113                return Collections.unmodifiableSet(stdErrors);
114        }
115        
116        
117        /**
118         * The error.
119         */
120        private final ErrorObject error;
121
122
123        /**
124         * Creates a new authorisation error response.
125         *
126         * @param redirectURI The base redirection URI. Must not be
127         *                    {@code null}.
128         * @param error       The error. Should match one of the
129         *                    {@link #getStandardErrors standard errors} for an
130         *                    authorisation error response. Must not be
131         *                    {@code null}.
132         * @param state       The state, {@code null} if not requested.
133         * @param rm          The implied response mode, {@code null} if
134         *                    unknown.
135         */
136        public AuthorizationErrorResponse(final URI redirectURI,
137                                          final ErrorObject error,
138                                          final State state,
139                                          final ResponseMode rm) {
140
141                super(redirectURI, state, rm);
142
143                if (error == null)
144                        throw new IllegalArgumentException("The error must not be null");
145
146                this.error = error;
147        }
148
149
150        @Override
151        public boolean indicatesSuccess() {
152
153                return false;
154        }
155        
156
157        @Override
158        public ErrorObject getErrorObject() {
159        
160                return error;
161        }
162
163
164        @Override
165        public ResponseMode impliedResponseMode() {
166
167                // Return "query" if not known, assumed the most frequent case
168                return getResponseMode() != null ? getResponseMode() : ResponseMode.QUERY;
169        }
170
171
172        @Override
173        public Map<String,String> toParameters() {
174
175                Map<String,String> params = new HashMap<>();
176
177                params.put("error", error.getCode());
178
179                if (error.getDescription() != null)
180                        params.put("error_description", error.getDescription());
181
182                if (error.getURI() != null)
183                        params.put("error_uri", error.getURI().toString());
184
185                if (getState() != null)
186                        params.put("state", getState().getValue());
187
188                return params;
189        }
190
191
192        /**
193         * Parses an authorisation error response.
194         *
195         * @param redirectURI The base redirection URI. Must not be
196         *                    {@code null}.
197         * @param params      The response parameters to parse. Must not be 
198         *                    {@code null}.
199         *
200         * @return The authorisation error response.
201         *
202         * @throws ParseException If the parameters couldn't be parsed to an
203         *                        authorisation error response.
204         */
205        public static AuthorizationErrorResponse parse(final URI redirectURI,
206                                                       final Map<String,String> params)
207                throws ParseException {
208
209                // Parse the error
210                if (StringUtils.isBlank(params.get("error")))
211                        throw new ParseException("Missing error code");
212
213                // Parse error code
214                String errorCode = params.get("error");
215
216                String errorDescription = params.get("error_description");
217
218                String errorURIString = params.get("error_uri");
219
220                URI errorURI = null;
221
222                if (errorURIString != null) {
223                        
224                        try {
225                                errorURI = new URI(errorURIString);
226                                
227                        } catch (URISyntaxException e) {
228                
229                                throw new ParseException("Invalid error URI: " + errorURIString, e);
230                        }
231                }
232
233                ErrorObject error = new ErrorObject(errorCode, errorDescription, HTTPResponse.SC_FOUND, errorURI);
234                
235                
236                // State
237                State state = State.parse(params.get("state"));
238                
239                return new AuthorizationErrorResponse(redirectURI, error, state, null);
240        }
241        
242        
243        /**
244         * Parses an authorisation error response.
245         *
246         * <p>Use a relative URI if the host, port and path details are not
247         * known:
248         *
249         * <pre>
250         * URI relUrl = new URI("https:///?error=invalid_request");
251         * </pre>
252         *
253         * <p>Example URI:
254         *
255         * <pre>
256         * https://client.example.com/cb?
257         * error=invalid_request
258         * &amp;error_description=the%20request%20is%20not%20valid%20or%20malformed
259         * &amp;state=af0ifjsldkj
260         * </pre>
261         *
262         * @param uri The URI to parse. Can be absolute or relative, with a
263         *            fragment or query string containing the authorisation
264         *            response parameters. Must not be {@code null}.
265         *
266         * @return The authorisation error response.
267         *
268         * @throws ParseException If the URI couldn't be parsed to an
269         *                        authorisation error response.
270         */
271        public static AuthorizationErrorResponse parse(final URI uri)
272                throws ParseException {
273                
274                Map<String,String> params;
275
276                if (uri.getRawFragment() != null) {
277
278                        params = URLUtils.parseParameters(uri.getRawFragment());
279
280                } else if (uri.getRawQuery() != null) {
281
282                        params = URLUtils.parseParameters(uri.getRawQuery());
283
284                } else {
285
286                        throw new ParseException("Missing URI fragment or query string");
287                }
288
289                
290                return parse(URIUtils.getBaseURI(uri), params);
291        }
292        
293        
294        /**
295         * Parses an authorisation error response from the specified initial
296         * HTTP 302 redirect response generated at the authorisation endpoint.
297         *
298         * <p>Example HTTP response:
299         *
300         * <pre>
301         * HTTP/1.1 302 Found
302         * Location: https://client.example.com/cb?error=invalid_request&amp;state=af0ifjsldkj
303         * </pre>
304         *
305         * @see #parse(HTTPRequest)
306         *
307         * @param httpResponse The HTTP response to parse. Must not be 
308         *                     {@code null}.
309         *
310         * @return The authorisation error response.
311         *
312         * @throws ParseException If the HTTP response couldn't be parsed to an 
313         *                        authorisation error response.
314         */
315        public static AuthorizationErrorResponse parse(final HTTPResponse httpResponse)
316                throws ParseException {
317
318                URI location = httpResponse.getLocation();
319
320                if (location == null) {
321                        throw new ParseException("Missing redirection URL / HTTP Location header");
322                }
323
324                return parse(location);
325        }
326
327
328        /**
329         * Parses an authorisation error response from the specified HTTP
330         * request at the client redirection (callback) URI. Applies to
331         * {@code query}, {@code fragment} and {@code form_post} response
332         * modes.
333         *
334         * <p>Example HTTP request (authorisation success):
335         *
336         * <pre>
337         * GET /cb?error=invalid_request&amp;state=af0ifjsldkj HTTP/1.1
338         * Host: client.example.com
339         * </pre>
340         *
341         * @see #parse(HTTPResponse)
342         *
343         * @param httpRequest The HTTP request to parse. Must not be
344         *                    {@code null}.
345         *
346         * @throws ParseException If the HTTP request couldn't be parsed to an
347         *                        authorisation error response.
348         */
349        public static AuthorizationErrorResponse parse(final HTTPRequest httpRequest)
350                throws ParseException {
351
352                final URI baseURI;
353
354                try {
355                        baseURI = httpRequest.getURL().toURI();
356
357                } catch (URISyntaxException e) {
358                        throw new ParseException(e.getMessage(), e);
359                }
360
361                if (httpRequest.getQuery() != null) {
362                        // For query string and form_post response mode
363                        return parse(baseURI, URLUtils.parseParameters(httpRequest.getQuery()));
364                } else if (httpRequest.getFragment() != null) {
365                        // For fragment response mode (never available in actual HTTP request from browser)
366                        return parse(baseURI, URLUtils.parseParameters(httpRequest.getFragment()));
367                } else {
368                        throw new ParseException("Missing URI fragment, query string or post body");
369                }
370        }
371}