From e3d1bd7a626b529d8e5330f98f98c4b79bcea4d2 Mon Sep 17 00:00:00 2001 From: Janis Hutz Date: Mon, 21 Sep 2026 15:51:23 +0200 Subject: [PATCH] feat: improve authentication checks and request lib --- browser/package.json | 2 +- browser/src/auth.ts | 20 +++++++++++--- browser/src/request.ts | 59 ++++++++++++++++++++++++++++++++++-------- 3 files changed, 66 insertions(+), 15 deletions(-) diff --git a/browser/package.json b/browser/package.json index 0de8313..8cec41e 100644 --- a/browser/package.json +++ b/browser/package.json @@ -1,6 +1,6 @@ { "name": "@janishutz/oidc-login-sdk-browser", - "version": "1.1.0", + "version": "1.2.0", "description": "SDK to communicate with and log into a backend running express-openid-connect", "homepage": "https://github.com/janishutz/oidc-login-sdk#readme", "bugs": { diff --git a/browser/src/auth.ts b/browser/src/auth.ts index c8b7022..518cdf9 100644 --- a/browser/src/auth.ts +++ b/browser/src/auth.ts @@ -3,12 +3,20 @@ import request, { } from './request.js'; import config from './config.js'; -export const login = ( returnTo?: string ) => { +/** + * Start the login flow. Please note that upon calling this, the page will be reloaded after completion of the login flow + * @param returnTo - The location to return to after login + */ +export const login = ( returnTo?: URL ) => { sessionStorage.setItem( 'redirect', location.pathname ); - location.href = ( config.get().loginEndpoint ?? '/auth/v2/login' ) + ( returnTo ? returnTo : '' ); + location.href = ( config.get().loginEndpoint ?? '/auth/v2/login' ) + ( returnTo ? returnTo.toString() : '' ); }; -export const check = async () => { +/** + * Check if a user is authenticated. This can also be done implicitly using a call to a protected endpoint + * @returns A promise resolving to a boolean indicating authentication status + */ +export const check = async (): Promise => { let status: boolean; try { @@ -31,6 +39,11 @@ export const check = async () => { return status; }; +/** + * Use this, if you did not set the login returnTo path, or if in any other case you need to redirect the user after login, + * such as after a navigation guard executing prior to state update + * @returns The location to redirect to + */ export const getRedirect = (): string | null => { const item = sessionStorage.getItem( 'redirect' ); @@ -39,6 +52,7 @@ export const getRedirect = (): string | null => { return item; }; +/** Logs the user out */ export const logout = async () => { location.href = config.get().logoutEndpoint ?? '/auth/v2/logout'; }; diff --git a/browser/src/request.ts b/browser/src/request.ts index 7c64ab2..0bfebb2 100644 --- a/browser/src/request.ts +++ b/browser/src/request.ts @@ -11,12 +11,26 @@ export class AuthError extends Error {} export class UnownedError extends Error {} +/** + * Perform an HTTP GET request + * @param url - The URL (just the path relative to website root) to fetch + * @param authErrorResolution - The mean of authentication error resolution + * @returns The reponse + */ const get = async ( url: string, authErrorResolution?: AuthErrorResolution ): Promise => { return await wrapper( url, { 'credentials': 'include' }, authErrorResolution ); }; +/** + * Perform an HTTP POST request + * @param url - The URL (just the path relative to website root) to fetch + * @param payload - The request body to send as a string + * @param mime - The MIME type of the payload + * @param authErrorResolution - The mean of authentication error resolution + * @returns The response + */ const post = async ( url: string, payload: string, mime: string = 'application/json', authErrorResolution?: AuthErrorResolution ): Promise => { return await wrapper( url, { 'credentials': 'include', @@ -28,6 +42,19 @@ const post = async ( url: string, payload: string, mime: string = 'application/j }, authErrorResolution ); }; +/** + * Perform an HTTP DELETE request + * @param url - The URL (just the path relative to website root) to fetch + * @param authErrorResolution - The mean of authentication error resolution + * @returns The response + */ +const deleteRequest = async ( url: string, authErrorResolution?: AuthErrorResolution ): Promise => { + return await wrapper( url, { + 'credentials': 'include', + 'method': 'delete' + }, authErrorResolution ); +}; + const wrapper = async ( url: string, opts: RequestInit, authErrorResolution?: AuthErrorResolution ): Promise => { const res = await fetch( config.get().backendURL + url, { 'redirect': 'manual', @@ -35,23 +62,33 @@ const wrapper = async ( url: string, opts: RequestInit, authErrorResolution?: Au } ); if ( res.type === 'opaqueredirect' ) { - if ( config.get().authErrorEvent ) { - document.dispatchEvent( new CustomEvent( 'autherror' ) ); - } - - if ( ( authErrorResolution && authErrorResolution === 'resolve' ) || ( !authErrorResolution && config.get().defaultAuthErrorResolution === 'resolve' ) ) { - login(); - } else { - throw new AuthError( 'ERR_USER_UNAUTHORIZED' ); - } + handleUnauth( authErrorResolution ); return res; } else { - return res; + if ( res.ok ) + return res; + else if ( res.status === 401 || res.status === 403 ) + throw new AuthError( 'ERR_USER_UNAUTHORIZED' ); + else + throw new Error( 'ERR_' + res.status ); + } +}; + +const handleUnauth = ( authErrorResolution: AuthErrorResolution ) => { + if ( config.get().authErrorEvent ) { + document.dispatchEvent( new CustomEvent( 'autherror' ) ); + } + + if ( ( authErrorResolution && authErrorResolution === 'resolve' ) || ( !authErrorResolution && config.get().defaultAuthErrorResolution === 'resolve' ) ) { + login(); + } else { + throw new AuthError( 'ERR_USER_UNAUTHORIZED' ); } }; export default { get, - post + post, + 'delete': deleteRequest };