Restrict jwt refresh token to only one endpoint - java

I have implemented JWT token authorization & authentication from Spring resource server dependency. Here is the config file:
#Configuration
#RequiredArgsConstructor
#EnableWebSecurity
public class WebSecurityConfig {
#Value("${app.chat.jwt.public.key}")
private RSAPublicKey publicKey;
#Value("${app.chat.jwt.private.key}")
private RSAPrivateKey privateKey;
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.exceptionHandling(
exceptions ->
exceptions
.authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
.accessDeniedHandler(new BearerTokenAccessDeniedHandler()));
http.authorizeHttpRequests()
.requestMatchers("/auth/sign-in").permitAll()
.requestMatchers("/auth/sign-up").permitAll()
.anyRequest().authenticated()
.and()
.httpBasic(Customizer.withDefaults())
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
return http.build();
}
#SneakyThrows
#Bean
public JwtEncoder jwtEncoder() {
var jwk = new RSAKey.Builder(publicKey).privateKey(privateKey).build();
var jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
return new NimbusJwtEncoder(jwks);
}
#SneakyThrows
#Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withPublicKey(publicKey).build();
}
#Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
var jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
var jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
#Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source =
new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}
It works fine. I have AuthController where I have implemented endpoints for sign-in, sign-up, and refresh token. In each endpoint, I return a response with an access token and a refresh token. Here is the controller:
#RestController
#RequestMapping("/auth")
#RequiredArgsConstructor
public class AuthController {
private final JwtTokenService tokenService;
private final AuthenticationManager authManager;
private final UserDetailsService usrDetailsService;
private final UserService userService;
record LoginRequest(String username, String password) {}
#PostMapping("/sign-in")
public TokensResponse login(#RequestBody LoginRequest request) {
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(request.username, request.password);
authManager.authenticate(authenticationToken);
var user = (User) usrDetailsService.loadUserByUsername(request.username);
String accessToken = tokenService.generateAccessToken(user);
String refreshToken = tokenService.generateRefreshToken(user);
return new TokensResponse(accessToken, refreshToken);
}
record SignUpRequest(String username, String password){}
#PostMapping("/sign-up")
public TokensResponse signUp(#RequestBody SignUpRequest signUpRequest) {
User registeredUser = userService.register(new AuthRequestDto(signUpRequest.username(), signUpRequest.password()));
String accessToken = tokenService.generateAccessToken(registeredUser);
String refreshToken = tokenService.generateRefreshToken(registeredUser);
return new TokensResponse(accessToken, refreshToken);
}
#PreAuthorize("hasRole('REFRESH_TOKEN')")
#GetMapping("/token/refresh")
public TokensResponse refreshToken(HttpServletRequest request) {
String headerAuth = request.getHeader("Authorization");
String previousRefreshToken = headerAuth.substring(7);
String username = tokenService.parseToken(previousRefreshToken);
var user = (User) usrDetailsService.loadUserByUsername(username);
String accessToken = tokenService.generateAccessToken(user);
String refreshToken = tokenService.generateRefreshToken(user);
return new TokensResponse(accessToken, refreshToken);
}
record TokensResponse(String accessToken, String refreshToken) {}
}
And here is TokenService class where I generate those tokens:
#Service
#RequiredArgsConstructor
public class JwtTokenServiceImpl implements JwtTokenService {
private final JwtEncoder jwtEncoder;
#Override
public String generateAccessToken(User user) {
Instant now = Instant.now();
String scope = user.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(" "));
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("self")
.issuedAt(now)
.expiresAt(now.plus(2, ChronoUnit.MINUTES))
.subject(user.getUsername())
.claim("scope", scope)
.build();
return this.jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
}
#Override
public String generateRefreshToken(User user) {
Instant now = Instant.now();
String scope = "ROLE_REFRESH_TOKEN";
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("self")
.issuedAt(now)
.expiresAt(now.plus(10, ChronoUnit.MINUTES))
.subject(user.getUsername())
.claim("scope", scope)
.build();
return this.jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
}
#Override
public String parseToken(String token) {
try {
SignedJWT decodedJWT = SignedJWT.parse(token);
return decodedJWT.getJWTClaimsSet().getSubject();
} catch (ParseException e) {
e.printStackTrace();
}
return null;
}
}
What I want to do is to restrict the refresh token to be used only for the refresh endpoint. Because what's the point of having a short-term live access token if you can use a refresh token for all endpoints? I have tried to give the refresh token scope REFRESH_TOKEN and added #PreAuthorize("hasRole('REFRESH_TOKEN')") annotation for the refresh token endpoint. But it doesn't work(I can still send access token to refresh endpoint and get new tokens), because Spring doesn't look for claims from token. He just loads the user from the database by username from token and checks his roles from here.
Please suggest how can I make the refresh token restricted only to one endpoint. Also would be great to make him one-time use but seems that I would need to store tokens somewhere for that.

