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