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