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.util.*;
022
023import com.nimbusds.oauth2.sdk.util.MultivaluedMapUtils;
024import net.jcip.annotations.Immutable;
025
026import com.nimbusds.jose.*;
027import com.nimbusds.jwt.*;
028
029
030/**
031 * JWT bearer grant. Used in access token requests with a JSON Web Token (JWT),
032 * such an OpenID Connect ID token.
033 *
034 * <p>The JWT assertion can be:
035 *
036 * <ul>
037 *     <li>Signed or MAC protected with JWS
038 *     <li>Encrypted with JWE
039 *     <li>Nested - signed / MAC protected with JWS and then encrypted with JWE
040 * </ul>
041 *
042 * <p>Related specifications:
043 *
044 * <ul>
045 *     <li>Assertion Framework for OAuth 2.0 Client Authentication and
046 *         Authorization Grants (RFC 7521), section 4.1.
047 *     <li>JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and
048 *         Authorization Grants (RFC 7523), section-2.1.
049 * </ul>
050 */
051@Immutable
052public class JWTBearerGrant extends AssertionGrant {
053
054
055        /**
056         * The grant type.
057         */
058        public static final GrantType GRANT_TYPE = GrantType.JWT_BEARER;
059
060        
061        private static final String UNSUPPORTED_GRANT_TYPE_MESSAGE = "The \"grant_type\" must be " + GRANT_TYPE;
062        
063        
064        private static final String PLAIN_ASSERTION_REJECTED_MESSAGE = "The JWT assertion must not be unsecured (plain)";
065        
066        
067        private static final String JWT_PARSE_MESSAGE = "The \"assertion\" is not a JWT";
068
069
070        /**
071         * Cached {@code unsupported_grant_type} exception.
072         */
073        private static final ParseException UNSUPPORTED_GRANT_TYPE_EXCEPTION
074                = new ParseException(UNSUPPORTED_GRANT_TYPE_MESSAGE,
075                        OAuth2Error.UNSUPPORTED_GRANT_TYPE.appendDescription(": " + UNSUPPORTED_GRANT_TYPE_MESSAGE));
076
077
078        /**
079         * Cached plain JOSE / JWT rejected exception.
080         */
081        private static final ParseException PLAIN_ASSERTION_REJECTED_EXCEPTION
082                = new ParseException(PLAIN_ASSERTION_REJECTED_MESSAGE,
083                        OAuth2Error.INVALID_REQUEST.appendDescription(": " + PLAIN_ASSERTION_REJECTED_MESSAGE));
084
085
086        /**
087         * Cached JWT assertion parse exception.
088         */
089        private static final ParseException JWT_PARSE_EXCEPTION
090                = new ParseException(JWT_PARSE_MESSAGE,
091                        OAuth2Error.INVALID_REQUEST.appendDescription(": " + JWT_PARSE_MESSAGE));
092
093        /**
094         * The assertion - signed JWT, encrypted JWT or nested signed+encrypted
095         * JWT.
096         */
097        private final JOSEObject assertion;
098
099
100        /**
101         * Creates a new signed JSON Web Token (JWT) bearer assertion grant.
102         *
103         * @param assertion The signed JSON Web Token (JWT) assertion. Must not
104         *                  be in a unsigned state or {@code null}. The JWT
105         *                  claims are not validated for compliance with the
106         *                  standard.
107         */
108        public JWTBearerGrant(final SignedJWT assertion) {
109
110                super(GRANT_TYPE);
111
112                if (assertion.getState().equals(JWSObject.State.UNSIGNED))
113                        throw new IllegalArgumentException("The JWT assertion must not be in a unsigned state");
114
115                this.assertion = assertion;
116        }
117
118
119        /**
120         * Creates a new nested signed and encrypted JSON Web Token (JWT)
121         * bearer assertion grant.
122         *
123         * @param assertion The nested signed and encrypted JSON Web Token
124         *                  (JWT) assertion. Must not be in a unencrypted state
125         *                  or {@code null}. The JWT claims are not validated
126         *                  for compliance with the standard.
127         */
128        public JWTBearerGrant(final JWEObject assertion) {
129
130                super(GRANT_TYPE);
131
132                if (assertion.getState().equals(JWEObject.State.UNENCRYPTED))
133                        throw new IllegalArgumentException("The JWT assertion must not be in a unencrypted state");
134
135                this.assertion = assertion;
136        }
137
138
139        /**
140         * Creates a new signed and encrypted JSON Web Token (JWT) bearer
141         * assertion grant.
142         *
143         * @param assertion The signed and encrypted JSON Web Token (JWT)
144         *                  assertion. Must not be in a unencrypted state or
145         *                  {@code null}. The JWT claims are not validated for
146         *                  compliance with the standard.
147         */
148        public JWTBearerGrant(final EncryptedJWT assertion) {
149
150                this((JWEObject) assertion);
151        }
152
153
154        /**
155         * Gets the JSON Web Token (JWT) bearer assertion.
156         *
157         * @return The assertion as a signed or encrypted JWT, {@code null} if
158         *         the assertion is a signed and encrypted JWT.
159         */
160        public JWT getJWTAssertion() {
161
162                return assertion instanceof JWT ?  (JWT)assertion : null;
163        }
164
165
166        /**
167         * Gets the JSON Web Token (JWT) bearer assertion.
168         *
169         * @return The assertion as a generic JOSE object (signed JWT,
170         *         encrypted JWT, or signed and encrypted JWT).
171         */
172        public JOSEObject getJOSEAssertion() {
173
174                return assertion;
175        }
176
177
178        @Override
179        public String getAssertion() {
180
181                return assertion.serialize();
182        }
183
184
185        @Override
186        public Map<String,List<String>> toParameters() {
187
188                Map<String,List<String>> params = new LinkedHashMap<>();
189                params.put("grant_type", Collections.singletonList(GRANT_TYPE.getValue()));
190                params.put("assertion", Collections.singletonList(assertion.serialize()));
191                return params;
192        }
193
194
195        /**
196         * Parses a JWT bearer grant from the specified request body
197         * parameters. The JWT claims are not validated for compliance with the
198         * standard.
199         *
200         * <p>Example:
201         *
202         * <pre>
203         * grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer
204         * &amp;assertion=eyJhbGciOiJFUzI1NiJ9.eyJpc3Mi[...omitted for brevity...].
205         * J9l-ZhwP[...omitted for brevity...]
206         * </pre>
207         *
208         * @param params The parameters.
209         *
210         * @return The JWT bearer grant.
211         *
212         * @throws ParseException If parsing failed.
213         */
214        public static JWTBearerGrant parse(final Map<String,List<String>> params)
215                throws ParseException {
216
217                // Parse grant type
218                String grantTypeString = MultivaluedMapUtils.getFirstValue(params, "grant_type");
219
220                if (grantTypeString == null)
221                        throw MISSING_GRANT_TYPE_PARAM_EXCEPTION;
222
223                if (! GrantType.parse(grantTypeString).equals(GRANT_TYPE))
224                        throw UNSUPPORTED_GRANT_TYPE_EXCEPTION;
225
226                // Parse JWT assertion
227                String assertionString = MultivaluedMapUtils.getFirstValue(params, "assertion");
228
229                if (assertionString == null || assertionString.trim().isEmpty())
230                        throw MISSING_ASSERTION_PARAM_EXCEPTION;
231
232                try {
233                        final JOSEObject assertion = JOSEObject.parse(assertionString);
234
235                        if (assertion instanceof PlainObject) {
236
237                                throw PLAIN_ASSERTION_REJECTED_EXCEPTION;
238
239                        } else if (assertion instanceof JWSObject) {
240
241                                return new JWTBearerGrant(new SignedJWT(
242                                                assertion.getParsedParts()[0],
243                                                assertion.getParsedParts()[1],
244                                                assertion.getParsedParts()[2]));
245
246                        } else {
247                                // JWE
248
249                                if ("JWT".equalsIgnoreCase(assertion.getHeader().getContentType())) {
250                                        // Assume nested: signed JWT inside JWE
251                                        // http://tools.ietf.org/html/rfc7519#section-5.2
252                                        return new JWTBearerGrant((JWEObject)assertion);
253                                } else {
254                                        // Assume encrypted JWT
255                                        return new JWTBearerGrant(new EncryptedJWT(
256                                                        assertion.getParsedParts()[0],
257                                                        assertion.getParsedParts()[1],
258                                                        assertion.getParsedParts()[2],
259                                                        assertion.getParsedParts()[3],
260                                                        assertion.getParsedParts()[4]));
261                                }
262                        }
263
264                } catch (java.text.ParseException e) {
265                        throw JWT_PARSE_EXCEPTION;
266                }
267        }
268}