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