the refresh token is bound to the
client to which it was issued.
source https://www.rfc-editor.org/rfc/rfc6749#section-6
Refresh token affect to client scope (it means all end-points). Therefore your expected is not feasibility.

Related

Spring Boot 2.7.5 Security 401 Unuathorized

I'm struggling with 401 unuathorized in postman while i try to enter a secured endpoint, I'm passing correct data, but spring security doesn't pass me on, I think I might miss something in DetailsService or somewhere Creating new user
Trying to authorize
Security config
#EnableWebSecurity
#RequiredArgsConstructor
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig {
private final CustomAuthProvider provider;
private final AuthenticationFailureHandler failureHandler;
#Bean
public AuthenticationManager authenticationManager (HttpSecurity http) throws Exception {
AuthenticationManagerBuilder builder = http.getSharedObject(AuthenticationManagerBuilder.class);
builder.authenticationProvider(provider);
return builder.build();
}
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests(auth -> {
auth.antMatchers(HttpMethod.POST, "/register").permitAll();
auth.antMatchers(HttpMethod.POST, "/login").hasRole("USER");
auth.antMatchers("/user/**").permitAll();
auth.antMatchers("/getusers").hasRole("USER");
auth.antMatchers("/moderator/**").hasRole("MODERATOR");
auth.antMatchers("/admin/**").hasRole("ADMIN");
})
.httpBasic(withDefaults())
.sessionManagement()
.sessionCreationPolicy(STATELESS);
return http.build();
}
Details Service
#Service
#RequiredArgsConstructor // Generates a constructor with required arguments.
public class DetailsService implements UserDetailsService {
private final UserService userService;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
var user = userService.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("Couldn't find \""+ username +"\" username");
}
if (user.getRoles() == null || user.getRoles().isEmpty()) {
throw new RuntimeException("User \"" + username + "\" has no roles");
}
Collection<GrantedAuthority> authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList());
return new User(user.getUsername(), user.getPassword(), user.isActive(),
!user.isExpired(), !user.isCredentialsexpired(), !user.isBlocked(), authorities);
}
Method with creating roles
#RequiredArgsConstructor
#Component
public class ApplicationStartRunner implements CommandLineRunner {
private final RoleRepository roleRepository;
#Override
public void run(String... args) throws Exception {
Role roleUser = new Role(1L, "123", "ROLE_USER");
Role roleModerator = new Role(2L, "456", "ROLE_MODERATOR");
Role roleAdmin = new Role(3L, "789", "ROLE_ADMIN");
roleRepository.saveAll(List.of(roleUser, roleAdmin, roleModerator));
}
EDIT: That's my password encoder bean
#Configuration
public class SecurityConfig {
#Bean
PasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}
}
And register method
#Override
public UserDTO createUser(RegisterDTO user) {
if (user.equals(userRepository.findByUsername(user.getUsername()))) {
throw new RuntimeException("This nickname is already taken");
}
if (user.equals(userRepository.findByEmail(user.getEmail()))) {
throw new RuntimeException("This email is already taken");
}
// Encoding password
user.setPassword(encoder.encode(user.getPassword()));
// On creating new Account it's going to have USER role
Role role = roleRepository.findByName("ROLE_USER");
String username = user.getUsername();
String password = user.getPassword();
String email = user.getEmail();
User dto = buildUser(username, password, email, role);
userRepository.save(dto);
return UserDTO.builder()
.username(username)
.password(password)
.email(email)
.build();
}
I think this is pretty clear.
That /getusers endpoint is protected and user must have USER role which obviously don't have. By default I think it should be ROLE_USER as spring is using that ROLE_ prefix. But you can specify any role that should be fetched from DB during user authorization.

Unable to set logged user from SecurityContextHolder in Spring Boot

