Connect an Identity Provider
- Register the Application
- Add the Dependency
- Configure the Provider
- Configure the Login
- Map the Provider’s Roles
- Log Out
- What This Guide Doesn’t Cover
In production, users usually log in with the account they already have at their organization’s identity provider, such as Keycloak, Microsoft Entra ID, or Okta. The provider checks their password and any second factor, and lets them move between applications without logging in again. A Vaadin application connects to such a provider through OpenID Connect (OIDC), using the OAuth2 client of Spring Security.
This guide replaces the in-memory users and the login view of Add Login with an identity provider. It uses Keycloak as the example. It then maps the roles that the provider knows to the roles that Protect Views and Protect Services check, and makes logging out of the application end the session at the provider as well. The application side is the same for every OIDC provider. What differs between providers is how you register the application, and how the provider puts roles in its tokens.
|
Note
|
Replaces SSO Kit
SSO Kit is deprecated, and the next major version of Vaadin doesn’t include it. Its major features are available in Spring Security and in Vaadin’s Spring Security integration, which this guide uses. To move an application that already uses the kit, see Migrating from SSO Kit to Spring Security.
|
Register the Application
The application is a client of the identity provider, and the provider only lets registered clients log users in. For local development, run Keycloak in a container as described in Keycloak’s Get Started with Docker guide, but map it to another port, such as 8180, since the Vaadin application uses 8080. Create a realm, and the users that you want to log in with.
Then create a client in the realm, under Clients > Create client, with the following settings:
- Client ID
-
An identifier for the application, such as
my-app. - Client authentication
-
On. The application then proves its identity to Keycloak with a client secret, which you find on the Credentials tab of the client once you’ve saved it.
- Valid redirect URIs
-
http://localhost:8080/login/oauth2/code/keycloak. After logging the user in, Keycloak sends them back to this URL, where Spring Security completes the login. The last part of the path is the registration ID that you choose in Configure the Provider. - Valid post logout redirect URIs
-
http://localhost:8080. After logging the user out, Keycloak sends them to this URL. See Log Out for details, including why there’s no trailing slash.
When you deploy the application, register its production URLs in the same way.
Add the Dependency
Add the OAuth2 client starter of Spring Boot. It includes the Spring Security starter that you added in Add Login, so it can replace that dependency:
Source code
XML
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security-oauth2-client</artifactId>
</dependency>Configure the Provider
Tell Spring Security where the provider is, and how the application identifies itself:
Source code
application.properties
application.propertiesspring.security.oauth2.client.registration.keycloak.client-id=my-app
spring.security.oauth2.client.registration.keycloak.client-secret=${KEYCLOAK_CLIENT_SECRET}
spring.security.oauth2.client.registration.keycloak.scope=openid,profile,email
spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:8180/realms/my-realm-
client-secretreads the secret from an environment variable, so that it doesn’t end up in version control. -
The
openidscope makes the login an OpenID Connect login. Theprofileandemailscopes give the application the user’s name and email address. -
issuer-uriis the URL of the realm. Spring Security reads the endpoints and signing keys of the provider from the discovery document at this URL.
The keycloak part of the property names is the registration ID. It’s yours to choose, and it appears in the URLs that Spring Security uses for the login, such as the redirect URI that you registered earlier.
For the other properties, and for providers that have no discovery document, see the Spring Security reference.
Configure the Login
Replace the login view in the security configuration with the login page of the provider:
Source code
SecurityConfig.java
SecurityConfig.java@EnableWebSecurity
@Configuration
class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.with(VaadinSecurityConfigurer.vaadin(), configurer -> {
configurer.oauth2LoginPage("/oauth2/authorization/keycloak"); 1
});
return http.build();
}
}-
Spring Security sends users who request this path to the provider registered as
keycloak.
Remove the UserDetailsManager bean and the login view: the provider handles both now. When users who haven’t logged in open a view, Vaadin redirects them to the provider’s login page. After they’ve logged in, the provider sends them back, and the application shows the view they originally requested.
The views and their security annotations stay as they are. However, until you map the provider’s roles as described in the next section, users have no roles, so they can only open views that are annotated with @PermitAll or @AnonymousAllowed.
Showing Who Is Logged In
The authenticated user is now an OidcUser, which holds the claims that the provider sent about the user, such as their name and email address. Get it from AuthenticationContext:
Source code
Java
@Route("")
@PermitAll
public class MainView extends Main {
public MainView(AuthenticationContext authenticationContext) {
authenticationContext.getAuthenticatedUser(OidcUser.class)
.map(OidcUser::getFullName) 1
.ifPresent(name -> add(new Span("Logged in as " + name)));
}
}-
Claims are optional in OpenID Connect, so check the ones you use against what your provider actually sends.
To refer to a user in your own data — for example, as the owner of a record — use the user’s subject identifier, the sub claim. The OpenID Connect specification requires it to be unique within the provider and never reassigned, whereas user names and email addresses can change. The getName() method of the Spring Security Authentication returns it by default. Protecting Individual Records shows how to use it in an application service.
Map the Provider’s Roles
After logging in through a provider, a user has no roles in Spring Security. By default, Spring Security grants only an OIDC_USER authority, and a SCOPE_ authority for each scope. Every view annotated with @RolesAllowed, and every service method that checks hasRole(), denies access.
Providers can send the roles of a user as a claim in the ID token, but OpenID Connect doesn’t standardize the name of the claim, and not every provider sends one by default. The general approach is to make the provider add the roles to the ID token, and to map that claim to roles in the application.
In Keycloak, open the client under Clients, and go to its Client scopes tab. The first scope on the tab, my-app-dedicated, holds the client’s own mappers. Keycloak creates it for every client and names it after the client ID. The Client scopes page of the realm doesn’t list it. Open the scope, and select Configure a new mapper > User Realm Role. If the scope already has mappers, the button is Add mapper > By configuration instead.
Set both Name and Token Claim Name to roles, and leave Add to ID token and Multivalued on. The ID token of a user then contains a roles claim that lists the realm roles of the user.
Name the realm roles in Keycloak exactly like the roles of the application, such as ADMIN and USER, since role names are case-sensitive.
Then map the claim in a GrantedAuthoritiesMapper bean. Spring Security applies the bean to the OAuth2 login without any further configuration:
Source code
SecurityConfig.java
SecurityConfig.java@EnableWebSecurity
@Configuration
class SecurityConfig {
...
@Bean
GrantedAuthoritiesMapper rolesClaimMapper() {
return authorities -> {
var mapped = new HashSet<GrantedAuthority>(authorities); 1
for (var authority : authorities) {
if (authority instanceof OidcUserAuthority oidcUser) {
var roles = oidcUser.getIdToken().getClaimAsStringList("roles"); 2
if (roles != null) {
roles.forEach(role ->
mapped.add(new SimpleGrantedAuthority("ROLE_" + role))); 3
}
}
}
return mapped;
};
}
}-
Keeps the authorities that Spring Security has already granted.
-
Reads the claim that you configured at the provider.
-
Spring Security stores roles as authorities with the
ROLE_prefix.@RolesAllowedandhasRole()add the prefix to the role names they check.
Users can now open the views that their roles allow. The tests from Testing Method Security and Test View Access Control don’t change, since @WithMockUser sets the roles directly, regardless of how users log in.
The same mapper works for other providers that put roles in a claim of the ID token. If your provider sends groups rather than roles, or uses names that differ from the roles of the application, translate them in the mapper — for example, with a Map from group name to role. To read claims that only the access token contains, replace the user service instead, as described in the Spring Security reference.
Testing the Mapping
If the provider stops sending the claim — for example, because someone changes the client configuration — users can still log in, but they lose their roles. Test the mapper with the claim it expects:
Source code
Java
class SecurityConfigTest {
@Test
void maps_the_roles_claim_to_roles() {
var idToken = OidcIdToken.withTokenValue("id-token")
.subject("user")
.claim("roles", List.of(Roles.ADMIN))
.build();
var authorities = new SecurityConfig().rolesClaimMapper()
.mapAuthorities(List.of(new OidcUserAuthority(idToken)));
assertThat(authorities)
.extracting(GrantedAuthority::getAuthority)
.contains("ROLE_" + Roles.ADMIN);
}
}To check the provider’s side, log in as users with different roles, and verify that each of them can open exactly the views that their roles allow.
Log Out
Logging out works as described in Add Logout: call logout() on AuthenticationContext.
With oauth2LoginPage(), logging out also ends the user’s session at the provider. Spring Security sends the browser to the provider’s logout endpoint, which ends the session and sends the user back to the application. This is called RP-Initiated Logout. Without it, the user would only be logged out of the application: the next time they log in, the provider would recognize the session and log them in without asking for a password.
By default, the provider sends the user back to the root URL of the application. The root view requires a login, so the user ends up on the provider’s login page.
The provider only redirects to URLs that are registered for the client as post-logout redirect URLs. Keycloak compares them exactly, and the default URL has no trailing slash. This is why the client in Register the Application has http://localhost:8080 rather than http://localhost:8080/. With a URL that isn’t registered, Keycloak shows an error page instead of logging the user out.
To send users somewhere else after logging out, such as a page that confirms the logout, pass its URL as the second argument:
Source code
Java
configurer.oauth2LoginPage("/oauth2/authorization/keycloak", "{baseUrl}/logged-out.html");Register the URL at the provider, and allow access to the page without authentication, as described in Configuring the Logout Success URL. For the variables that the URL can contain, see OAuth2 Authentication.
When the User Logs Out Elsewhere
Logging out of the application ends the session at the provider, but it doesn’t work the other way around. When the user logs out of another application that shares the session at the provider, or when an administrator ends the session there, the user stays logged in to the Vaadin application until their session expires.
To end the application’s session as well, enable Back-Channel Logout. The provider then notifies the application directly when a session ends:
Source code
SecurityConfig.java
SecurityConfig.java@EnableWebSecurity
@Configuration
class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.with(VaadinSecurityConfigurer.vaadin(), configurer -> {
configurer.oauth2LoginPage("/oauth2/authorization/keycloak");
});
http.oidcLogout(logout -> logout.backChannel(Customizer.withDefaults()));
return http.build();
}
@Bean
HttpSessionEventPublisher httpSessionEventPublisher() { 1
return new HttpSessionEventPublisher();
}
...
}-
Lets Spring Security forget about sessions that end in other ways, such as by timing out.
In Keycloak, set the Backchannel logout URL of the client to http://localhost:8080/logout/connect/back-channel/keycloak, where keycloak is the registration ID. The provider has to be able to reach the application at that URL.
When the provider ends a session, the application invalidates the corresponding HTTP session. The browser finds out on its next request to the server, and the user ends up on the provider’s login page.
Two details matter in production:
-
To invalidate the session, Spring Security calls the application’s own logout endpoint, at the address where the notification from the provider arrived. If the application can’t reach itself at that address — for example, because it runs behind a proxy — set an internal address with
backChannel.logoutUri(…). -
Spring Security remembers which provider session belongs to which HTTP session in memory. In a cluster, the server that receives the notification might not be the one that holds the session. To share the information between servers, store it in a shared location, such as a database, with an
OidcSessionRegistryof your own. See the Spring Security reference for details.
What This Guide Doesn’t Cover
-
Security Assertion Markup Language (SAML) 2.0.
VaadinSecurityConfigurerhas no configuration specific to SAML, and this guide covers only OpenID Connect. Spring Security supports SAML 2.0 on its own: see SAML 2.0 Login. If your identity provider supports both protocols, use OpenID Connect. -
React views. The server-side configuration is the same for React views. The client side differs from Secure React Views, since the provider shows the login form. See Migrating a Hilla Application for how to give the client the user’s details and protect routes.
-
Several identity providers. Spring Security supports several client registrations, but
oauth2LoginPage()takes a single login page. To let users choose between providers, point it to a view of your own that links to each provider’s/oauth2/authorization/{registrationId}path. -
Multi-tenancy. See Tenant Isolation.