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 Identifier in Authorization
080 *         Response (draft-ietf-oauth-iss-auth-resp-00).
081 * </ul>
082 */
083@Immutable
084public class AuthorizationErrorResponse
085        extends AuthorizationResponse
086        implements ErrorResponse {
087
088
089        /**
090         * The standard OAuth 2.0 errors for an Authorisation error response.
091         */
092        private static final Set<ErrorObject> stdErrors = new HashSet<>();
093        
094        
095        static {
096                stdErrors.add(OAuth2Error.INVALID_REQUEST);
097                stdErrors.add(OAuth2Error.UNAUTHORIZED_CLIENT);
098                stdErrors.add(OAuth2Error.ACCESS_DENIED);
099                stdErrors.add(OAuth2Error.UNSUPPORTED_RESPONSE_TYPE);
100                stdErrors.add(OAuth2Error.INVALID_SCOPE);
101                stdErrors.add(OAuth2Error.SERVER_ERROR);
102                stdErrors.add(OAuth2Error.TEMPORARILY_UNAVAILABLE);
103        }
104        
105        
106        /**
107         * Gets the standard OAuth 2.0 errors for an Authorisation error 
108         * response.
109         *
110         * @return The standard errors, as a read-only set.
111         */
112        public static Set<ErrorObject> getStandardErrors() {
113        
114                return Collections.unmodifiableSet(stdErrors);
115        }
116        
117        
118        /**
119         * The error.
120         */
121        private final ErrorObject error;
122
123
124        /**
125         * Creates a new authorisation error response.
126         *
127         * @param redirectURI The base redirection URI. Must not be
128         *                    {@code null}.
129         * @param error       The error. Should match one of the
130         *                    {@link #getStandardErrors standard errors} for an
131         *                    authorisation error response. Must not be
132         *                    {@code null}.
133         * @param state       The state, {@code null} if not requested.
134         * @param rm          The implied response mode, {@code null} if
135         *                    unknown.
136         */
137        public AuthorizationErrorResponse(final URI redirectURI,
138                                          final ErrorObject error,
139                                          final State state,
140                                          final ResponseMode rm) {
141
142                this(redirectURI, error, state, null, rm);
143        }
144
145
146        /**
147         * Creates a new authorisation error response.
148         *
149         * @param redirectURI The base redirection URI. Must not be
150         *                    {@code null}.
151         * @param error       The error. Should match one of the
152         *                    {@link #getStandardErrors standard errors} for an
153         *                    authorisation error response. Must not be
154         *                    {@code null}.
155         * @param state       The state, {@code null} if not requested.
156         * @param issuer      The issuer, {@code null} if not specified.
157         * @param rm          The implied response mode, {@code null} if
158         *                    unknown.
159         */
160        public AuthorizationErrorResponse(final URI redirectURI,
161                                          final ErrorObject error,
162                                          final State state,
163                                          final Issuer issuer,
164                                          final ResponseMode rm) {
165
166                super(redirectURI, state, issuer, rm);
167
168                if (error == null)
169                        throw new IllegalArgumentException("The error must not be null");
170
171                this.error = error;
172        }
173
174
175        /**
176         * Creates a new JSON Web Token (JWT) secured authorisation error
177         * response.
178         *
179         * @param redirectURI The base redirection URI. Must not be
180         *                    {@code null}.
181         * @param jwtResponse The JWT-secured response. Must not be
182         *                    {@code null}.
183         * @param rm          The implied response mode, {@code null} if
184         *                    unknown.
185         */
186        public AuthorizationErrorResponse(final URI redirectURI,
187                                          final JWT jwtResponse,
188                                          final ResponseMode rm) {
189
190                super(redirectURI, jwtResponse, rm);
191
192                error = null;
193        }
194
195
196        @Override
197        public boolean indicatesSuccess() {
198
199                return false;
200        }
201        
202
203        @Override
204        public ErrorObject getErrorObject() {
205        
206                return error;
207        }
208
209
210        @Override
211        public ResponseMode impliedResponseMode() {
212
213                // Return "query" if not known, assumed the most frequent case
214                return getResponseMode() != null ? getResponseMode() : ResponseMode.QUERY;
215        }
216
217
218        @Override
219        public Map<String,List<String>> toParameters() {
220
221                Map<String,List<String>> params = new HashMap<>();
222                
223                if (getJWTResponse() != null) {
224                        // JARM, no other top-level parameters
225                        params.put("response", Collections.singletonList(getJWTResponse().serialize()));
226                        return params;
227                }
228
229                params.putAll(getErrorObject().toParameters());
230
231                if (getState() != null)
232                        params.put("state", Collections.singletonList(getState().getValue()));
233                
234                if (getIssuer() != null)
235                        params.put("iss", Collections.singletonList(getIssuer().getValue()));
236
237                return params;
238        }
239
240
241        /**
242         * Parses an authorisation error response.
243         *
244         * @param redirectURI The base redirection URI. Must not be
245         *                    {@code null}.
246         * @param params      The response parameters to parse. Must not be 
247         *                    {@code null}.
248         *
249         * @return The authorisation error response.
250         *
251         * @throws ParseException If the parameters couldn't be parsed to an
252         *                        authorisation error response.
253         */
254        public static AuthorizationErrorResponse parse(final URI redirectURI,
255                                                       final Map<String,List<String>> params)
256                throws ParseException {
257                
258                // JARM, ignore other top level params
259                String responseString = MultivaluedMapUtils.getFirstValue(params, "response");
260                if (responseString != null) {
261                        JWT jwtResponse;
262                        try {
263                                jwtResponse = JWTParser.parse(responseString);
264                        } catch (java.text.ParseException e) {
265                                throw new ParseException("Invalid JWT response: " + e.getMessage(), e);
266                        }
267                        
268                        return new AuthorizationErrorResponse(redirectURI, jwtResponse, ResponseMode.JWT);
269                }
270
271                // Parse the error
272                ErrorObject error = ErrorObject.parse(params);
273                
274                if (StringUtils.isBlank(error.getCode())) {
275                        throw new ParseException("Missing error code");
276                }
277                error = error.setHTTPStatusCode(HTTPResponse.SC_FOUND); // need a status code
278                
279                // State
280                State state = State.parse(MultivaluedMapUtils.getFirstValue(params, "state"));
281                
282                // Parse optional issuer, draft-ietf-oauth-iss-auth-resp-00
283                Issuer issuer = Issuer.parse(MultivaluedMapUtils.getFirstValue(params, "iss"));
284                
285                return new AuthorizationErrorResponse(redirectURI, error, state, issuer, null);
286        }
287        
288        
289        /**
290         * Parses an authorisation error response.
291         *
292         * <p>Use a relative URI if the host, port and path details are not
293         * known:
294         *
295         * <pre>
296         * URI relUrl = new URI("https:///?error=invalid_request");
297         * </pre>
298         *
299         * <p>Example URI:
300         *
301         * <pre>
302         * https://client.example.com/cb?
303         * error=invalid_request
304         * &amp;error_description=the%20request%20is%20not%20valid%20or%20malformed
305         * &amp;state=af0ifjsldkj
306         * </pre>
307         *
308         * @param uri The URI to parse. Can be absolute or relative, with a
309         *            fragment or query string containing the authorisation
310         *            response parameters. Must not be {@code null}.
311         *
312         * @return The authorisation error response.
313         *
314         * @throws ParseException If the URI couldn't be parsed to an
315         *                        authorisation error response.
316         */
317        public static AuthorizationErrorResponse parse(final URI uri)
318                throws ParseException {
319                
320                return parse(URIUtils.getBaseURI(uri), parseResponseParameters(uri));
321        }
322        
323        
324        /**
325         * Parses an authorisation error response from the specified initial
326         * HTTP 302 redirect response generated at the authorisation endpoint.
327         *
328         * <p>Example HTTP response:
329         *
330         * <pre>
331         * HTTP/1.1 302 Found
332         * Location: https://client.example.com/cb?error=invalid_request&amp;state=af0ifjsldkj
333         * </pre>
334         *
335         * @see #parse(HTTPRequest)
336         *
337         * @param httpResponse The HTTP response to parse. Must not be 
338         *                     {@code null}.
339         *
340         * @return The authorisation error response.
341         *
342         * @throws ParseException If the HTTP response couldn't be parsed to an 
343         *                        authorisation error response.
344         */
345        public static AuthorizationErrorResponse parse(final HTTPResponse httpResponse)
346                throws ParseException {
347
348                URI location = httpResponse.getLocation();
349
350                if (location == null) {
351                        throw new ParseException("Missing redirection URL / HTTP Location header");
352                }
353
354                return parse(location);
355        }
356
357
358        /**
359         * Parses an authorisation error response from the specified HTTP
360         * request at the client redirection (callback) URI. Applies to
361         * {@code query}, {@code fragment} and {@code form_post} response
362         * modes.
363         *
364         * <p>Example HTTP request (authorisation success):
365         *
366         * <pre>
367         * GET /cb?error=invalid_request&amp;state=af0ifjsldkj HTTP/1.1
368         * Host: client.example.com
369         * </pre>
370         *
371         * @see #parse(HTTPResponse)
372         *
373         * @param httpRequest The HTTP request to parse. Must not be
374         *                    {@code null}.
375         *
376         * @return The authorisation error response.
377         *
378         * @throws ParseException If the HTTP request couldn't be parsed to an
379         *                        authorisation error response.
380         */
381        public static AuthorizationErrorResponse parse(final HTTPRequest httpRequest)
382                throws ParseException {
383
384                return parse(httpRequest.getURI(), parseResponseParameters(httpRequest));
385        }
386}