I am trying to implement authentication using JWT in Spring Boot. In the login function I am setting the authentication in the SecurityContextHolder in order to be able to get it when requested. The login functionality works, but when I try to get the current logged user, I am getting unathorized. I debugged and the SecurityContextHolder gives anonymous user. Why is this happening?
UserController class:
#RestController
#CrossOrigin(origins = "http://localhost:3000")
#RequestMapping("/api")
public class UserController {
#Autowired
private UserService userService;
#Autowired
private CustomAuthenticationManager authenticationManager;
#Autowired
private JwtEncoder jwtEncoder;
#PostMapping("/user/login")
public ResponseEntity<User> login(#RequestBody #Valid AuthDto request) {
try {
Authentication authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword()));
String userEmail = (String) authentication.getPrincipal();
User user = userService.findUserByEmail(userEmail);
Instant now = Instant.now();
long expiry = 36000L;
String scope = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(joining(" "));
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("uni.pu")
.issuedAt(now)
.expiresAt(now.plusSeconds(expiry))
.subject(format("%s,%s", user.getId(), user.getEmail()))
.claim("roles", scope)
.build();
String token = this.jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
SecurityContextHolder.getContext().setAuthentication(authentication);
return ResponseEntity.ok()
.header(HttpHeaders.AUTHORIZATION, token)
.body(user);
} catch (BadCredentialsException ex) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
#GetMapping("/user/current")
public ResponseEntity<User> getLoggedUser(){
try{
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return ResponseEntity.ok()
.body((User)auth.getPrincipal());
}
catch(Exception e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
}
WebSecurityConfig:
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true, jsr250Enabled = true, prePostEnabled = true)
public class WebSecurityConfig {
private static final String[] WHITE_LIST_URLS = {"/api/user/login", "/api/user/current"};
#Autowired
private MyUserDetailsService userDetailsService;
#Value("${jwt.public.key}")
private RSAPublicKey rsaPublicKey;
#Value("${jwt.private.key}")
private RSAPrivateKey rsaPrivateKey;
#Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder(10);
}
#Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(encoder());
return authProvider;
}
#Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// Enable CORS and disable CSRF
http = http.cors().and().csrf().disable();
// Set session management to stateless
http = http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and();
// Set unauthorized requests exception handler
http = http.exceptionHandling(
(exceptions) -> exceptions.authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
.accessDeniedHandler(new BearerTokenAccessDeniedHandler()));
http = http.authenticationProvider(authenticationProvider());
// Set permissions on endpoints
http.authorizeHttpRequests().antMatchers(WHITE_LIST_URLS).permitAll().antMatchers("/api/**").authenticated()
// Our private endpoints
.anyRequest().authenticated()
// Set up oauth2 resource server
.and().httpBasic(Customizer.withDefaults()).oauth2ResourceServer().jwt();
return http.build();
}
#Bean
public JwtEncoder jwtEncoder() {
JWK jwk = new RSAKey.Builder(this.rsaPublicKey).privateKey(this.rsaPrivateKey).build();
JWKSource<SecurityContext> jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
return new NimbusJwtEncoder(jwks);
}
// Used by JwtAuthenticationProvider to decode and validate JWT tokens
#Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withPublicKey(this.rsaPublicKey).build();
}
// Extract authorities from the roles claim
#Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
#Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
#Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration)
throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
#Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
String hierarchy = "ROLE_ADMIN > ROLE_INSPECTOR \n ROLE_INSPECTOR > ROLE_STUDENT";
roleHierarchy.setHierarchy(hierarchy);
return roleHierarchy;
}
}
In Spring documentation, section Storing the SecurityContext between requests says :
Depending on the type of application, there may need to be a strategy
in place to store the security context between user operations. In a
typical web application, a user logs in once and is subsequently
identified by their session Id. The server caches the principal
information for the duration session. In Spring Security, the
responsibility for storing the SecurityContext between requests falls
to the SecurityContextPersistenceFilter, which by default stores the
context as an HttpSession attribute between HTTP requests. It restores
the context to the SecurityContextHolder for each request and,
crucially, clears the SecurityContextHolder when the request completes
So basically, when you create the security context manually no session object is created. Only when the request finishes processing does the Spring Security mechanism realize that the session object is null (when it tries to store the security context to the session after the request has been processed).
At the end of the request Spring Security creates a new session object and session ID. However this new session ID never makes it to the browser because it occurs at the end of the request, after the response to the browser has been made. This causes the new session ID (and hence the Security context containing my manually logged on user) to be lost when the next request contains the previous session ID.
I found two solutions to hande this situation:
1.First solution : Save SecurityContext object in session and then extract it from session when needed :
HttpSession session = request.getSession(true);
session.setAttribute("SPRING_SECURITY_CONTEXT", securityContext);
and then, extract it from session.
Second solution according to this answer would be to refactor your login function like this:
private void doAutoLogin(String username, String password, HttpServletRequest request) {
try {
// Must be called from request filtered by Spring Security, otherwise SecurityContextHolder is not updated
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
token.setDetails(new WebAuthenticationDetails(request));
Authentication authentication = this.authenticationProvider.authenticate(token);
logger.debug("Logging in with [{}]", authentication.getPrincipal());
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (Exception e) {
SecurityContextHolder.getContext().setAuthentication(null);
logger.error("Failure in autoLogin", e);
}
};
This is how you shoud get authenticationProvider :
#Configuration public class WebConfig extends WebSecurityConfigurerAdapter {
#Bean
public AuthenticationManager authenticationProvider() throws Exception{
return super.authenticationManagerBean();
}
}

