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