Login

DraftAccessible

Login components include React components, context and hooks for handling user authorisation, api tokens and session polling.

A work in progress!
The HDS Login system is a set of components the HDS team is currently making. This means that this component is subject to change, and we don't recommend using it in production.

Usage

Table of contents

Components

HDS login components cover most use cases and also handle errors when logging in or getting tokens.

Detailed information about all properties of each component is listed on the API page.

Important

All components and hooks require a LoginProvider to provide context for them.

LoginButton

This component handles the redirection to the OIDC server and also errors if the server rejects the request for OpenID configuration.

Button text is passed and rendered as a child.

<LoginButton errorText="Login failed. Try again!">Log in</LoginButton>

All component properties are listed on the API page.

LoginCallbackHandler

This component handles the response when the OIDC server redirects the browser back to the given callback route. When the response is parsed, the browser is usually redirected to another path.

const onSuccess = (user: User) => {
// Handle successful login
// Usually redirect to a page
};
const onError = (error: OidcClientError) => {
//Handle error
};
return (
<LoginProvider {...providerProperties}>
<LoginCallbackHandler onSuccess={onSuccess} onError={onError}>
<div>Logging in...</div>
</LoginCallbackHandler>
</LoginProvider>
);

All component properties are listed on the API page.

LoginProvider

This component creates a React context and initialises all modules. All components and hooks are required to render inside the LoginProvider or React Context will throw an error. Position the context in the component tree like any other React context. The context does not change and cause re-renders often, so it can be at the top level.

const providerProperties: LoginProviderProps = {
userManagerSettings: {
authority: 'https://tunnistamo.dev.hel.ninja/',
client_id: 'exampleapp-ui-dev',
scope: 'openid profile email',
redirect_uri: 'https://service.fi/callback',
},
apiTokensClientSettings: { url: 'https://tunnistamo.dev.hel.ninja/api-tokens/' },
sessionPollerSettings: { pollIntervalInMs: 300000 },
};
const MyApp = () => {
const { isAuthenticated, getUser, logout } = useOidcClient();
const { getApiTokens } = useApiTokens();
const { addListener } = useSignalListener();
if (isAuthenticated()) {
return (
<>
<p>Hello, {user.name}!</p>
<Button onClick={() => logout()}>Log out</Button>
</>
);
} else {
return (
<>
<p>You are not logged in.</p>
<LoginButton errorText="Login failed. Try again!">Log in</LoginButton>
</>
);
}
};
<LoginProvider {...providerProperties}>
<MyApp />
</LoginProvider>;

All component properties are listed on the API page.

SessionEndedHandler

This component listens to the session-ended signal. It is emitted from the Session poller, if it receives a response indicating the user's session has ended. Then the component shows an HDS Dialog and forces the user to log out. The Dialog can be seen in the Storybook. This component can be placed anywhere inside the LoginProvider.

<SessionEndedHandler
content={{
title: 'Session has ended!',
text: 'Your session on the server has ended. You will be logged out in this window too.',
buttonText: 'Logout',
closeButtonLabelText: 'Logout',
}}
/>

All component properties are listed on the API page.

WithAuthentication

Component for rendering components conditionally, depending on whether the user is authenticated or not.

const HelloUser = ({ user }: { user: User }) => {
return <p>Hello, {user.name}!</p>;
};
const LoginComponent = () => {
return <LoginButton errorText="Login failed. Try again!">Log in</LoginButton>;
};
<WithAuthentication AuthorisedComponent={HelloUser} UnauthorisedComponent={LoginComponent} />;

All component properties are listed on the API page.

WithAuthenticatedUser

Renders its children only if the user is authenticated. Uses WithAuthentication, but allows multiple children.

<WithAuthenticatedUser>
<p>This is rendered, if the user is authenticated</p>
</WithAuthenticatedUser>

All component properties are listed on the API page.

WithoutAuthenticatedUser

Renders its children only if the user is not authenticated. Uses WithAuthentication, but allows multiple children.

<WithoutAuthenticatedUser>
<p>This is rendered, if the user is not authenticated</p>
</WithoutAuthenticatedUser>

All component properties are listed on the API page.

Oidc client hooks

More detailed information about Oidc client hooks is listed on the API page.

useAuthenticatedUser

Returns a user object if the object is valid, authenticated and passes the isValidUser() check.

useCachedAmr

Returns the user's amr value. It is cached because in some cases it must be decrypted from an id_token.

useOidcClient

Returns the Oidc client module.

useOidcClientTracking

Returns an array of [signal, reset function, oidcClient instance]. The hook re-renders the component each time the client emits a signal.