Can I generate a JWT for my Spring Security app without have a user?

I want to generate a JWT with expiration date for people to access the system without have to register and create a user. Is this posible? I have tried with JwtTokenProvider but it needs a LoginRequest to work also with Jwts.builder() also needs a user.
if you want to use spring security you can create security configration and extends WebSecurityConfigurerAdapter. Then important point is custom provider.
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private CustomAuthenticationProvider customAuthenticationProvider;
#Autowired
private JWTConfigurer securityConfigurerAdapter;
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
//you can write customAuth provider
auth.authenticationProvider(customAuthenticationProvider);
}
#Override
public void configure(WebSecurity web) throws Exception {
//Some ignore etc.
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.and()
.csrf()
.disable().and()
.headers()
.frameOptions()
.disable()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
//important here
.antMatchers("/api/v1/authentication/**").permitAll()
.anyRequest().authenticated()
.and()
.apply(securityConfigurerAdapter);
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return this.authenticationManager();
}
}
This is Filter class which extends genericFilterBean. Every request is monitored in this class
You will check to it is right token
I create token TokenProvider class and depend into JWTFilter then use valideToken method.
if token is sended and not validate then throw exception
if token is not sended then go super method so the flow is continue and works auth.authenticationProvider. Spring knows to start customAuthenticationProvider behind the scene becouse of you set into SecurityConfiguration class
#Component
public class JWTFilter extends GenericFilterBean {
private final Logger log = LoggerFactory.getLogger(JWTFilter.class);
#Autowired
private TokenProvider tokenProvider;
#Autowired
private MessageSource msgSource;
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
try {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
//Resolve method is optional what you want to use
String jwt = resolveToken(httpServletRequest);
if (StringUtils.hasText(jwt)) {
//token validation is important becouse of expires date into token
// and you will check expired date
if (this.tokenProvider.validateToken(jwt)) {
String jwtMd5 = DigestUtils.md5Hex(jwt);
MDC.put("jwt",jwtMd5);
Authentication authentication = this.tokenProvider.getAuthentication(jwt);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(servletRequest, servletResponse);
}catch(Exception ex){
handleException((HttpServletResponse) servletResponse,ex);
}
}
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(JWTConfigurer.AUTHENTICATION_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
String jwt = bearerToken.substring(7, bearerToken.length());
return jwt;
}
String jwt = request.getParameter(JWTConfigurer.AUTHENTICATION_TOKEN);
if (StringUtils.hasText(jwt)) {
return jwt;
}
return null;
}
}
You can use this class for create token or validate token
you define expire date for token expiration into create method.
#Component public class TokenProvider {
private final Logger log = LoggerFactory.getLogger(TokenProvider.class);
private static final String AUTHORITIES_KEY = "auth";
private static final String WTS_USER_ID = "wtsUserId";
private static final String CHANNEL_PERMISSIONS = "channelPermissions";
private static final String APP_ROLES = "appRoles";
private String secretKey;
private long tokenValidityInSeconds;
#Autowired private ApplicationProperties applicationProperties;
#PostConstruct public void init() {
this.tokenValidityInSeconds = 1000;
}
public String createToken(Authentication authentication, Boolean rememberMe) { List<String> authorities = authentication.getAuthorities().stream().map(authority -> authority.getAuthority())
.collect(Collectors.toList());
//Token creation format is this
// token will be three part important parts are claims and sign
// claims refers to body to use datas
// sign will use to validation
return Jwts.builder().setSubject(authentication.getName()).claim(AUTHORITIES_KEY, authorities)
.claim(WTS_USER_ID, ((JWTAuthentication) authentication).getWtsUserId())
.claim(CHANNEL_PERMISSIONS, ((JWTAuthentication) authentication).getChannelPermissions())
.claim(APP_ROLES, ((JWTAuthentication) authentication).getAppRoles())
.signWith(SignatureAlgorithm.HS512, secretKey).setExpiration(tokenValidityInSeconds).compact(); }
#SuppressWarnings("unchecked") public Authentication getAuthentication(String token) { Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody();
List<String> list = (List<String>) claims.get(AUTHORITIES_KEY); Collection<? extends GrantedAuthority> authorities = list.stream()
.map(authority -> new SimpleGrantedAuthority(authority)).collect(Collectors.toList()); Integer wtsUserId = (Integer) claims.get(WTS_USER_ID); List<String> appRoles = (List<String>) claims.get(APP_ROLES);
ObjectMapper objectMapper = new ObjectMapper(); List<ChannelPermission> channelPermissions = objectMapper.convertValue(claims.get(CHANNEL_PERMISSIONS),
new TypeReference<List<ChannelPermission>>() {
});
return new JWTAuthentication(token, wtsUserId, claims.getSubject(), authorities, channelPermissions, appRoles); }
public boolean validateToken(String authToken) {
try {
Jwts.parser().setSigningKey(secretKey).parseClaimsJws(authToken);
return true;
} catch (SignatureException e) {
log.info("Invalid JWT signature: " + e.getMessage());
return false;
} } }
This is controller who anonymous people get a JWT token .You can give a new JWT token all request and this JWT has expires date becouse of you set a expiration date into provider class.
#RequestMapping(value = "/login", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
public ApiResponse login(#RequestBody #Validated AuthenticationRequestDTO authenticationRequest) {
Authentication authentication = this.authenticationManager.authenticate(new JWTAuthentication(
RandomUid, RandomPwd, "anonymous"));
SecurityContextHolder.getContext().setAuthentication(authentication);
String token = tokenProvider.createToken(authentication, false);
return new ApiResponse(ApiResponseStatus.SUCCESS, new AuthenticationResponseDTO(token));
}

