View Javadoc

1   /*
2    * Copyright (C) 2017-2019 Centre National d'Etudes Spatiales (CNES).
3    *
4    * This library is free software; you can redistribute it and/or
5    * modify it under the terms of the GNU Lesser General Public
6    * License as published by the Free Software Foundation; either
7    * version 3.0 of the License, or (at your option) any later version.
8    *
9    * This library is distributed in the hope that it will be useful,
10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12   * Lesser General Public License for more details.
13   *
14   * You should have received a copy of the GNU Lesser General Public
15   * License along with this library; if not, write to the Free Software
16   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
17   * MA 02110-1301  USA
18   */
19  package fr.cnes.doi.security;
20  
21  import fr.cnes.doi.db.AbstractTokenDBHelper;
22  import fr.cnes.doi.db.model.DOIProject;
23  import fr.cnes.doi.exception.DOIDbException;
24  import fr.cnes.doi.exception.DoiRuntimeException;
25  import fr.cnes.doi.exception.TokenSecurityException;
26  import fr.cnes.doi.plugin.PluginFactory;
27  import fr.cnes.doi.settings.Consts;
28  import fr.cnes.doi.settings.DoiSettings;
29  import fr.cnes.doi.utils.UniqueProjectName;
30  import fr.cnes.doi.utils.spec.Requirement;
31  import io.jsonwebtoken.Claims;
32  import io.jsonwebtoken.ExpiredJwtException;
33  import io.jsonwebtoken.Jws;
34  import io.jsonwebtoken.Jwts;
35  import io.jsonwebtoken.MalformedJwtException;
36  import io.jsonwebtoken.SignatureAlgorithm;
37  import io.jsonwebtoken.SignatureException;
38  import io.jsonwebtoken.UnsupportedJwtException;
39  import io.jsonwebtoken.impl.TextCodec;
40  import io.jsonwebtoken.impl.crypto.MacProvider;
41  import java.security.Key;
42  import java.time.Instant;
43  import java.util.ArrayList;
44  import java.util.Calendar;
45  import java.util.Date;
46  import java.util.List;
47  import java.util.Map;
48  import java.util.stream.Collectors;
49  import org.apache.logging.log4j.LogManager;
50  import org.apache.logging.log4j.Logger;
51  import org.restlet.data.Status;
52  
53  /**
54   * Security class for token generation.
55   *
56   * @author Jean-Christophe Malapert (jean-christophe.malapert@cnes.fr)
57   */
58  @Requirement(reqId = Requirement.DOI_AUTH_020, reqName = Requirement.DOI_AUTH_020_NAME)
59  public final class TokenSecurity {
60  
61      /**
62       * token key.
63       */
64      private String tokenKey;
65  
66      /**
67       * Project ID name in token.
68       */
69      public static final String PROJECT_ID = "projectID";
70  
71      /**
72       * Project name in token.
73       */
74      public static final String PROJECT_NAME = "projectName";
75  
76      /**
77       * Default token key.
78       */
79      public static final String DEFAULT_TOKEN_KEY = "Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=";
80  
81      /**
82       * Default date format for the token {@value #DATE_FORMAT}
83       */
84      public static final String DATE_FORMAT = "EEE MMM dd HH:mm:ss z yyyy";
85  
86      /**
87       * Plugin for token database.
88       */
89      private static final AbstractTokenDBHelper TOKEN_DB = PluginFactory.getToken();
90  
91      /**
92       * Logger.
93       */
94      private static final Logger LOG = LogManager.getLogger(TokenSecurity.class.getName());
95  
96      /**
97       * Access to unique INSTANCE of Settings
98       *
99       * @return the configuration instance.
100      */
101     public static TokenSecurity getInstance() {
102         LOG.traceEntry();
103         return LOG.traceExit(TokenSecurityHolder.INSTANCE);
104     }
105 
106     /**
107      * Creates a key for token signature encoded with the algorithm HS256
108      *
109      * @return key encoded in Base64
110      * @see <a href="https://fr.wikipedia.org/wiki/Base64">Base64</a>
111      */
112     public static String createKeySignatureHS256() {
113         LOG.traceEntry();
114         final Key key = MacProvider.generateKey(SignatureAlgorithm.HS256);
115         return LOG.traceExit(TextCodec.BASE64.encode(key.getEncoded()));
116     }
117 
118     /**
119      * Private constructor.
120      */
121     private TokenSecurity() {
122         LOG.traceEntry();
123         init();
124         LOG.traceExit();
125     }
126 
127     /**
128      * Init.
129      */
130     private void init() {
131         LOG.traceEntry();
132         final String token = DoiSettings.getInstance().getString(Consts.TOKEN_KEY);
133         this.tokenKey = (token == null) ? DEFAULT_TOKEN_KEY : token;
134         LOG.traceExit();
135     }
136 
137     /**
138      * Creates a token.
139      *
140      * @param userID The user that creates the token
141      * @param projectID The project ID
142      * @param timeUnit The time unit for the date expiration
143      * @param amount the amount of timeUnit for the date expiration
144      * @return JWT token
145      * @throws fr.cnes.doi.exception.TokenSecurityException if the projectID is
146      * not first registered
147      */
148     public String generate(final String userID,
149             final int projectID,
150             final TokenSecurity.TimeUnit timeUnit,
151             final int amount) throws TokenSecurityException {
152         LOG.traceEntry("Parameters : {}, {}, {} and {}", userID, projectID, timeUnit, amount);
153         List<DOIProject> projects;
154         try {
155             projects = UniqueProjectName.getInstance().getProjects();
156         } catch (DOIDbException ex) {
157             projects = new ArrayList<>();
158         }
159         final Map<Integer, String> result = projects.stream().collect(
160                 Collectors.toMap(DOIProject::getSuffix, DOIProject::getProjectname));
161         final String projectName = result.get(projectID);
162         if (projectName.isEmpty()) {
163             throw LOG.throwing(new TokenSecurityException(
164                     Status.CLIENT_ERROR_BAD_REQUEST,
165                     "No register " + PROJECT_ID + ", please create one")
166             );
167         }
168         final Date now = Date.from(Instant.now());
169         final Date expirationTime = computeExpirationDate(now, timeUnit.getTimeUnit(), amount);
170 
171         final String token = Jwts.builder()
172                 .setIssuer(DoiSettings.getInstance().getString(Consts.APP_NAME))
173                 .setIssuedAt(Date.from(Instant.now()))
174                 .setSubject(userID)
175                 .claim(PROJECT_ID, projectID)
176                 .claim(PROJECT_NAME, projectName)
177                 .setExpiration(expirationTime)
178                 .signWith(
179                         SignatureAlgorithm.HS256,
180                         TextCodec.BASE64.decode(getTokenKey())
181                 )
182                 .compact();
183         LOG.debug(String.format("token generated : %s", token));
184         return LOG.traceExit(token);
185     }
186 
187     /**
188      * Creates a token. (no project ID required)
189      *
190      * @param userID The user that creates the token
191      * @param timeUnit The time unit for the date expiration
192      * @param amount the amount of timeUnit for the date expiration
193      * @return JWT token
194      */
195     public String generate(final String userID,
196             final TokenSecurity.TimeUnit timeUnit,
197             final int amount) {
198         LOG.traceEntry("Parameters : {}, {} and {}", userID, timeUnit, amount);
199 
200         final Date now = Date.from(Instant.now());
201         final Date expirationTime = computeExpirationDate(now, timeUnit.getTimeUnit(), amount);
202 
203         final String token = Jwts.builder()
204                 .setIssuer(DoiSettings.getInstance().getString(Consts.APP_NAME))
205                 .setIssuedAt(Date.from(Instant.now()))
206                 .setSubject(userID)
207                 .setExpiration(expirationTime)
208                 .signWith(
209                         SignatureAlgorithm.HS256,
210                         TextCodec.BASE64.decode(getTokenKey())
211                 )
212                 .compact();
213         LOG.debug(String.format("token generated : %s", token));
214         return LOG.traceExit(token);
215     }
216 
217     /**
218      * Returns the token key computed by the algorithm HS256.
219      *
220      * @return the token key encoded in base64
221      */
222     public String getTokenKey() {
223         LOG.traceEntry();
224         return LOG.traceExit(this.tokenKey);
225     }
226 
227     /**
228      * Sets a custom token key.
229      *
230      * @param tokenKey token key
231      */
232     public void setTokenKey(final String tokenKey) {
233         LOG.traceEntry("Parameter : {}", tokenKey);
234         this.tokenKey = tokenKey;
235         LOG.debug(String.format("Set tokenKey to %s", tokenKey));
236         LOG.traceExit();
237     }
238 
239     /**
240      * Returns true when the token is expired otherwise false.
241      *
242      * @param token token
243      * @return true when the token is expired otherwise false.
244      */
245     public boolean isExpired(final String token) {
246         LOG.traceEntry("Parameter\n\ttoken: {}", token);
247         final Jws<Claims> jws = this.getTokenInformation(token);
248         return LOG.traceExit(jws == null);
249     }
250 
251     /**
252      * Returns the token information.
253      *
254      * @param jwtToken token JWT
255      * @return the information or null when the token is expired.
256      * @throws DoiRuntimeException - if an error happens getting information
257      * from the token
258      */
259     public Jws<Claims> getTokenInformation(final String jwtToken) throws DoiRuntimeException {
260         LOG.traceEntry("Parameter : {}", jwtToken);
261         Jws<Claims> token;
262         try {
263             token = Jwts.parser()
264                     .requireIssuer(DoiSettings.getInstance().getString(Consts.APP_NAME))
265                     .setSigningKey(TextCodec.BASE64.decode(getTokenKey()))
266                     .parseClaimsJws(jwtToken);
267         } catch (UnsupportedJwtException | MalformedJwtException | SignatureException | IllegalArgumentException ex) {
268             throw LOG.throwing(new DoiRuntimeException("Unable to get the token information", ex));
269         } catch (ExpiredJwtException e) {
270             LOG.info("Cannot get the token information", e);
271             getTokenDB().deleteToken(jwtToken);
272             token = null;
273         }
274         return LOG.traceExit(token);
275     }
276 
277     /**
278      * Returns the token DB.
279      *
280      * @return the token DB
281      */
282     public AbstractTokenDBHelper getTokenDB() {
283         LOG.traceEntry();
284         return LOG.traceExit(TokenSecurity.TOKEN_DB);
285     }
286 
287     /**
288      * Computes the expiration date.
289      *
290      * @param now the current date
291      * @param calendarTime the unit time associated to the amount
292      * @param amount amount
293      * @return the expiration date
294      */
295     private Date computeExpirationDate(final Date now,
296             final int calendarTime,
297             final int amount) {
298         LOG.traceEntry("Parameters : {}, {} and {}", now, calendarTime, amount);
299         final Calendar calendar = Calendar.getInstance();
300         calendar.setTime(now);
301         calendar.add(calendarTime, amount);
302         return LOG.traceExit(calendar.getTime());
303     }
304 
305     /**
306      * Class to handle the instance
307      *
308      */
309     private static class TokenSecurityHolder {
310 
311         /**
312          * Unique Instance unique
313          */
314         private static final TokenSecurity INSTANCE = new TokenSecurity();
315     }
316 
317     /**
318      * Time unit.
319      */
320     public enum TimeUnit {
321         /**
322          * Hour.
323          */
324         HOUR(Calendar.HOUR),
325         /**
326          * Day.
327          */
328         DAY(Calendar.DATE),
329         /**
330          * Year.
331          */
332         YEAR(Calendar.YEAR);
333 
334         /**
335          * time unit
336          */
337         private final int timeUnit;
338 
339         /**
340          * Constructor.
341          *
342          * @param timeUnit time unit
343          */
344         TimeUnit(final int timeUnit) {
345             this.timeUnit = timeUnit;
346         }
347 
348         /**
349          * Returns the time unit.
350          *
351          * @return the time unit
352          */
353         public int getTimeUnit() {
354             return this.timeUnit;
355         }
356 
357         /**
358          * Returns the time unit from a value.
359          *
360          * @param value vale
361          * @return time unit
362          */
363         public static TimeUnit getTimeUnitFrom(final int value) {
364             TimeUnit result = null;
365             final TimeUnit[] units = TimeUnit.values();
366             for (final TimeUnit unit : units) {
367                 if (unit.getTimeUnit() == value) {
368                     result = unit;
369                     break;
370                 }
371             }
372             return result;
373         }
374     }
375 }