I'm developing a Java application (using Spring Boot) and I need some help:
This application receives as input a JWT token which I process in a method. The method for now is the following:
private UsernamePasswordAuthenticationToken myMethod(HttpServletRequest request) {
// Code that gets the 'token' String
try{
Map<String, String> registry = ((Map<String, String>) (token.getBody().get("registry")));
String sub = ((String) (parsedToken.getBody().get("sub")));
UsernamePasswordAuthenticationToken finalToken = new UsernamePasswordAuthenticationToken(sub, null, null);
return finalToken;
} catch (ExpiredJwtException exception) { // Only here I have the certainty that the token has expired!
// Code that handles the exception
}
}
However, I need to implement a logic that must check in several places whether the token obtained has expired or not, without running this method every time. The only way I have to know if token has expired is the exception raised by ExpiredJwtException.
Is there any way to know if the token has expired without going through the catched exception? For example, it would be very useful if there was a "token" class that has an .isExpired attribute, or something like that.
I don't want to go into handling the exception because it means that I would always depend on the (long) code of the try block every time I need to check if a token has expired or not, and I don't want it to be.
If you use a different JWT library, you can do it easily enough. The auth0 JWT library has methods to parse and, optionally, verify the token:
import com.auth0.jwt.JWT;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
DecodedJWT jwt = JWT.decode(token);
if( jwt.getExpiresAt().before(new Date())) {
System.out.println("token is expired");
}
For this you can create another class for handling the token validation like below, in the class you will handle the ExpiredJwtException and give the extracted value back , other exceptions will throw it from the class
public class TokenValidator {
Boolean tokenExpired = true;
public boolean isTokenValid(String token) throws Exception {
validateToken(token);
return tokenExpired;
}
public Map<String,String> getExtractedData(String token) throws Exception {
return validateToken(token);
}
private Map<String, String> validateToken (String token) throws Exception{
try {
Claims claims = Jwts.parser().setSigningKey("jwtSecretKey")
.parseClaimsJws(token).getBody();
tokenExpired = false;
return getClaimsInMap(claims);
} catch (ExpiredJwtException ex) {
DefaultClaims claims = (DefaultClaims) ex.getClaims();
return getClaimsInMap(claims);
} catch (Exception e) {
throw new Exception(e);
}
}
private Map<String,String> getClaimsInMap(Claims claims) {
Map<String,String> expectedMap = new HashMap<>();
claims.entrySet().stream().forEach(entry -> expectedMap.put(entry.getKey(),entry.getValue().toString()));
return expectedMap;
}
}
This can be achieved by using claims. Below sample code can help.
// Get Expiration and compare it with new Date()
public boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
public <T> T extractClaim(String token , Function<Claims, T> claimResolver) {
final Claims claim= extractAllClaims(token);
return claimResolver.apply(claim);
}
private Claims extractAllClaims(String token) {
return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
}
Related
I've been asked to generate a token depending on the username that is asking for it. Now I'm creating a token just with a single subject but I don't know how to change the subject dinamically before creating the token depending on the body of the request.
This is what I've done so far to generate a token with a single subject:
The service class:
#Component
#RequiredArgsConstructor
public class JwtService {
#Value("${issuer}")
private String issuer;
#Value("${kid}")
private String keyId;
#Value("#{'${audience}'.split(',')}")
private List<String> audiences;
#Value("#{'${subject}'.split(',')}")
private List<String> subject;
private final JwtKeyProvider jwtKeyProvider;
public String generateToken() throws JoseException {
JwtClaims claims = new JwtClaims();
claims.setIssuer(issuer);
claims.setAudience(Lists.newArrayList(audiences));
claims.setExpirationTimeMinutesInTheFuture(60);
claims.setJwtId(keyId);
claims.setIssuedAtToNow();
claims.setNotBeforeMinutesInThePast(0);
claims.setSubject(subject);
JsonWebSignature jws = new JsonWebSignature();
jws.setPayload(claims.toJson());
jws.setHeader("typ", "JWT");
jws.setKey(jwtKeyProvider.getPrivateKey());
jws.setKeyIdHeaderValue(keyId);
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);
String jwt = jws.getCompactSerialization();
return jwt;
}
}
And the controller:
#RestController
#RequiredArgsConstructor
public class JWTController {
private final JwtService jwtService;
#PostMapping("/getToken")
public ResponseEntity getJwt(#RequestBody JwtRequest request) throws JoseException {
return ResponseEntity.ok(
JwtResponse.builder()
.token(jwtService.generateToken())
.build()
);
}
}
I could do it doing like this:
#PostMapping("/getToken")
public ResponseEntity getJwt(#RequestBody JwtRequest request) throws JoseException {
return ResponseEntity.ok(
JwtResponse.builder()
.token(jwtService.generateToken(request.getUsername()))
.build()
);
}
}
But I don't want to send any parameters in the generateToken function as I would have to change a lot of code then.
To resume I want to assign to the subject the value of the username that is sent in the body. So is there a way in the JwtService class to receive that username and set as the subject after?
Thanks in advance!
First you need to put whitelist=user1,user2 in your application.properties, because sometimes names might trigger as system variables (for example username does)
Then in JWTController you need to check if not user equals, but contains in list
#Value("#{'${whitelist}'.split(',')}")
private List<String> whitelist;
#PostMapping("/getToken")
public ResponseEntity<?> getJwt(#RequestBody JwtRequest request) throws JoseException {
if(whitelist.contains(request.username())) {
return ResponseEntity.ok(
JwtResponse.builder()
.token(jwtService.generateToken(request.username()))
.build()
);
} else {
return ResponseEntity.ok("Invalid username");
}
}
In your JWTService you need to set JWT Subject to username which passed through whitelist
claims.setSubject(username);
And finally you need to do JSON request to server
{
"username": "user2"
}
I'm using the Spring boot resource server. The authentication server issues a JWT. This JWT is re-encoded(with AES) with a key and in the Resource server, I should decode the JWT (from AES) before sending it to the JwtAuthenticator.
Now, I have a security configuration.
#Override
protected void configure(HttpSecurity http) throws Exception {
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new KeycloakRoleConverter());
http
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/users/status/check")
.hasRole("developer")
.anyRequest().authenticated()
.and()
.oauth2ResourceServer()
.jwt()
.decoder(new JWTDecoder())
.jwtAuthenticationConverter(jwtAuthenticationConverter);
}
and a JWT Decoder
import com.nimbusds.jwt.JWT;
import com.nimbusds.jwt.JWTParser;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtException;
import java.text.ParseException;
public class JWTDecoder implements JwtDecoder {
#Override
public Jwt decode(String token) throws JwtException {
//decrypt from AES here
JWT jwt = null;
try {
jwt = JWTParser.parse(token);
} catch (ParseException e) {
e.printStackTrace();
}
return null;
}
}
What should I do then? The function should return org.springframework.security.oauth2.jwt.Jwt. How can I convert String token to Jwt?
I tried the following, but a problem occurred.
private Jwt createJwt(String token, JWT parsedJwt) {
try {
Map<String, Object> headers = new LinkedHashMap<>(parsedJwt.getHeader().toJSONObject());
Map<String, Object> claims = parsedJwt.getJWTClaimsSet().getClaims();
return Jwt.withTokenValue(token)
.headers(h -> h.putAll(headers))
.claims(c -> c.putAll(claims))
.build();
} catch (Exception ex) {
if (ex.getCause() instanceof ParseException) {
throw new JwtException("There is a problem parsing the JWT.");
} else {
throw new JwtException("There is a problem decoding the JWT.");
}
}
}
The error I received:
java.lang.IllegalArgumentException: timestamps must be of type Instant: java.lang.Long
I'm using Keycloak to generate the JWT. So, the exp field of the token in the jwt.io is "exp": 1657363340,. But after parsing the JWT in my code, It changes to the Date format. So, I changed the exp to Instant and my final method is like the following:
private Jwt createJwt(String token, JWT parsedJwt) {
try {
Map<String, Object> headers = new LinkedHashMap<>(parsedJwt.getHeader().toJSONObject());
Map<String, Object> claims = parsedJwt.getJWTClaimsSet().getClaims();
Jwt.Builder finalJwt = Jwt.withTokenValue(token)
.headers(h -> h.putAll(headers))
.claims(c -> c.putAll(claims));
finalJwt.expiresAt(((Date) claims.get("exp")).toInstant());
return finalJwt.build();
} catch (Exception ex) {
if (ex.getCause() instanceof ParseException) {
throw new JwtException("There is a problem parsing the JWT: " + ex.getMessage());
} else {
throw new JwtException("There is a problem decoding the JWT: " + ex.getMessage());
}
}
}
But the problem exists yet.
It could be due to expiration date of your token is a timestamp, and it should be a number (Long). Or you are trying to parse a timestamp to number Long.
As #Jose told me, I set the value of expiration time with an Instant type of the timestamp. Then, I set it to both the exp and iat fields of the JWT. My final function is like the following:
Map<String, Object> headers = new LinkedHashMap<>(parsedJwt.getHeader().toJSONObject());
Map<String, Object> claims = new HashMap<>();
for (String key : parsedJwt.getJWTClaimsSet().getClaims().keySet()) {
Object value = parsedJwt.getJWTClaimsSet().getClaims().get(key);
if (key.equals("exp") || key.equals("iat")) {
value = ((Date) value).toInstant();
}
claims.put(key, value);
}
return Jwt.withTokenValue(token)
.headers(h -> h.putAll(headers))
.claims(c -> c.putAll(claims))
.build();
I had a sense from tutorials that returning a throwable, shouldn't change method return type.
Here are my tries:
When using handle, everything is fine until I add .timeout(), then function return type is changed to Flux<Object>
private Flux<String> exampleHandle()
{
MutableHttpRequest<String> req = HttpRequest.GET("http://localhost:8080");
return httpClient.exchange(req, TokenResponse.class)
.handle((response, sink) -> {
Optional<TokenResponse> optionalBody = response.getBody();
if (optionalBody.isEmpty()) {
sink.error(new InitializationException("Failed to fetch authentication token. Body is null."));
} else {
TokenResponse tokenResponse = optionalBody.get();
String accessToken = tokenResponse.getAccessToken();
if (accessToken != null) {
sink.next(accessToken);
} else {
sink.error(new InitializationException("Failed to fetch authentication token. Authentication token is null."));
}
}
});
// .timeout(Duration.ofSeconds(10)); // Timeout changes return type to Flux<Object>
}
When using map and Flux.error (i tried Mono.error also), function return type is changed to Flux<Object> when I introduce Flux.error in map
private Flux<String> exampleMap()
{
MutableHttpRequest<String> req = HttpRequest.GET("http://localhost:8080");
return httpClient.exchange(req, TokenResponse.class)
.map(response -> {
Optional<TokenResponse> optionalBody = response.getBody();
if (optionalBody.isEmpty()) {
return Flux.error(new InitializationException("Failed to fetch authentication token. Body is null."));
} else {
TokenResponse tokenResponse = optionalBody.get();
String accessToken = tokenResponse.getAccessToken();
if (accessToken != null) {
return accessToken;
} else {
return Flux.error(new InitializationException("Failed to fetch authentication token. Authentication token is null."));
}
}
});
}
Can someone more knowledgeable explain me what am I doing wrong? Thank you!
You should move timeout before handle:
return httpClient.exchange(req, TokenResponse.class)
.timeout(Duration.ofSeconds(10))
.handle((response, sink) -> {
As for the map case, take a look at the method signature:
Flux map(Function<? super T,? extends V> mapper)
map takes a Function<T, U> and returns a Flux<U>, returning Flux.error is invalid. You could simply use throw inside map and Reactor will turn it into a proper error signal but I think handle fits better in this case.
Useful links:
Correct way of throwing exceptions with Reactor
map vs flatMap in reactor
I make a request (any, authorization, registration, etc.) and only then I find out that I need to update the ACCESS-TOKEN, that is, I get the error 401.
Here is the authorization request:
BaseApplication.getApiClient()
.signIn(accessToken, body)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new SingleObserver<UserProfile>() {
#Override
public void onSubscribe(Disposable d) {
Log.d("-- SignInOnSubscribe", "Subscribed!");
}
#Override
public void onSuccess(UserProfile userProfile) {
if (userProfile.getErrorDetails() != null) {
onSignInFinishedCallback.onLoginFailure(userProfile.getErrorDetails());
Log.d("-- SignInOnError", userProfile.getErrorDetails());
} else {
onSignInFinishedCallback.onLoginSuccess(userProfile);
profileRepository.updateUserProfile(userProfile);
Log.d("-- SignInOnSuccess", userProfile.getName());
}
}
#Override
public void onError(Throwable e) {
Log.d("-- SignInOnError", e.getMessage());
if (e.getMessage().equals(Constants.CODE_UNAUTHORIZED)){
// Action on error 401
}
onSignInFinishedCallback.onLoginFailure(e.getMessage());
}
});
The API requests:
#POST("/api/login")
Single<UserProfile> getAccessToken(#Body Map<String, String> requestBody);
#POST("/api/abonent/login")
Single<UserProfile> signIn(#Header("X-ACCESS-TOKEN") String accessToken,
#Body Map<String, String> requestBody);
For example, the request for authorization is request 1, the request to receive TOKEN is query 2.
Question: How can I update TOKEN if I get an error in query 1 and after query 2 succeeds, back to do query 1?
I'm not sure how you receive the new token, since the return type of getAccessToken() is Single<UserProfile>. I suppose it should be Single<String> instead. Maybe this is not the case and you receive the token in a header or as a field of UserProfile. In either case, you can get an idea from the below solution and adjust it to your case.
The approach is that we create a new observable from your original one that uses a token store, which holds the most up-to-date token. We handle the 401 error using compose and onErrorResumeNext so that a token refresh request is made, the new token is saved to the token store, and the original request is retried with the new token this time.
For a more detailed explanation, see the comments in the code below:
public void signIn(final Map<String, String> body) {
Single
// Wrap the original request with a "defer" so that the access token is
// evaluated each time it is called. This is important because the refreshed
// access token should be used the second time around.
.defer(new Callable<SingleSource<UserProfile>>() {
#Override
public SingleSource<UserProfile> call() throws Exception {
return BaseApplication.getApiClient()
.signIn(accessTokenStore.getAccessToken(), body);
}
})
// Compose it with a transformer that refreshes the token in the token store and
// retries the original request, this time with the refreshed token.
.compose(retryOnNotAuthorized(body))
// The code remains the same from here.
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new SingleObserver<UserProfile>() {
#Override
public void onSubscribe(Disposable d) {
Log.d("-- SignInOnSubscribe", "Subscribed!");
}
#Override
public void onSuccess(UserProfile userProfile) {
if (userProfile.getErrorDetails() != null) {
onSignInFinishedCallback.onLoginFailure(userProfile.getErrorDetails());
Log.d("-- SignInOnError", userProfile.getErrorDetails());
} else {
onSignInFinishedCallback.onLoginSuccess(userProfile);
profileRepository.updateUserProfile(userProfile);
Log.d("-- SignInOnSuccess", userProfile.getName());
}
}
#Override
public void onError(Throwable e) {
Log.d("-- SignInOnError", e.getMessage());
if (e.getMessage().equals(Constants.CODE_UNAUTHORIZED)) {
// Action on error 401
}
onSignInFinishedCallback.onLoginFailure(e.getMessage());
}
});
}
#NonNull
private SingleTransformer<UserProfile, UserProfile> retryOnNotAuthorized(final Map<String, String> body) {
return new SingleTransformer<UserProfile, UserProfile>() {
#Override
public SingleSource<UserProfile> apply(final Single<UserProfile> upstream) {
// We use onErrorResumeNext to continue our Single stream with the token refresh
// and the retrial of the request.
return upstream.onErrorResumeNext(new Function<Throwable, SingleSource<? extends UserProfile>>() {
#Override
public SingleSource<UserProfile> apply(Throwable throwable) throws Exception {
if (throwable instanceof HttpException
&& ((HttpException) throwable).code() == 401) {
return BaseApplication.getApiClient().getAccessToken(body)
// I always use doOnSuccess() for non-Rx side effects, such as caching the token.
// I think it's clearer than doing the caching in a map() or flatMap().
.doOnSuccess(new Consumer<String>() {
#Override
public void accept(String accessToken) throws Exception {
// Save the access token to the store for later use.
accessTokenStore.storeAccessToken(accessToken);
}
})
// We don't need the result of getAccessToken() any more, so I
// think it's cleaner to convert the stream to a Completable.
.toCompletable()
// After the token is refreshed and stored, the original request
// should be repeated.
.andThen(upstream);
}
// If the error was not 401, pass through the original error
return Single.error(throwable);
}
});
}
};
}
Update: The token store is just a regular interface with a get and a store method. You should implement it either as a POJO (storing the token in a field) or you could store the token in a shared preference so that the token survives app restarts.
Using Spring Boot I am configuring the following filter
#Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilter() {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
Map<String, String> filterChainDefinitionMapping = new HashMap<>();
/*
* URL path expressions are evaluated against an incoming request in the order they are defined and the FIRST MATCH WINS. For example, let's asume that there are the following chain definitions:
/account/** = ssl, authc
/account/signup = anon
If an incoming request is intended to reach /account/signup/index.html (accessible by all 'anon'ymous users), it will never be handled!. The reason is that the /account/** pattern matched the incoming request first and 'short-circuited' all remaining definitions.
Always remember to define your filter chains based on a FIRST MATCH WINS policy!
* */
filterChainDefinitionMapping.put("/login.html", "authc");
filterChainDefinitionMapping.put("/logout", "logout");
filterChainDefinitionMapping.put("/css/**", "anon");
filterChainDefinitionMapping.put("/register/**", "anon");
filterChainDefinitionMapping.put("/**", "authc");
shiroFilter.setFilterChainDefinitionMap(filterChainDefinitionMapping);
shiroFilter.setSecurityManager(securityManager());
shiroFilter.setLoginUrl("/login.html");
shiroFilter.setSuccessUrl("/");
shiroFilter.setUnauthorizedUrl("/unauthorized.html");
Map<String, Filter> filters = new HashMap<>();
filters.put("anon", new AnonymousFilter());
filters.put("authc", new FormAuthenticationFilter());
LogoutFilter logoutFilter = new LogoutFilter();
logoutFilter.setRedirectUrl("/login.html?logout");
filters.put("logout", logoutFilter);
filters.put("roles", new RolesAuthorizationFilter());
filters.put("user", new UserFilter());
shiroFilter.setFilters(filters);
return shiroFilter;
}
However, whenever I try to login with wrong credentials the redirection never happens. I do get the "shiroLoginFailure" attribute holding the UnknownUserException.
(Logging in with the correct credentials works fine)
Any ideas?
Mariosk89, how do you resolve the /login.html?
It might be need to resolve redirect like this:
#RequestMapping("/login")
public String login(String username, String password) {
Subject currentUser = SecurityUtils.getSubject();
if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
try {
currentUser.login(new UsernamePasswordToken(username, password));
} catch (Exception e) {
logger.error(e.getLocalizedMessage(), e);
return "login";
}
return "redirect:index";
} else {
return "login";
}
}
Reference: https://github.com/lenicliu/examples/tree/master/examples-spring-boot/examples-spring-boot-shiro
For more exception solution, refer http://shiro.apache.org/10-minute-tutorial.html
try {
currentUser.login( token );
//if no exception, that's it, we're done!
} catch ( UnknownAccountException uae ) {
//username wasn't in the system, show them an error message?
} catch ( IncorrectCredentialsException ice ) {
//password didn't match, try again?
} catch ( LockedAccountException lae ) {
//account for that username is locked - can't login. Show them a message?
}
... more types exceptions to check if you want ...
} catch ( AuthenticationException ae ) {
//unexpected condition - error?
}