View Javadoc
1   /*
2    * Copyright (c) 2002-2015, Mairie de Paris
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions
7    * are met:
8    *
9    *  1. Redistributions of source code must retain the above copyright notice
10   *     and the following disclaimer.
11   *
12   *  2. Redistributions in binary form must reproduce the above copyright notice
13   *     and the following disclaimer in the documentation and/or other materials
14   *     provided with the distribution.
15   *
16   *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
17   *     contributors may be used to endorse or promote products derived from
18   *     this software without specific prior written permission.
19   *
20   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21   * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22   * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
24   * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25   * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26   * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27   * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28   * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30   * POSSIBILITY OF SUCH DAMAGE.
31   *
32   * License 1.0
33   */
34  package fr.paris.lutece.plugins.franceconnect.oidc.jwt;
35  
36  import com.nimbusds.jose.Algorithm;
37  import com.nimbusds.jose.JWSAlgorithm;
38  
39  import com.nimbusds.jwt.JWT;
40  import com.nimbusds.jwt.PlainJWT;
41  import com.nimbusds.jwt.ReadOnlyJWTClaimsSet;
42  import com.nimbusds.jwt.SignedJWT;
43  
44  import fr.paris.lutece.plugins.franceconnect.oidc.AuthClientConf;
45  import fr.paris.lutece.plugins.franceconnect.oidc.AuthServerConf;
46  import fr.paris.lutece.plugins.franceconnect.oidc.IDToken;
47  import fr.paris.lutece.plugins.franceconnect.oidc.Token;
48  import fr.paris.lutece.plugins.franceconnect.web.Constants;
49  
50  import org.apache.log4j.Logger;
51  
52  import java.text.ParseException;
53  
54  import java.util.Date;
55  
56  
57  /**
58   * MitreJWTParser
59   *
60   * Some parts of the code come from the MITRE project and especialy
61   * from the class : com.plec.artistes.security.CustomOIDCAuthenticationFilter
62   */
63  public class MitreJWTParser implements JWTParser
64  {
65      // Allow for time sync issues by having a window of X seconds.
66      private int _nTimeSkewAllowance = 300;
67  
68      /**
69       * {@inheritDoc }
70       */
71      @Override
72      public void parseJWT( Token token, AuthClientConf clientConfig, AuthServerConf serverConfig, String strStoredNonce,
73          Logger logger ) throws TokenValidationException
74      {
75          JWT jwt;
76          IDToken idToken = new IDToken(  );
77  
78          try
79          {
80              jwt = com.nimbusds.jwt.JWTParser.parse( token.getIdTokenString(  ) );
81          }
82          catch ( ParseException ex )
83          {
84              throw new TokenValidationException( "Unable to parse JWT : " + ex.getMessage(  ), ex );
85          }
86  
87          // validate our ID Token over a number of tests
88          ReadOnlyJWTClaimsSet idClaims;
89  
90          try
91          {
92              idClaims = jwt.getJWTClaimsSet(  );
93          }
94          catch ( ParseException ex )
95          {
96              throw new TokenValidationException( "Unable to get Claims set from JWT : " + ex.getMessage(  ), ex );
97          }
98  
99          Algorithm tokenAlg = jwt.getHeader(  ).getAlgorithm(  );
100 
101         Algorithm clientAlg = clientConfig.getIdTokenSignedResponseAlg(  );
102 
103         if ( clientAlg != null )
104         {
105             if ( !clientAlg.equals( tokenAlg ) )
106             {
107                 throw new TokenValidationException( "Token algorithm " + tokenAlg +
108                     " does not match expected algorithm " + clientAlg );
109             }
110         }
111 
112         if ( jwt instanceof PlainJWT )
113         {
114             logger.debug( "ID token is a Plain JWT" );
115 
116             if ( clientAlg == null )
117             {
118                 throw new TokenValidationException( 
119                     "Unsigned ID tokens can only be used if explicitly configured in client." );
120             }
121 
122             if ( ( tokenAlg != null ) && !tokenAlg.equals( JWSAlgorithm.NONE ) )
123             {
124                 throw new TokenValidationException( "Unsigned token received, expected signature with " + tokenAlg );
125             }
126         }
127         else if ( jwt instanceof SignedJWT )
128         {
129             logger.debug( "ID token is a signed JWT" );
130 
131             /*
132              // check the signature
133              JwtSigningAndValidationService jwtValidator = null;
134 
135              SignedJWT signedIdToken = (SignedJWT) idToken;
136 
137              if (tokenAlg.equals(JWSAlgorithm.HS256)
138              || tokenAlg.equals(JWSAlgorithm.HS384)
139              || tokenAlg.equals(JWSAlgorithm.HS512))
140              {
141 
142              // generate one based on client secret
143              jwtValidator = symmetricCacheService.getSymmetricValidtor( clientConfig.getClient());
144              }
145              else
146              {
147              // otherwise load from the server's public key
148              jwtValidator = validationServices.getValidator( serverConfig.getJwksUri() );
149              }
150 
151              if (jwtValidator != null)
152              {
153              if (!jwtValidator.validateSignature(signedIdToken))
154              {
155              throw new TokenValidationException("Signature validation failed");
156              }
157              }
158              else
159              {
160              _logger.error("No validation service found. Skipping signature validation");
161              throw new TokenValidationException("Unable to find an appropriate signature validator for ID Token.");
162              }
163              */
164         } // TODO: encrypted id tokens
165 
166         // check the issuer
167         if ( idClaims.getIssuer(  ) == null )
168         {
169             throw new TokenValidationException( "Id Token Issuer is null" );
170         }
171         else if ( !idClaims.getIssuer(  ).equals( serverConfig.getIssuer(  ) ) )
172         {
173             throw new TokenValidationException( "Issuers do not match, expected " + serverConfig.getIssuer(  ) +
174                 " got " + idClaims.getIssuer(  ) );
175         }
176 
177         // check expiration
178         if ( idClaims.getExpirationTime(  ) == null )
179         {
180             throw new TokenValidationException( "Id Token does not have required expiration claim" );
181         }
182         else
183         {
184             // it's not null, see if it's expired
185             Date now = new Date( System.currentTimeMillis(  ) - ( _nTimeSkewAllowance * 1000 ) );
186 
187             if ( now.after( idClaims.getExpirationTime(  ) ) )
188             {
189                 throw new TokenValidationException( "Id Token is expired: " + idClaims.getExpirationTime(  ) );
190             }
191         }
192 
193         // check not before
194         if ( idClaims.getNotBeforeTime(  ) != null )
195         {
196             Date now = new Date( System.currentTimeMillis(  ) + ( _nTimeSkewAllowance * 1000 ) );
197 
198             if ( now.before( idClaims.getNotBeforeTime(  ) ) )
199             {
200                 throw new TokenValidationException( "Id Token not valid untill: " + idClaims.getNotBeforeTime(  ) );
201             }
202         }
203 
204         // check issued at
205         if ( idClaims.getIssueTime(  ) == null )
206         {
207             throw new TokenValidationException( "Id Token does not have required issued-at claim" );
208         }
209         else
210         {
211             // since it's not null, see if it was issued in the future
212             Date now = new Date( System.currentTimeMillis(  ) + ( _nTimeSkewAllowance * 1000 ) );
213 
214             if ( now.before( idClaims.getIssueTime(  ) ) )
215             {
216                 throw new TokenValidationException( "Id Token was issued in the future: " + idClaims.getIssueTime(  ) );
217             }
218         }
219 
220         // check audience
221         if ( idClaims.getAudience(  ) == null )
222         {
223             throw new TokenValidationException( "Id token audience is null" );
224         }
225         else if ( !idClaims.getAudience(  ).contains( clientConfig.getClientId(  ) ) )
226         {
227             throw new TokenValidationException( "Audience does not match, expected " + clientConfig.getClientId(  ) +
228                 " got " + idClaims.getAudience(  ) );
229         }
230 
231         // compare the nonce to our stored claim
232         String strNonce = null;
233 
234         try
235         {
236             strNonce = idClaims.getStringClaim( "nonce" );
237         }
238         catch ( ParseException ex )
239         {
240             throw new TokenValidationException( "ID token did not contain a nonce claim." );
241         }
242 
243         if ( ( strNonce == null ) || strNonce.equals( "" ) )
244         {
245             logger.error( "ID token did not contain a nonce claim." );
246 
247             throw new TokenValidationException( "ID token did not contain a nonce claim." );
248         }
249 
250         if ( !strNonce.equals( strStoredNonce ) )
251         {
252             logger.error( "Possible replay attack detected! The comparison of the nonce in the returned " +
253                 "ID Token to the session " + Constants.NONCE_SESSION_VARIABLE + " failed. Expected " + strStoredNonce +
254                 " got " + strNonce + "." );
255 
256             throw new TokenValidationException( 
257                 "Possible replay attack detected! The comparison of the nonce in the returned " +
258                 "ID Token to the session " + Constants.NONCE_SESSION_VARIABLE + " failed. Expected " + strStoredNonce +
259                 " got " + strNonce + "." );
260         }
261 
262         logger.debug( "Nonce has been validated" );
263 
264         // Get IDP 
265         String strIdp;
266 
267         try
268         {
269             strIdp = idClaims.getStringClaim( "idp" );
270         }
271         catch ( ParseException ex )
272         {
273             throw new TokenValidationException( "ID token did not contain an idp claim.", ex );
274         }
275 
276         // Get ACR
277         String strAcr;
278 
279         try
280         {
281             strAcr = idClaims.getStringClaim( "acr" );
282         }
283         catch ( ParseException ex )
284         {
285             throw new TokenValidationException( "ID token did not contain an acr claim.", ex );
286         }
287 
288         idToken.setNonce( strNonce );
289         idToken.setSubject( idClaims.getSubject(  ) );
290         idToken.setIdProvider( strIdp );
291         idToken.setExpiration( String.valueOf( idClaims.getExpirationTime(  ).getTime(  ) / 1000L ) );
292         idToken.setIssueAt( String.valueOf( idClaims.getIssueTime(  ).getTime(  ) / 1000L ) );
293         idToken.setIssuer( idClaims.getIssuer(  ) );
294         idToken.setAudience( idClaims.getAudience(  ).get( 0 ) );
295         idToken.setAcr( strAcr );
296         logger.debug( "ID Token retrieved : " + idToken );
297 
298         token.setIdToken( idToken );
299     }
300 }