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();
}
}
Related
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.
I inherited a Spring Boot 1.5 project which cannot be migrated up at the moment and need to work on session registry to manage users (ex: list logged users, email users of production updates etc.)
I tried all existing SO solutions I could find, but all of them provide a null result for sessionRegistry().getAllPrincipals(). My security config looks like:
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.httpBasic().disable()
.formLogin().disable()
.headers().frameOptions().sameOrigin()
.and()
.sessionManagement().maximumSessions(1).sessionRegistry(sessionRegistry());
}
#Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
#Bean
public ServletListenerRegistrationBean<HttpSessionEventPublisher> httpSessionEventPublisher() {
return new ServletListenerRegistrationBean<>(new HttpSessionEventPublisher());
}
}
My Application config looks like:
#EnableRedisHttpSession(maxInactiveIntervalInSeconds = 3600000)
#EnableDiscoveryClient
#EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
#EnableZuulProxy
#EnableFeignClients
#EnableSwagger2
#SpringBootApplication
#RibbonClients({
#RibbonClient(name = "employeeService", configuration = StickySessionEditorRibbonConfiguration.class)
})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
#Bean
public PreFilter preFilter() {
return new PreFilter();
}
#Bean
public PostFilter postFilter() {
return new PostFilter();
}
#Bean
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("OPTIONS");
config.addAllowedMethod("HEAD");
config.addAllowedMethod("GET");
config.addAllowedMethod("PUT");
config.addAllowedMethod("POST");
config.addAllowedMethod("DELETE");
config.addAllowedMethod("PATCH");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
#Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("JSESSIONID");
serializer.setCookiePath("/");
serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
serializer.setCookieMaxAge(3600000);
return serializer;
}
}
Relevant code to access the session registry looks like this:
public class TestController extends BaseController {
#Autowired
private SessionRegistry sessionRegistry;
...
public List<User> findAllLoggedInUsers() {
final List<Object> allPrincipals = sessionRegistry.getAllPrincipals();
}
}
Using the actuator/bean endpoint I can see that the SessionRegistry Bean is active.
I am logging in successfully from a couple of browsers but allPrincipals is always size 0 before and after the logins.
Am at a loss why, and any help here is much appreciated.
Based on #M.Deinum 's comment regarding disabled login I want to add that the project uses Zuul Filters (preFilter and PostFilter) as indicated in the application config. We have an account-manager service completely different from this api-gateway service, which authenticates users based on simple login/password.
The logic in preFilter looks like this:
public class PreFilter extends BaseFilter {
#Autowired
SessionRegistry sessionRegistry;
#Autowired
private SessionRepository sessionRepository;
#Override
public String filterType() {
return "pre";
}
#Override
public boolean shouldFilter() {
return true;
}
#Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest req = ctx.getRequest();
HttpSession session = req.getSession();
try {
String uri = req.getRequestURI();
// user assumed is not authenticated
String authToken = null;
//Login code
if (uri.contains("/api/public/authorization/login") && req.getMethod().equals("POST")) {
session.removeAttribute(AUTH_TOKEN_HEADER);
LoginRequest request = createLoginRequest(req);
/* LoginRequest basically contains "userName" and "password" entered by user */
ResponseEntity<MessageWrapper<String>> response = accountManagerFeignClient.authenticate(loginRequest);
authToken = response.getBody().getData();
if (authToken != null) {
session.setAttribute(AUTH_TOKEN_HEADER, authToken);
ctx.setResponseStatusCode(HttpStatus.OK.value());
ctx.setSendZuulResponse(false);
return null;
}
// authToken == null implies the user was not authenticated by accountManager
} else if ("internal or public apis are called, they won't need authentication") {
// user remains unauthenticated, which is fine for public or internal apis
return null;
} else {
// Assume this is a protected API and provide authToken if one exists
authToken = (String) session.getAttribute(AUTH_TOKEN_HEADER);
}
if (authToken == null)
throw new Exception(UNAUTHORIZED + ". Log String: " + logString);
// Validated user will go through here
ctx.addZuulRequestHeader(AUTH_TOKEN_HEADER, authToken);
} catch (Exception ex) {
ctx.setResponseBody(UNAUTHORIZED);
ctx.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
ctx.setSendZuulResponse(false);
}
return null;
}
}
The only relevant logic in postFilter (similar to preFilter) disables sessions during logout in this manner:
if (authToken != null) {
session.removeAttribute(AUTH_TOKEN_HEADER);
session.removeAttribute(StickySessionEditorRule.STICKY_ID);
session.removeAttribute(StickySessionWSGRule.STICKY_ID);
ctx.setResponseBody(LOGGED_OUT);
ctx.setResponseStatusCode(HttpStatus.OK.value());
ctx.setSendZuulResponse(false);
}
session.invalidate();
My other time-consuming option would be to use the HTTPSessionBindingListener as shown here . I have not tried that yet.
Finally, if none of the above work, how could I work directly with redis and do a findAll() ? It looks like there is a SessionRepository, but I could not find a documented way of using it.
Thank You.
I'm trying to get a new access token using a refresh token in Spring Boot with OAuth2. It should be done as following: POST: url/oauth/token?grant_type=refresh_token&refresh_token=....
It works fine if I'm using InMemoryTokenStore because the token is tiny and contains only digits/letters but right now I'm using a JWT token and as you probably know it has 3 different parts which probably are breaking the code.
I'm using the official migration guide to 2.4.
When I try to access the URL above, I'm getting the following message:
{
"error": "invalid_token",
"error_description": "Cannot convert access token to JSON"
}
How do I pass a JWT token in the params? I tried to set a breakpoint on that message, so I could see what the actual argument was, but it didn't get to it for some reason.
/**
* The Authorization Server is responsible for generating tokens specific to a client.
* Additional information can be found here: https://www.devglan.com/spring-security/spring-boot-security-oauth2-example.
*/
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
#Value("${user.oauth2.client-id}")
private String clientId;
#Value("${user.oauth2.client-secret}")
private String clientSecret;
#Value("${user.oauth2.accessTokenValidity}")
private int accessTokenValidity;
#Value("${user.oauth2.refreshTokenValidity}")
private int refreshTokenValidity;
#Autowired
private ClientDetailsService clientDetailsService;
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.inMemory()
.withClient(clientId)
.secret(bCryptPasswordEncoder.encode(clientSecret))
.authorizedGrantTypes("password", "authorization_code", "refresh_token")
.scopes("read", "write", "trust")
.resourceIds("api")
.accessTokenValiditySeconds(accessTokenValidity)
.refreshTokenValiditySeconds(refreshTokenValidity);
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)
.tokenStore(tokenStore())
.userApprovalHandler(userApprovalHandler())
.accessTokenConverter(accessTokenConverter());
}
#Bean
public UserApprovalHandler userApprovalHandler() {
ApprovalStoreUserApprovalHandler userApprovalHandler = new ApprovalStoreUserApprovalHandler();
userApprovalHandler.setApprovalStore(approvalStore());
userApprovalHandler.setClientDetailsService(clientDetailsService);
userApprovalHandler.setRequestFactory(new DefaultOAuth2RequestFactory(clientDetailsService));
return userApprovalHandler;
}
#Bean
public TokenStore tokenStore() {
JwtTokenStore tokenStore = new JwtTokenStore(accessTokenConverter());
tokenStore.setApprovalStore(approvalStore());
return tokenStore;
}
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
final RsaSigner signer = new RsaSigner(KeyConfig.getSignerKey());
JwtAccessTokenConverter converter = new JwtAccessTokenConverter() {
private JsonParser objectMapper = JsonParserFactory.create();
#Override
protected String encode(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
String content;
try {
content = this.objectMapper.formatMap(getAccessTokenConverter().convertAccessToken(accessToken, authentication));
} catch (Exception ex) {
throw new IllegalStateException("Cannot convert access token to JSON", ex);
}
Map<String, String> headers = new HashMap<>();
headers.put("kid", KeyConfig.VERIFIER_KEY_ID);
return JwtHelper.encode(content, signer, headers).getEncoded();
}
};
converter.setSigner(signer);
converter.setVerifier(new RsaVerifier(KeyConfig.getVerifierKey()));
return converter;
}
#Bean
public ApprovalStore approvalStore() {
return new InMemoryApprovalStore();
}
#Bean
public JWKSet jwkSet() {
RSAKey.Builder builder = new RSAKey.Builder(KeyConfig.getVerifierKey())
.keyUse(KeyUse.SIGNATURE)
.algorithm(JWSAlgorithm.RS256)
.keyID(KeyConfig.VERIFIER_KEY_ID);
return new JWKSet(builder.build());
}
}
I assume that the Cannot convert access token to JSON might have been due to incorrectly pasted token.
As for Invalid refresh token, it occurs because when JwtTokenStore reads the refresh token, it validates the scopes and revocation with InMemoryApprovalStore. However, for this implementation, the approvals are registered only during authorization through /oauth/authorize URL (Authorisation Code Grant) by the ApprovalStoreUserApprovalHandler.
Especially for the Authorisation Code Grant (authorization_code), you want to have this validation, so that the refresh token request will not be called with an extended scope without the user knowledge. Moreover, it's optional to store approvals for future revocation.
The solution is to fill the ApprovalStore with the Approval list for all resource owners either statically or dynamically. Additionally, you might be missing setting the user details service endpoints.userDetailsService(userDetailsService) which is used during the refresh process.
Update:
You can verify this by creating pre-filled InMemoryApprovalStore:
#Bean
public ApprovalStore approvalStore() {
InMemoryApprovalStore approvalStore = new InMemoryApprovalStore();
Date expirationDate = Date.from(Instant.now().plusSeconds(3600));
List<Approval> approvals = Stream.of("read", "write", "trust")
.map(scope -> new Approval("admin", "trusted", scope, expirationDate,
ApprovalStatus.APPROVED))
.collect(Collectors.toList());
approvalStore.addApprovals(approvals);
return approvalStore;
}
I would also take a look at implementing it in the storeRefreshToken()/storeAccessToken() methods of JwtTokenStore, as they have an empty implementation, and the method parameters contain all the necessary data.
I have problem with validating user credentials. When I give correct credentials first time everything goes OK but giving invalid credentials first and then give correct ones I get invalid credentials error. I use Postman Basic
Auth.
My config class:
#Configuration
#EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserService userService;
#Autowired
private CustomAuthenticationEntryPoint authenticationEntryPoint;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable().authorizeRequests()
.antMatchers(HttpMethod.POST ,"/login").permitAll()
.antMatchers("/admin").hasAuthority("ADMIN")
.anyRequest().authenticated().and().exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.ALWAYS).and()
.logout()
.deleteCookies("remove")
.invalidateHttpSession(true);
http.rememberMe().disable();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(this.userService)
.and().eraseCredentials(true);
}
#Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
And my controller class
#PostMapping
public ResponseEntity<?> loginButtonClicked(HttpServletRequest request) {
HttpSession session = request.getSession();
final String authorization = request.getHeader("Authorization");
String[] authorizationData=null;
if (authorization != null && authorization.startsWith("Basic")) {
// Authorization: Basic base64credentials
String base64Credentials = authorization.substring("Basic" .length()).trim();
String credentials = new String(Base64.getDecoder().decode(base64Credentials),
Charset.forName("UTF-8"));
// credentials = username:password
authorizationData = credentials.split(":", 2);
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(authorizationData[0], authorizationData[1],Arrays.asList(new SimpleGrantedAuthority("USER")));
User user = userService.findUserEntityByLogin(authorizationData[0]);
if(user != null && user.getFromWhenAcceptLoginAttempts() != null && (user.getFromWhenAcceptLoginAttempts()).isBefore(LocalDateTime.now())){
// Authenticate the user
Authentication authentication = authenticationManager.authenticate(authRequest);
SecurityContext securityContext = SecurityContextHolder.getContext();
securityContext.setAuthentication(authentication);
// Create a new session and add the security context.
session = request.getSession();
session.setAttribute("SPRING_SECURITY_CONTEXT", securityContext);
return new ResponseEntity<>(new LoginResponseObject(200,"ACCESS GRANTED. YOU HAVE BEEN AUTHENTICATED"), HttpStatus.OK);
}else{
session.getId();
SecurityContextHolder.clearContext();
if(session != null) {
session.invalidate();
}
return new ResponseEntity<>(new ErrorObject(403,"TOO MANY LOGIN REQUESTS","YOU HAVE ENTERED TOO MANY WRONG CREDENTIALS. YOUR ACCOUNT HAS BEEN BLOCKED FOR 15 MINUTES.", "/login"), HttpStatus.FORBIDDEN);
}
}else{
session.getId();
SecurityContextHolder.clearContext();
if(session != null) {
session.invalidate();
}
return new ResponseEntity<>(new ErrorObject(401,"INVALID DATA","YOU HAVE ENTERED WRONG USERNAME/PASSWORD CREDENTIALS", "/login"), HttpStatus.UNAUTHORIZED);
}
}
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public ObjectMapper objectMapper(){
return new ObjectMapper();
}
#Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
The problem is that the request is stored in cache due to your sessionCreationPolicy.
To avoid this problem, you could add .requestCache().requestCache(new NullRequestCache()) in your http security config to override the default request cache configuration, but be careful because this could create another side effect (it depends on your application).
In case you do not need the session, you can choose another session policy:
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).
Another alternative is to relay in Spring's BasicAuthenticationFilter. This filter does all the authentication logic for you. To enable it, you only have to add .httpBasic()in your http security configuration.
You may want to add a custom logic on authentication success/failure. In that case, you only have to create a custom filter (CustomBasicAuthenticationFilter) that extends BasicAuthenticationFilter class and overrides the methods onSuccessfulAuthentication()and onUnsuccessfulAuthentication(). You will not need to add .httpBasic() but you will need to insert your custom filter in the correct place:
.addFilterAfter(new CustomBasicAuthenticationFilter(authenticationManager), LogoutFilter.class).
Any of that 3 solutions will avoid your problem.
Try to write .deleteCookies("JSESSONID") in your SpringSecurityConfig class.
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") );
}