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") );
}
Related
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();
}
}
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 am unable to extract user info from the access token generated by keycloak. I have a protected route where I am expecting Principal or Authentication objects to be populated correctly.
#Configuration
#EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
private final String SIGNING_KEY = "MIIBCgKCAQ...AB";
#Override
public void configure(HttpSecurity http) throws Exception {
http
.requestMatcher(new RequestHeaderRequestMatcher("Authorization"))
.authorizeRequests()
.antMatchers("/api/register").anonymous()
.antMatchers("/api/token").anonymous()
.anyRequest().authenticated();
}
#Override
public void configure(ResourceServerSecurityConfigurer config) {
config.tokenServices(createTokenServices());
}
#Bean
#Primary
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());
// converter.setSigningKey(SIGNING_KEY);
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;
}
}
}
In Profile controller, I would like to display the details from the token passed in.
#GetMapping("/api/profile/details")
public Object details(final Principal authentication) {
// Object details = authentication.getDetails();
// if (details instanceof OAuth2AuthenticationDetails) {
// OAuth2AuthenticationDetails oAuth2AuthenticationDetails = (OAuth2AuthenticationDetails) details;
// Map<String, Object> decodedDetails = (Map<String, Object>) oAuth2AuthenticationDetails.getDecodedDetails();
//
// return decodedDetails;
// }
return authentication.getName();
}
I get the following error when I call the above endpooint.
{
"error": "invalid_token",
"error_description": "Cannot convert access token to JSON"
}
Whether I pass in the signing key or not it doesn't make a difference.
Thanks for helping.
In my Spring boot application I'm trying to configure Oauth2 & JWT, it works fine but I would like hide additionnal informations from the oauth2 token because they are in plain text and the same informations are duplicated in the JWT token.
This is my Oauth2ServerConfig :
#Configuration
public class OAuth2ServerConfiguration {
#Configuration
#EnableAuthorizationServer
protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
private final AuthenticationManager authenticationManager;
private final OAuth2ApprovalRepository oAuth2ApprovalRepository;
private final OAuth2CodeRepository oAuth2CodeRepository;
private final OAuth2ClientDetailsRepository oAuth2ClientDetailsRepository;
public AuthorizationServerConfiguration(#Qualifier("authenticationManagerBean") AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
#Bean
public ApprovalStore approvalStore() {
return new MyDBApprovalStore(oAuth2ApprovalRepository);
}
#Bean
protected AuthorizationCodeServices authorizationCodeServices() {
return new MyDBAuthorizationCodeServices(oAuth2CodeRepository);
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), accessTokenConverter()));
endpoints.authorizationCodeServices(authorizationCodeServices())
.approvalStore(approvalStore())
.tokenStore(tokenStore())
.tokenEnhancer(tokenEnhancerChain)
.authenticationManager(authenticationManager);
}
#Bean
public TokenEnhancer tokenEnhancer() {
return new CustomTokenEnhancer();
}
#Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("123");
return converter;
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(new MyClientDetailsService(oAuth2ClientDetailsRepository));
}
}
}
And my custom information adding :
public class CustomTokenEnhancer implements TokenEnhancer {
#Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
Map<String, Object> additionalInfo = new HashMap<>();
additionalInfo.put("organizationId", "123");
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return accessToken;
}
}
This is an example of the response of my authenticating WS call :
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdhbml6YXRpb25JZCI6IjEyMyIsImF1ZCI6WyJyZXNfYmh1YiJdLCJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbInJlYWQiLCJ3cml0ZSJdLCJleHAiOjE0OTc4NjkyNDMsImF1dGhvcml0aWVzIjpbIlJPTEVfQURNSU4iLCJST0xFX1VTRVIiXSwianRpIjoiOGNhYTZjN2YtNTU0Yy00OTZmLTkwYTUtZTA4MjAyM2I3ZTFlIiwiY2xpZW50X2lkIjoiYmh1YmFwcCJ9.B58c2_tmfuV_L1py8ZzOPuTK3OZAhVFviL9W1gxRoec",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdhbml6YXRpb25JZCI6IjEyMyIsImF1ZCI6WyJyZXNfYmh1YiJdLCJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbInJlYWQiLCJ3cml0ZSJdLCJhdGkiOiI4Y2FhNmM3Zi01NTRjLTQ5NmYtOTBhNS1lMDgyMDIzYjdlMWUiLCJleHAiOjE0OTc4Njk0NDMsImF1dGhvcml0aWVzIjpbIlJPTEVfQURNSU4iLCJST0xFX1VTRVIiXSwianRpIjoiMGJjNWJhYzctMWI3Ny00OGFiLWI1N2MtNDM4ZjMyN2JmNGM2IiwiY2xpZW50X2lkIjoiYmh1YmFwcCJ9.DkQoCEX47PmmxOEj0n9kb2L5Yu6DqFgmUh7HBSTO_z4",
"expires_in": 1799,
"scope": "read write",
"organizationId": "123",
"jti": "8caa6c7f-554c-496f-90a5-e082023b7e1e"
}
I don't want to expose the organizationId of this token to external world and would like to encode this information in only the JWT token (access_token) .
How it can be implemented with Spring Boot, OAuth2, JWT ?
If the connection is over HTTPS (as it should be) then the information won't be exposed to the external world (just the client which is requesting it).
In any case, the access token you have is only a JWS (it's not encrypted) so the information isn't hidden if you put it in there (it's just Base64 encoded).
I found a solution here:
Spring OAuth 2 + JWT Inlcuding additional info JUST in access token
I also changed the configure method...
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer()));
endpoints
.tokenStore(tokenStore())
.tokenEnhancer(tokenEnhancerChain)
.reuseRefreshTokens(false)
.userDetailsService(userDetailsService)
.authenticationManager(authenticationManager);
}