1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
|
package org.psesquared.server.config;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SignatureException;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
/**
* The service class responsible for creating, managing and validating JWTs.
*/
@Service
@RequiredArgsConstructor
public class JwtService {
/**
* The boolean value for expressing that a JWT is not valid.
*/
private static final boolean INVALID = false;
/**
* The 1h lifespan of an access token.
*/
private static final long ACCESS_TOKEN_LIFESPAN_MILLIS
= 1000 * 60 * (long) 60;
/**
* The 24h lifespan of a URL token (verification/resetting password).
*/
private static final long URL_TOKEN_LIFESPAN_MILLIS
= ACCESS_TOKEN_LIFESPAN_MILLIS * 24;
/**
* The properties class that is used to return externally stored signing key.
*/
private final SecurityConfigProperties securityConfigProperties;
/**
* Extracts the username from a JWT for authentication.
*
* @param token The JWT
* @return The extracted username
*/
public String extractAuthUsername(final String token) {
return extractClaim(token, getAuthSigningKey(), Claims::getSubject);
}
/**
* Extracts a generic claim from the JWT.
*
* @param <T> The type of the claim
* @param token The JWT
* @param signingKey The JWT signing key
* @param claimsResolver The function to resolve the claim
* @return The extracted generic claim
* @throws ExpiredJwtException If the JWT has expired
* @throws UnsupportedJwtException If the JWT is not supported
* @throws MalformedJwtException If the JWT is malformed
* @throws SignatureException If the signature doesn't match
* @throws IllegalArgumentException If the token has an inappropriate format
*/
public <T> T extractClaim(final String token,
final Key signingKey,
final Function<Claims, T> claimsResolver)
throws ExpiredJwtException,
UnsupportedJwtException,
MalformedJwtException,
SignatureException,
IllegalArgumentException {
final Claims claims = extractAllClaims(token, signingKey);
return claimsResolver.apply(claims);
}
/**
* Generates the JWT with additional claims and a lifespan for the
* {@link org.psesquared.server.model.User} with the given details.
*
* @param additionalClaims The {@link Map} with additional claims
* @param userDetails The user details
* @param tokenLifespan The lifespan of the token
* @param signingKey The JWT signing key
* @return The generated JWT
*/
public String generateTokenString(final Map<String, Object> additionalClaims,
final UserDetails userDetails,
final long tokenLifespan,
final Key signingKey) {
return Jwts.builder()
.setClaims(additionalClaims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + tokenLifespan))
.signWith(signingKey, SignatureAlgorithm.HS256)
.compact();
}
/**
* Generates a JWT access token for the
* {@link org.psesquared.server.model.User} with the given details.
* (no additional claims).
*
* @param userDetails The user details
* @return The generated JWT access token
*/
public String generateAccessTokenString(final UserDetails userDetails) {
//no additional claims supported but open for extension with roles e.g.
return generateTokenString(new HashMap<>(),
userDetails,
ACCESS_TOKEN_LIFESPAN_MILLIS,
getAuthSigningKey());
}
/**
* Generates a JWT URL token required for authentication/resetting password
* for the {@link org.psesquared.server.model.User} with the given details
* (no additional claims).
*
* @param userDetails The user details
* @return The generated JWT access token
*/
public String generateUrlTokenString(final UserDetails userDetails) {
return generateTokenString(new HashMap<>(),
userDetails,
URL_TOKEN_LIFESPAN_MILLIS,
getUrlSigningKey());
}
/**
* Validates the given JWT for authentication against the given
* {@link UserDetails} and checks if it has not expired.
*
* @param token The to be validated JWT
* @param userDetails The user details
* @return {@code true} if the JWT is valid,
* <br>
* {@code false} otherwise
*/
public boolean isAuthTokenValid(final String token,
final UserDetails userDetails) {
return isTokenValid(token, userDetails, getAuthSigningKey());
}
/**
* Validates the given JWT for URLs against the given {@link UserDetails}
* and checks if it has not expired.
*
* @param token The to be validated JWT
* @param userDetails The user details
* @return {@code true} if the JWT is valid,
* <br>
* {@code false} otherwise
*/
public boolean isUrlTokenValid(final String token,
final UserDetails userDetails) {
return isTokenValid(token, userDetails, getUrlSigningKey());
}
/**
* Validates the given JWT against the given {@link UserDetails}
* with the given signing key and checks if it has not expired.
*
* @param token The to be validated JWT
* @param userDetails The user details
* @param signingKey The JWT signing key
* @return {@code true} if the JWT is valid,
* <br>
* {@code false} otherwise
*/
private boolean isTokenValid(final String token,
final UserDetails userDetails,
final Key signingKey) {
try {
final String username = extractUsername(token, signingKey);
return username.equals(userDetails.getUsername())
&& !isTokenExpired(token, signingKey);
} catch (ExpiredJwtException
| UnsupportedJwtException
| MalformedJwtException
| SignatureException
| IllegalArgumentException e) {
return INVALID;
}
}
/**
* Checks if the given JWT is expired.
*
* @param token The JWT
* @param signingKey The JWT signing key
* @return {@code true} if the JWT is expired,
* <br>
* {@code false} otherwise
*/
private boolean isTokenExpired(final String token, final Key signingKey) {
return extractExpiration(token, signingKey).before(new Date());
}
/**
* Extracts the username from a JWT.
*
* @param token The JWT
* @param signingKey The JWT signing key
* @return The extracted username
*/
private String extractUsername(final String token, final Key signingKey) {
return extractClaim(token, signingKey, Claims::getSubject);
}
/**
* Extracts the expiration {@link Date} of the JWT.
*
* @param token The JWT
* @param signingKey The JWT signing key
* @return The expiration date
*/
private Date extractExpiration(final String token, final Key signingKey) {
return extractClaim(token, signingKey, Claims::getExpiration);
}
/**
* Extracts all claims from the JWT in order for
* {@link #extractClaim(String, Key, Function)} to be able
* to filter out one claim.
*
* @param token The JWT
* @param signingKey The JWT signing key
* @return All claims of the JWT
* @throws ExpiredJwtException If the JWT has expired
* @throws UnsupportedJwtException If the JWT is not supported
* @throws MalformedJwtException If the JWT is malformed
* @throws SignatureException If the signature doesn't match
* @throws IllegalArgumentException If the token has an inappropriate format
*/
private Claims extractAllClaims(final String token, final Key signingKey)
throws ExpiredJwtException,
UnsupportedJwtException,
MalformedJwtException,
SignatureException,
IllegalArgumentException {
return Jwts.parserBuilder()
.setSigningKey(signingKey)
.build()
.parseClaimsJws(token)
.getBody();
}
/**
* Returns the signing {@link Key} for signing the JWT.
*
* @return The signing key
*/
private Key getAuthSigningKey() {
byte[] keyBytes
= Decoders.BASE64.decode(securityConfigProperties.jwtAuthSigningKey());
return Keys.hmacShaKeyFor(keyBytes);
}
/**
* Returns the signing {@link Key} for signing the JWT.
*
* @return The signing key
*/
private Key getUrlSigningKey() {
byte[] keyBytes
= Decoders.BASE64.decode(securityConfigProperties.jwtUrlSigningKey());
return Keys.hmacShaKeyFor(keyBytes);
}
}
|