Docs

Getting Started with SSO Kit

Step-by-step guide on how to use SSO Kit in your 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 --react <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 of the essential authentication state is already available in a global variable and can be used as application state. Create a new file named useAuth.tsx and define a React Context:

import { createContext, Dispatch, SetStateAction } from "react";
import SingleSignOnData from "./generated/dev/hilla/sso/starter/SingleSignOnData";

// Used for access control
export type AccessProps = Readonly<{
    requiresLogin?: boolean;
}>;

// The context type
export type Authentication = Readonly<{
    state: SingleSignOnData;
    hasAccess: (route: AccessProps) => boolean;
    clearAuthInfo: () => void;
}>;

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

// The context itself.
export const AuthContext = createContext<Authentication>({
    state: initialState,
    hasAccess: () => false,
    clearAuthInfo: () => { },
});

// The hook to use the context.
export const useAuth = (
    state: SingleSignOnData,
    setState: Dispatch<SetStateAction<SingleSignOnData>>
): Authentication => {
    return {
        state,
        hasAccess: (route: AccessProps) => {
            return !route.requiresLogin || state.authenticated;
        },
        clearAuthInfo: () => {
            setState({
                ...state,
                authenticated: false,
                backChannelLogoutEnabled: false,
                logoutLink: undefined,
                roles: [],
            });
        }
    }
};

Next, add the state and the context to App.tsx and wrap the RouterProvider like so:

import router from 'Frontend/routes.js';
import { useState } from 'react';
import { RouterProvider } from 'react-router-dom';
import { AuthContext, initialState, useAuth } from './useAuth';

export default function App() {
  const [state, setState] = useState(initialState);

  return <AuthContext.Provider value={useAuth(state, setState)}>
    <RouterProvider router={router} />
  </AuthContext.Provider >;
}

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';
import { Button } from '@hilla/react-components/Button.js';
import { AuthContext } from 'Frontend/useAuth';
import { Suspense, useContext } from 'react';

// Use the AuthContext.
const { state } = useContext(AuthContext);

// Define button event handlers.
async function signOut() {
  await logout(); // Logout on the server
  location.href = state.logoutLink!;
};

function signIn() {
  location.href = state.loginLink;
};

// Add the buttons to the footer.
<footer slot="drawer">
  {state.authenticated
    ? <Button onClick={signOut}>Sign out</Button>
    : <Button onClick={signIn}>Sign in</Button>
  }
</footer>

3.3. Add Access Control

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

Open the frontend/routes.tsx and add the requiredLogin parameter to a view:

import { AccessProps } from './useAuth';

// Enrich the ViewRouteObject type with AccessProps.
export type ViewRouteObject = (IndexViewRouteObject | NonIndexViewRouteObject) & AccessProps;

// Add requiresLogin to the About View.
{
  path: '/about',
  element: <AboutView />,
  handle: { icon: 'la la-file', title: 'About' },
  requiresLogin: true,
},

Next, in the main layout, filter the menu:

// Gather the hasAccess function.
const { state, hasAccess } = useContext(AuthContext);

// Filter the menu when rendering.
{menuRoutes.filter(hasAccess).map(({ path, handle: { icon, title } }) => (
  // ...

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 { useEffect, useState } from "react";

// Store the authenticated user.
const [user, setUser] = useState<User | undefined>();

// Fetch the authenticated user from the server.
useEffect(() => {
  UserEndpoint.getAuthenticatedUser().then(setUser);
}, []);

// Add some output.
<p>Username: {user?.preferredUsername}</p>
<p>Full name: {user?.fullName}</p>
<p>Email: {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

As an example, show a dialog when the user is logged out from outside the application. You can do that in the main layout file like this:

import { ConfirmDialog } from '@hilla/react-components/ConfirmDialog.js';
import { BackChannelLogoutEndpoint } from 'Frontend/generated/endpoints';
import { Suspense, useContext, useEffect, useState } from 'react';

// Add a state that is modified when the log-out event happens.
const [backChannelLogout, setBackChannelLogout] = useState(false);

// Gather the clearAuthInfo function from the AuthContext.
const { state, hasAccess, clearAuthInfo } = useContext(AuthContext);

// Subscribe to the endpoint and update the state, accordingly.
useEffect(() => {
  if (state.backChannelLogoutEnabled) {
    const subscription = BackChannelLogoutEndpoint.subscribe();
    subscription.onNext(() => {
      setBackChannelLogout(true);
      subscription.cancel();
    });
  }
}, []);

// Add the click event handlers.
async function loginAgain() {
  await logout(); // Logout on the server
  location.href = state.loginLink;
}

async function stayOnPage() {
  setBackChannelLogout(false);
  await logout(); // Logout on the server.
  clearAuthInfo(); // Clear the user info on the client.
}

// Finally, add the dialog.
<ConfirmDialog header='Logged out' cancelButtonVisible opened={backChannelLogout}
  onConfirm={loginAgain} onCancel={stayOnPage}>
  <p>You have been logged out. Do you want to log in again?</p>
</ConfirmDialog>

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.