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