Docs

Getting Started with SSO Kit

Step-by-step guide on how to use SSO Kit in an application.

SSO Kit builds upon Spring Boot and Spring Security. It comes with a starter module for configuring the security settings needed to authenticate with an identity provider.

1. Create a Hilla Application

You can create a Hilla application without authentication by entering the following from the command-line:

npx @hilla/cli init <your-project-name>

2. Backend

Once you’ve created the Hilla application, you can begin securing it by installing and configuring SSO Kit on the backend. The following section shows how to block unauthorized users from using a button in an application. You can install and update SSO Kit by adding it as a dependency to your application in the pom.xml file.

2.1. Add SSO Kit Dependency

Add the sso-kit-starter module and other required dependencies to the pom.xml file of a Vaadin application like so:

<dependency>
    <groupId>dev.hilla</groupId>
    <artifactId>sso-kit-starter</artifactId>
    <version>2.0.0</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
Note
Version Number
See the SSO Kit releases page for the latest version, or a different version of the sso-kit-starter dependency.

2.2. Configure Endpoints

For Hilla to be able to parse and generate endpoints from an application and from SSO Kit for your frontend, you have to add a configuration to the Hilla Maven plugin with your application and SSO Kit package names like this:

<plugin>
    <groupId>dev.hilla</groupId>
    <artifactId>hilla-maven-plugin</artifactId>
    <version>${hilla.version}</version>
    <!-- Add this configuration -->
    <configuration>
        <parser>
            <packages>
                <package>com.example.application</package>
                <package>dev.hilla.sso.starter</package>
            </packages>
        </parser>
    </configuration>
    <!-- ... -->
</plugin>

By default, the package name in generated Hilla projects is com.example.application. SSO Kit uses the package name dev.hilla.sso.starter. To be able to find the annotated endpoints in SSO Kit that are required, you have to allow-list its package name in your Spring Boot application. To do this, open your Application.java file and add these packages to the @SpringBootApplication annotation like so:

@SpringBootApplication(scanBasePackages = {
  "com.example.application", // Application package.
  "dev.hilla.sso.starter" // SSO Kit package.
})
public class Application // ...

2.3. Configure SSO Provider in Spring

Next, you need to set some configuration properties to connect SSO Kit to your OpenID Connect provider. These properties can be added to your application.properties file where you give the provider URL and the client registration details, such as credentials and scope.

Provider definition is configured within the spring.security.oauth2.provider namespace where you give a key to identify your provider, such as keycloak. You can use the same key to register the client for that provider within the spring.security.oauth2.registration namespace, where you specify client credentials and the requested scope.

The scope is a list of keywords to request the provider for a specific set of information, such as user profile, email or roles. The following is an example of the properties to set to enable a Keycloak instance to perform authentication:

hilla.sso.login-route=/oauth2/authorization/keycloak
spring.security.oauth2.client.registration.keycloak.scope=profile,openid,email,roles
# Customize the following property values for your Keycloak configuration:
spring.security.oauth2.client.provider.keycloak.issuer-uri=https://my-keycloak.io/realms/my-realm
spring.security.oauth2.client.registration.keycloak.client-id=my-client
spring.security.oauth2.client.registration.keycloak.client-secret=very-secret-value

2.4. Secure the Application

A Hilla application includes front-end code and backend endpoints. Both of them can and should benefit from authentication protection.

2.4.1. Protect Example Endpoint

Hilla allows fine-grained authorization on endpoints and endpoint methods. You can use annotations like @PermitAll or @RolesAllowed(…​) to declare who can access what.

To try this feature, replace the @AnonymousAllowed annotation in HelloWorldEndpoint.java with @PermitAll, so that unauthenticated users are unable to access all endpoint methods. You could also apply the same annotation at the method level for more fine-grained control.

Start the application using the mvnw command. Then try the application in the browser. It should work correctly, except that when you click on the Say Hello button, nothing happens. This is because the endpoint is no longer accessible without authentication.

3. Frontend

Once the backend is secure, you can begin extending authentication features to the frontend. The following section shows how to display user information, for example a name, on secured views and enable users to log in and out.

3.1. Implement Authentication State

All the essential authentication state is already available in a global variable and can be added to the application state, which is inside the AppStore class in app-store.ts:

import SingleSignOnData from 'Frontend/generated/dev/hilla/sso/starter/SingleSignOnData';

// All necessary data is already loaded in the Hilla global variable.
authInfo = (window as any).Hilla.SSO as SingleSignOnData;

3.2. Add Log-In & Log-Out Buttons

As an example, add two buttons to the drawer footer — one to sign in, and another to sign out. When signing out, it’s important to invoke the logout function provided by Hilla to perform logout on the server. Then, load the SSO provider logout page.

import { logout } from '@hilla/frontend';

