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                        sb.append('?');
163                } else if (rm.equals(ResponseMode.FRAGMENT)) {
164                        sb.append('#');
165                } else {
166                        throw new SerializeException("The (implied) response mode must be query or fragment");
167                }
168
169                sb.append(URLUtils.serializeParameters(toParameters()));
170
171                try {
172                        return new URI(sb.toString());
173
174                } catch (URISyntaxException e) {
175
176                        throw new SerializeException("Couldn't serialize response: " + e.getMessage(), e);
177                }
178        }
179
180
181        /**
182         * Returns an HTTP response for this authorisation response. Applies to
183         * the {@code query} or {@code fragment} response mode using HTTP 302
184         * redirection.
185         *
186         * <p>Example HTTP response (authorisation success):
187         *
188         * <pre>
189         * HTTP/1.1 302 Found
190         * Location: http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA
191         * &amp;state=xyz
192         * &amp;token_type=example
193         * &amp;expires_in=3600
194         * </pre>
195         *
196         * @see #toHTTPRequest()
197         *
198         * @return An HTTP response for this authorisation response.
199         *
200         * @throws SerializeException If the response couldn't be serialised to
201         *                            an HTTP response.
202         */
203        @Override
204        public HTTPResponse toHTTPResponse()
205                throws SerializeException {
206
207                if (ResponseMode.FORM_POST.equals(rm)) {
208                        throw new SerializeException("The response mode must not be form_post");
209                }
210
211                HTTPResponse response= new HTTPResponse(HTTPResponse.SC_FOUND);
212                response.setLocation(toURI());
213                return response;
214        }
215
216
217        /**
218         * Returns an HTTP request for this authorisation response. Applies to
219         * the {@code form_post} response mode.
220         *
221         * <p>Example HTTP request (authorisation success):
222         *
223         * <pre>
224         * GET /cb?code=SplxlOBeZQQYbYS6WxSbIA&amp;state=xyz HTTP/1.1
225         * Host: client.example.com
226         * </pre>
227         *
228         * @see #toHTTPResponse()
229         *
230         * @return An HTTP request for this authorisation response.
231         *
232         * @throws SerializeException If the response couldn't be serialised to
233         *                            an HTTP request.
234         */
235        public HTTPRequest toHTTPRequest()
236                throws SerializeException {
237
238                if (! ResponseMode.FORM_POST.equals(rm)) {
239                        throw new SerializeException("The response mode must be form_post");
240                }
241
242                // Use HTTP POST
243                HTTPRequest request;
244
245                try {
246                        request = new HTTPRequest(HTTPRequest.Method.POST, redirectURI.toURL());
247
248                } catch (MalformedURLException e) {
249                        throw new SerializeException(e.getMessage(), e);
250                }
251
252                request.setContentType(CommonContentTypes.APPLICATION_URLENCODED);
253                request.setQuery(URLUtils.serializeParameters(toParameters()));
254                return request;
255        }
256
257
258        /**
259         * Parses an authorisation response.
260         *
261         * @param redirectURI The base redirection URI. Must not be
262         *                    {@code null}.
263         * @param params      The response parameters to parse. Must not be
264         *                    {@code null}.
265         *
266         * @return The authorisation success or error response.
267         *
268         * @throws ParseException If the parameters couldn't be parsed to an
269         *                        authorisation success or error response.
270         */
271        public static AuthorizationResponse parse(final URI redirectURI, final Map<String,String> params)
272                throws ParseException {
273
274                if (StringUtils.isNotBlank(params.get("error"))) {
275                        return AuthorizationErrorResponse.parse(redirectURI, params);
276                } else {
277                        return AuthorizationSuccessResponse.parse(redirectURI, params);
278                }
279        }
280
281
282        /**
283         * Parses an authorisation response.
284         *
285         * <p>Use a relative URI if the host, port and path details are not
286         * known:
287         *
288         * <pre>
289         * URI relUrl = new URI("http://?code=Qcb0Orv1...&state=af0ifjsldkj");
290         * </pre>
291         *
292         * @param uri The URI to parse. May be absolute or relative, with a
293         *            fragment or query string containing the authorisation
294         *            response parameters. Must not be {@code null}.
295         *
296         * @return The authorisation success or error response.
297         *
298         * @throws ParseException If no authorisation response parameters were
299         *                        found in the URL.
300         */
301        public static AuthorizationResponse parse(final URI uri)
302                throws ParseException {
303
304                Map<String,String> params;
305
306                if (uri.getRawFragment() != null) {
307                        params = URLUtils.parseParameters(uri.getRawFragment());
308                } else if (uri.getQuery() != null) {
309                        params = URLUtils.parseParameters(uri.getQuery());
310                } else {
311                        throw new ParseException("Missing URI fragment or query string");
312                }
313
314                return parse(URIUtils.getBaseURI(uri), params);
315        }
316
317
318        /**
319         * Parses an authorisation response from the specified initial HTTP 302
320         * redirect response output at the authorisation endpoint.
321         *
322         * <p>Example HTTP response (authorisation success):
323         *
324         * <pre>
325         * HTTP/1.1 302 Found
326         * Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA&amp;state=xyz
327         * </pre>
328         *
329         * @see #parse(HTTPRequest)
330         *
331         * @param httpResponse The HTTP response to parse. Must not be
332         *                     {@code null}.
333         *
334         * @throws ParseException If the HTTP response couldn't be parsed to an
335         *                        authorisation response.
336         */
337        public static AuthorizationResponse parse(final HTTPResponse httpResponse)
338                throws ParseException {
339
340                URI location = httpResponse.getLocation();
341
342                if (location == null) {
343                        throw new ParseException("Missing redirection URI / HTTP Location header");
344                }
345
346                return parse(location);
347        }
348
349
350        /**
351         * Parses an authorisation response from the specified HTTP request at
352         * the client redirection (callback) URI. Applies to the {@code query},
353         * {@code fragment} and {@code form_post} response modes.
354         *
355         * <p>Example HTTP request (authorisation success):
356         *
357         * <pre>
358         * GET /cb?code=SplxlOBeZQQYbYS6WxSbIA&amp;state=xyz HTTP/1.1
359         * Host: client.example.com
360         * </pre>
361         *
362         * @see #parse(HTTPResponse)
363         *
364         * @param httpRequest The HTTP request to parse. Must not be
365         *                    {@code null}.
366         *
367         * @throws ParseException If the HTTP request couldn't be parsed to an
368         *                        authorisation response.
369         */
370        public static AuthorizationResponse parse(final HTTPRequest httpRequest)
371                throws ParseException {
372
373                final URI baseURI;
374
375                try {
376                        baseURI = httpRequest.getURL().toURI();
377
378                } catch (URISyntaxException e) {
379                        throw new ParseException(e.getMessage(), e);
380                }
381
382                if (httpRequest.getQuery() != null) {
383                        // For query string and form_post response mode
384                        return parse(baseURI, URLUtils.parseParameters(httpRequest.getQuery()));
385                } else if (httpRequest.getFragment() != null) {
386                        // For fragment response mode (never available in actual HTTP request from browser)
387                        return parse(baseURI, URLUtils.parseParameters(httpRequest.getFragment()));
388                } else {
389                        throw new ParseException("Missing URI fragment, query string or post body");
390                }
391        }
392}