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.URLUtils;
032
033import java.util.*;
034
035
036/**
037 * Base abstract class for JSON Web Token (JWT) based client authentication at 
038 * the Token endpoint.
039 *
040 * <p>Related specifications:
041 *
042 * <ul>
043 *     <li>OAuth 2.0 (RFC 6749), section 3.2.1.
044 *     <li>JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and
045 *         Authorization Grants (RFC 7523).
046 *     <li>OpenID Connect Core 1.0, section 9.
047 * </ul>
048 */
049public abstract class JWTAuthentication extends ClientAuthentication {
050
051
052        /**
053         * The expected client assertion type, corresponding to the
054         * {@code client_assertion_type} parameter. This is a URN string set to
055         * "urn:ietf:params:oauth:client-assertion-type:jwt-bearer".
056         */
057        public static final String CLIENT_ASSERTION_TYPE = 
058                "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
059        
060
061        /**
062         * The client assertion, corresponding to the {@code client_assertion}
063         * parameter. The assertion is in the form of a signed JWT.
064         */
065        private final SignedJWT clientAssertion;
066
067
068        /**
069         * The JWT authentication claims set for the client assertion.
070         */
071        private final JWTAuthenticationClaimsSet jwtAuthClaimsSet;
072
073
074        /**
075         * Parses the client identifier from the specified signed JWT that
076         * represents a client assertion.
077         *
078         * @param jwt The signed JWT to parse. Must not be {@code null}.
079         *
080         * @return The parsed client identifier.
081         *
082         * @throws IllegalArgumentException If the client identifier couldn't
083         *                                  be parsed.
084         */
085        private static ClientID parseClientID(final SignedJWT jwt) {
086
087                String subjectValue;
088                String issuerValue;
089                try {
090                        JWTClaimsSet jwtClaimsSet = jwt.getJWTClaimsSet();
091                        subjectValue = jwtClaimsSet.getSubject();
092                        issuerValue = jwtClaimsSet.getIssuer();
093
094                } catch (java.text.ParseException e) {
095
096                        throw new IllegalArgumentException(e.getMessage(), e);
097                }
098
099                if (subjectValue == null)
100                        throw new IllegalArgumentException("Missing subject in client JWT assertion");
101
102                if (issuerValue == null)
103                        throw new IllegalArgumentException("Missing issuer in client JWT assertion");
104
105                return new ClientID(subjectValue);
106        }
107        
108        
109        /**
110         * Creates a new JSON Web Token (JWT) based client authentication.
111         *
112         * @param method          The client authentication method. Must not be
113         *                        {@code null}.
114         * @param clientAssertion The client assertion, corresponding to the
115         *                        {@code client_assertion} parameter, in the
116         *                        form of a signed JSON Web Token (JWT). Must
117         *                        be signed and not {@code null}.
118         *
119         * @throws IllegalArgumentException If the client assertion is not
120         *                                  signed or doesn't conform to the
121         *                                  expected format.
122         */
123        protected JWTAuthentication(final ClientAuthenticationMethod method, 
124                                    final SignedJWT clientAssertion) {
125        
126                super(method, parseClientID(clientAssertion));
127
128                if (! clientAssertion.getState().equals(JWSObject.State.SIGNED))
129                        throw new IllegalArgumentException("The client assertion JWT must be signed");
130                        
131                this.clientAssertion = clientAssertion;
132
133                try {
134                        jwtAuthClaimsSet = JWTAuthenticationClaimsSet.parse(clientAssertion.getJWTClaimsSet());
135
136                } catch (Exception e) {
137
138                        throw new IllegalArgumentException(e.getMessage(), e);
139                }
140        }
141        
142        
143        /**
144         * Gets the client assertion, corresponding to the 
145         * {@code client_assertion} parameter.
146         *
147         * @return The client assertion, in the form of a signed JSON Web Token 
148         *         (JWT).
149         */
150        public SignedJWT getClientAssertion() {
151        
152                return clientAssertion;
153        }
154        
155        
156        /**
157         * Gets the client authentication claims set contained in the client
158         * assertion JSON Web Token (JWT).
159         *
160         * @return The client authentication claims.
161         */
162        public JWTAuthenticationClaimsSet getJWTAuthenticationClaimsSet() {
163
164                return jwtAuthClaimsSet;
165        }
166        
167        
168        @Override
169        public Set<String> getFormParameterNames() {
170                
171                return Collections.unmodifiableSet(new HashSet<>(Arrays.asList("client_assertion", "client_assertion_type", "client_id")));
172        }
173        
174        
175        /**
176         * Returns the parameter representation of this JSON Web Token (JWT) 
177         * based client authentication. Note that the parameters are not 
178         * {@code application/x-www-form-urlencoded} encoded.
179         *
180         * <p>Parameters map:
181         *
182         * <pre>
183         * "client_assertion" = [serialised-JWT]
184         * "client_assertion_type" = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
185         * </pre>
186         *
187         * @return The parameters map, with keys "client_assertion" and
188         *         "client_assertion_type".
189         */
190        public Map<String,List<String>> toParameters() {
191        
192                Map<String,List<String>> params = new HashMap<>();
193                
194                try {
195                        params.put("client_assertion", Collections.singletonList(clientAssertion.serialize()));
196                
197                } catch (IllegalStateException e) {
198                
199                        throw new SerializeException("Couldn't serialize JWT to a client assertion string: " + e.getMessage(), e);
200                }       
201                
202                params.put("client_assertion_type", Collections.singletonList(CLIENT_ASSERTION_TYPE));
203                
204                return params;
205        }
206        
207        
208        @Override
209        public void applyTo(final HTTPRequest httpRequest) {
210                
211                if (httpRequest.getMethod() != HTTPRequest.Method.POST)
212                        throw new SerializeException("The HTTP request method must be POST");
213                
214                ContentType ct = httpRequest.getEntityContentType();
215                
216                if (ct == null)
217                        throw new SerializeException("Missing HTTP Content-Type header");
218                
219                if (! ct.matches(ContentType.APPLICATION_URLENCODED))
220                        throw new SerializeException("The HTTP Content-Type header must be " + ContentType.APPLICATION_URLENCODED);
221
222                Map<String, List<String>> params;
223                try {
224                        params = new LinkedHashMap<>(httpRequest.getBodyAsFormParameters());
225                } catch (ParseException e) {
226                        throw new SerializeException(e.getMessage(), e);
227                }
228                params.putAll(toParameters());
229                
230                httpRequest.setBody(URLUtils.serializeParameters(params));
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                return clientIDString != null ? new ClientID(clientIDString) : null;
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}