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