Spring security JWT refresh token not expiring

i am new to spring and i'm working on spring boot REST with spring security and currently I implemented JWT token. I have some questions but can't seem to find an answer to them. I tried adding a refresh token.
At first i thought i will store it in database with user, but spring security does everything automatically and i can't seem to find how to store it at a given field of table user.
So, moving on i decided i will try sticking with spring security automation and I set refresh token expiration time to 10 seconds to test if it expires, but sadly it does not work as intended - I can use refresh token for as long as I want and generate new tokens with it.
So here I have a couple of questions:
1. How do i make refresh token expire after given time? Here's my security config
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Value("${security.signing-key}")
private String signingKey;
#Value("${security.encoding-strength}")
private Integer encodingStrength;
#Value("${security.security-realm}")
private String securityRealm;
#Autowired
private UserDetailsService userDetailsService;
#Bean
#Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
#Bean
public PasswordEncoder passwordEncoder() {
PasswordEncoder encoder = new BCryptPasswordEncoder();
return encoder;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().httpBasic()
.realmName(securityRealm).and().csrf().disable();
}
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(signingKey);
return converter;
}
#Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
#Bean
#Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
defaultTokenServices.setRefreshTokenValiditySeconds(10);
return defaultTokenServices;
}
}
Is it possible to pass refresh token to database and manually check if token is valid, because that was my first idea.
I did find an answer, just forgot to update my ticket. So here it goes, by default JwtTokenStore does not support refresh tokens. Here's JwtTokenStore source code.
So what this means, is that enabling token in settings, won't actually make it work. What i did, was create my own JWT token store that extends JwtTokenStore and write my own refresh token logic.
public class MyJwtTokenStore extends JwtTokenStore {
#Autowired
UserRepository userRepository;
public MyJwtTokenStore(JwtAccessTokenConverter jwtTokenEnhancer) {
super(jwtTokenEnhancer);
}
#Override
public void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication) {
String username = authentication.getUserAuthentication().getName();
User user = userRepository.findByEmail(username);
user.setToken(refreshToken.getValue());
userRepository.save(user);
}
#Override
public OAuth2RefreshToken readRefreshToken(String token) {
OAuth2Authentication authentication = super.readAuthentication(token);
String username = authentication.getUserAuthentication().getName();
User user = userRepository.findByEmail(username);
if (!token.equals(user.getToken())) {
return null;
}
OAuth2RefreshToken refreshToken = new DefaultOAuth2RefreshToken(token);
return refreshToken;
}
#Override
public void removeRefreshToken(OAuth2RefreshToken token) {
OAuth2Authentication authentication = super.readAuthentication(token.getValue());
String username = authentication.getUserAuthentication().getName();
User user = userRepository.findByEmail(username);
user.setToken(null);
userRepository.save(user);
}
}
After this, i just updated my TokenStore Bean
#Bean
public TokenStore tokenStore() {
MyJwtTokenStore jwtTokenStore = new MyJwtTokenStore(accessTokenConverter());
return jwtTokenStore;
// return new JwtTokenStore(accessTokenConverter());
}
And at the end I added remaining settings to my AuthorizationServerConfigurerAdapter class to support refresh token and give it time of validity.
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
#Value("${security.jwt.client-id}")
private String clientId;
#Value("${security.jwt.client-secret}")
private String clientSecret;
#Value("${security.jwt.grant-type}")
private String grantType;
#Value("${security.jwt.grant-type-other}")
private String grantTypeRefresh;
#Value("${security.jwt.scope-read}")
private String scopeRead;
#Value("${security.jwt.scope-write}")
private String scopeWrite = "write";
#Value("${security.jwt.resource-ids}")
private String resourceIds;
#Autowired
private TokenStore tokenStore;
#Autowired
private JwtAccessTokenConverter accessTokenConverter;
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
private AppUserDetailsService userDetailsService;
#Autowired
private PasswordEncoder passwordEncoder;
#Override
public void configure(ClientDetailsServiceConfigurer configurer) throws Exception {
configurer.inMemory()
.withClient(clientId)
.secret(passwordEncoder.encode(clientSecret))
.authorizedGrantTypes(grantType, grantTypeRefresh).scopes(scopeRead, scopeWrite)
.resourceIds(resourceIds).autoApprove(false).accessTokenValiditySeconds(1800) // 30min
.refreshTokenValiditySeconds(86400); //24 hours
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
enhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
endpoints.tokenStore(tokenStore).accessTokenConverter(accessTokenConverter).tokenEnhancer(enhancerChain)
.reuseRefreshTokens(false).authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}
#Bean
public FilterRegistrationBean corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*"); // http://localhost:4200
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return bean;
}
}

