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