spring-bootkeycloakfederated-identity

Keycloak and spring boot rest api - user specific data strategy


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)


Solution

  • 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.