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