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