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