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