This lightweight package provides helpers to manage user session.
- Create a backend API to create & renew JWT sessions. It can be inspired or referenced from Plume Admin: https://github.com/Coreoz/Plume-admin/blob/master/plume-admin-ws/src/main/java/com/coreoz/plume/admin/webservices/SessionWs.java. Make sure OWASP considerations regarding JWT are correctly implemented.
- Create an API that implements the interface
SessionRefresher
. See below for an example. - Create the
User
type that matches the JWT and that will be used in the project. See below for an example. - A variant of the User type with the field
exp
can be optionally created in order to use a simplerUser
object through the project. See below for an example. - Create the
SessionService
that will be used by component that will interact with the user sessions. See below for an example. - Bind the services in the dependency injection system in the
services-module.ts
file. See below for an example. - In
index.tsx
file, try to initialize the session at startup, and make sure local storage sessions are synchronized across different browser tabs See below for an example. - Create some protected routes that will require a current user session. See below for an example.
- Create a page that will authenticate users. See below for an example.
export type SessionCredentials = {
userName: string,
password: string,
};
export default class SessionApi implements SessionRefresher {
constructor(private readonly httpClient: ApiHttpClient) {
}
authenticate(credentials: SessionCredentials) {
return this
.httpClient
.restRequest<RefreshableJwtToken>(HttpMethod.POST, '/admin/session')
.jsonBody(credentials)
.execute();
}
refresh(webSessionToken: string) {
return this
.httpClient
.restRequest<RefreshableJwtToken>(HttpMethod.PUT, '/admin/session')
.body(webSessionToken)
.execute();
}
}
export type User = {
idUser: string,
userName: string,
fullName: string,
permissions: string[],
};
export type UserWithExpiration = User & {
exp: number;
}
const THRESHOLD_IN_MILLIS_TO_DETECT_EXPIRED_SESSION = 60 * 1000; // 1 minutes
const LOCAL_STORAGE_CURRENT_SESSION = 'user-session';
const HTTP_ERROR_ALREADY_EXPIRED_SESSION_TOKEN = 'ALREADY_EXPIRED_SESSION_TOKEN';
export default class SessionService {
private jwtSessionManager: JwtSessionManager<UserWithExpiration>;
constructor(
private readonly sessionApi: SessionApi,
private readonly scheduler: Scheduler,
private readonly pageActivityManager: PageActivityManager,
private readonly idlenessDetector: IdlenessDetector
) {
this.jwtSessionManager = new JwtSessionManager<UserWithExpiration>(
sessionApi,
scheduler,
pageActivityManager,
idlenessDetector,
{
localStorageCurrentSession: LOCAL_STORAGE_CURRENT_SESSION,
thresholdInMillisToDetectExpiredSession: THRESHOLD_IN_MILLIS_TO_DETECT_EXPIRED_SESSION,
httpErrorAlreadyExpiredSessionToken: HTTP_ERROR_ALREADY_EXPIRED_SESSION_TOKEN,
},
);
}
// data access
getSessionToken() {
return this.jwtSessionManager.getSessionToken();
}
getCurrentUser() {
return this.jwtSessionManager.getCurrentUser();
}
isAuthenticated() {
return this.jwtSessionManager.isAuthenticated();
}
hasPermission(permission: Permission) {
return this.jwtSessionManager.getCurrentUser().select((user) => user?.permissions.includes(permission) ?? false);
}
// actions
authenticate(credentials: SessionCredentials) {
return this
.sessionApi
.authenticate(credentials)
.then((sessionToken) => this.jwtSessionManager.registerNewSession(sessionToken));
}
disconnect() {
this.jwtSessionManager.disconnect();
}
tryInitializingSessionFromStorage() {
this.jwtSessionManager.tryInitializingSessionFromStorage();
}
synchronizeSessionFromOtherBrowserTags() {
this.jwtSessionManager.synchronizeSessionFromOtherBrowserTags();
}
}
// browser dependent services
injector.registerSingleton(BrowserUserActivityListener, UserActivityListener);
// other services
injector.registerSingleton(IdlenessDetector);
injector.registerSingleton(SessionService);
In index.ts
file:
const sessionService = injector.getInstance(SessionService);
sessionService.tryInitializingSessionFromStorage();
sessionService.synchronizeSessionFromOtherBrowserTags();
<Routes>
<Route path="/login" element={<Login />} />
<Route
path="/*"
element={(
<ConditionalRoute shouldDisplayRoute={sessionService.isAuthenticated()} defaultRoute="/login">
<div id="main-layout">
<Navigation />
<div id="content-layout">
<Header />
<Router />
</div>
</div>
</ConditionalRoute>
)}
/>
</Routes>
// the session authentication API call
const tryAuthenticate = (credentials: SessionCredentials) => {
loader.monitor(sessionService.authenticate(credentials));
};
// check that the user is not already authenticated
// => if that's the case, then skip the login page and display the authenticated page already!
const isAuthenticated = useObservable(sessionService.isAuthenticated());
useOnDependenciesChange(() => {
if (isAuthenticated) {
navigate({ pathname: HOME });
}
}, [isAuthenticated]);
// return form onSubmit={tryAuthenticate}