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