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