View Javadoc
1   /*
2    * Copyright (c) 2002-2021, City of 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.oauth2.jwt;
35  
36  import java.nio.charset.StandardCharsets;
37  import java.util.Map;
38  import java.util.concurrent.ConcurrentHashMap;
39  
40  import org.apache.log4j.Logger;
41  
42  import com.fasterxml.jackson.core.JsonProcessingException;
43  import com.fasterxml.jackson.databind.ObjectMapper;
44  
45  import fr.paris.lutece.plugins.oauth2.business.AuthClientConf;
46  import fr.paris.lutece.plugins.oauth2.business.AuthServerConf;
47  import fr.paris.lutece.plugins.oauth2.business.IDToken;
48  import fr.paris.lutece.plugins.oauth2.business.Token;
49  import fr.paris.lutece.plugins.oauth2.service.CachingHttpAccessService;
50  import fr.paris.lutece.plugins.oauth2.web.Constants;
51  import fr.paris.lutece.util.httpaccess.HttpAccess;
52  import fr.paris.lutece.util.httpaccess.HttpAccessService;
53  import fr.paris.lutece.util.httpaccess.PropertiesHttpClientConfiguration;
54  import io.jsonwebtoken.Claims;
55  import io.jsonwebtoken.ExpiredJwtException;
56  import io.jsonwebtoken.Header;
57  import io.jsonwebtoken.Jws;
58  import io.jsonwebtoken.Jwt;
59  import io.jsonwebtoken.JwtParser;
60  import io.jsonwebtoken.JwtParserBuilder;
61  import io.jsonwebtoken.Jwts;
62  import io.jsonwebtoken.MalformedJwtException;
63  import io.jsonwebtoken.UnsupportedJwtException;
64  import io.jsonwebtoken.security.Keys;
65  import io.jsonwebtoken.security.SignatureException;
66  
67  /**
68   * Jjwt JWTParser
69   */
70  public class JjwtJWTParser implements JWTParser
71  {
72      private final Map<String, KeyLocator> _keyLocatorsMap = new ConcurrentHashMap<>( );
73      private final HttpAccess _httpAccess;
74  
75      public JjwtJWTParser( )
76      {
77          HttpAccessService accessService = new CachingHttpAccessService( new PropertiesHttpClientConfiguration( ) );
78          this._httpAccess = new HttpAccess( accessService );
79      }
80  
81      private KeyLocator getKeyLocator( String strwksEndpointUri )
82      {
83          return _keyLocatorsMap.computeIfAbsent( strwksEndpointUri, uri -> new KeyLocator( uri, _httpAccess ) );
84      }
85  
86      /**
87       * {@inheritDoc }
88       */
89      @Override
90      public void parseJWT( Token token, AuthClientConf clientConfig, AuthServerConf serverConfig, String strStoredNonce, Logger logger )
91              throws TokenValidationException
92      {
93          String strCompactJwt = token.getIdTokenString( );
94  
95          try
96          {
97              Claims claims = getClaims( strCompactJwt, clientConfig, serverConfig );
98  
99              IDTokenoauth2/business/IDToken.html#IDToken">IDToken idToken = new IDToken( );
100             idToken.setAudience( claims.getAudience( ) );
101             idToken.setIssuer( claims.getIssuer( ) );
102             idToken.setSubject( claims.getSubject( ) );
103 
104             // Claims that should be verified
105             idToken.setNonce( getVerifiedNonce( claims, strStoredNonce ) );
106             idToken.setExpiration( getExpiration( claims ) );
107             idToken.setIssueAt( getIssueAt( claims ) );
108 
109             // Extra claims for Oauth2
110             idToken.setIdProvider( (String) claims.get( Constants.CLAIM_IDP ) );
111             idToken.setAcr( (String) claims.get( Constants.CLAIM_ACR ) );
112 
113             logger.debug( "ID Token retrieved by JJWT parser implementation : " + idToken );
114 
115             token.setIdToken( idToken );
116         }
117         catch( SignatureException ex )
118         {
119             throw new TokenValidationException( ex.getMessage( ), ex );
120         }
121         catch( ExpiredJwtException ex )
122         {
123             throw new TokenValidationException( ex.getMessage( ), ex );
124         }
125         catch( IllegalArgumentException ex )
126         {
127             throw new TokenValidationException( ex.getMessage( ), ex );
128         }
129         catch( MalformedJwtException ex )
130         {
131             throw new TokenValidationException( ex.getMessage( ), ex );
132         }
133         catch( UnsupportedJwtException ex )
134         {
135             throw new TokenValidationException( ex.getMessage( ), ex );
136         }
137     }
138 
139     /**
140      * Retrieve and check the nonce
141      * 
142      * @param claims
143      *            Claims set
144      * @param strStoredNonce
145      *            The stored nonce
146      * @return The verified nonce
147      * @throws TokenValidationException
148      *             if the nonce is not valid
149      */
150     private String getVerifiedNonce( Claims claims, String strStoredNonce ) throws TokenValidationException
151     {
152         // Check nonce
153         String strNonce = (String) claims.get( Constants.CLAIM_NONCE );
154 
155         if ( strNonce == null )
156         {
157             throw new TokenValidationException( "The token doesn't contains the nonce info." );
158         }
159 
160         if ( !strNonce.equals( strStoredNonce ) )
161         {
162             throw new TokenValidationException( "The nonce info has not the value expected." );
163         }
164 
165         return strNonce;
166     }
167 
168     /**
169      * Retrieve the expiration date
170      * 
171      * @param claims
172      *            Claims set
173      * @return The expiration date
174      */
175     private String getExpiration( Claims claims )
176     {
177         long lExpiration = claims.getExpiration( ).getTime( );
178 
179         return String.valueOf( lExpiration / 1000L );
180     }
181 
182     /**
183      * Retrieve the issue at date
184      * 
185      * @param claims
186      *            Claims set
187      * @return The issue at date
188      */
189     private String getIssueAt( Claims claims )
190     {
191         long lIssueAt = claims.getIssuedAt( ).getTime( );
192 
193         return String.valueOf( lIssueAt / 1000L );
194     }
195 
196     @Override
197     public String parseJWT( String strJwt, AuthClientConf clientConfig, AuthServerConf serverConfig, Logger logger ) throws TokenValidationException
198     {
199         String strClaims;
200 
201         try
202         {
203             Claims claims = getClaims( strJwt, clientConfig, serverConfig );
204             strClaims = new ObjectMapper( ).writeValueAsString( claims );
205         } catch ( TokenValidationException | JsonProcessingException e )
206         {
207             throw new TokenValidationException( e.getMessage( ), e );
208         }
209 
210         return strClaims;
211     }
212     
213     /**
214      * Get claims
215      * @param strCompactJwt
216      * @param clientConfig
217      * @param serverConfig
218      * @return claims
219      * @throws TokenValidationException
220      */
221     private Claims getClaims ( String strCompactJwt, AuthClientConf clientConfig, AuthServerConf serverConfig) throws TokenValidationException
222     {
223         JwtParserBuilder parserBuilder = Jwts.parser( );
224 
225         if ( serverConfig.getJwksEndpointUri( ) != null )
226         {
227             parserBuilder.keyLocator( getKeyLocator( serverConfig.getJwksEndpointUri( ) ) );
228         }
229         else
230         {
231             parserBuilder.verifyWith( Keys.hmacShaKeyFor( clientConfig.getClientSecret( ).getBytes( StandardCharsets.UTF_8 ) ) );
232         }
233 
234         JwtParser parser = parserBuilder.build( );
235         Claims claims;
236         if ( serverConfig == null || serverConfig.getIDTokenSignatureAlgorithmNames( ) == null )
237         {
238             // claims should be unsigned
239             Jwt<Header, Claims> jwt = parser.parse( strCompactJwt ).accept( Jwt.UNSECURED_CLAIMS );
240             claims = jwt.getPayload( );
241         }
242         else
243         {
244             // claims should be signed
245             Jws<Claims> jws = parser.parse( strCompactJwt ).accept( Jws.CLAIMS );
246             if ( !serverConfig.getIDTokenSignatureAlgorithmNames( ).contains( jws.getHeader( ).getAlgorithm( ) ) )
247             {
248                 throw new TokenValidationException( "Expected alg is one of <" + serverConfig.getIDTokenSignatureAlgorithmNames( ) + "> but got <" + jws.getHeader( ).getAlgorithm( ) + ">" );
249             }
250             claims = jws.getPayload( );
251         }
252         
253         return claims;
254     }
255 }