001package com.nimbusds.oauth2.sdk;
002
003
004import java.net.MalformedURLException;
005import java.net.URL;
006import java.util.Collections;
007import java.util.HashMap;
008import java.util.HashSet;
009import java.util.Map;
010import java.util.Set;
011
012import net.jcip.annotations.Immutable;
013
014import org.apache.commons.lang3.StringUtils;
015
016import com.nimbusds.oauth2.sdk.id.State;
017import com.nimbusds.oauth2.sdk.http.HTTPResponse;
018import com.nimbusds.oauth2.sdk.util.URLUtils;
019
020
021/**
022 * Authorisation error response. This class is immutable.
023 *
024 * <p>Standard authorisation errors:
025 *
026 * <ul>
027 *     <li>{@link OAuth2Error#INVALID_REQUEST}
028 *     <li>{@link OAuth2Error#UNAUTHORIZED_CLIENT}
029 *     <li>{@link OAuth2Error#ACCESS_DENIED}
030 *     <li>{@link OAuth2Error#UNSUPPORTED_RESPONSE_TYPE}
031 *     <li>{@link OAuth2Error#INVALID_SCOPE}
032 *     <li>{@link OAuth2Error#SERVER_ERROR}
033 *     <li>{@link OAuth2Error#TEMPORARILY_UNAVAILABLE}
034 * </ul>
035 *
036 * <p>Example HTTP response:
037 *
038 * <pre>
039 * HTTP/1.1 302 Found
040 * Location: https://client.example.com/cb?
041 * error=invalid_request
042 * &amp;error_description=the%20request%20is%20not%20valid%20or%20malformed
043 * &amp;state=af0ifjsldkj
044 * </pre>
045 *
046 * <p>Related specifications:
047 *
048 * <ul>
049 *     <li>OAuth 2.0 (RFC 6749), sections 4.1.2.1 and 4.2.2.1.
050 * </ul>
051 *
052 * @author Vladimir Dzhuvinov
053 */
054@Immutable
055public class AuthorizationErrorResponse
056        extends AuthorizationResponse
057        implements ErrorResponse {
058
059
060        /**
061         * The standard OAuth 2.0 errors for an Authorisation error response.
062         */
063        private static Set<ErrorObject> stdErrors = new HashSet<ErrorObject>();
064        
065        
066        static {
067                stdErrors.add(OAuth2Error.INVALID_REQUEST);
068                stdErrors.add(OAuth2Error.UNAUTHORIZED_CLIENT);
069                stdErrors.add(OAuth2Error.ACCESS_DENIED);
070                stdErrors.add(OAuth2Error.UNSUPPORTED_RESPONSE_TYPE);
071                stdErrors.add(OAuth2Error.INVALID_SCOPE);
072                stdErrors.add(OAuth2Error.SERVER_ERROR);
073                stdErrors.add(OAuth2Error.TEMPORARILY_UNAVAILABLE);
074        }
075        
076        
077        /**
078         * Gets the standard OAuth 2.0 errors for an Authorisation error 
079         * response.
080         *
081         * @return The standard errors, as a read-only set.
082         */
083        public static Set<ErrorObject> getStandardErrors() {
084        
085                return Collections.unmodifiableSet(stdErrors);
086        }
087        
088        
089        /**
090         * The error.
091         */
092        private final ErrorObject error;
093        
094        
095        /**
096         * The response type, used to determine redirect URL composition. If
097         * unknown {@code null}.
098         */
099        private final ResponseType rt;
100        
101        
102        /**
103         * Creates a new authorisation error response.
104         *
105         * @param redirectURI The base redirect URI. Must not be {@code null}.
106         * @param error       The error. Should match one of the 
107         *                    {@link #getStandardErrors standard errors} for an 
108         *                    authorisation error response. Must not be 
109         *                    {@code null}.
110         * @param rt          The response type, used to determine the redirect
111         *                    URI composition. If unknown {@code null}.
112         * @param state       The state, {@code null} if not requested.
113         */
114        public AuthorizationErrorResponse(final URL redirectURI,
115                                          final ErrorObject error,
116                                          final ResponseType rt,
117                                          final State state) {
118                                          
119                super(redirectURI, state);
120                
121                if (error == null)
122                        throw new IllegalArgumentException("The error must not be null");
123                        
124                this.error = error;
125                
126                this.rt = rt;
127        }
128        
129
130        @Override
131        public ErrorObject getErrorObject() {
132        
133                return error;
134        }
135        
136        
137        /**
138         * Gets the response type.
139         *
140         * @return The response type, {@code null} if not specified.
141         */
142        public ResponseType getResponseType() {
143        
144                return rt;
145        }
146
147
148        @Override
149        public Map<String,String> toParameters() {
150
151                Map<String,String> params = new HashMap<String,String>();
152
153                params.put("error", error.getCode());
154
155                if (error.getDescription() != null)
156                        params.put("error_description", error.getDescription());
157
158                if (error.getURI() != null)
159                        params.put("error_uri", error.getURI().toString());
160
161                if (getState() != null)
162                        params.put("state", getState().getValue());
163
164                return params;
165        }
166        
167        
168        @Override
169        public URL toURI()
170                throws SerializeException {
171                
172                StringBuilder sb = new StringBuilder(getRedirectionURI().toString());
173                
174                if (rt == null || rt.contains(ResponseType.Value.TOKEN))
175                        sb.append("#");
176                else
177                        sb.append("?");
178
179                sb.append(URLUtils.serializeParameters(toParameters()));
180                
181                try {
182                        return new URL(sb.toString());
183                        
184                } catch (MalformedURLException e) {
185                
186                        throw new SerializeException("Couldn't serialize redirect URL: " + e.getMessage(), e);
187                }
188        }
189
190
191        /**
192         * Parses an authorisation error response from the specified redirect
193         * URI and parameters.
194         *
195         * @param redirectURI The base redirect URI. Must not be {@code null}.
196         * @param params      The response parameters to parse. Must not be 
197         *                    {@code null}.
198         *
199         * @return The authorisation error response.
200         *
201         * @throws ParseException If the parameters couldn't be parsed to an
202         *                        authorisation error response.
203         */
204        public static AuthorizationErrorResponse parse(final URL redirectURI, 
205                                                       final Map<String,String> params)
206                throws ParseException {
207
208                // Parse the error
209                if (StringUtils.isBlank(params.get("error")))
210                        throw new ParseException("Missing error code");
211
212                // Parse error code
213                String errorCode = params.get("error");
214
215                String errorDescription = params.get("error_description");
216
217                String errorURIString = params.get("error_uri");
218
219                URL errorURI = null;
220
221                if (errorURIString != null) {
222                        
223                        try {
224                                errorURI = new URL(errorURIString);
225                                
226                        } catch (MalformedURLException e) {
227                
228                                throw new ParseException("Invalid error URI: " + errorURIString, e);
229                        }
230                }
231
232
233                ErrorObject error = new ErrorObject(errorCode, errorDescription, HTTPResponse.SC_FOUND, errorURI);
234                
235                
236                // State
237                State state = State.parse(params.get("state"));
238                
239                return new AuthorizationErrorResponse(redirectURI, error, null, state);
240        }
241        
242        
243        /**
244         * Parses an authorisation error response from the specified URI.
245         *
246         * <p>Example URI:
247         *
248         * <pre>
249         * https://client.example.com/cb?
250         * error=invalid_request
251         * &amp;error_description=the%20request%20is%20not%20valid%20or%20malformed
252         * &amp;state=af0ifjsldkj
253         * </pre>
254         *
255         * @param uri The URI to parse. Can be absolute or relative. Must not 
256         *            be {@code null}.
257         *
258         * @return The authorisation error response.
259         *
260         * @throws ParseException If the URI couldn't be parsed to an
261         *                        authorisation error response.
262         */
263        public static AuthorizationErrorResponse parse(final URL uri)
264                throws ParseException {
265                
266                Map<String,String> params = null;
267                
268                if (uri.getRef() != null)
269                        params = URLUtils.parseParameters(uri.getRef());
270
271                else if (uri.getQuery() != null)
272                        params = URLUtils.parseParameters(uri.getQuery());
273
274                else
275                        throw new ParseException("Missing URL fragment or query string");
276
277                
278                return parse(URLUtils.getBaseURL(uri), params);
279        }
280        
281        
282        /**
283         * Parses an authorisation error response from the specified HTTP
284         * response.
285         *
286         * <p>Example HTTP response:
287         *
288         * <pre>
289         * HTTP/1.1 302 Found
290         * Location: https://client.example.com/cb?
291         * error=invalid_request
292         * &amp;error_description=the%20request%20is%20not%20valid%20or%20malformed
293         * &amp;state=af0ifjsldkj
294         * </pre>
295         *
296         * @param httpResponse The HTTP response to parse. Must not be 
297         *                     {@code null}.
298         *
299         * @return The authorisation error response.
300         *
301         * @throws ParseException If the HTTP response couldn't be parsed to an 
302         *                        authorisation error response.
303         */
304        public static AuthorizationErrorResponse parse(final HTTPResponse httpResponse)
305                throws ParseException {
306                
307                if (httpResponse.getStatusCode() != HTTPResponse.SC_FOUND)
308                        throw new ParseException("Unexpected HTTP status code, must be 302 (Found): " + 
309                                                 httpResponse.getStatusCode());
310                
311                URL location = httpResponse.getLocation();
312                
313                if (location == null)
314                        throw new ParseException("Missing redirect URL / HTTP Location header");
315                
316                return parse(location);
317        }
318}