001package com.nimbusds.oauth2.sdk;
002
003
004import java.net.MalformedURLException;
005import java.net.URI;
006import java.net.URISyntaxException;
007import java.util.Map;
008
009import org.apache.commons.lang3.StringUtils;
010
011import com.nimbusds.oauth2.sdk.id.State;
012import com.nimbusds.oauth2.sdk.http.CommonContentTypes;
013import com.nimbusds.oauth2.sdk.http.HTTPRequest;
014import com.nimbusds.oauth2.sdk.http.HTTPResponse;
015import com.nimbusds.oauth2.sdk.util.URIUtils;
016import com.nimbusds.oauth2.sdk.util.URLUtils;
017
018
019/**
020 * The base abstract class for authorisation success and error responses.
021 *
022 * <p>Related specifications:
023 *
024 * <ul>
025 *     <li>OAuth 2.0 (RFC 6749), section 3.1.
026 *     <li>OAuth 2.0 Multiple Response Type Encoding Practices 1.0.
027 *     <li>OAuth 2.0 Form Post Response Mode 1.0.
028 * </ul>
029 */
030public abstract class AuthorizationResponse implements Response {
031
032
033        /**
034         * The base redirection URI.
035         */
036        private final URI redirectURI;
037
038
039        /**
040         * The optional state parameter to be echoed back to the client.
041         */
042        private final State state;
043
044
045        /**
046         * The optional explicit response mode.
047         */
048        private final ResponseMode rm;
049
050
051        /**
052         * Creates a new authorisation response.
053         *
054         * @param redirectURI The base redirection URI. Must not be
055         *                    {@code null}.
056         * @param state       The state, {@code null} if not requested.
057         * @param rm          The response mode, {@code null} if not specified.
058         */
059        protected AuthorizationResponse(final URI redirectURI, final State state, final ResponseMode rm) {
060
061                if (redirectURI == null) {
062                        throw new IllegalArgumentException("The redirection URI must not be null");
063                }
064
065                this.redirectURI = redirectURI;
066
067                this.state = state;
068
069                this.rm = rm;
070        }
071
072
073        /**
074         * Returns the base redirection URI.
075         *
076         * @return The base redirection URI (without the appended error
077         *         response parameters).
078         */
079        public URI getRedirectionURI() {
080
081                return redirectURI;
082        }
083
084
085        /**
086         * Returns the optional state.
087         *
088         * @return The state, {@code null} if not requested.
089         */
090        public State getState() {
091
092                return state;
093        }
094
095
096        /**
097         * Returns the optional explicit response mode.
098         *
099         * @return The response mode, {@code null} if not specified.
100         */
101        public ResponseMode getResponseMode() {
102
103                return rm;
104        }
105
106
107        /**
108         * Determines the implied response mode.
109         *
110         * @return The implied response mode.
111         */
112        public abstract ResponseMode impliedResponseMode();
113
114
115        /**
116         * Returns the parameters of this authorisation response.
117         *
118         * <p>Example parameters (authorisation success):
119         *
120         * <pre>
121         * access_token = 2YotnFZFEjr1zCsicMWpAA
122         * state = xyz
123         * token_type = example
124         * expires_in = 3600
125         * </pre>
126         *
127         * @return The parameters as a map.
128         *
129         * @throws SerializeException If this response couldn't be serialised
130         *                            to a parameters map.
131         */
132        public abstract Map<String,String> toParameters()
133                throws SerializeException;
134
135
136        /**
137         * Returns a URI representation (redirection URI + fragment / query
138         * string) of this authorisation response.
139         *
140         * <p>Example URI:
141         *
142         * <pre>
143         * http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA
144         * &amp;state=xyz
145         * &amp;token_type=example
146         * &amp;expires_in=3600
147         * </pre>
148         *
149         * @return A URI representation of this authorisation response.
150         *
151         * @throws SerializeException If this response couldn't be serialised
152         *                            to a URI.
153         */
154        public URI toURI()
155                throws SerializeException {
156
157                final ResponseMode rm = impliedResponseMode();
158
159                StringBuilder sb = new StringBuilder(getRedirectionURI().toString());
160
161                if (rm.equals(ResponseMode.QUERY)) {
162                        if (StringUtils.isBlank(getRedirectionURI().getQuery())) {
163                                sb.append('?');
164                        } else {
165                                // The original redirect_uri may contain query params,
166                                // see http://tools.ietf.org/html/rfc6749#section-3.1.2
167                                sb.append('&');
168                        }
169                } else if (rm.equals(ResponseMode.FRAGMENT)) {
170                        sb.append('#');
171                } else {
172                        throw new SerializeException("The (implied) response mode must be query or fragment");
173                }
174
175                sb.append(URLUtils.serializeParameters(toParameters()));
176
177                try {
178                        return new URI(sb.toString());
179
180                } catch (URISyntaxException e) {
181
182                        throw new SerializeException("Couldn't serialize response: " + e.getMessage(), e);
183                }
184        }
185
186
187        /**
188         * Returns an HTTP response for this authorisation response. Applies to
189         * the {@code query} or {@code fragment} response mode using HTTP 302
190         * redirection.
191         *
192         * <p>Example HTTP response (authorisation success):
193         *
194         * <pre>
195         * HTTP/1.1 302 Found
196         * Location: http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA
197         * &amp;state=xyz
198         * &amp;token_type=example
199         * &amp;expires_in=3600
200         * </pre>
201         *
202         * @see #toHTTPRequest()
203         *
204         * @return An HTTP response for this authorisation response.
205         *
206         * @throws SerializeException If the response couldn't be serialised to
207         *                            an HTTP response.
208         */
209        @Override
210        public HTTPResponse toHTTPResponse()
211                throws SerializeException {
212
213                if (ResponseMode.FORM_POST.equals(rm)) {
214                        throw new SerializeException("The response mode must not be form_post");
215                }
216
217                HTTPResponse response= new HTTPResponse(HTTPResponse.SC_FOUND);
218                response.setLocation(toURI());
219                return response;
220        }
221
222
223        /**
224         * Returns an HTTP request for this authorisation response. Applies to
225         * the {@code form_post} response mode.
226         *
227         * <p>Example HTTP request (authorisation success):
228         *
229         * <pre>
230         * GET /cb?code=SplxlOBeZQQYbYS6WxSbIA&amp;state=xyz HTTP/1.1
231         * Host: client.example.com
232         * </pre>
233         *
234         * @see #toHTTPResponse()
235         *
236         * @return An HTTP request for this authorisation response.
237         *
238         * @throws SerializeException If the response couldn't be serialised to
239         *                            an HTTP request.
240         */
241        public HTTPRequest toHTTPRequest()
242                throws SerializeException {
243
244                if (! ResponseMode.FORM_POST.equals(rm)) {
245                        throw new SerializeException("The response mode must be form_post");
246                }
247
248                // Use HTTP POST
249                HTTPRequest request;
250
251                try {
252                        request = new HTTPRequest(HTTPRequest.Method.POST, redirectURI.toURL());
253
254                } catch (MalformedURLException e) {
255                        throw new SerializeException(e.getMessage(), e);
256                }
257
258                request.setContentType(CommonContentTypes.APPLICATION_URLENCODED);
259                request.setQuery(URLUtils.serializeParameters(toParameters()));
260                return request;
261        }
262
263
264        /**
265         * Parses an authorisation response.
266         *
267         * @param redirectURI The base redirection URI. Must not be
268         *                    {@code null}.
269         * @param params      The response parameters to parse. Must not be
270         *                    {@code null}.
271         *
272         * @return The authorisation success or error response.
273         *
274         * @throws ParseException If the parameters couldn't be parsed to an
275         *                        authorisation success or error response.
276         */
277        public static AuthorizationResponse parse(final URI redirectURI, final Map<String,String> params)
278                throws ParseException {
279
280                if (StringUtils.isNotBlank(params.get("error"))) {
281                        return AuthorizationErrorResponse.parse(redirectURI, params);
282                } else {
283                        return AuthorizationSuccessResponse.parse(redirectURI, params);
284                }
285        }
286
287
288        /**
289         * Parses an authorisation response.
290         *
291         * <p>Use a relative URI if the host, port and path details are not
292         * known:
293         *
294         * <pre>
295         * URI relUrl = new URI("http://?code=Qcb0Orv1...&state=af0ifjsldkj");
296         * </pre>
297         *
298         * @param uri The URI to parse. May be absolute or relative, with a
299         *            fragment or query string containing the authorisation
300         *            response parameters. Must not be {@code null}.
301         *
302         * @return The authorisation success or error response.
303         *
304         * @throws ParseException If no authorisation response parameters were
305         *                        found in the URL.
306         */
307        public static AuthorizationResponse parse(final URI uri)
308                throws ParseException {
309
310                Map<String,String> params;
311
312                if (uri.getRawFragment() != null) {
313                        params = URLUtils.parseParameters(uri.getRawFragment());
314                } else if (uri.getQuery() != null) {
315                        params = URLUtils.parseParameters(uri.getQuery());
316                } else {
317                        throw new ParseException("Missing URI fragment or query string");
318                }
319
320                return parse(URIUtils.getBaseURI(uri), params);
321        }
322
323
324        /**
325         * Parses an authorisation response from the specified initial HTTP 302
326         * redirect response output at the authorisation endpoint.
327         *
328         * <p>Example HTTP response (authorisation success):
329         *
330         * <pre>
331         * HTTP/1.1 302 Found
332         * Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA&amp;state=xyz
333         * </pre>
334         *
335         * @see #parse(HTTPRequest)
336         *
337         * @param httpResponse The HTTP response to parse. Must not be
338         *                     {@code null}.
339         *
340         * @throws ParseException If the HTTP response couldn't be parsed to an
341         *                        authorisation response.
342         */
343        public static AuthorizationResponse parse(final HTTPResponse httpResponse)
344                throws ParseException {
345
346                URI location = httpResponse.getLocation();
347
348                if (location == null) {
349                        throw new ParseException("Missing redirection URI / HTTP Location header");
350                }
351
352                return parse(location);
353        }
354
355
356        /**
357         * Parses an authorisation response from the specified HTTP request at
358         * the client redirection (callback) URI. Applies to the {@code query},
359         * {@code fragment} and {@code form_post} response modes.
360         *
361         * <p>Example HTTP request (authorisation success):
362         *
363         * <pre>
364         * GET /cb?code=SplxlOBeZQQYbYS6WxSbIA&amp;state=xyz 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         * @throws ParseException If the HTTP request couldn't be parsed to an
374         *                        authorisation response.
375         */
376        public static AuthorizationResponse parse(final HTTPRequest httpRequest)
377                throws ParseException {
378
379                final URI baseURI;
380
381                try {
382                        baseURI = httpRequest.getURL().toURI();
383
384                } catch (URISyntaxException e) {
385                        throw new ParseException(e.getMessage(), e);
386                }
387
388                if (httpRequest.getQuery() != null) {
389                        // For query string and form_post response mode
390                        return parse(baseURI, URLUtils.parseParameters(httpRequest.getQuery()));
391                } else if (httpRequest.getFragment() != null) {
392                        // For fragment response mode (never available in actual HTTP request from browser)
393                        return parse(baseURI, URLUtils.parseParameters(httpRequest.getFragment()));
394                } else {
395                        throw new ParseException("Missing URI fragment, query string or post body");
396                }
397        }
398}