// Replace the `footer` in the rendered `html`.
<footer slot="drawer">
  ${appStore.authInfo.authenticated
    ? html`<vaadin-button @click="${this.signOut}">Sign out</vaadin-button>`
    : html`<vaadin-button @click="${this.signIn}">Sign in</vaadin-button>`
  }
</footer>

// Add the needed functions inside the class.
private signOut = async () => {
  await logout(); // Logout on the server
  location.href = appStore.authInfo.logoutLink!;
};

private signIn = () => {
  location.href = appStore.authInfo.loginLink;
};

3.3. Add Access Control

You can protect your views by verifying that each authentication has happened before loading the view.

In app-store.ts, add a new type definition and a function to check access rights using that type:

export type AccessProps = {
  requiresLogin?: boolean;
};

// Put this function inside the AppStore class.
hasAccess = (route: AccessProps) => {
  return !route.requiresLogin || this.authInfo.authenticated;
};

In the frontend/routes.ts file, use the AccessProps type and protect the About view:

import { AccessProps } from './stores/app-store';

// Add AccessProps to the ViewRoute type.
export type ViewRoute = Route & AccessProps & {
  // ...
}

// Add the requiresLogin attribute to the About view.
{
  path: 'about',
  // ...
  requiresLogin: true,
},

Then, filter the menu excluding unauthorized views by amending the view filter in main-layout.ts:

// Add a new condition in getMenuRoutes that checks for authentication.
private getMenuRoutes(): RouteInfo[] {
  return views.filter((route) => route.title).filter(appStore.hasAccess) as RouteInfo[];
}

Now the About item in the menu appears only when authenticated.

3.4. Show User Information

The SSO Kit provides a default endpoint to get information about the authenticated user. You can implement yours if you want to customize the returned object and its fields.

Since the About page is now protected, it’s a perfect place to show some information about the current user:

import User from 'Frontend/generated/dev/hilla/sso/starter/endpoint/User';
import { UserEndpoint } from 'Frontend/generated/endpoints';
import { property } from 'lit/decorators.js';

// Add a property for the user.
@property()
user: User | undefined;

// Add the keyword `async` to connectedCallback and then load the user inside the function.
async connectedCallback() {
  // ...
  this.user = await UserEndpoint.getAuthenticatedUser();
}

// Add some output.
<p>Username: ${this.user?.preferredUsername}</p>
<p>Full name: ${this.user?.fullName}</p>
<p>Email: ${this.user?.email}</p>

4. Single Sign-Off

SSO Kit provides two methods for logging out the user. They’re defined by the OpenID Connect specification like so:

4.1. RP-Initiated Logout

RP-initiated logout (i.e., Relaying Party, the application) enables the user to logout from the application itself, ensuring the connected provider session is terminated.

4.2. Back-Channel Logout

Back-Channel Logout is a feature that enables the provider to close user sessions from outside the application. For example, from the provider’s user dashboard or from another application.

4.2.1. Enable the Feature

To enable the feature in the application, you need to set the hilla.sso.back-channel-logout property to true. You would do that like you see here:

hilla.sso.back-channel-logout=true

The client should then be configured on the provider’s dashboard to send logout requests to a specific application URL: /logout/back-channel/{registration-key}, where {registration-key} is the provider key.

4.2.2. Modify the Frontend

State about back-channel logout can be added to app-store.ts:

import { BackChannelLogoutEndpoint } from 'Frontend/generated/endpoints';

// Will become true when back-channel logout happens.
backChannelLogoutHappened = false;

constructor() {
  makeAutoObservable(this);

  // Add this to the constructor to subscribe to back-channel logout events.
  if (this.authInfo.backChannelLogoutEnabled) {
    const subscription = BackChannelLogoutEndpoint.subscribe();
    subscription.onNext(() => {
      this.backChannelLogoutHappened = true;
      subscription.cancel();
    });
  }
}

// Clears authInfo without reloading the page.
clearAuthInfo() {
  this.authInfo = {
      ... this.authInfo,
      authenticated: false,
      backChannelLogoutEnabled: false,
      logoutLink: undefined,
      roles: [],
  };
}

Now, a dialog can be added to the application layout to notify the user:

import '@vaadin/confirm-dialog';

// Add the dialog to the rendered html.
<vaadin-confirm-dialog
  header="Logged out"
  cancel-button-visible
  @confirm="${this.loginAgain}"
  @cancel="${this.stayOnPage}"
  .opened="${appStore.backChannelLogoutHappened}"
>
  <p>You have been logged out. Do you want to log in again?</p>
</vaadin-confirm-dialog>

// Then add the event handlers.
private async stayOnPage() {
  await logout(); // Logout on the server
  appStore.clearAuthInfo(); // Logout on the client
}

private async loginAgain() {
  await logout(); // Logout on the server
  location.href = appStore.authInfo.loginLink!;
}

You can trigger a logout externally with the provider tools. For Keycloak, you can sign out a session from the admin console or visit the page https://my-keycloak.io/realms/my-realm/protocol/openid-connect/logout.