import { useOidcClient, useOidcClientTracking, useAuthenticatedUser, useCachedAmr } from 'hds-react';
const OidcClientHooks = () => {
const client = useOidcClient();
const [lastSignal, reset, oidcClient] = useOidcClientTracking();
const user = useAuthenticatedUser();
const amr = useCachedAmr();
if (user) {
return <p>Your amr is: {amr ? amr[0] : 'none'}</p>;
} else {
return (
<Button
onClick={() => {
client.login();
}}
>
Log in!
</Button>
);
}
};

Api tokens client hooks

More detailed information about Api tokens client hooks is listed on the API page.

useApiTokens

Returns functions for checking tokens and the status of renewal.

useApiTokensClient

Returns the Api tokens client module.

useApiTokensClientTracking

Returns an array of [signal, reset function, apiTokensClient instance]. The hook re-renders the component each time the client emits a signal.

import { useApiTokensClient, useApiTokens, useApiTokensClientTracking } from 'hds-react';
const ApiTokensClientHooks = () => {
const client = useApiTokensClient();
const { getStoredApiTokens, isRenewing } = useApiTokens();
const [lastSignal, reset, apiTokensClient] = useApiTokensClientTracking();
if (isRenewing()) {
return <p>Your API tokens are being renewed!</p>;
}
if (getStoredApiTokens()) {
return <p>You have API tokens!</p>;
}
};

Session poller hooks

More detailed information about Session poller hooks is listed on the API page.

useSessionPoller

Returns the Session poller module.

useSessionPollerTracking

Returns an array of [signal, reset function, sessionPoller instance]. The hook re-renders the component each time the client emits a signal.

import { useSessionPoller, useSessionPollerTracking } from 'hds-react';
const SessionPollerHooks = () => {
const poller = useSessionPoller();
const [lastSignal, reset, sessionPoller] = useSessionPollerTracking();
if (isSessionPollerStoppedSignal(lastSignal)) {
<Button
onClick={() => {
poller.start();
}}
>
Start polling!
</Button>;
}
};

Generic signal hooks

Hooks are the easiest way to listen to signals. Most hooks return the last triggered signal and re-render the component. Re-rendering is done only if the listener returns true. So listeners added by hooks must return a boolean.

Hooks dispose of the listeners when the component unmounts. Hooks cannot emit signals.

Detailed information about these hooks is listed on the API page.

useSignalListener

Adds a single listener for all signals. The hook re-renders the component each time a signal is emitted if the listener function returns true.

useSignalTrackingWithCallback

Creates a trigger from given props and calls the callback function if any emitted signals match the trigger.

useSignalTrackingWithReturnValue

Creates a trigger from given props and re-renders the component if any emitted signals match the trigger.

Important! The listener passed to useSignalListener must be memoized or the hook will attach a new listener on each render because the props changed. The old one is disposed of but to avoid unnecessary listeners, use memoization with useMemo or useCallback. Note that all triggerFor... functions are constants and do not need memoization.

useSignalTrackingWithReturnValue and useSignalListener store their arguments in React refs, so memoization is not needed.

The array returned by the useSignalListener and the useSignalTrackingWithReturnValue have the last signal, if any, and a reset function that clears the last signal. This can be used for tracking certain errors and then resetting the array when the user has seen a notification about the error.

import {
useSignalListener,
useSignalListenerWithReturnValue,
triggerForAllSessionPollerErrors,
isSessionEndedSignal,
} from 'hds-react';
const ListenerComponent = () => {
const [ showDialog, setShowDialog ] = useState(false);
// Listener function must be memoized when useSignalListener is used!
const myListener = useCallback(( signal )=>{
if( isSessionEndedSignal(signal) ){
setShowDialog(true)
}
},[]);
useSignalListener(myListener);
if ( showDialog ) {
return <Dialog ... />
}
};
const ListenerComponentWithReturnValue = () => {
const [showDialog, setShowDialog] = useState(false);
const [sessionEndedSignal] = useSignalTrackingWithReturnValue(triggerForAllSessionPollerErrors);
if(isSessionEndedSignal(sessionEndedSignal)){
setShowDialog(true)
}
if (showDialog) {
return <Dialog ... />
}
};

The properties and methods of all hooks are listed on the API page.

Modules

Modules are automatically created by the LoginProvider. Module settings are included in the LoginProvider settings and passed to each module.

Properties, methods, errors and signals of all modules are listed on the API page.

Oidc client

The Oidc client settings are defined in the userManagerSettings property, which is passed to the LoginProvider.

The module is exposed with the useOidcClient hook.

import { useOidcClient } from 'hds-react';
const LogoutComponent = () => {
const { logout } = useOidcClient();
return <Button onClick={() => logout()}>Log out</Button>;
};
Requirements

