Authentication and Authorization using Keycloak and OAuth 2.0

Hey everyone :wave:,

If anyone is interested how to implement user authentication and role-based authorization in a Hilla app using Keycloak and OAuth 2.0, here are two examples:

Hilla with Keycloak and the Authorization Code Flow of OAuth 2.0

When using the Authorization Code Flow, authentication takes place via a Keycloak user interface. Other authentication options, such as MFA, can be added or configured via Keycloak in this case.

Hilla with Keycloak and the Resource Owner Password Flow of OAuth 2.0

When using the Resource Owner Password Flow, authentication takes place via a Hilla user interface. In this case, you have more control over the user experience and users are not redirected to another application to log in there.

6 Likes

That’s really nice-looking documentation! One thing I’d maybe emphasize is that you can add security checks for any backend service invocations for additional hardening. That way you’re not only relying on the endpoint security.

2 Likes

I agree. That is why I mentioned it in the outlook section of each blog post.

That’s the endpoint security, yes. Once you have multiple endpoints with different security measures, though, you need to be careful that the endpoint service Java code doesn’t accidentally invoke any other services that should be restricted from the current user. You can check the current Principal’s roles before the service invocations to make sure that doesn’t happen.

4 Likes

All right, now I know what you mean :+1: Thank you for describing it again. I agree with you, of course.

1 Like

Nice implementation examples and blog posts within the security domain! Thank you for creating it! :bowing_man:

(Very detailed, thorough with lot of code examples within the blog post)

1 Like

Thank you for sharing this with the detailed explanation!

I’ve followed the Resource Owner Password Flow (but using Flow instead of Hilla) and get a little bit confused. After we create BearerTokenAuthenticationToken within the code of CustomAuthenticationProvider it is placed to the SecurityContextHolder. I wonder is it plays the role of an authenticated entity, i.e. if I obtained the token it means I’ve been authenticated?

If yes, then problem is that BearerTokenAuthenticationToken has a value of authenticated equals false. And that is not allows you to pass @PermitAll checks of the view. If I set it to true explicitly the check starting to work.

The next problem I’ve encountered is a roles checks using @RolesAllowed. I suppose it is not working since BearerTokenAuthenticationToken has an empty list of granted authorities. Is it implies I should also fill them explicitlty by extracting them from the token?

Once you successfully obtained an access token from the OAuth2AuthorizedClient it means, that the user is authenticated.

To proper support @RolesAllowed you have to use an OAuth2AuthenticationToken instead of an BearerTokenAuthenticationToken. An OAuth2AuthenticationToken can hold a list of granted authorities, that represent the roles of a user. These authorities are provided as a claim called roles in the userinfo endpoint response of Keycloak.

I’ll try to update my blog post and the code to reflect this changes, but I guess it will take some time.

Thank you for the answer! I actually tried to make an anonymous BearerTokenAuthenticationToken object with getAuthorities() override but for some reason it haven’t worked:

BearerTokenAuthenticationToken token = new BearerTokenAuthenticationToken(
		authorizedClient.getAccessToken().getTokenValue()) {

	private static final long serialVersionUID = 7122656233878115610L;

	@Override
	public Collection<GrantedAuthority> getAuthorities() {
		return Collections.singleton(new SimpleGrantedAuthority("IC"));
	};
};

Concerning OAuth2AuthenticationToken I’ve also tried the hardcoded way to check, but it doesn’t worked for @RolesAllowed("IC") as well:

OAuth2User user = new DefaultOAuth2User(Collections.singleton(new SimpleGrantedAuthority("IC")),
		Collections.singletonMap("preferred_username", "jd"), "preferred_username");

OAuth2AuthenticationToken token = new OAuth2AuthenticationToken(user, user.getAuthorities(),
		authorizedClient.getClientRegistration().getRegistrationId());

I’ve found the problem about the role checks. Spring security by default operates on granted authorities having ROLE_ prefix. So if using new SimpleGrantedAuthority("ROLE_IC") it will work correctly. Alternatively, you can define other or empty prefix if needed:

@Bean
static GrantedAuthorityDefaults grantedAuthorityDefaults() {
	return new GrantedAuthorityDefaults("");
}

I’ve also followed the Authorization Code Flow (also using Flow instead of Hilla) and it works like a charm! Thanks for the great article!

The only thing that I have problem with is a logout. In your article the logout button just updates current url (see UserAuthenticationService.getLogoutUrl()) resulting in a direct call of keycloak logout endpoint. It works as expected:

  • invalidates keyclock session
  • redirects to the provided url

but that is not enough. In Vaadin application you’re dealing with three different sessions:

  • sso (keycloak) session
  • http session
  • vaadin session

In the above case only sso session had been invalidated, while http session leave untouched. Http session is still valid and related authentication is still located in the security context allowing user to work with an application until http session will be expired.

I suggest to apply some changes to your code. In a SecurityConfiguration instead of using HttpStatusReturningLogoutSuccessHandler apply OidcClientInitiatedLogoutSuccessHandler as it provides all the neccessary functionality out of the box:

OidcClientInitiatedLogoutSuccessHandler logoutSuccessHandler = new OidcClientInitiatedLogoutSuccessHandler(
		clientRegistrationRepository);

logoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}/foo");

http.logout(logout -> logout.logoutSuccessHandler(logoutSuccessHandler)
		.logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET")));

The value of postLogoutRedirectUri will be used as a post_logout_redirect_uri query parameter when sending logout request to the keycloak. Note also the new AntPathRequestMatcher("/logout", "GET"). It will force to map logout endpoint to the GET method to avoid Spring’s native CSRF checks.

To logout you just need to change current url to the /logout path. Here is an example using Flow:

Button logoutButton = new Button("Logout", event -> UI.getCurrent().getPage().setLocation("/logout"));

Thank you for sharing your ideas to improve the example code. I will have a closer a look at them when I find the time to update the blog post and the example code.