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