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.SignedJWT;
025import com.nimbusds.oauth2.sdk.ParseException;
026import com.nimbusds.oauth2.sdk.SerializeException;
027import com.nimbusds.oauth2.sdk.http.HTTPRequest;
028import com.nimbusds.oauth2.sdk.id.ClientID;
029import com.nimbusds.oauth2.sdk.util.MultivaluedMapUtils;
030import com.nimbusds.oauth2.sdk.util.URLUtils;
031
032import java.util.*;
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;
222                try {
223                        params = new LinkedHashMap<>(httpRequest.getBodyAsFormParameters());
224                } catch (ParseException e) {
225                        throw new SerializeException(e.getMessage(), e);
226                }
227                params.putAll(toParameters());
228                
229                httpRequest.setBody(URLUtils.serializeParameters(params));
230        }
231        
232        
233        /**
234         * Ensures the specified parameters map contains an entry with key 
235         * "client_assertion_type" pointing to a string that equals the expected
236         * {@link #CLIENT_ASSERTION_TYPE}. This method is intended to aid 
237         * parsing of JSON Web Token (JWT) based client authentication objects.
238         *
239         * @param params The parameters map to check. The parameters must not be
240         *               {@code null} and 
241         *               {@code application/x-www-form-urlencoded} encoded.
242         *
243         * @throws ParseException If expected "client_assertion_type" entry 
244         *                        wasn't found.
245         */
246        protected static void ensureClientAssertionType(final Map<String,List<String>> params)
247                throws ParseException {
248                
249                final String clientAssertionType = MultivaluedMapUtils.getFirstValue(params, "client_assertion_type");
250                
251                if (clientAssertionType == null)
252                        throw new ParseException("Missing client_assertion_type parameter");
253                
254                if (! clientAssertionType.equals(CLIENT_ASSERTION_TYPE))
255                        throw new ParseException("Invalid client_assertion_type parameter, must be " + CLIENT_ASSERTION_TYPE);
256        }
257        
258        
259        /**
260         * Parses the specified parameters map for a client assertion. This
261         * method is intended to aid parsing of JSON Web Token (JWT) based 
262         * client authentication objects.
263         *
264         * @param params The parameters map to parse. It must contain an entry
265         *               with key "client_assertion" pointing to a string that
266         *               represents a signed serialised JSON Web Token (JWT).
267         *               The parameters must not be {@code null} and
268         *               {@code application/x-www-form-urlencoded} encoded.
269         *
270         * @return The client assertion as a signed JSON Web Token (JWT).
271         *
272         * @throws ParseException If a "client_assertion" entry couldn't be
273         *                        retrieved from the parameters map.
274         */
275        protected static SignedJWT parseClientAssertion(final Map<String,List<String>> params)
276                throws ParseException {
277                
278                final String clientAssertion = MultivaluedMapUtils.getFirstValue(params, "client_assertion");
279                
280                if (clientAssertion == null)
281                        throw new ParseException("Missing client_assertion parameter");
282                
283                try {
284                        return SignedJWT.parse(clientAssertion);
285                        
286                } catch (java.text.ParseException e) {
287                
288                        throw new ParseException("Invalid client_assertion JWT: " + e.getMessage(), e);
289                }
290        }
291        
292        /**
293         * Parses the specified parameters map for an optional client 
294         * identifier. This method is intended to aid parsing of JSON Web Token 
295         * (JWT) based client authentication objects.
296         *
297         * @param params The parameters map to parse. It may contain an entry
298         *               with key "client_id" pointing to a string that 
299         *               represents the client identifier. The parameters must 
300         *               not be {@code null} and 
301         *               {@code application/x-www-form-urlencoded} encoded.
302         *
303         * @return The client identifier, {@code null} if not specified.
304         */
305        protected static ClientID parseClientID(final Map<String,List<String>> params) {
306                
307                String clientIDString = MultivaluedMapUtils.getFirstValue(params, "client_id");
308
309                return clientIDString != null ? new ClientID(clientIDString) : null;
310        }
311        
312        
313        /**
314         * Parses the specified HTTP request for a JSON Web Token (JWT) based
315         * client authentication.
316         *
317         * @param httpRequest The HTTP request to parse. Must not be {@code null}.
318         *
319         * @return The JSON Web Token (JWT) based client authentication.
320         *
321         * @throws ParseException If a JSON Web Token (JWT) based client 
322         *                        authentication couldn't be retrieved from the
323         *                        HTTP request.
324         */
325        public static JWTAuthentication parse(final HTTPRequest httpRequest)
326                throws ParseException {
327                
328                httpRequest.ensureMethod(HTTPRequest.Method.POST);
329                httpRequest.ensureEntityContentType(ContentType.APPLICATION_URLENCODED);
330                
331                String query = httpRequest.getQuery();
332                
333                if (query == null)
334                        throw new ParseException("Missing HTTP POST request entity body");
335                
336                Map<String,List<String>> params = URLUtils.parseParameters(query);
337                
338                JWSAlgorithm alg = parseClientAssertion(params).getHeader().getAlgorithm();
339                        
340                if (ClientSecretJWT.supportedJWAs().contains(alg))
341                        return ClientSecretJWT.parse(params);
342                                
343                else if (PrivateKeyJWT.supportedJWAs().contains(alg))
344                        return PrivateKeyJWT.parse(params);
345                        
346                else
347                        throw new ParseException("Unsupported signed JWT algorithm: " + alg);
348        }
349}