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.State;
031import com.nimbusds.oauth2.sdk.util.MultivaluedMapUtils;
032import com.nimbusds.oauth2.sdk.util.StringUtils;
033import com.nimbusds.oauth2.sdk.util.URIUtils;
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.putAll(getErrorObject().toParameters());
203
204                if (getState() != null)
205                        params.put("state", Collections.singletonList(getState().getValue()));
206
207                return params;
208        }
209
210
211        /**
212         * Parses an authorisation error response.
213         *
214         * @param redirectURI The base redirection URI. Must not be
215         *                    {@code null}.
216         * @param params      The response parameters to parse. Must not be 
217         *                    {@code null}.
218         *
219         * @return The authorisation error response.
220         *
221         * @throws ParseException If the parameters couldn't be parsed to an
222         *                        authorisation error response.
223         */
224        public static AuthorizationErrorResponse parse(final URI redirectURI,
225                                                       final Map<String,List<String>> params)
226                throws ParseException {
227                
228                // JARM, ignore other top level params
229                if (params.get("response") != null) {
230                        JWT jwtResponse;
231                        try {
232                                jwtResponse = JWTParser.parse(MultivaluedMapUtils.getFirstValue(params, "response"));
233                        } catch (java.text.ParseException e) {
234                                throw new ParseException("Invalid JWT response: " + e.getMessage(), e);
235                        }
236                        
237                        return new AuthorizationErrorResponse(redirectURI, jwtResponse, ResponseMode.JWT);
238                }
239
240                // Parse the error
241                ErrorObject error = ErrorObject.parse(params);
242                
243                if (StringUtils.isBlank(error.getCode())) {
244                        throw new ParseException("Missing error code");
245                }
246                error = error.setHTTPStatusCode(HTTPResponse.SC_FOUND); // need a status code
247                
248                // State
249                State state = State.parse(MultivaluedMapUtils.getFirstValue(params, "state"));
250                
251                return new AuthorizationErrorResponse(redirectURI, error, state, null);
252        }
253        
254        
255        /**
256         * Parses an authorisation error response.
257         *
258         * <p>Use a relative URI if the host, port and path details are not
259         * known:
260         *
261         * <pre>
262         * URI relUrl = new URI("https:///?error=invalid_request");
263         * </pre>
264         *
265         * <p>Example URI:
266         *
267         * <pre>
268         * https://client.example.com/cb?
269         * error=invalid_request
270         * &amp;error_description=the%20request%20is%20not%20valid%20or%20malformed
271         * &amp;state=af0ifjsldkj
272         * </pre>
273         *
274         * @param uri The URI to parse. Can be absolute or relative, with a
275         *            fragment or query string containing the authorisation
276         *            response parameters. Must not be {@code null}.
277         *
278         * @return The authorisation error response.
279         *
280         * @throws ParseException If the URI couldn't be parsed to an
281         *                        authorisation error response.
282         */
283        public static AuthorizationErrorResponse parse(final URI uri)
284                throws ParseException {
285                
286                return parse(URIUtils.getBaseURI(uri), parseResponseParameters(uri));
287        }
288        
289        
290        /**
291         * Parses an authorisation error response from the specified initial
292         * HTTP 302 redirect response generated at the authorisation endpoint.
293         *
294         * <p>Example HTTP response:
295         *
296         * <pre>
297         * HTTP/1.1 302 Found
298         * Location: https://client.example.com/cb?error=invalid_request&amp;state=af0ifjsldkj
299         * </pre>
300         *
301         * @see #parse(HTTPRequest)
302         *
303         * @param httpResponse The HTTP response to parse. Must not be 
304         *                     {@code null}.
305         *
306         * @return The authorisation error response.
307         *
308         * @throws ParseException If the HTTP response couldn't be parsed to an 
309         *                        authorisation error response.
310         */
311        public static AuthorizationErrorResponse parse(final HTTPResponse httpResponse)
312                throws ParseException {
313
314                URI location = httpResponse.getLocation();
315
316                if (location == null) {
317                        throw new ParseException("Missing redirection URL / HTTP Location header");
318                }
319
320                return parse(location);
321        }
322
323
324        /**
325         * Parses an authorisation error response from the specified HTTP
326         * request at the client redirection (callback) URI. Applies to
327         * {@code query}, {@code fragment} and {@code form_post} response
328         * modes.
329         *
330         * <p>Example HTTP request (authorisation success):
331         *
332         * <pre>
333         * GET /cb?error=invalid_request&amp;state=af0ifjsldkj HTTP/1.1
334         * Host: client.example.com
335         * </pre>
336         *
337         * @see #parse(HTTPResponse)
338         *
339         * @param httpRequest The HTTP request to parse. Must not be
340         *                    {@code null}.
341         *
342         * @return The authorisation error response.
343         *
344         * @throws ParseException If the HTTP request couldn't be parsed to an
345         *                        authorisation error response.
346         */
347        public static AuthorizationErrorResponse parse(final HTTPRequest httpRequest)
348                throws ParseException {
349
350                return parse(httpRequest.getURI(), parseResponseParameters(httpRequest));
351        }
352}