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