Accessing a Spring OAuth 2 JWT payload inside the Resource Server controller?

I'm going through this tutorial on how to setup spring boot oauth with jwt. It covers decoding the JWT token using Angular, but how do we decode it and get access to custom claims inside the Resource Server controller?
For example with JJWT it can be done like this (Based on this article):
String subject = "HACKER";
try {
Jws jwtClaims =
Jwts.parser().setSigningKey(key).parseClaimsJws(jwt);
subject = claims.getBody().getSubject();
//OK, we can trust this JWT
} catch (SignatureException e) {
//don't trust the JWT!
}
And Spring has a JWTAccessTokenConverter.decode() method, but the javadoc is lacking, and it is protected.
Here is how I am accessing custom JWT claims in Spring Boot:
1) Get Spring to copy JWT content into Authentication:
#Configuration
#EnableResourceServer
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends ResourceServerConfigurerAdapter{
#Override
public void configure(ResourceServerSecurityConfigurer config) {
config.tokenServices( createTokenServices() );
}
#Bean
public DefaultTokenServices createTokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore( createTokenStore() );
return defaultTokenServices;
}
#Bean
public TokenStore createTokenStore() {
return new JwtTokenStore( createJwtAccessTokenConverter() );
}
#Bean
public JwtAccessTokenConverter createJwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setAccessTokenConverter( new JwtConverter() );
return converter;
}
public static class JwtConverter extends DefaultAccessTokenConverter implements JwtAccessTokenConverterConfigurer {
#Override
public void configure(JwtAccessTokenConverter converter) {
converter.setAccessTokenConverter(this);
}
#Override
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
OAuth2Authentication auth = super.extractAuthentication(map);
auth.setDetails(map); //this will get spring to copy JWT content into Authentication
return auth;
}
}
}
2) Access token content anywhere in your code:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Object details = authentication.getDetails();
if ( details instanceof OAuth2AuthenticationDetails ){
OAuth2AuthenticationDetails oAuth2AuthenticationDetails = (OAuth2AuthenticationDetails)details;
Map<String, Object> decodedDetails = (Map<String, Object>)oAuth2AuthenticationDetails.getDecodedDetails();
System.out.println( "My custom claim value: " + decodedDetails.get("MyClaim") );
}

Categories