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