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.List;
025import java.util.Map;
026
027import com.nimbusds.common.contenttype.ContentType;
028import com.nimbusds.jwt.JWT;
029import com.nimbusds.jwt.JWTClaimsSet;
030import com.nimbusds.oauth2.sdk.http.HTTPRequest;
031import com.nimbusds.oauth2.sdk.http.HTTPResponse;
032import com.nimbusds.oauth2.sdk.id.State;
033import com.nimbusds.oauth2.sdk.jarm.JARMUtils;
034import com.nimbusds.oauth2.sdk.jarm.JARMValidator;
035import com.nimbusds.oauth2.sdk.util.MultivaluedMapUtils;
036import com.nimbusds.oauth2.sdk.util.StringUtils;
037import com.nimbusds.oauth2.sdk.util.URIUtils;
038import com.nimbusds.oauth2.sdk.util.URLUtils;
039
040
041/**
042 * The base abstract class for authorisation success and error responses.
043 *
044 * <p>Related specifications:
045 *
046 * <ul>
047 *     <li>OAuth 2.0 (RFC 6749), section 3.1.
048 *     <li>OAuth 2.0 Multiple Response Type Encoding Practices 1.0.
049 *     <li>OAuth 2.0 Form Post Response Mode 1.0.
050 *     <li>Financial-grade API: JWT Secured Authorization Response Mode for
051 *         OAuth 2.0 (JARM).
052 * </ul>
053 */
054public abstract class AuthorizationResponse implements Response {
055
056
057        /**
058         * The base redirection URI.
059         */
060        private final URI redirectURI;
061
062
063        /**
064         * The optional state parameter to be echoed back to the client.
065         */
066        private final State state;
067        
068        
069        /**
070         * For a JWT-secured response.
071         */
072        private final JWT jwtResponse;
073
074
075        /**
076         * The optional explicit response mode.
077         */
078        private final ResponseMode rm;
079
080
081        /**
082         * Creates a new authorisation response.
083         *
084         * @param redirectURI The base redirection URI. Must not be
085         *                    {@code null}.
086         * @param state       The state, {@code null} if not requested.
087         * @param rm          The response mode, {@code null} if not specified.
088         */
089        protected AuthorizationResponse(final URI redirectURI, final State state, final ResponseMode rm) {
090
091                if (redirectURI == null) {
092                        throw new IllegalArgumentException("The redirection URI must not be null");
093                }
094
095                this.redirectURI = redirectURI;
096                
097                jwtResponse = null;
098
099                this.state = state;
100
101                this.rm = rm;
102        }
103
104
105        /**
106         * Creates a new JSON Web Token (JWT) secured authorisation response.
107         *
108         * @param redirectURI The base redirection URI. Must not be
109         *                    {@code null}.
110         * @param jwtResponse The JWT response. Must not be {@code null}.
111         * @param rm          The response mode, {@code null} if not specified.
112         */
113        protected AuthorizationResponse(final URI redirectURI, final JWT jwtResponse, final ResponseMode rm) {
114
115                if (redirectURI == null) {
116                        throw new IllegalArgumentException("The redirection URI must not be null");
117                }
118
119                this.redirectURI = redirectURI;
120
121                if (jwtResponse == null) {
122                        throw new IllegalArgumentException("The JWT response must not be null");
123                }
124                
125                this.jwtResponse = jwtResponse;
126                
127                this.state = null;
128
129                this.rm = rm;
130        }
131
132
133        /**
134         * Returns the base redirection URI.
135         *
136         * @return The base redirection URI (without the appended error
137         *         response parameters).
138         */
139        public URI getRedirectionURI() {
140
141                return redirectURI;
142        }
143
144
145        /**
146         * Returns the optional state.
147         *
148         * @return The state, {@code null} if not requested or if the response
149         *         is JWT-secured in which case the state parameter may be
150         *         included as a JWT claim.
151         */
152        public State getState() {
153
154                return state;
155        }
156        
157        
158        /**
159         * Returns the JSON Web Token (JWT) secured response.
160         *
161         * @return The JWT-secured response, {@code null} for a regular
162         *         authorisation response.
163         */
164        public JWT getJWTResponse() {
165                
166                return jwtResponse;
167        }
168        
169        
170        /**
171         * Returns the optional explicit response mode.
172         *
173         * @return The response mode, {@code null} if not specified.
174         */
175        public ResponseMode getResponseMode() {
176
177                return rm;
178        }
179
180
181        /**
182         * Determines the implied response mode.
183         *
184         * @return The implied response mode.
185         */
186        public abstract ResponseMode impliedResponseMode();
187
188
189        /**
190         * Returns the parameters of this authorisation response.
191         *
192         * <p>Example parameters (authorisation success):
193         *
194         * <pre>
195         * access_token = 2YotnFZFEjr1zCsicMWpAA
196         * state = xyz
197         * token_type = example
198         * expires_in = 3600
199         * </pre>
200         *
201         * @return The parameters as a map.
202         */
203        public abstract Map<String,List<String>> toParameters();
204
205
206        /**
207         * Returns a URI representation (redirection URI + fragment / query
208         * string) of this authorisation response.
209         *
210         * <p>Example URI:
211         *
212         * <pre>
213         * http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA
214         * &amp;state=xyz
215         * &amp;token_type=example
216         * &amp;expires_in=3600
217         * </pre>
218         *
219         * @return A URI representation of this authorisation response.
220         */
221        public URI toURI() {
222
223                final ResponseMode rm = impliedResponseMode();
224
225                StringBuilder sb = new StringBuilder(getRedirectionURI().toString());
226
227                if (ResponseMode.QUERY.equals(rm) || ResponseMode.QUERY_JWT.equals(rm)) {
228                        if (StringUtils.isBlank(getRedirectionURI().getRawQuery())) {
229                                sb.append('?');
230                        } else {
231                                // The original redirect_uri may contain query params,
232                                // see http://tools.ietf.org/html/rfc6749#section-3.1.2
233                                sb.append('&');
234                        }
235                } else if (ResponseMode.FRAGMENT.equals(rm) || ResponseMode.FRAGMENT_JWT.equals(rm)) {
236                        sb.append('#');
237                } else {
238                        throw new SerializeException("The (implied) response mode must be query or fragment");
239                }
240
241                sb.append(URLUtils.serializeParameters(toParameters()));
242
243                try {
244                        return new URI(sb.toString());
245
246                } catch (URISyntaxException e) {
247
248                        throw new SerializeException("Couldn't serialize response: " + e.getMessage(), e);
249                }
250        }
251
252
253        /**
254         * Returns an HTTP response for this authorisation response. Applies to
255         * the {@code query} or {@code fragment} response mode using HTTP 302
256         * redirection.
257         *
258         * <p>Example HTTP response (authorisation success):
259         *
260         * <pre>
261         * HTTP/1.1 302 Found
262         * Location: http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA
263         * &amp;state=xyz
264         * &amp;token_type=example
265         * &amp;expires_in=3600
266         * </pre>
267         *
268         * @see #toHTTPRequest()
269         *
270         * @return An HTTP response for this authorisation response.
271         */
272        @Override
273        public HTTPResponse toHTTPResponse() {
274
275                if (ResponseMode.FORM_POST.equals(rm)) {
276                        throw new SerializeException("The response mode must not be form_post");
277                }
278
279                HTTPResponse response= new HTTPResponse(HTTPResponse.SC_FOUND);
280                response.setLocation(toURI());
281                return response;
282        }
283
284
285        /**
286         * Returns an HTTP request for this authorisation response. Applies to
287         * the {@code form_post} response mode.
288         *
289         * <p>Example HTTP request (authorisation success):
290         *
291         * <pre>
292         * GET /cb?code=SplxlOBeZQQYbYS6WxSbIA&amp;state=xyz HTTP/1.1
293         * Host: client.example.com
294         * </pre>
295         *
296         * @see #toHTTPResponse()
297         *
298         * @return An HTTP request for this authorisation response.
299         */
300        public HTTPRequest toHTTPRequest() {
301
302                if (! ResponseMode.FORM_POST.equals(rm)) {
303                        throw new SerializeException("The response mode must be form_post");
304                }
305
306                // Use HTTP POST
307                HTTPRequest request;
308
309                try {
310                        request = new HTTPRequest(HTTPRequest.Method.POST, redirectURI.toURL());
311
312                } catch (MalformedURLException e) {
313                        throw new SerializeException(e.getMessage(), e);
314                }
315
316                request.setEntityContentType(ContentType.APPLICATION_URLENCODED);
317                request.setQuery(URLUtils.serializeParameters(toParameters()));
318                return request;
319        }
320        
321        
322        /**
323         * Casts this response to an authorisation success response.
324         *
325         * @return The authorisation success response.
326         */
327        public AuthorizationSuccessResponse toSuccessResponse() {
328                
329                return (AuthorizationSuccessResponse) this;
330        }
331        
332        
333        /**
334         * Casts this response to an authorisation error response.
335         *
336         * @return The authorisation error response.
337         */
338        public AuthorizationErrorResponse toErrorResponse() {
339                
340                return (AuthorizationErrorResponse) this;
341        }
342
343
344        /**
345         * Parses an authorisation response.
346         *
347         * @param redirectURI The base redirection URI. Must not be
348         *                    {@code null}.
349         * @param params      The response parameters to parse. Must not be
350         *                    {@code null}.
351         *
352         * @return The authorisation success or error response.
353         *
354         * @throws ParseException If the parameters couldn't be parsed to an
355         *                        authorisation success or error response.
356         */
357        public static AuthorizationResponse parse(final URI redirectURI, final Map<String,List<String>> params)
358                throws ParseException {
359
360                return parse(redirectURI, params, null);
361        }
362
363
364        /**
365         * Parses an authorisation response which may be JSON Web Token (JWT)
366         * secured.
367         *
368         * @param redirectURI   The base redirection URI. Must not be
369         *                      {@code null}.
370         * @param params        The response parameters to parse. Must not be
371         *                      {@code null}.
372         * @param jarmValidator The validator of JSON Web Token (JWT) secured
373         *                      authorisation responses (JARM), {@code null} if
374         *                      a plain response is expected.
375         *
376         * @return The authorisation success or error response.
377         *
378         * @throws ParseException If the parameters couldn't be parsed to an
379         *                        authorisation success or error response, or
380         *                        if validation of the JWT secured response
381         *                        failed.
382         */
383        public static AuthorizationResponse parse(final URI redirectURI,
384                                                  final Map<String,List<String>> params,
385                                                  final JARMValidator jarmValidator)
386                throws ParseException {
387                
388                Map<String,List<String>> workParams = params;
389                
390                String jwtResponseString = MultivaluedMapUtils.getFirstValue(params, "response");
391                
392                if (jarmValidator != null) {
393                        if (StringUtils.isBlank(jwtResponseString)) {
394                                throw new ParseException("Missing JWT-secured (JARM) authorization response parameter");
395                        }
396                        try {
397                                JWTClaimsSet jwtClaimsSet = jarmValidator.validate(jwtResponseString);
398                                workParams = JARMUtils.toMultiValuedStringParameters(jwtClaimsSet);
399                        } catch (Exception e) {
400                                throw new ParseException("Invalid JWT-secured (JARM) authorization response: " + e.getMessage());
401                        }
402                }
403
404                if (StringUtils.isNotBlank(MultivaluedMapUtils.getFirstValue(workParams, "error"))) {
405                        return AuthorizationErrorResponse.parse(redirectURI, workParams);
406                } else if (StringUtils.isNotBlank(jwtResponseString)) {
407                        // JARM that wasn't validated, peek into JWT if signed only
408                        boolean likelyError = JARMUtils.impliesAuthorizationErrorResponse(jwtResponseString);
409                        if (likelyError) {
410                                return AuthorizationErrorResponse.parse(redirectURI, workParams);
411                        } else {
412                                return AuthorizationSuccessResponse.parse(redirectURI, workParams);
413                        }
414                        
415                } else {
416                        return AuthorizationSuccessResponse.parse(redirectURI, workParams);
417                }
418        }
419
420
421        /**
422         * Parses an authorisation response.
423         *
424         * <p>Use a relative URI if the host, port and path details are not
425         * known:
426         *
427         * <pre>
428         * URI relUrl = new URI("https:///?code=Qcb0Orv1...&amp;state=af0ifjsldkj");
429         * </pre>
430         *
431         * @param uri The URI to parse. Can be absolute or relative, with a
432         *            fragment or query string containing the authorisation
433         *            response parameters. Must not be {@code null}.
434         *
435         * @return The authorisation success or error response.
436         *
437         * @throws ParseException If no authorisation response parameters were
438         *                        found in the URL.
439         */
440        public static AuthorizationResponse parse(final URI uri)
441                throws ParseException {
442
443                return parse(URIUtils.getBaseURI(uri), parseResponseParameters(uri));
444        }
445
446
447        /**
448         * Parses and validates a JSON Web Token (JWT) secured authorisation
449         * response.
450         *
451         * <p>Use a relative URI if the host, port and path details are not
452         * known:
453         *
454         * <pre>
455         * URI relUrl = new URI("https:///?response=eyJhbGciOiJSUzI1NiIsI...");
456         * </pre>
457         *
458         * @param uri           The URI to parse. Can be absolute or relative,
459         *                      with a fragment or query string containing the
460         *                      authorisation response parameters. Must not be
461         *                      {@code null}.
462         * @param jarmValidator The validator of JSON Web Token (JWT) secured
463         *                      authorisation responses (JARM). Must not be
464         *                      {@code null}.
465         *
466         * @return The authorisation success or error response.
467         *
468         * @throws ParseException If no authorisation response parameters were
469         *                        found in the URL of if validation of the JWT
470         *                        response failed.
471         */
472        public static AuthorizationResponse parse(final URI uri, final JARMValidator jarmValidator)
473                throws ParseException {
474                
475                if (jarmValidator == null) {
476                        throw new IllegalArgumentException("The JARM validator must not be null");
477                }
478
479                return parse(URIUtils.getBaseURI(uri), parseResponseParameters(uri), jarmValidator);
480        }
481
482
483        /**
484         * Parses an authorisation response from the specified initial HTTP 302
485         * redirect response output at the authorisation endpoint.
486         *
487         * <p>Example HTTP response (authorisation success):
488         *
489         * <pre>
490         * HTTP/1.1 302 Found
491         * Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA&amp;state=xyz
492         * </pre>
493         *
494         * @see #parse(HTTPRequest)
495         *
496         * @param httpResponse The HTTP response to parse. Must not be
497         *                     {@code null}.
498         *
499         * @return The authorisation response.
500         *
501         * @throws ParseException If the HTTP response couldn't be parsed to an
502         *                        authorisation response.
503         */
504        public static AuthorizationResponse parse(final HTTPResponse httpResponse)
505                throws ParseException {
506
507                URI location = httpResponse.getLocation();
508
509                if (location == null) {
510                        throw new ParseException("Missing redirection URI / HTTP Location header");
511                }
512
513                return parse(location);
514        }
515
516
517        /**
518         * Parses and validates a JSON Web Token (JWT) secured authorisation
519         * response from the specified initial HTTP 302 redirect response
520         * output at the authorisation endpoint.
521         *
522         * <p>Example HTTP response (authorisation success):
523         *
524         * <pre>
525         * HTTP/1.1 302 Found
526         * Location: https://client.example.com/cb?response=eyJhbGciOiJSUzI1...
527         * </pre>
528         *
529         * @see #parse(HTTPRequest)
530         *
531         * @param httpResponse  The HTTP response to parse. Must not be
532         *                      {@code null}.
533         * @param jarmValidator The validator of JSON Web Token (JWT) secured
534         *                      authorisation responses (JARM). Must not be
535         *                      {@code null}.
536         *
537         * @return The authorisation response.
538         *
539         * @throws ParseException If the HTTP response couldn't be parsed to an
540         *                        authorisation response or if validation of
541         *                        the JWT response failed.
542         */
543        public static AuthorizationResponse parse(final HTTPResponse httpResponse,
544                                                  final JARMValidator jarmValidator)
545                throws ParseException {
546
547                URI location = httpResponse.getLocation();
548
549                if (location == null) {
550                        throw new ParseException("Missing redirection URI / HTTP Location header");
551                }
552
553                return parse(location, jarmValidator);
554        }
555
556
557        /**
558         * Parses an authorisation response from the specified HTTP request at
559         * the client redirection (callback) URI. Applies to the {@code query},
560         * {@code fragment} and {@code form_post} response modes.
561         *
562         * <p>Example HTTP request (authorisation success):
563         *
564         * <pre>
565         * GET /cb?code=SplxlOBeZQQYbYS6WxSbIA&amp;state=xyz HTTP/1.1
566         * Host: client.example.com
567         * </pre>
568         *
569         * @see #parse(HTTPResponse)
570         *
571         * @param httpRequest The HTTP request to parse. Must not be
572         *                    {@code null}.
573         *
574         * @return The authorisation response.
575         *
576         * @throws ParseException If the HTTP request couldn't be parsed to an
577         *                        authorisation response.
578         */
579        public static AuthorizationResponse parse(final HTTPRequest httpRequest)
580                throws ParseException {
581                
582                return parse(httpRequest.getURI(), parseResponseParameters(httpRequest));
583        }
584
585
586        /**
587         * Parses and validates a JSON Web Token (JWT) secured authorisation
588         * response from the specified HTTP request at the client redirection
589         * (callback) URI. Applies to the {@code query.jwt},
590         * {@code fragment.jwt} and {@code form_post.jwt} response modes.
591         *
592         * <p>Example HTTP request (authorisation success):
593         *
594         * <pre>
595         * GET /cb?response=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9... HTTP/1.1
596         * Host: client.example.com
597         * </pre>
598         *
599         * @see #parse(HTTPResponse)
600         *
601         * @param httpRequest   The HTTP request to parse. Must not be
602         *                      {@code null}.
603         * @param jarmValidator The validator of JSON Web Token (JWT) secured
604         *                      authorisation responses (JARM). Must not be
605         *                      {@code null}.
606         *
607         * @return The authorisation response.
608         *
609         * @throws ParseException If the HTTP request couldn't be parsed to an
610         *                        authorisation response or if validation of
611         *                        the JWT response failed.
612         */
613        public static AuthorizationResponse parse(final HTTPRequest httpRequest,
614                                                  final JARMValidator jarmValidator)
615                throws ParseException {
616                
617                if (jarmValidator == null) {
618                        throw new IllegalArgumentException("The JARM validator must not be null");
619                }
620
621                return parse(httpRequest.getURI(), parseResponseParameters(httpRequest), jarmValidator);
622        }
623        
624        
625        /**
626         * Parses the relevant authorisation response parameters. This method
627         * is intended for internal SDK usage only.
628         *
629         * @param uri The URI to parse its query or fragment parameters. Must
630         *            not be {@code null}.
631         *
632         * @return The authorisation response parameters.
633         *
634         * @throws ParseException If parsing failed.
635         */
636        public static Map<String,List<String>> parseResponseParameters(final URI uri)
637                throws ParseException {
638                
639                if (uri.getRawFragment() != null) {
640                        return URLUtils.parseParameters(uri.getRawFragment());
641                } else if (uri.getRawQuery() != null) {
642                        return URLUtils.parseParameters(uri.getRawQuery());
643                } else {
644                        throw new ParseException("Missing URI fragment or query string");
645                }
646        }
647        
648        
649        /**
650         * Parses the relevant authorisation response parameters. This method
651         * is intended for internal SDK usage only.
652         *
653         * @param httpRequest The HTTP request. Must not be {@code null}.
654         *
655         * @return The authorisation response parameters.
656         *
657         * @throws ParseException If parsing failed.
658         */
659        public static Map<String,List<String>> parseResponseParameters(final HTTPRequest httpRequest)
660                throws ParseException {
661                
662                if (httpRequest.getQuery() != null) {
663                        // For query string and form_post response mode
664                        return URLUtils.parseParameters(httpRequest.getQuery());
665                } else if (httpRequest.getFragment() != null) {
666                        // For fragment response mode (never available in actual HTTP request from browser)
667                        return URLUtils.parseParameters(httpRequest.getFragment());
668                } else {
669                        throw new ParseException("Missing URI fragment, query string or post body");
670                }
671        }
672}