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