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