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                return new ClientID(subjectValue);
105        }
106        
107        
108        /**
109         * Creates a new JSON Web Token (JWT) based client authentication.
110         *
111         * @param method          The client authentication method. Must not be
112         *                        {@code null}.
113         * @param clientAssertion The client assertion, corresponding to the
114         *                        {@code client_assertion} parameter, in the
115         *                        form of a signed JSON Web Token (JWT). Must
116         *                        be signed and not {@code null}.
117         *
118         * @throws IllegalArgumentException If the client assertion is not
119         *                                  signed or doesn't conform to the
120         *                                  expected format.
121         */
122        protected JWTAuthentication(final ClientAuthenticationMethod method, 
123                                    final SignedJWT clientAssertion) {
124        
125                super(method, parseClientID(clientAssertion));
126
127                if (! clientAssertion.getState().equals(JWSObject.State.SIGNED))
128                        throw new IllegalArgumentException("The client assertion JWT must be signed");
129                        
130                this.clientAssertion = clientAssertion;
131
132                try {
133                        jwtAuthClaimsSet = JWTAuthenticationClaimsSet.parse(clientAssertion.getJWTClaimsSet());
134
135                } catch (Exception e) {
136
137                        throw new IllegalArgumentException(e.getMessage(), e);
138                }
139        }
140        
141        
142        /**
143         * Gets the client assertion, corresponding to the 
144         * {@code client_assertion} parameter.
145         *
146         * @return The client assertion, in the form of a signed JSON Web Token 
147         *         (JWT).
148         */
149        public SignedJWT getClientAssertion() {
150        
151                return clientAssertion;
152        }
153        
154        
155        /**
156         * Gets the client authentication claims set contained in the client
157         * assertion JSON Web Token (JWT).
158         *
159         * @return The client authentication claims.
160         */
161        public JWTAuthenticationClaimsSet getJWTAuthenticationClaimsSet() {
162
163                return jwtAuthClaimsSet;
164        }
165        
166        
167        @Override
168        public Set<String> getFormParameterNames() {
169                
170                return Collections.unmodifiableSet(new HashSet<>(Arrays.asList("client_assertion", "client_assertion_type", "client_id")));
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" and
187         *         "client_assertion_type".
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.getEntityContentType();
214                
215                if (ct == null)
216                        throw new SerializeException("Missing HTTP Content-Type header");
217                
218                if (! ct.matches(ContentType.APPLICATION_URLENCODED))
219                        throw new SerializeException("The HTTP Content-Type header must be " + ContentType.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                return clientIDString != null ? new ClientID(clientIDString) : null;
308        }
309        
310        
311        /**
312         * Parses the specified HTTP request for a JSON Web Token (JWT) based
313         * client authentication.
314         *
315         * @param httpRequest The HTTP request to parse. Must not be {@code null}.
316         *
317         * @return The JSON Web Token (JWT) based client authentication.
318         *
319         * @throws ParseException If a JSON Web Token (JWT) based client 
320         *                        authentication couldn't be retrieved from the
321         *                        HTTP request.
322         */
323        public static JWTAuthentication parse(final HTTPRequest httpRequest)
324                throws ParseException {
325                
326                httpRequest.ensureMethod(HTTPRequest.Method.POST);
327                httpRequest.ensureEntityContentType(ContentType.APPLICATION_URLENCODED);
328                
329                String query = httpRequest.getQuery();
330                
331                if (query == null)
332                        throw new ParseException("Missing HTTP POST request entity body");
333                
334                Map<String,List<String>> params = URLUtils.parseParameters(query);
335                
336                JWSAlgorithm alg = parseClientAssertion(params).getHeader().getAlgorithm();
337                        
338                if (ClientSecretJWT.supportedJWAs().contains(alg))
339                        return ClientSecretJWT.parse(params);
340                                
341                else if (PrivateKeyJWT.supportedJWAs().contains(alg))
342                        return PrivateKeyJWT.parse(params);
343                        
344                else
345                        throw new ParseException("Unsupported signed JWT algorithm: " + alg);
346        }
347}