I have a requirement in my current project where I need to do some operation just after authentication user by calling /oauth/token. So I wrote a class like this
#Component
public class AuthSuccessListener implements ApplicationListener<AuthenticationSuccessEvent> {
#Value("${ationet.auth.url}")
private String ationetUrl;
private final TokenStore tokenStore;
private final RestTemplate restTemplate;
private final AtionetService ationetService;
public AuthSuccessListener(
TokenStore tokenStore, RestTemplate restTemplate, AtionetService ationetService) {
this.tokenStore = tokenStore;
this.restTemplate = restTemplate;
this.ationetService = ationetService;
}
#Override
public void onApplicationEvent(AuthenticationSuccessEvent event) {
RedisService<String> redisService = new RedisService<>();
String bespokeAccessToken = null;
String ationetAccessToken = null;
UserDetails userDetails = (UserDetails) event.getAuthentication().getPrincipal();
List<OAuth2AccessToken> tokens =
(List<OAuth2AccessToken>)
tokenStore.findTokensByClientIdAndUserName("unipet", userDetails.getUsername());
bespokeAccessToken =
tokens.stream()
.filter(Objects::nonNull)
.findFirst()
.map(OAuth2AccessToken::getValue)
.orElse(null);
try {
String ationetAccesstoken =
ationetService.getAccessToken("abc#gmail.com", "password");
if (ationetAccesstoken != null) {
redisService.setValue(bespokeAccessToken, ationetAccessToken);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
But the problem is this method is being called after every api call. As a result api response became slow and the same thing happening again and again after every api call. So my requirement is I want to do this operation once just after authenticating a user, not after every api call.
Any suggestions will be appreciated.
You can add a judgment condition.
if (event.getSource().getClass().getName().equals("org.springframework.security.authentication.UsernamePasswordAuthenticationToken"))
I am creating simple Rest social media application with Spring Boot. I use JWT for authentication in application.
In my mobile application when users register, i am getting some information from users and create account and profile of the user.
By the way, you can see (simplified) database object of account and profile. I use Mongo DB for database.
account:
{
“_id”: “b6164102-926e-47d8-b9ff-409c44dc47c0“,
“email”: “xxx#yy.com”
….
}
profile:
{
“_id”: “35b06171-c16a-4559-90f3-df81ace6d64a“,
“accountId”: “b6164102-926e-47d8-b9ff-409c44dc47c0”,
profileImages: [
{
“imageId”: “1431b0bc-feb7-436d-9d3a-7b9094547bf6”,
“imageLink”: “https://this_is_some_link_to_image.com
}
….
]
….
}
When user login to app, i add accountId to JWT and then in my mobile app i call below endpoint to get profile information of user. I take accountId from jwt and find profile of that account id.
#GetMapping("/profiles")
public ResponseEntity<BaseResponse> getUserProfile(#AuthenticationPrincipal AccountId accountId) {
var query = new Query(accountId);
var presenter = new GetUserProfilePresenter();
useCase.execute(query, presenter);
return presenter.getViewModel();
}
In the app, users can upload photo to their profile using below endpoint;
#PostMapping(path = "/profiles/{profileId}/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<BaseResponse> uploadProfileImage(
#PathVariable("profileId") UUID profileId, #RequestParam("image") MultipartFile image) throws IOException {
......
}
Everything works fine but the problem is someone can use their token to call this url with another person’s profileId. Because profileId is not a hidden id. In my mobile app users can shuffle and see other users profile using below url.
This url is accessible by any authenticated users.
#GetMapping(path = "/profiles/{profileId}")
public ResponseEntity<BaseResponse> getProfile(#PathVariable("profileId") UUID profileId) {
......
}
Now, my question is how can i make "/profiles/{profileId}/images" this url is only accesible for user of this profile without changing path format.
For exampe;
User A - Profile Id = XXX
User B - Profile Id = YYY
I want that if User A calls this url with own JWT Token, uploads image only to own profile not another one profile.
I have come up with some solutions but these solutions cause me to change the url path;
Solution 1:
I can use accountId in the jwt. Find profile of user with this accountId so that, every call to this url guaranteed upload image only to profile of token user.
But this solution change url path like below because i dont need to get any profileId from path.
#PostMapping(path = "/profiles/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<BaseResponse> uploadProfileImage(
#AuthenticationPrincipal AccountId accountId, #RequestParam("image") MultipartFile image) throws IOException {
......
}
Solution 2:
This is very similar to first solution only different is when i create jwt for user. I will put profileId of user to inside of JWT. So when the user calls the url i will get profileId from jwt and put inside of Authentication object. And in the controller i will get this profileId for using to find profile of user then upload image to this spesific profile.
But also, this solution change url path format because i dont need to get profileId from url path.
So if i back to my main question. What is the best practices and solutions for these kinda problems and situations?
~~~EDIT~~~
For those whose wonder, i didn't change my path. Actually i implemented solution 1 with a twist.
Now i use accountId from JWT and profileId at the same time so when i want to find a profile of exactly that user i search the database using accountId and profileId together.
With this change, i didn't need to change other paths.
For example; (GET) /profiles/{profileId} this path still meaningful for all authenticated users.
But (POST) /profiles/{profileId}/images this path only meaningful for that spesific (owner of token) user.
By the way, i starts paths with "api/admin/**" prefix for my admin role operations.
Final code (Controller);
#PostMapping(path = "/profiles/{profileId}/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<BaseResponse> uploadProfileImage(
#AuthenticationPrincipal AccountId accountId,
#PathVariable("profileId") UUID profileId,
#RequestParam("image") MultipartFile image) throws IOException {
....
}
Final code (Repository);
#Repository
public interface ProfileJpaRepository extends MongoRepository<ProfileDto, String> {
Optional<ProfileDto> findByAccountId(String accountId);
Optional<ProfileDto> findByIdAndAccountId(String profileId, String accountId);
}
The best practice to handle this kind of scenarios is to have two endpoints, each needing different kind of permissions:
"/profiles/{profileId}/images" will be available for admins, so that if an admin wants to change another user's profile image, they can do so by calling this endpoint.
"/profiles/images" will be responsible for changing the most generic users with the lowest privileges.
So, in both scenarios you need to extract the AccountId from the JWT and you should not get the AccountId from the user directly, unless for administration purposes where you check the privileges to authorize the user.
Now, the best way to implement such a system, is to use Spring Security and to create a custom AuthenticationToken, then to customize AbstractUserDetailsAuthenticationProvider, * AbstractAuthenticationProcessingFilter* and UsernamePasswordAuthenticationToken.
After doing so, you can then configure Spring to use the custom provider for authentication.
UsernamePasswordAuthenticationToken
public class JwtAuthenticationToken extends UsernamePasswordAuthenticationToken {
private Payload payload; // Payload can be any model class that encapsulates the payload of the JWT.
private boolean creationAllowed;
public JwtAuthenticationToken(String jwtToken) throws Exception {
super(null, jwtToken);
// Verify JWT and get the payload
this.payload = // set the payload
}
public JwtAuthenticationToken(String principal, JwtAuthenticationToken authToken, Collection<? extends GrantedAuthority> authorities) {
super(principal, authToken.getCredentials(), authorities);
this.payload = authToken.payload;
authToken.eraseCredentials(); // not sure if this is needed
}
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
} else {
super.setAuthenticated(false);
}
}
public Payload getPayload() {
return this.firebaseToken;
}
public boolean isCreationAllowed() {
return creationAllowed;
}
public void setCreationAllowed(boolean creationAllowed) {
this.creationAllowed = creationAllowed;
}
}
AbstractUserDetailsAuthenticationProvider
#Component
public class JwtAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
#Autowired
AppUserService appUserService;
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(JwtAuthenticationToken.class, authentication, () ->
this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only JwtAuthenticationToken is supported")
);
JwtAuthenticationToken jwtAuthToken = (JwtAuthenticationToken) authentication;
String principal;
try {
principal = jwtAuthToken.getPayload().getEmail(); // Here I'm using email as the user identifier, this can be anything, for example AccountId
} catch (RuntimeException re) {
throw new AuthenticationException("Could not extract user's email address.");
}
AppUser user = (AppUser) this.retrieveUser(principal, jwtAuthToken);
return this.createSuccessAuthentication(principal, jwtAuthToken, user);
}
#Override
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
JwtAuthenticationToken result = new JwtAuthenticationToken((String) principal, (JwtAuthenticationToken) authentication, user.getAuthorities());
result.setDetails(user);
return result;
}
#Override
public UserDetails retrieveUser(String s, UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken) throws AuthenticationException {
UserDetails userDetails = appUserService.loadUserByUsername(s);
JwtAuthenticationToken jwtAuthToken = (JwtAuthenticationToken) usernamePasswordAuthenticationToken;
if (userDetails != null)
return userDetails; // You need to create an UserDetails which will be set by the framework to the Security Context as the authenticated user, this will be useful later when you want to check the privileges.
else
throw new AuthenticationException("Creating the user details is not allowed.");
}
#Override
protected void additionalAuthenticationChecks(final UserDetails d, final UsernamePasswordAuthenticationToken auth) {
// Nothing to do
}
#Override
public boolean supports(Class<?> authentication) {
return (JwtAuthenticationToken.class.isAssignableFrom(authentication));
}
}
AbstractAuthenticationProcessingFilter
public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public JwtAuthenticationFilter() {
super("/**"); // The path that this filter needs to process, use "/**" to make sure all paths must be proessed.
}
#Override
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
return true; // Here I am returning true to require authentication for all requests.
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String authorization = request.getHeader("Authorization");
if (authorization == null || !authorization.startsWith("Bearer "))
throw new AuthenticationException("No JWT token found in request headers");
String authToken = authorization.substring(7);
JwtAuthenticationToken token = new JwtAuthenticationToken(authToken);
return getAuthenticationManager().authenticate(token);
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
super.successfulAuthentication(request, response, chain, authResult);
// Authentication process succeed, filtering the request in.
// As this authentication is in HTTP header, after success we need to continue the request normally
// and return the response as if the resource was not secured at all
chain.doFilter(request, response);
}
#Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
super.unsuccessfulAuthentication(request, response, failed);
// Authentication process failed, filtering the request out.
}
}
UserDetails
public class AppUser implements UserDetails {
// A class to be used as a container for user details, you can add more details specific to your application here.
}
Finally, you need to configure Spring boot to use this classes:
SecurityConfig
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final RequestMatcher PUBLIC_URLS = new OrRequestMatcher(
// -- public paths, for example: swagger ui paths
new AntPathRequestMatcher("/swagger-ui.html"),
new AntPathRequestMatcher("/swagger-resources/**"),
new AntPathRequestMatcher("/v2/api-docs"),
new AntPathRequestMatcher("/webjars/**")
);
private JwtAuthenticationProvider provider;
public SecurityConfig(JwtAuthenticationProvider provider) {
this.provider = provider;
}
#Override
public void configure(final WebSecurity web) {
web.ignoring()
.antMatchers(HttpMethod.OPTIONS) // Allowing browser pre-flight
.requestMatchers(PUBLIC_URLS);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling()
// this entry point handles when you request a protected page and you are not yet authenticated
//.defaultAuthenticationEntryPointFor(forbiddenEntryPoint(), PROTECTED_URLS)
.authenticationEntryPoint(forbiddenEntryPoint())
.and()
.authenticationProvider(this.provider)
.addFilterBefore(jwtAuthenticationFilter(), AnonymousAuthenticationFilter.class)
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
}
#Bean
JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
final JwtAuthenticationFilter filter = new JwtAuthenticationFilter();
filter.setAuthenticationManager(this.authenticationManager());
filter.setAuthenticationSuccessHandler(this.successHandler());
filter.setAuthenticationFailureHandler(this.failureHandler());
return filter;
}
#Bean
JwtAuthenticationSuccessHandler successHandler() {
return new JwtAuthenticationSuccessHandler();
}
#Bean
JwtAuthenticationFailureHandler failureHandler() {
return new JwtAuthenticationFailureHandler();
}
/**
* Disable Spring boot automatic filter registration.
*/
#Bean
FilterRegistrationBean disableAutoRegistration(JwtAuthenticationFilter filter) {
final FilterRegistrationBean registration = new FilterRegistrationBean(filter);
registration.setEnabled(false);
return registration;
}
#Bean
AuthenticationEntryPoint forbiddenEntryPoint() {
return new HttpStatusEntryPoint(FORBIDDEN);
}
}
AuthenticationFailureHandler
public class JwtAuthenticationFailureHandler implements AuthenticationFailureHandler {
private ObjectMapper objectMapper = new ObjectMapper();
#Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
Map<String, Object> data = new HashMap<>();
data.put("exception", e.getMessage());
httpServletResponse.getOutputStream().println(objectMapper.writeValueAsString(data));
}
}
AuthenticationSuccessHandler
public class JwtAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
#Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
}
}
OKAY!
Now that you have implemented the security correctly, you can access user details and privileges from anywhere using the last piece:
UserDetailsService
#Service
public class AppUserService implements UserDetailsService {
#Autowired
private AppUserRepository appUserRepository;
public AppUser getCurrentAppUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null)
return (AppUser) authentication.getDetails();
return null;
}
public String getCurrentPrincipal() {
return (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
#Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
Optional<AppUser> appUserOptional = this.appUserRepository.findByEmailsContains(new EmailEntity(s)); // This should be changed in your case if you are using something like AccountId
appUserOptional.ifPresent(AppUser::loadAuthorities);
return appUserOptional.orElse(null);
}
}
Great.
Let's see how to use it in your Controllers:
#PostMapping(path = "/profiles/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<BaseResponse> uploadProfileImage(#RequestParam("image") MultipartFile image) throws IOException {
AppUser user = this.appUserService.getCurrentAppUser();
Long id = user.getAccountId(); // Or profile id or any other identifier that you needed and extracted from the JWT after verification.
// set the profile picture.
// save changes of repository and return.
}
For admin purposes:
#PreAuthorize ("hasRole('ROLE_ADMIN')")
#PostMapping(path = "/profiles/{profileId}/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<BaseResponse> uploadProfileImage(
#PathVariable("profileId") UUID profileId, #RequestParam("image") MultipartFile image) throws IOException {
AppUser user = this.appUserService.getCurrentAppUser();
// set the profile picture using profileId parameter
// save changes of repository and return.
}
The only remaining task is to assign the ROLE_ADMIN to the right user when loading it from the database. To do this, there are a lot of different approaches and it totally depends on your requirements. Overall, you can save a role in the database and relate it to a specific user and simply load it using an Entity.
Let's get few things right here , I am assuming that you have like two entities - Account and Profile and you wish to upload/update new profile image using same API -
#PostMapping(path = "/profiles/{profileId}/images
If ADMIN role , update profile image for #PathVariable("profileId") OR if USER role update their own profile image using #PathVariable("profileId") and not any other Profile entity image using ProfileId if current user is authenticated.
Please check this link for Role-Permission Authentication
Spring Boot : Custom Role - Permission Authorization using SpEL
User Principal
#Getter
#Setter
#Builder
public class UserPrincipal implements UserDetails {
/**
* Generated Serial ID
*/
private static final long serialVersionUID = -8983688752985468522L;
private Long id;
private String email;
private String password;
private Collection<? extends GrantedAuthority> authorities;
private Collection<? extends GrantedAuthority> permissions;
public static UserPrincipal createUserPrincipal(Account account) {
if (userDTO != null) {
List<GrantedAuthority> authorities = userDTO.getRoles().stream().filter(Objects::nonNull)
.map(role -> new SimpleGrantedAuthority(role.getName().name()))
.collect(Collectors.toList());
List<GrantedAuthority> permissions = account.getRoles().stream().filter(Objects::nonNull)
.map(Role::getPermissions).flatMap(Collection::stream)
.map(permission -> new SimpleGrantedAuthority(permissionDTO.getName().name()))
.collect(Collectors.toList());
return UserPrincipal.builder()
.id(account.getId())
.email(account.getEmail())
.authorities(authorities)
.permissions(permissions)
.build();
}
return null;
}
AuthenticationFilter
public class AuthTokenFilter extends OncePerRequestFilter {
#Autowired
private JwtUtils jwtUtils;
#Autowired
private CustomUserDetailsService customUserDetailsService;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
String jwtToken = getJwtTokenFromHttpRequest(request);
if (StringUtils.isNotBlank(jwtToken) && jwtUtils.validateToken(jwtToken)) {
Long accountId = jwtUtils.getAccountIdFromJwtToken(jwtToken);
UserDetails userDetails = customUserDetailsService.loadUserByUserId(accountId);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails
.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception exception) {
}
filterChain.doFilter(request, response);
}
private String getJwtTokenFromHttpRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (!StringUtils.isEmpty(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7, bearerToken.length());
}
return null;
}
}
AuthUtil
#UtilityClass
public class AuthUtils {
public boolean isAdmin(UserPrincipal userPrincipal){
if(CollectionUtils.isNotEmpty(userPrincipal.getAuthorities())){
return userPrincipal.getRoles().stream()
.filter(Objects::nonNull)
.map(GrantedAuthority::getName)
.anyMatch(role -> role.equals("ROLE_ADMIN"));
}
return false;
}
}
Profile Service
#Service
public class ProfileService {
#Autowired
private ProfileRepository profileRepository;
public Boolean validateProfileIdForAccountId(Integer profileId, Long accountId) throws NotOwnerException,NotFoundException {
Profile profile = profileRepository.findByAccountId(profileId,accountId);
if(profile == null){
throw new NotFoundException("Profile does not exists for this account");
} else if(profile.getId() != profileId){
throw new NotOwnerException();
}
return true;
}
}
ProfileController
#PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_USER')")
#PostMapping(path = "/profiles/{profileId}/images", consumes =
MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<BaseResponse> uploadProfileImage(
#AuthenticationPrincipal UserPrincipal currentUser,
#PathVariable("profileId") UUID profileId,
#RequestParam("image") MultipartFile image) throws IOException {
if(!AuthUtils.isAdmin(currentUser)){
profileService.validateProfileIdForAccountId(profileId, currentUser.getId());
}
}
Now you can validate whether the #PathVariable("profileId") does indeed belong to the authenticated CurrentUser, you are also checking if the CurrentUser is ADMIN.
You can also add & check any specific permission for ROLES for facilitating UPLOAD/UPDATE
#PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_USER') or hasPermission('UPDATE')")
In our last feign client security configuration we have this Bean:
#Bean
public RequestInterceptor oauth2FeignRequestInterceptor(
ClientCredentialsResourceDetails oauth2RemoteResource) {
return new OAuth2FeignRequestInterceptor(
new DefaultOAuth2ClientContext(),
oauth2RemoteResource
);
}
In 2.3 spring version OAuth2FeignRequestInterceptor is deprecated! But we cannot found the new one.
Anyone knows something about that?
You can create your own RequestInterceptor to add the Authorization header.
There's an example here:
https://developer.okta.com/blog/2018/02/13/secure-spring-microservices-with-oauth
I had the same problem, I needed a request interceptor to call through a Feign client to a another microservice.
The idea is very easy, The only thing that I needed to implement was a custom RequestInterceptor annonted with #Component that inject the current JWT from the security context to the Authorization Header.
You can view this component as follows:
#Component
#Slf4j
public class FeignClientInterceptor implements RequestInterceptor {
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String TOKEN_TYPE = "Bearer";
#Override
public void apply(RequestTemplate requestTemplate) {
log.debug("FeignClientInterceptor -> apply CALLED");
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication instanceof JwtAuthenticationToken) {
final JwtAuthenticationToken jwtAuthToken = (JwtAuthenticationToken) authentication;
requestTemplate.header(AUTHORIZATION_HEADER, String.format("%s %s", TOKEN_TYPE, jwtAuthToken.getToken().getTokenValue()));
}
}
}
Next, I can use the feign client successfully
final APIResponse<ProcessedFileDTO> response = filesMetadataClient.getProcessedFileByName(uploadFile.getOriginalFilename());
if (response.getStatus() == ResponseStatusEnum.ERROR
&& response.getHttpStatusCode() == HttpStatus.NOT_FOUND) {
sendFileToSftp(uploadFile);
} else {
throw new FileAlreadyProcessedException();
}
In Springboot webflux, I can get the current principle using this code
Object principal = ReactiveSecurityContextHolder.getContext().getAuthentication().getPrincipal();
If the user is authenticated. But I have a case in which the JWT token will be sent as a query paramenter not as the authorization header, I know how to convert the token into Authentication object
How i can inject that Authentication object into the current ReactiveSecurityContextHolder
You can set your own Authentication and take the token from query params as follows:
#Component
public class CustomAuthentication implements ServerSecurityContextRepository {
private static final String TOKEN_PREFIX = "Bearer ";
#Autowired
private ReactiveAuthenticationManager authenticationManager;
#Override
public Mono<Void> save(ServerWebExchange serverWebExchange, SecurityContext securityContext) {
throw new UnsupportedOperationException("No support");
}
#Override
public Mono<SecurityContext> load(ServerWebExchange serverWebExchange) {
ServerHttpRequest request = serverWebExchange.getRequest();
String authJwt = request.getQueryParams().getFirst("Authentication");
if (authJwt != null && authJwt.startsWith(TOKEN_PREFIX)) {
authJwt = authJwt.replace(TOKEN_PREFIX, "");
Authentication authentication =
new UsernamePasswordAuthenticationToken(getPrincipalFromJwt(authJwt), authJwt);
return this.authenticationManager.authenticate(authentication).map((authentication1 -> new SecurityContextImpl(authentication)));
}
return Mono.empty();
}
private String getPrincipalFromJwt(String authJwt) {
return authJwt;
}
}
This is a simple code block demonstrating how can you achieve your goal. You can improve getPrincipalFromJwt() method to return a different object that you would like to set as principal. Or you can use a different implementation of Authentication (as opposed to UsernamePasswordAuthenticationToken in this example) altogether.
Keycloak is a user federated identity solution running seperately (standalone) from other systems referencing to it (for authorization for example) having its own database.
Question:
How would I reference / create user specific data in my rest api database?
How would I reference the user in the rest api database to have user specific data?
Think of an table like Post
title,
date,
content,
author (here would be the reference to the user)
We have a similar requirement in a Java EE application, where a user can create data via a JSF website. Data is stored to postrgesql with audit information (username, userid, timestamps,...) so exactly what you want to achieve I suppose.
We have implemented by simply retrieving the information via the access token that is currently available in the session. We also introduced a new user attribute in keycloak itself, which is a custom account id. The user sets it on keycloak GUI and we retrieve it via accessToken.getOtherClaims().get("ACCOUNT_ID") to query user specific data.
The token itself is handled in a filter and used in another bean to retrieve the data which looks like
#WebFilter(value = "/*")
public class RefreshTokenFilter implements Filter {
#Inject
private ServletOAuthClient oauthClient;
#Inject
private UserData userData;
#Context
KeycloakSecurityContext sc;
#Override
public void init(FilterConfig filterConfig) throws ServletException {
}
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
if (request.getUserPrincipal() != null) {
KeycloakSecurityContext keycloakSecurityContext = ((KeycloakPrincipal) request.getUserPrincipal()).getKeycloakSecurityContext();
userData.setAccessToken(keycloakSecurityContext.getToken());
userData.setIdToken(keycloakSecurityContext.getIdToken());
}
filterChain.doFilter(request, response);
}
#Override
public void destroy() {
}
}
and here I have the bean that handles the data access
#SessionScoped
#Named("userData")
public class UserData implements Serializable {
private static final String ACCOUNT_ID = "accountId";
private AccessToken accessToken;
private IDToken idToken;
public String getUserFullName() {
return isHasAccessToken() ? accessToken.getName() : null;
}
public String getUserName() {
return isHasAccessToken() ? accessToken.getPreferredUsername() : null;
}
public String getUserId() {
return isHasAccessToken() ? accessToken.getSubject() : null;
}
public String getRoles() {
StringBuilder roles = new StringBuilder();
if (isHasAccessToken()) {
accessToken.getRealmAccess().getRoles().stream().forEach(s -> roles.append(s).append(" "));
}
return roles.toString();
}
public boolean hasApplicationRole(String role) {
return accessToken.getRealmAccess().isUserInRole(role);
}
public boolean isHasAccessToken() {
return accessToken != null;
}
public List<String> getAccountIds() {
return isHasAccessToken() && accessToken.getOtherClaims().get(ACCOUNT_ID)!=null ? (List<String>) accessToken.getOtherClaims().get(ACCOUNT_ID) : new ArrayList<>();
}
public void setAccessToken(AccessToken accessToken) {
this.accessToken = accessToken;
}
public void setIdToken(IDToken idToken) {
this.idToken = idToken;
}
}
I would assume spring boot will give you similar options to deal with the KeycloakSecurityContext.