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