diff --git a/.env.test b/.env.test index e62df1ce..92147064 100644 --- a/.env.test +++ b/.env.test @@ -40,9 +40,17 @@ VITE_AUTH_PROVIDER_MODULE_AUTH0_HELPTEXT='Login with your => { const responseMap = this.getResponseMap(hashParts); const code = responseMap.get('code'); - let state = ''; - if (browser) { - state = sessionStorage.getItem('state') || ''; - } - if (!code || state !== this.state) { + const responseState = responseMap.get('state') || ''; + const localState = this.state; + + if (!code || localState !== responseState) { + console.debug( + 'RAS authentication failed code: ', + code, + ' state: ', + responseState, + ' localState: ', + localState, + ); return true; } + try { - const newUser: User = await api.post('psama/okta/authentication', { code }); + const newUser: OktaUser = await api.post('psama/authentication/ras', { code }); if (newUser?.token) { - UserLogin(newUser.token); + await UserLogin(newUser.token); + newUser.oktaIdToken && localStorage.setItem('oktaIdToken', newUser.oktaIdToken); return false; } else { return true; @@ -59,13 +88,33 @@ class RAS extends AuthProvider implements RasData { let redirectUrl = '/'; if (browser) { redirectUrl = this.getRedirectURI(); - window.location.href = encodeURI( - `${this.uri}/oauth2/default/v1/authorize?response_type=code&scope=openid&client_id=${this.clientid}&provider=${type}&redirect_uri=${redirectUrl}&state=${this.state}`, - ); + redirectUrl = redirectUrl.replace(/\/$/, ''); + this.saveState(redirectTo, type, this.idp); + const rasClientID = encodeURIComponent(this.clientid); + const rasIdpId = encodeURIComponent(this.oktaidpid); + window.location.href = `${this.uri}?response_type=code&scope=openid&client_id=${rasClientID}&idp=${rasIdpId}&redirect_uri=${redirectUrl}&state=${this.state}`; } }; - logout = (): Promise => { - throw new Error('Method not implemented.'); + logout = (): Promise => { + localStorage.removeItem('state'); + + const redirect = encodeURI( + `${window.location.protocol}//${window.location.hostname}${ + window.location.port ? ':' + window.location.port : '' + }/login`, + ); + + const oktaIdToken = localStorage.getItem('oktaIdToken'); + const oktaRedirect = + this.oktalogouturl + + '?id_token_hint=' + + oktaIdToken + + '&post_logout_redirect_uri=' + + redirect; + + const oktaEncodedRedirect = encodeURIComponent(oktaRedirect); + const logoutUrl = this.logouturl + oktaEncodedRedirect; + return Promise.resolve(logoutUrl); }; } diff --git a/src/lib/components/LoginButton.svelte b/src/lib/components/LoginButton.svelte index 751c53b4..84f19b9e 100644 --- a/src/lib/components/LoginButton.svelte +++ b/src/lib/components/LoginButton.svelte @@ -29,7 +29,7 @@ on:click={() => login(redirectTo, provider.type)} > {#if imageSrc} - {provider.imageAlt} + {provider.imageAlt} {/if} {buttonText} diff --git a/src/lib/components/Navigation.svelte b/src/lib/components/Navigation.svelte index aff0b9a8..44d93138 100644 --- a/src/lib/components/Navigation.svelte +++ b/src/lib/components/Navigation.svelte @@ -5,7 +5,13 @@ import { user, userRoutes, logout } from '$lib/stores/User'; import type { Route } from '$lib/models/Route'; import Logo from '$lib/components/Logo.svelte'; - + import type AuthData from '$lib/models/AuthProvider.ts'; + import type AuthProvider from '$lib/models/AuthProvider.ts'; + import { browser } from '$app/environment'; + import { createInstance } from '$lib/AuthProviderRegistry.ts'; + import { onMount } from 'svelte'; + let providerData: AuthData; + let providerInstance: AuthProvider | undefined = undefined; function setDropdown(path: string) { dropdownPath = path; } @@ -36,6 +42,16 @@ return $page.url.pathname.includes(route.path) ? 'page' : undefined; }; + onMount(async () => { + if (browser && $page) { + const providerType = sessionStorage.getItem('type'); + if (providerType) { + providerData = $page.data?.providers.find((p: AuthProvider) => p.type === providerType); + providerInstance = await createInstance(providerData); + } + } + }); + $: dropdownPath = ''; @@ -116,7 +132,7 @@ id="user-logout-btn" class="btn variant-ringed-primary" title="Logout" - on:click={logout}>Logout logout(providerInstance)}>Logout {:else} diff --git a/src/lib/models/AuthProvider.ts b/src/lib/models/AuthProvider.ts index 80dc69c7..b0071104 100644 --- a/src/lib/models/AuthProvider.ts +++ b/src/lib/models/AuthProvider.ts @@ -11,6 +11,7 @@ export interface AuthData extends Indexable { alt: boolean; imageSrc?: string; imageAlt?: string; + logouturl?: string; } export default class AuthProvider implements AuthData { @@ -35,6 +36,7 @@ export default class AuthProvider implements AuthData { this.alt = data.alt || false; this.imageSrc = data.imagesrc; this.imageAlt = data.imagealt; + this.logouturl = data.logouturl; } protected getRedirectURI(): string { @@ -83,7 +85,7 @@ export default class AuthProvider implements AuthData { login = async (redirectTo: string, type: string): Promise => { throw new Error('Method not implemented.'); }; - logout = async (): Promise => { + logout = async (): Promise => { throw new Error('Method not implemented.'); }; } diff --git a/src/lib/models/User.ts b/src/lib/models/User.ts index 2c987d9b..4e52e924 100644 --- a/src/lib/models/User.ts +++ b/src/lib/models/User.ts @@ -19,6 +19,10 @@ export interface ExtendedUser extends User { roles: string[]; } +export interface OktaUser extends User { + readonly oktaIdToken: string; +} + // TODO: Replace metadata nad query types /* eslint-disable @typescript-eslint/no-explicit-any */ export function mapExtendedUser(data: any) { diff --git a/src/lib/stores/User.ts b/src/lib/stores/User.ts index bba6e92f..5489d994 100644 --- a/src/lib/stores/User.ts +++ b/src/lib/stores/User.ts @@ -1,6 +1,5 @@ import { get, writable, derived, type Writable, type Readable } from 'svelte/store'; import { browser } from '$app/environment'; - import * as api from '$lib/api'; import type { Route } from '$lib/models/Route'; import type { User } from '$lib/models/User'; @@ -8,6 +7,7 @@ import { BDCPrivileges, PicsurePrivileges } from '$lib/models/Privilege'; import { routes, features, resources } from '$lib/configuration'; import { goto } from '$app/navigation'; import type { QueryInterface } from '$lib/models/query/Query'; +import type AuthProvider from '$lib/models/AuthProvider.ts'; export const user: Writable = writable(restoreUser()); @@ -134,14 +134,35 @@ export async function login(token: string) { } } -export async function logout() { +export async function logout(authProvider: AuthProvider | undefined) { if (browser) { const token = localStorage.getItem('token'); token && api.get('/psama/logout'); token && localStorage.removeItem('token'); } - user.set({}); - goto('/login'); + + // get the auth provider + if (authProvider) { + authProvider + .logout() + .then((redirect) => { + user.set({}); + if (typeof redirect === 'string') { + location.replace(redirect); + } else { + // If no redirect is provided, go to the login page + console.error('Error logging out: ' + redirect); + goto('/login'); + } + }) + .catch((error) => { + console.error('Error logging out: ' + error); + goto('/login'); + }); + } else { + user.set({}); + goto('/login'); + } } export function isTokenExpired(token: string) { diff --git a/src/routes/(authentication)/login/loading/+page.svelte b/src/routes/(authentication)/login/loading/+page.svelte index abee1514..5286f5c5 100644 --- a/src/routes/(authentication)/login/loading/+page.svelte +++ b/src/routes/(authentication)/login/loading/+page.svelte @@ -29,6 +29,7 @@ failed = true; } if (!provider || failed) { + console.error('Provider not found'); goto('/login/error'); } const providerInstance = await createInstance(provider); @@ -36,7 +37,7 @@ if ($page.url.search.startsWith('?')) { hashParts = $page.url.search.substring(1).split('&') || []; } - console.log('hashParts', hashParts); + failed = await providerInstance.authenticate(hashParts); let filtersJson = sessionStorage.getItem('filters'); diff --git a/src/routes/(picsure)/+layout.server.ts b/src/routes/(picsure)/+layout.server.ts new file mode 100644 index 00000000..04e5572e --- /dev/null +++ b/src/routes/(picsure)/+layout.server.ts @@ -0,0 +1,9 @@ +import type { LayoutServerLoad } from './$types'; +import { getAllProviderData } from '$lib/AuthProviderRegistry.ts'; + +export const load: LayoutServerLoad = async () => { + const providers = getAllProviderData(); + return { + providers: providers, + }; +};