Using the new spring-authorization-server 0.2.3 and following https://github.com/spring-projects/spring-authorization-server/tree/main/samples as reference I was able to setup an authorization server, resource server and a client successfully when using an InMemoryUserDetailsManager as follows
#EnableWebSecurity
public class DefaultSecurityConfig {
#Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
.build();
}
#Bean
UserDetailsService users() {
User.UserBuilder users = User.withDefaultPasswordEncoder();
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(users.username("user1").password("password").roles("USER").build());
manager.createUser(users.username("admin").password("password").roles("USER", "ADMIN").authorities("r1","r2","r3").build());
return manager;
}
}
This works well, In the client, I can see the authorities Granted Authorities=["r1","r2","r3"] present.
Now when I attempt to implement my own UserDetailsService which retrieves users from a Mongo Database, I stop seeing the GrantedAuthorities being passed to the client and only see Granted Authorities=[ROLE_USER, SCOPE_openid]
This is what I now have in the DefaultSecurityConfig
#EnableWebSecurity
public class DefaultSecurityConfig {
#Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
.build();
}
#Autowired
private MongoTemplate mongoTemplate;
#Bean
UserDetailsService users() {
return new CustomUserDetailsService(mongoTemplate);
}
}
And my CustomUserDetailsService looks like the following:
public class CustomUserDetailsService implements UserDetailsService {
private final MongoTemplate mongoTemplate;
public CustomUserDetailsService(MongoTemplate mongoTemplate) {
this.mongoTemplate = mongoTemplate;
}
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Criteria criteria = Criteria.where("email").is(username);
CustomUser user = mongoTemplate.findOne(new Query(criteria), CustomUser.class, "vOAuthUser");
if (user != null) {
log.info("Found user {}", user.email());
List<GrantedAuthority> authorities = getUserAuthority(user.groups());
return buildUserForAuthentication(user, authorities);
} else {
throw new UsernameNotFoundException("username not found");
}
}
private UserDetails buildUserForAuthentication(CustomUser user, List<GrantedAuthority> authorities) {
return new org.springframework.security.core.userdetails.User(user.email(), user.password(), authorities);
}
private List<GrantedAuthority> getUserAuthority(Set<String> groups) {
List<GrantedAuthority> authorities = new ArrayList<>();
groups.forEach(s -> {
Criteria criteria = Criteria.where("name").is(s);
CustomRole role = mongoTemplate.findOne(new Query(criteria), CustomRole.class, "vRole");
if (role != null) {
authorities.addAll(role.grantedAuthorities());
}
});
return authorities;
}
}
Any help is greatly appreciated.
Have you defined a OAuth2TokenCustomizer bean in your security configuration? You can add Granted Authorities there if you need, like in the following code:
#Bean
#SuppressWarnings("unused")
OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
return context -> {
JoseHeader.Builder headers = context.getHeaders();
JwtClaimsSet.Builder claims = context.getClaims();
Authentication principal = context.getPrincipal();
Set<String> authorities = principal.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toSet());
claims.claim("authorities", authorities);
};
}
Related
It seems that configuring Keycloak is impossible to do in new version of Spring Boot 3 and Spring Security.
I tried writing SecurityFilterChain but first it skipped the whole Chain and allowed everyone who has valid bearer token to view protected resource.
I found out that Spring Boot isn't picking up roles and putting them in Granted Authorities, just scope from JWT. Is it good idea to write my own token converter?
WebSecurityConfig.java
#RequiredArgsConstructor
#EnableWebSecurity
#Configuration
public class WebSecurityConfig {
private final JwtAuthConverter jwtAuthConverter;
#Bean
public KeycloakConfigResolver keycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
#Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
http.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(jwtAuthConverter);
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/**","api/**","/api").hasAuthority(ADMINISTRATION)
.anyRequest().authenticated()
);
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.cors().and().csrf().disable();
return http.build();
}
#Bean
public JwtDecoder jwtDecoder(OAuth2ResourceServerProperties oAuth2ResourceServerProperties) {
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(oAuth2ResourceServerProperties.getJwt().getJwkSetUri()).build();
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(oAuth2ResourceServerProperties.getJwt().getIssuerUri()));
return jwtDecoder;
}
private static final String ADMINISTRATION = "ROLE_administration";
}
When I added #Configuration annotation to the Class all requests were rejected.
Here are the classes if you need them to answer the question.
JwtAuthConverter.java
#Component
public class JwtAuthConverter implements Converter<Jwt, AbstractAuthenticationToken> {
private final JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
private final JwtAuthConverterProperties properties;
public JwtAuthConverter(JwtAuthConverterProperties properties) {
this.properties = properties;
}
#Override
public AbstractAuthenticationToken convert(Jwt jwt) {
Collection<GrantedAuthority> authorities = Stream.concat(
jwtGrantedAuthoritiesConverter.convert(jwt).stream(),
extractResourceRoles(jwt).stream()).collect(Collectors.toSet());
return new JwtAuthenticationToken(jwt, authorities, getPrincipalClaimName(jwt));
}
private String getPrincipalClaimName(Jwt jwt) {
String claimName = JwtClaimNames.SUB;
if (properties.getPrincipalAttribute() != null) {
claimName = properties.getPrincipalAttribute();
}
return jwt.getClaim(claimName);
}
private Collection<? extends GrantedAuthority> extractResourceRoles(Jwt jwt) {
Map<String, Object> resourceAccess = jwt.getClaim("resource_access");
Map<String, Object> resource;
Collection<String> resourceRoles;
if (resourceAccess == null
|| (resource = (Map<String, Object>) resourceAccess.get(properties.getResourceId())) == null
|| (resourceRoles = (Collection<String>) resource.get("roles")) == null) {
return Collections.emptySet();
}
return resourceRoles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toSet());
}
}
JwtAuthConverter.java
#Data
#Validated
#Configuration
#ConfigurationProperties(prefix = "jwt.auth.converter")
public class JwtAuthConverterProperties {
#NotBlank
private String resourceId;
private String principalAttribute;
}
First you need to make custom JWT converter to extract Authorities from Keycloaks nested structure:
"realm_access": {
"roles": [
"offline_access",
"uma_authorization"
]
}
This is an example of that class (you can use it to get other claims like email, username, ...):
#AllArgsConstructor
#Component
public class CustomJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
private JwtAuthenticationConverter jwtAuthenticationConverter;
private JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter;
public CustomJwtAuthenticationConverter() {
this.jwtAuthenticationConverter = new JwtAuthenticationConverter();
this.jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
}
#Override
public AbstractAuthenticationToken convert(Jwt jwt) {
Collection<GrantedAuthority> authorities = extractAuthorities(jwt);
return new JwtAuthenticationToken(jwt, authorities);
}
private Collection<GrantedAuthority> extractAuthorities(Jwt jwt) {
if(jwt.getClaim("realm_access") != null) {
Map<String, Object> realmAccess = jwt.getClaim("realm_access");
ObjectMapper mapper = new ObjectMapper();
List<String> roles = mapper.convertValue(realmAccess.get("roles"), new TypeReference<List<String>>(){});
List<GrantedAuthority> authorities = new ArrayList<>();
for (String role : roles) {
authorities.add(new SimpleGrantedAuthority(role));
}
return authorities;
}
return new ArrayList<>();
}
}
After that you include it in SecurityFilterChain and define which paths you want to secure:
#Configuration
#RequiredArgsConstructor
public class WebSecurityConfig {
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authz -> authz
.requestMatchers(HttpMethod.GET, "/api/**").hasAuthority(SOMEROLE)
.requestMatchers("/api/**").authenticated()
.anyRequest().permitAll()
);
http.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(new CustomJwtAuthenticationConverter());
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.cors().and().csrf().disable();
return http.build();
}
private static final String SOMEROLE = "SOMEROLE";
}
I wanna authenticate users via Keycloak, but I need to add additional roles to Authentication object, that is using by Spring Security. Adding roles are saved in Postgres database.
I tried to override configureGlobal with custom AuthenticationProvider, but it didn't work.
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
ApplicationAuthenticationProvider provider = new ApplicationAuthenticationProvider();
provider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(provider);
}
#Component
public class ApplicationAuthenticationProvider extends KeycloakAuthenticationProvider {
#Autowired
private UserService userService;
private GrantedAuthoritiesMapper grantedAuthoritiesMapper;
public void setGrantedAuthoritiesMapper(GrantedAuthoritiesMapper grantedAuthoritiesMapper) {
this.grantedAuthoritiesMapper = grantedAuthoritiesMapper;
}
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
KeycloakAuthenticationToken token = (KeycloakAuthenticationToken) authentication;
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
String username = ((KeycloakAuthenticationToken) authentication)
.getAccount().getKeycloakSecurityContext().getToken().getPreferredUsername();
List<Role> roles = userService.findRoles(username);
for (Role role : roles) {
grantedAuthorities.add(new KeycloakRole(role.toString()));
}
return new KeycloakAuthenticationToken(token.getAccount(), token.isInteractive(), mapAuthorities(grantedAuthorities));
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
private Collection<? extends GrantedAuthority> mapAuthorities(
Collection<? extends GrantedAuthority> authorities) {
return grantedAuthoritiesMapper != null
? grantedAuthoritiesMapper.mapAuthorities(authorities)
: authorities;
}
}
Tried to add additional filter, but i'm not sure in correct configuration.
#Bean
#Override
protected KeycloakAuthenticationProcessingFilter keycloakAuthenticationProcessingFilter() throws Exception {
RequestMatcher requestMatcher =
new OrRequestMatcher(
new AntPathRequestMatcher("/api/login"),
new QueryParamPresenceRequestMatcher(OAuth2Constants.ACCESS_TOKEN),
// We're providing our own authorization header matcher
new IgnoreKeycloakProcessingFilterRequestMatcher()
);
return new KeycloakAuthenticationProcessingFilter(authenticationManagerBean(), requestMatcher);
}
// Matches request with Authorization header which value doesn't start with "Basic " prefix
private class IgnoreKeycloakProcessingFilterRequestMatcher implements RequestMatcher {
IgnoreKeycloakProcessingFilterRequestMatcher() {
}
public boolean matches(HttpServletRequest request) {
String authorizationHeaderValue = request.getHeader("Authorization");
return authorizationHeaderValue != null && !authorizationHeaderValue.startsWith("Basic ");
}
}
Now I use Keycloak only for login/password. Roles and permissions now saved in local DB.
I am new to Spring-Boot.I want to create an API which will have role based access with JWT token based authentication. But, unable to implement that.
I am not using JPA & Hibernate to fetch and map data. Instead I am using Ibatis.I have tried with #PreAuthorize and antMatchers & hasRole, but failed. By getting user id from JWT token, I am fetching details and roles and setting those to SecurityContextHolder.getContext().setAuthentication, still not working.
SecurityConfig
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilter(new JwtAuthorizationFilter(authenticationManager()))
.authorizeRequests()
.anyRequest().authenticated()
.antMatchers("api/management/reports").hasRole("Supervisor");
}
Controller
#RestController
#RequestMapping("api")
#CrossOrigin
public class MyController {
#PreAuthorize("hasRole('Supervisor')")
#GetMapping("username")
public String reports(){
SecurityContext securityContext = SecurityContextHolder.getContext();
return securityContext.getAuthentication().getName();
}
}
AuthorizationFilter
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {
public JwtAuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String header = request.getHeader(JwtProperties.HEADER_STRING);
if (header == null || !header.startsWith(JwtProperties.TOKEN_PREFIX)) {
chain.doFilter(request, response);
return;
}
Authentication authentication = getUsernamePasswordAuthentication(request,header);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
}
private Authentication getUsernamePasswordAuthentication(HttpServletRequest request, String header) {
try {
String token = header.replace(JwtProperties.TOKEN_PREFIX,"");
String userName = JWT.require(HMAC512(JwtProperties.SECRET.getBytes()))
.build()
.verify(token)
.getSubject();
List<User> searchedUserList = getUserDetailsDAO().getUserDetails(userName);
if (null !=searchedUserList && searchedUserList.size()>0) {
User searchedUser = new User();
searchedUser = searchedUserList.get(0);
List<RoleAccess> roleAccessList = new ArrayList<RoleAccess>();
XrefUsrRole oXrefUsrRole = new XrefUsrRole();
oXrefUsrRole.setUserName(searchedUser.getUsername());
roleAccessList = getRoleAccessDAO().getAccessDetails(oXrefUsrRole);
List<GrantedAuthority> authorities = uildUserAuthority(roleAccessList);
org.springframework.security.core.userdetails.User newUser = buildUserForAuthentication(searchedUser, authorities);
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(newUser, null,authorities);
return auth;
}
return null;
}
return null;
} catch (IOException e) {
return null;
}
private org.springframework.security.core.userdetails.User buildUserForAuthentication(User searchedUser, List<GrantedAuthority> authorities) {
return new org.springframework.security.core.userdetails.User(searchedUser.getUsername(), searchedUser.getPassword(), true, true, true, true, authorities);
}
private List<GrantedAuthority> buildUserAuthority(List<RoleAccess> roleAccessList) {
Set<GrantedAuthority> setAuths = new HashSet<GrantedAuthority>();
// Build user's authorities
for (RoleAccess userRole : roleAccessList) {
setAuths.add(new SimpleGrantedAuthority("ROLE_"+userRole.getModifiedBy()));
}
List<GrantedAuthority> Result = new ArrayList<GrantedAuthority>(setAuths);
return Result;
}
In this case api/username should not accessible except users having Supervisor role.
You have ROLE_"+userRole.getModifiedBy()) which means you are granting roles with ROLE_NAME and in PreAuthorize you have Supervisor which is causing the issue. You can store role as ROLE_SUPERVISOR in a database then use it as below
// Build user's authorities
for (RoleAccess userRole : roleAccessList) {
setAuths.add(new SimpleGrantedAuthority("ROLE_"+userRole.getModifiedBy()));
}
use
#PreAuthorize("hasRole('ROLE_SUPERVISOR')")
.antMatchers("api/management/reports").hasRole("SUPERVISOR");
I implemented the login of my Spring Boot web app using OAuth2 and everything works fine.
The only problem is that the logged in user does not has the authorities information saved inside the session so each time I request a url and the controller has the annotation #PreAuthorize("hasRole('USER')") I get rejected.
SecurityConfiguration class:
#EnableGlobalMethodSecurity(prePostEnabled = true)
#EnableWebSecurity
#EnableJpaRepositories(basePackageClasses = UserRepository.class)
#Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private CustomOAuth2UserService customOAuth2UserService;
#Autowired
private CustomUserDetailsService userDetailsService;
#Autowired
private OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
auth
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.formLogin()
.loginPage("/login")
.failureUrl("/login?error=true")
.and()
.logout()
.logoutSuccessUrl("/")
.deleteCookies("JSESSIONID")
.invalidateHttpSession(true)
.and()
.oauth2Login()
.loginPage("/login")
.failureUrl("/login?error=true")
.userInfoEndpoint()
.userService(customOAuth2UserService)
.and()
.failureHandler(oAuth2AuthenticationFailureHandler);
}
#Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
This is the CustomOAuth2UserService class:
#Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
#Autowired
private UserService userService;
#Override
public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(oAuth2UserRequest);
try {
return processOAuth2User(oAuth2UserRequest, oAuth2User);
}catch (Exception ex) {
// Throwing an instance of AuthenticationException will trigger the OAuth2AuthenticationFailureHandler
throw new InternalAuthenticationServiceException(ex.getMessage(), ex.getCause());
}
}
private OAuth2User processOAuth2User(OAuth2UserRequest oAuth2UserRequest, OAuth2User oAuth2User) {
OAuth2UserInfo oAuth2UserInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(oAuth2UserRequest.getClientRegistration().getRegistrationId(), oAuth2User.getAttributes());
if(StringUtils.isEmpty(oAuth2UserInfo.getEmail())) {
throw new RuntimeException("Id not found from OAuth2 provider");
}
User user;
try {
user = userService.getByEmail(oAuth2UserInfo.getEmail());
if(!user.getProvider().toString().equalsIgnoreCase(oAuth2UserRequest.getClientRegistration().getRegistrationId())) throw new EmailAlreadyTakenException("email-already-taken");
} catch (UserNotFoundException e) {
user = registerNewUser(oAuth2UserRequest, oAuth2UserInfo);
}
return new CustomUserDetails(user);
}
private User registerNewUser(OAuth2UserRequest oAuth2UserRequest, OAuth2UserInfo oAuth2UserInfo) {
User user = new User();
user.setProvider(AuthProvider.valueOf(oAuth2UserRequest.getClientRegistration().getRegistrationId()));
Identity identity = new Identity(user);
if(oAuth2UserInfo.getFirstName() != null && !oAuth2UserInfo.getFirstName().equalsIgnoreCase(""))
identity.setFirstName(oAuth2UserInfo.getFirstName());
if(oAuth2UserInfo.getLastName() != null && !oAuth2UserInfo.getLastName().equalsIgnoreCase(""))
identity.setSecondName(oAuth2UserInfo.getLastName());
user.setIdentity(identity);
user.setEmail(oAuth2UserInfo.getEmail());
user.setConfirmedRegistration(true);
boolean flag = false;
String username = oAuth2UserInfo.getName().toLowerCase().replaceAll("\\s+", "");
user.setUsername(username);
return userService.addFacebookUser(user);
}
}
This a part of the application.properties file:
spring.security.oauth2.client.registration.facebook.client-id=***
spring.security.oauth2.client.registration.facebook.client-secret=***
spring.security.oauth2.client.registration.facebook.scope=email,public_profile
spring.security.oauth2.client.registration.google.client-id=***
spring.security.oauth2.client.registration.google.client-secret=***
spring.security.oauth2.client.registration.google.scope=email,profile
spring.security.oauth2.client.provider.facebook.authorizationUri = https://www.facebook.com/v3.0/dialog/oauth
spring.security.oauth2.client.provider.facebook.tokenUri = https://graph.facebook.com/v3.0/oauth/access_token
spring.security.oauth2.client.provider.facebook.userInfoUri = https://graph.facebook.com/v3.0/me?fields=id,first_name,middle_name,last_name,name,email,verified,is_verified,picture
Once logged in the user can call this url /users/{username} but when he login with facebook or google through OAuth2, he gets rejected because the authorities list is empty. When he login with his webapp credential, the authorities list contains USER_ROLE and he is allowed to procede.
#PreAuthorize("hasRole('USER')")
#GetRequest("users/{username}")
public String getUser(#PathVariable String username, #PathVariable String subsection, Model model, Principal principal) throws IllegalAccessException, UserNotFoundException {
User user = userService.getByUsername(principal.getName());
model.addAttribute("user", user);
return "user";
}
Inside principal object there are:
When logged in with OAuth2:
principal: type CustomUserDetails (user information)
authorizedClientRegistrationId: type String ("google", "facebook")
authorities: type Collections$UnmodifiableRandomAccessList (empty)
details: null
authenticated: type boolean (true)
When logged in with local credentials:
principal: type CustomUserDetails (user information)
credentials: null
authorities: type Collections$UnmodifiableRandomAccessList
index:0 type SimpleGrantedAuthority ("USER_ROLE")
details: type WebAuthenticationDetails (remote address, sessionId)
authenticated: type boolean (true)
After some time of debugging I found the solution! I was not configuring correctly the roles of my user.
Inside the registerNewUser method of my custom OAuth2UserService I wasn't setting the Role of the User. I just added the line:
user.setRoles(new HashSet<>(Collections.singletonList(new Role("ROLE_USER"))));
and everything started to work! So now when the OAuth2User's authorities get asked, it just calls the getAuthorities of CustomUserDetails (my implementation of OAuth2User) and it calls the getRoles method of the User.
CustomUserDetails class:
public class CustomUserDetails extends User implements UserDetails, OAuth2User {
public CustomUserDetails() {
}
public CustomUserDetails(String username, String email, String password, Set<Role> roles) {
super(username, email, password, roles);
}
public CustomUserDetails(User user) {
super(user.getUsername(), user.getEmail(), user.getPassword(), user.getRoles());
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return getRoles()
.stream()
.map(role -> new SimpleGrantedAuthority(role.getRole()))
.collect(Collectors.toList());
}
#Override
public Map<String, Object> getAttributes() {
return null;
}
#Override
public boolean isAccountNonExpired() {
return true;
}
#Override
public boolean isAccountNonLocked() {
return true;
}
#Override
public boolean isCredentialsNonExpired() {
return true;
}
#Override
public boolean isEnabled() {
return true;
}
#Override
public String getName() {
return null;
}
}
I have implemented JWT and LDAP Authentication using Spring Security Oauth2. It seems to be working fine and I can login with my LDAP credentials.
Now, there is one requirement that I need to use the currently logged in user info to save details in database - specifically like when that user add/update a new record. I tried to get that using Spring security way using
SecurityContextHolder.getContext().getAuthentication().getDetails()
but it doesn't return all that information which I have in JWT. It just returns Remote IP,the JWT token value and authenticated true. It doesn't even return name().
I am new to JWT, so not sure if I need to extract it by reading that token and even how we can achieve it.
Any pointers will be appreciated.
Thanks.
The first thing you need to do is store the user information inside the JWT when it is created, then you have to extract it when it is used. I had a similar situation and I solved it by extending both the TokenEnhancer and JwtAccessTokenConverter.
I use the TokenEnhancer to embed my extended principal of type CustomUserDetailsinside the JWT additional information.
public class CustomAccessTokenEnhancer implements TokenEnhancer {
#Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
Authentication userAuthentication = authentication.getUserAuthentication();
if (userAuthentication != null) {
Object principal = authentication.getUserAuthentication().getPrincipal();
if (principal instanceof CustomUserDetails) {
Map<String, Object> additionalInfo = new HashMap<>();
additionalInfo.put("userDetails", principal);
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
}
}
return accessToken;
}
}
And then manually extract the extended principal when building the Authentication object when processing an authenticated request.
public class CustomJwtAccessTokenConverter extends JwtAccessTokenConverter {
#Override
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
OAuth2Authentication authentication = super.extractAuthentication(map);
Authentication userAuthentication = authentication.getUserAuthentication();
if (userAuthentication != null) {
LinkedHashMap userDetails = (LinkedHashMap) map.get("userDetails");
if (userDetails != null) {
// build your principal here
String localUserTableField = (String) userDetails.get("localUserTableField");
CustomUserDetails extendedPrincipal = new CustomUserDetails(localUserTableField);
Collection<? extends GrantedAuthority> authorities = userAuthentication.getAuthorities();
userAuthentication = new UsernamePasswordAuthenticationToken(extendedPrincipal,
userAuthentication.getCredentials(), authorities);
}
}
return new OAuth2Authentication(authentication.getOAuth2Request(), userAuthentication);
}
}
and the AuthorizationServer configuration to tie it all together.
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
private UserDetailsService userDetailsService;
#Autowired
private DataSource dataSource;
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
CustomJwtAccessTokenConverter accessTokenConverter = new CustomJwtAccessTokenConverter();
accessTokenConverter.setSigningKey("a1b2c3d4e5f6g");
return accessTokenConverter;
}
#Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
#Bean
#Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
#Bean
public TokenEnhancer tokenEnhancer() {
return new CustomAccessTokenEnhancer();
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource).passwordEncoder(passwordEncoder());
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), accessTokenConverter()));
endpoints
.tokenStore(tokenStore())
.tokenEnhancer(tokenEnhancerChain)
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}
#Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.passwordEncoder(passwordEncoder());
security.checkTokenAccess("isAuthenticated()");
}
}
I am then able to access my extended principal in my resource controller like this
#RestController
public class SomeResourceController {
#RequestMapping("/some-resource")
public ResponseEntity<?> someResource(Authentication authentication) {
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
return ResponseEntity.ok("woo hoo!");
}
}
Hope this helps!
In your REST Service, add the OAuth2Authentication Class as an argument
#RequestMapping(value = "/{id}/products", method = RequestMethod.POST)
public ResourceResponse<String> processProducts(OAuth2Authentication auth) {
Springboot will automatically map the logged-in user details to this object. Now, you can do the following to access the username
auth.getPrincipal().toString()