Usage of the Oidc client requires

  • An OIDC provider.
  • At least client id, authority, scope and registered callback URLs at the OIDC provider.
  • Specific silent_renew.html for silent token renewal. This is a poorly documented feature in the oidc-client-ts. This file has a javascript snippet that communicates with the oidc provider "silently" in a hidden iframe. More about token renewal. An example can be found in the silent renewal section.
API

All Oidc client settings, properties, methods and signals are detailed on the API page:

Hooks

Detailed information about these hooks is listed on the API page.

Api tokens client

The Api tokens client listens to the Oidc client signals and waits until the user is authenticated and then fetches the API tokens. It retries if fetch fails and terminates when max retries are reached. When the user is expiring and renews, API tokens are also renewed automatically.

Important! The Helsinki Tunnistus OIDC provider (Keycloak) uses a different kind of API token requests. This module does not support that, but a new module will be added later.

Requirements

URL where to get the tokens from. A user's access token is also required, so the user must be authenticated to fetch API tokens.

API

All Api tokens client settings, properties, methods and signals are detailed on the API page:

Hooks

Detailed information about these hooks is listed on the API page.

Session poller

The user's session could end outside of the current browser window. The Session poller calls the userinfo endpoint and notifies when it receives an unauthorized or forbidden response. Successes and other errors are ignored.

Requirements

Polling requires a user and the userManager from the Oidc client. The polling endpoint is fetched from userManager's metadata.

API

All Session poller settings, properties, methods and signals are detailed on the API page:

Hooks

Detailed information about these hooks is listed on the API page.

Silent renewal

The oidc-client-ts uses silent_renew.html for user token renewal. Save this file to your server and place the URL in the settings as silent_redirect_uri.

<!DOCTYPE html>
<html lang="en">
<head>
<title>Silent renewal</title>
</head>
<body>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/oidc-client-ts/2.2.2/browser/oidc-client-ts.min.js"
integrity="sha512-pt8b5O4w5Y9/xZpIhPN8Soo/YbC95SxHn0P/Mu39iYB2Ih/09TMS3Id5XPqve2f8DPC6voXOzgQNojCuqO6A4w=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
></script>
<script>
var mgr = new oidc.UserManager({});
mgr.signinSilentCallback().catch(error => {
console.error('silent_renew.html error', error);
});
</script>
</body>
</html>

Signals

Signals can be listened to with hooks. Each module has its own hook for listening to signals only from that module. There are also hooks for listening to all signals and filtering is possible with triggers.

Detailed information is in the API section.

Listening to signals

A signal listener is a function that receives one argument: the signal. A listener can listen to all signals or just one type or signal with a certain namespace. Listeners can be even more specific and listen to signals with certain payloads. In short, a listener can listen to any properties of the signal and is triggered when all properties match.

The listener is called only if the emitted signal matches the given props.

For example, if the trigger props (the first argument) passed to beacon.addListener(trigger, listener) is { type:'error' }, the listener (second argument) is called when the emitted signal has a matching type. It does not matter what other props the signal has.

If the trigger props are { namespace:'myModule', payload:{type:'click'} }, the emitted signal must have those properties with the same, exact values. Other properties are not checked.

The trigger can also be a function. Internally all triggers are converted to functions.

const listener = (signal) => {
// Do something with the signal.
};
// Listen to all error signals
const trigger = { type: 'error' };
useSignalTrackingWithCallback(trigger, listener);

Getting signal payloads

Sometimes the most significant part of a signal is the payload. There are predefined payload getters for the Oidc client. If the given signal is not from the Oidc client or is not of the given type, the function returns null. So there is no need to pre-check the signal namespace or type.

import {
useSignalListenerWithReturnValue,
triggerForAllOidcClientSignals,
getOidcClientStateChangePayload,
oidcClientStates,
} from 'hds-react';
const StateChangeIndicator = () => {
const [lastSignal, reset] = useSignalListenerWithReturnValue(triggerForAllOidcClientSignals);
const payload = lastSignal ? getOidcClientStateChangePayload(lastSignal) : null;
if (payload && payload.state === oidcClientStates.LOGGING_OUT) {
return <p>Logging out....</p>;
}
};

Using without React

When React is not used, the beacon and modules can be created and initialized with createLoginSystem(props). Props are the same as those passed to LoginProvider. The function returns a beacon instance that holds all modules. There are dozens of utility functions to use without React. See modules and signals.

import { createLoginSystem } from 'hds-react';
// Actual properties are omitted from this example to keep it simple
const loginSystemProperties = {...}
const beacon = createLoginSystem(loginSystemProperties);

Packages

PackageIncludedStorybook linkSource link
HDS React
Yes
View in StorybookView source
HDS Core
No
--