Skip to content

Commit

Permalink
[ALS-7330][ALS-7329] RAS login and logout button (#253)
Browse files Browse the repository at this point in the history
[ALS-7330][ALS-7329] RAS login and logout button
  • Loading branch information
Gcolon021 authored and JamesPeck committed Oct 11, 2024
1 parent 9e76eed commit c276194
Show file tree
Hide file tree
Showing 9 changed files with 135 additions and 25 deletions.
10 changes: 9 additions & 1 deletion .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,17 @@ VITE_AUTH_PROVIDER_MODULE_AUTH0_HELPTEXT='Login with your <a href="https://googl

VITE_AUTH_PROVIDER_MODULE_RAS=true
VITE_AUTH_PROVIDER_MODULE_RAS_TYPE=RAS
VITE_AUTH_PROVIDER_MODULE_RAS_IDP=ras
VITE_AUTH_PROVIDER_MODULE_RAS_CLIENTID=12345ABCD
VITE_AUTH_PROVIDER_MODULE_RAS_URI=http://pic-sure.org/ras
VITE_AUTH_PROVIDER_MODULE_RAS_DESCRIPTION="Login with RAS"
VITE_AUTH_PROVIDER_MODULE_RAS_OKTAIDPID=12345ABCDE
VITE_AUTH_PROVIDER_MODULE_RAS_CONNECTION=okta-ras
VITE_AUTH_PROVIDER_MODULE_RAS_DESCRIPTION='Login with Researcher Auth Service (RAS)'
VITE_AUTH_PROVIDER_MODULE_RAS_ALT=false
VITE_AUTH_PROVIDER_MODULE_RAS_IMAGESRC=NIH_2013_logo_vertical_text_removed.svg
VITE_AUTH_PROVIDER_MODULE_RAS_IMAGEALT='NIH 2013 Logo'
VITE_AUTH_PROVIDER_MODULE_RAS_LOGOUTURL=https://authtest.nih.gov/siteminderagent/smlogoutredirector.asp?TARGET=
VITE_AUTH_PROVIDER_MODULE_RAS_OKTALOGOUTURL=https://hms-srce.oktapreview.com/oauth2/default/v1/logout

VITE_AUTH_PROVIDER_MODULE_FENCE=true
VITE_AUTH_PROVIDER_MODULE_FENCE_TYPE=FENCE
Expand Down
79 changes: 64 additions & 15 deletions src/lib/auth/RAS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,46 @@ import { browser } from '$app/environment';
import type { AuthData } from '$lib/models/AuthProvider';
import AuthProvider from '$lib/models/AuthProvider';
import * as api from '$lib/api';
import type { User } from '$lib/models/User';
import type { OktaUser } from '$lib/models/User';
import { login as UserLogin } from '$lib/stores/User';

interface RasData extends AuthData {
uri: string;
clientid: string;
state?: string;
idp: string;
oktaidpid: string;
oktalogouturl: string;
}

class RAS extends AuthProvider implements RasData {
uri: string;
clientid: string;
state: string;
oktaidpid: string;
oktalogouturl: string;
idp: string;

constructor(data: RasData) {
super(data);
this.state = localStorage.getItem('state') || 'ras-' + this.generateRandomState();
localStorage.setItem('state', this.state);

if (
data.uri === undefined ||
data.clientid === undefined ||
data.oktaidpid === undefined ||
data.logouturl === undefined ||
data.oktalogouturl === undefined
) {
throw new Error('Missing required RAS parameter(s).');
}

this.uri = data.uri;
this.clientid = data.clientid;
this.state = data.state ?? this.generateRandomState();
this.oktaidpid = data.oktaidpid;
this.idp = data.idp;
this.oktalogouturl = data.oktalogouturl;
}

private generateRandomState() {
Expand All @@ -29,22 +50,30 @@ class RAS extends AuthProvider implements RasData {
return randomPart + timePart;
}

//TODO: create real return types
// eslint-disable-next-line @typescript-eslint/no-unused-vars
authenticate = async (hashParts: string[]): Promise<boolean> => {
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;
Expand All @@ -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<void> => {
throw new Error('Method not implemented.');
logout = (): Promise<string> => {
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);
};
}

Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/LoginButton.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
on:click={() => login(redirectTo, provider.type)}
>
{#if imageSrc}
<img src={imageSrc} alt={provider.imageAlt} class="h-8 mr-2" />
<img src={imageSrc} alt={provider.imageAlt} class="mr-2 h-8" />
{/if}
{buttonText}
</button>
Expand Down
20 changes: 18 additions & 2 deletions src/lib/components/Navigation.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 = '';
</script>

Expand Down Expand Up @@ -116,7 +132,7 @@
id="user-logout-btn"
class="btn variant-ringed-primary"
title="Logout"
on:click={logout}>Logout</button
on:click={() => logout(providerInstance)}>Logout</button
>
</div>
{:else}
Expand Down
4 changes: 3 additions & 1 deletion src/lib/models/AuthProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface AuthData extends Indexable {
alt: boolean;
imageSrc?: string;
imageAlt?: string;
logouturl?: string;
}

export default class AuthProvider implements AuthData {
Expand All @@ -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 {
Expand Down Expand Up @@ -83,7 +85,7 @@ export default class AuthProvider implements AuthData {
login = async (redirectTo: string, type: string): Promise<void> => {
throw new Error('Method not implemented.');
};
logout = async (): Promise<void> => {
logout = async (): Promise<string | void> => {
throw new Error('Method not implemented.');
};
}
4 changes: 4 additions & 0 deletions src/lib/models/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
29 changes: 25 additions & 4 deletions src/lib/stores/User.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
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';
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<User> = writable(restoreUser());

Expand Down Expand Up @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion src/routes/(authentication)/login/loading/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,15 @@
failed = true;
}
if (!provider || failed) {
console.error('Provider not found');
goto('/login/error');
}
const providerInstance = await createInstance(provider);
let hashParts = $page.url.hash?.split('&') || [];
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');
Expand Down
9 changes: 9 additions & 0 deletions src/routes/(picsure)/+layout.server.ts
Original file line number Diff line number Diff line change
@@ -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,
};
};

0 comments on commit c276194

Please sign in to comment.