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         * Parses an authorisation response.
267         *
268         * @param redirectURI The base redirection URI. Must not be
269         *                    {@code null}.
270         * @param params      The response parameters to parse. Must not be
271         *                    {@code null}.
272         *
273         * @return The authorisation success or error response.
274         *
275         * @throws ParseException If the parameters couldn't be parsed to an
276         *                        authorisation success or error response.
277         */
278        public static AuthorizationResponse parse(final URI redirectURI, final Map<String,String> params)
279                throws ParseException {
280
281                if (StringUtils.isNotBlank(params.get("error"))) {
282                        return AuthorizationErrorResponse.parse(redirectURI, params);
283                } else {
284                        return AuthorizationSuccessResponse.parse(redirectURI, params);
285                }
286        }
287
288
289        /**
290         * Parses an authorisation response.
291         *
292         * <p>Use a relative URI if the host, port and path details are not
293         * known:
294         *
295         * <pre>
296         * URI relUrl = new URI("https:///?code=Qcb0Orv1...&state=af0ifjsldkj");
297         * </pre>
298         *
299         * @param uri The URI to parse. Can be absolute or relative, with a
300         *            fragment or query string containing the authorisation
301         *            response parameters. Must not be {@code null}.
302         *
303         * @return The authorisation success or error response.
304         *
305         * @throws ParseException If no authorisation response parameters were
306         *                        found in the URL.
307         */
308        public static AuthorizationResponse parse(final URI uri)
309                throws ParseException {
310
311                Map<String,String> params;
312
313                if (uri.getRawFragment() != null) {
314                        params = URLUtils.parseParameters(uri.getRawFragment());
315                } else if (uri.getRawQuery() != null) {
316                        params = URLUtils.parseParameters(uri.getRawQuery());
317                } else {
318                        throw new ParseException("Missing URI fragment or query string");
319                }
320
321                return parse(URIUtils.getBaseURI(uri), params);
322        }
323
324
325        /**
326         * Parses an authorisation response from the specified initial HTTP 302
327         * redirect response output at the authorisation endpoint.
328         *
329         * <p>Example HTTP response (authorisation success):
330         *
331         * <pre>
332         * HTTP/1.1 302 Found
333         * Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA&amp;state=xyz
334         * </pre>
335         *
336         * @see #parse(HTTPRequest)
337         *
338         * @param httpResponse The HTTP response to parse. Must not be
339         *                     {@code null}.
340         *
341         * @throws ParseException If the HTTP response couldn't be parsed to an
342         *                        authorisation response.
343         */
344        public static AuthorizationResponse parse(final HTTPResponse httpResponse)
345                throws ParseException {
346
347                URI location = httpResponse.getLocation();
348
349                if (location == null) {
350                        throw new ParseException("Missing redirection URI / HTTP Location header");
351                }
352
353                return parse(location);
354        }
355
356
357        /**
358         * Parses an authorisation response from the specified HTTP request at
359         * the client redirection (callback) URI. Applies to the {@code query},
360         * {@code fragment} and {@code form_post} response modes.
361         *
362         * <p>Example HTTP request (authorisation success):
363         *
364         * <pre>
365         * GET /cb?code=SplxlOBeZQQYbYS6WxSbIA&amp;state=xyz HTTP/1.1
366         * Host: client.example.com
367         * </pre>
368         *
369         * @see #parse(HTTPResponse)
370         *
371         * @param httpRequest The HTTP request to parse. Must not be
372         *                    {@code null}.
373         *
374         * @throws ParseException If the HTTP request couldn't be parsed to an
375         *                        authorisation response.
376         */
377        public static AuthorizationResponse parse(final HTTPRequest httpRequest)
378                throws ParseException {
379
380                final URI baseURI;
381
382                try {
383                        baseURI = httpRequest.getURL().toURI();
384
385                } catch (URISyntaxException e) {
386                        throw new ParseException(e.getMessage(), e);
387                }
388
389                if (httpRequest.getQuery() != null) {
390                        // For query string and form_post response mode
391                        return parse(baseURI, URLUtils.parseParameters(httpRequest.getQuery()));
392                } else if (httpRequest.getFragment() != null) {
393                        // For fragment response mode (never available in actual HTTP request from browser)
394                        return parse(baseURI, URLUtils.parseParameters(httpRequest.getFragment()));
395                } else {
396                        throw new ParseException("Missing URI fragment, query string or post body");
397                }
398        }
399}