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