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.auth;
019
020
021import java.util.Collections;
022import java.util.HashMap;
023import java.util.List;
024import java.util.Map;
025import javax.mail.internet.ContentType;
026
027import com.nimbusds.jose.JWSAlgorithm;
028import com.nimbusds.jose.JWSObject;
029import com.nimbusds.jwt.SignedJWT;
030import com.nimbusds.oauth2.sdk.ParseException;
031import com.nimbusds.oauth2.sdk.SerializeException;
032import com.nimbusds.oauth2.sdk.http.CommonContentTypes;
033import com.nimbusds.oauth2.sdk.http.HTTPRequest;
034import com.nimbusds.oauth2.sdk.id.ClientID;
035import com.nimbusds.oauth2.sdk.util.MultivaluedMapUtils;
036import com.nimbusds.oauth2.sdk.util.URLUtils;
037
038
039/**
040 * Base abstract class for JSON Web Token (JWT) based client authentication at 
041 * the Token endpoint.
042 *
043 * <p>Related specifications:
044 *
045 * <ul>
046 *     <li>OAuth 2.0 (RFC 6749), section 3.2.1.
047 *     <li>JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and
048 *         Authorization Grants (RFC 7523).
049 *     <li>OpenID Connect Core 1.0, section 9.
050 * </ul>
051 */
052public abstract class JWTAuthentication extends ClientAuthentication {
053
054
055        /**
056         * The expected client assertion type, corresponding to the
057         * {@code client_assertion_type} parameter. This is a URN string set to
058         * "urn:ietf:params:oauth:client-assertion-type:jwt-bearer".
059         */
060        public static final String CLIENT_ASSERTION_TYPE = 
061                "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
062        
063
064        /**
065         * The client assertion, corresponding to the {@code client_assertion}
066         * parameter. The assertion is in the form of a signed JWT.
067         */
068        private final SignedJWT clientAssertion;
069
070
071        /**
072         * The JWT authentication claims set for the client assertion.
073         */
074        private final JWTAuthenticationClaimsSet jwtAuthClaimsSet;
075
076
077        /**
078         * Parses the client identifier from the specified signed JWT that
079         * represents a client assertion.
080         *
081         * @param jwt The signed JWT to parse. Must not be {@code null}.
082         *
083         * @return The parsed client identifier.
084         *
085         * @throws IllegalArgumentException If the client identifier couldn't
086         *                                  be parsed.
087         */
088        private static ClientID parseClientID(final SignedJWT jwt) {
089
090                String subjectValue;
091                String issuerValue;
092
093                try {
094                        subjectValue = jwt.getJWTClaimsSet().getSubject();
095                        issuerValue = jwt.getJWTClaimsSet().getIssuer();
096
097                } catch (java.text.ParseException e) {
098
099                        throw new IllegalArgumentException(e.getMessage(), e);
100                }
101
102                if (subjectValue == null)
103                        throw new IllegalArgumentException("Missing subject in client JWT assertion");
104
105                if (issuerValue == null)
106                        throw new IllegalArgumentException("Missing issuer in client JWT assertion");
107
108                if (!subjectValue.equals(issuerValue))
109                        throw new IllegalArgumentException("Issuer and subject in client JWT assertion must designate the same client identifier");
110
111                return new ClientID(subjectValue);
112        }
113        
114        
115        /**
116         * Creates a new JSON Web Token (JWT) based client authentication.
117         *
118         * @param method          The client authentication method. Must not be
119         *                        {@code null}.
120         * @param clientAssertion The client assertion, corresponding to the
121         *                        {@code client_assertion} parameter, in the
122         *                        form of a signed JSON Web Token (JWT). Must
123         *                        be signed and not {@code null}.
124         *
125         * @throws IllegalArgumentException If the client assertion is not
126         *                                  signed or doesn't conform to the
127         *                                  expected format.
128         */
129        protected JWTAuthentication(final ClientAuthenticationMethod method, 
130                                    final SignedJWT clientAssertion) {
131        
132                super(method, parseClientID(clientAssertion));
133
134                if (! clientAssertion.getState().equals(JWSObject.State.SIGNED))
135                        throw new IllegalArgumentException("The client assertion JWT must be signed");
136                        
137                this.clientAssertion = clientAssertion;
138
139                try {
140                        jwtAuthClaimsSet = JWTAuthenticationClaimsSet.parse(clientAssertion.getJWTClaimsSet());
141
142                } catch (Exception e) {
143
144                        throw new IllegalArgumentException(e.getMessage(), e);
145                }
146        }
147        
148        
149        /**
150         * Gets the client assertion, corresponding to the 
151         * {@code client_assertion} parameter.
152         *
153         * @return The client assertion, in the form of a signed JSON Web Token 
154         *         (JWT).
155         */
156        public SignedJWT getClientAssertion() {
157        
158                return clientAssertion;
159        }
160        
161        
162        /**
163         * Gets the client authentication claims set contained in the client
164         * assertion JSON Web Token (JWT).
165         *
166         * @return The client authentication claims.
167         */
168        public JWTAuthenticationClaimsSet getJWTAuthenticationClaimsSet() {
169
170                return jwtAuthClaimsSet;
171        }
172        
173        
174        /**
175         * Returns the parameter representation of this JSON Web Token (JWT) 
176         * based client authentication. Note that the parameters are not 
177         * {@code application/x-www-form-urlencoded} encoded.
178         *
179         * <p>Parameters map:
180         *
181         * <pre>
182         * "client_assertion" = [serialised-JWT]
183         * "client_assertion_type" = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
184         * </pre>
185         *
186         * @return The parameters map, with keys "client_assertion",
187         *         "client_assertion_type" and "client_id".
188         */
189        public Map<String,List<String>> toParameters() {
190        
191                Map<String,List<String>> params = new HashMap<>();
192                
193                try {
194                        params.put("client_assertion", Collections.singletonList(clientAssertion.serialize()));
195                
196                } catch (IllegalStateException e) {
197                
198                        throw new SerializeException("Couldn't serialize JWT to a client assertion string: " + e.getMessage(), e);
199                }       
200                
201                params.put("client_assertion_type", Collections.singletonList(CLIENT_ASSERTION_TYPE));
202                
203                return params;
204        }
205        
206        
207        @Override
208        public void applyTo(final HTTPRequest httpRequest) {
209                
210                if (httpRequest.getMethod() != HTTPRequest.Method.POST)
211                        throw new SerializeException("The HTTP request method must be POST");
212                
213                ContentType ct = httpRequest.getContentType();
214                
215                if (ct == null)
216                        throw new SerializeException("Missing HTTP Content-Type header");
217                
218                if (! ct.match(CommonContentTypes.APPLICATION_URLENCODED))
219                        throw new SerializeException("The HTTP Content-Type header must be " + CommonContentTypes.APPLICATION_URLENCODED);
220                
221                Map<String,List<String>> params = httpRequest.getQueryParameters();
222                
223                params.putAll(toParameters());
224                
225                String queryString = URLUtils.serializeParameters(params);
226                
227                httpRequest.setQuery(queryString);
228        }
229        
230        
231        /**
232         * Ensures the specified parameters map contains an entry with key 
233         * "client_assertion_type" pointing to a string that equals the expected
234         * {@link #CLIENT_ASSERTION_TYPE}. This method is intended to aid 
235         * parsing of JSON Web Token (JWT) based client authentication objects.
236         *
237         * @param params The parameters map to check. The parameters must not be
238         *               {@code null} and 
239         *               {@code application/x-www-form-urlencoded} encoded.
240         *
241         * @throws ParseException If expected "client_assertion_type" entry 
242         *                        wasn't found.
243         */
244        protected static void ensureClientAssertionType(final Map<String,List<String>> params)
245                throws ParseException {
246                
247                final String clientAssertionType = MultivaluedMapUtils.getFirstValue(params, "client_assertion_type");
248                
249                if (clientAssertionType == null)
250                        throw new ParseException("Missing \"client_assertion_type\" parameter");
251                
252                if (! clientAssertionType.equals(CLIENT_ASSERTION_TYPE))
253                        throw new ParseException("Invalid \"client_assertion_type\" parameter, must be " + CLIENT_ASSERTION_TYPE);
254        }
255        
256        
257        /**
258         * Parses the specified parameters map for a client assertion. This
259         * method is intended to aid parsing of JSON Web Token (JWT) based 
260         * client authentication objects.
261         *
262         * @param params The parameters map to parse. It must contain an entry
263         *               with key "client_assertion" pointing to a string that
264         *               represents a signed serialised JSON Web Token (JWT).
265         *               The parameters must not be {@code null} and
266         *               {@code application/x-www-form-urlencoded} encoded.
267         *
268         * @return The client assertion as a signed JSON Web Token (JWT).
269         *
270         * @throws ParseException If a "client_assertion" entry couldn't be
271         *                        retrieved from the parameters map.
272         */
273        protected static SignedJWT parseClientAssertion(final Map<String,List<String>> params)
274                throws ParseException {
275                
276                final String clientAssertion = MultivaluedMapUtils.getFirstValue(params, "client_assertion");
277                
278                if (clientAssertion == null)
279                        throw new ParseException("Missing \"client_assertion\" parameter");
280                
281                try {
282                        return SignedJWT.parse(clientAssertion);
283                        
284                } catch (java.text.ParseException e) {
285                
286                        throw new ParseException("Invalid \"client_assertion\" JWT: " + e.getMessage(), e);
287                }
288        }
289        
290        /**
291         * Parses the specified parameters map for an optional client 
292         * identifier. This method is intended to aid parsing of JSON Web Token 
293         * (JWT) based client authentication objects.
294         *
295         * @param params The parameters map to parse. It may contain an entry
296         *               with key "client_id" pointing to a string that 
297         *               represents the client identifier. The parameters must 
298         *               not be {@code null} and 
299         *               {@code application/x-www-form-urlencoded} encoded.
300         *
301         * @return The client identifier, {@code null} if not specified.
302         */
303        protected static ClientID parseClientID(final Map<String,List<String>> params) {
304                
305                String clientIDString = MultivaluedMapUtils.getFirstValue(params, "client_id");
306
307                if (clientIDString == null)
308                        return null;
309
310                else
311                        return new ClientID(clientIDString);
312        }
313        
314        
315        /**
316         * Parses the specified HTTP request for a JSON Web Token (JWT) based
317         * client authentication.
318         *
319         * @param httpRequest The HTTP request to parse. Must not be {@code null}.
320         *
321         * @return The JSON Web Token (JWT) based client authentication.
322         *
323         * @throws ParseException If a JSON Web Token (JWT) based client 
324         *                        authentication couldn't be retrieved from the
325         *                        HTTP request.
326         */
327        public static JWTAuthentication parse(final HTTPRequest httpRequest)
328                throws ParseException {
329                
330                httpRequest.ensureMethod(HTTPRequest.Method.POST);
331                httpRequest.ensureContentType(CommonContentTypes.APPLICATION_URLENCODED);
332                
333                String query = httpRequest.getQuery();
334                
335                if (query == null)
336                        throw new ParseException("Missing HTTP POST request entity body");
337                
338                Map<String,List<String>> params = URLUtils.parseParameters(query);
339                
340                JWSAlgorithm alg = parseClientAssertion(params).getHeader().getAlgorithm();
341                        
342                if (ClientSecretJWT.supportedJWAs().contains(alg))
343                        return ClientSecretJWT.parse(params);
344                                
345                else if (PrivateKeyJWT.supportedJWAs().contains(alg))
346                        return PrivateKeyJWT.parse(params);
347                        
348                else
349                        throw new ParseException("Unsupported signed JWT algorithm: " + alg);
350        }
351}