Getting started with SSO Kit
SSO Kit builds upon Spring Boot and Spring Security. It comes with a starter module that configures the security settings needed to authenticate with an identity provider.
1. Create a Hilla application without authentication
You can create a Hilla application 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 the SSO Kit dependency
Add the sso-kit-starter
module and other required dependencies to the pom.xml
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
|
Check 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 whitelist its package name in your Spring Boot application. To do this, open your Application.java
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 the 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 back-end endpoints. Both of them can and should benefit from the authentication protection.
2.4.1. Protect the 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 will be 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 (.\mvnw
on Windows). 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 (e.g., 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
:
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 and 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.
As the About page is now protected, that’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, it can be done 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 this 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:
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 using 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
.