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