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