Skip to content

Commit

Permalink
Rework auth hook logic to support SSO
Browse files Browse the repository at this point in the history
  • Loading branch information
skovati committed Dec 8, 2023
1 parent 4b328fa commit 6b3a794
Showing 1 changed file with 111 additions and 38 deletions.
149 changes: 111 additions & 38 deletions src/hooks.server.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import type { Handle } from '@sveltejs/kit';
import { parse } from 'cookie';
import { parse, type CookieSerializeOptions } from 'cookie';
import jwtDecode from 'jwt-decode';
import type { BaseUser, ParsedUserToken, User } from './types/app';
import effects from './utilities/effects';
import { isLoginEnabled } from './utilities/login';
import { ADMIN_ROLE } from './utilities/permissions';
import type { ReqAuthResponse, ReqSessionResponse } from './types/auth';
import { reqGatewayForwardCookies } from './utilities/requests';
import { base } from '$app/paths';

export const handle: Handle = async ({ event, resolve }) => {
try {
Expand All @@ -20,49 +23,119 @@ export const handle: Handle = async ({ event, resolve }) => {
rolePermissions,
token: '',
};
} else {
const cookieHeader = event.request.headers.get('cookie') ?? '';
const cookies = parse(cookieHeader);
const { activeRole: activeRoleCookie = null, user: userCookie = null } = cookies;

if (userCookie) {
const userBuffer = Buffer.from(userCookie, 'base64');
const userStr = userBuffer.toString('utf-8');
const baseUser: BaseUser = JSON.parse(userStr);
const { success } = await effects.session(baseUser);
const decodedToken: ParsedUserToken = jwtDecode(baseUser.token);

if (success) {
const allowedRoles = decodedToken['https://hasura.io/jwt/claims']['x-hasura-allowed-roles'];
const defaultRole = decodedToken['https://hasura.io/jwt/claims']['x-hasura-default-role'];
const activeRole = activeRoleCookie ?? defaultRole;
const user: User = {
...baseUser,
activeRole,
allowedRoles,
defaultRole,
permissibleQueries: null,
rolePermissions: null,
};
const permissibleQueries = await effects.getUserQueries(user);

const rolePermissions = await effects.getRolePermissions(user);
event.locals.user = {
...user,
permissibleQueries,
rolePermissions,
};
} else {
event.locals.user = null;
}
} else {
event.locals.user = null;

return await resolve(event);
}

const cookieHeader = event.request.headers.get('cookie') ?? '';
const cookies = parse(cookieHeader);
const { activeRole: activeRoleCookie = null, user: userCookie = null } = cookies;

// try to get role with current JWT
if (userCookie) {
const user = await computeRolesFromCookies(userCookie, activeRoleCookie);
if (user) {
event.locals.user = user;
return await resolve(event);
}
}

console.log(`trying SSO, since JWT was invalid`);

// pass all cookies to the gateway, who can determine if we have any valid SSO tokens
const validationData = await reqGatewayForwardCookies<ReqSessionResponse>('/auth/validateSSO', cookieHeader);

if (!validationData.success) {
console.log('Invalid SSO token, redirecting to login UI page');
// if we're already on the login page, don't redirect
// otherwise we get stuck in a redirect loop
return event.url.pathname.startsWith('/login')
? await resolve(event)
: new Response(null, {
status: 307,
headers: {

Check failure on line 56 in src/hooks.server.ts

View workflow job for this annotation

GitHub Actions / lint

Expected object keys to be in ascending order. 'headers' should be before 'status'
// message field from gateway response will contain our login UI URL
location: `${validationData.message}`,
},
});
}

// otherwise, if we have a valid token, login with token to generate new JWT
const loginData = await reqGatewayForwardCookies<ReqAuthResponse>('/auth/loginSSO', cookieHeader);

if (loginData.success) {
const user: BaseUser = {
id: loginData.message,
token: loginData.token ?? '',
};

const roles = await computeRolesFromJWT(user, activeRoleCookie);

if (roles) {
console.log(`successfully SSO'd for user ${user.id}`);

event.locals.user = roles;

// create and set cookies
const userStr = JSON.stringify(user);
const userCookie = Buffer.from(userStr).toString('base64');
const cookieOpts: CookieSerializeOptions = {
path: `${base}/`,
sameSite: 'none',
httpOnly: false,

Check failure on line 85 in src/hooks.server.ts

View workflow job for this annotation

GitHub Actions / lint

Expected object keys to be in ascending order. 'httpOnly' should be before 'sameSite'
};
event.cookies.set('user', userCookie, cookieOpts);
event.cookies.set('activeRole', roles.defaultRole, cookieOpts);

return await resolve(event);
}
}

// otherwise, we can't auth
console.log('unable to auth with JWT or SSO token');
event.locals.user = null;
} catch (e) {
console.log(e);
event.locals.user = null;
}

return await resolve(event);
};

async function computeRolesFromCookies(
userCookie: string | null,
activeRoleCookie: string | null,
): Promise<User | null> {
const userBuffer = Buffer.from(userCookie ?? '', 'base64');
const userStr = userBuffer.toString('utf-8');
const baseUser: BaseUser = JSON.parse(userStr);

return computeRolesFromJWT(baseUser, activeRoleCookie);
}

async function computeRolesFromJWT(baseUser: BaseUser, activeRole: string | null): Promise<User | null> {
const { success } = await effects.session(baseUser);
if (!success) return null;

Check failure on line 118 in src/hooks.server.ts

View workflow job for this annotation

GitHub Actions / lint

Expected { after 'if' condition

const decodedToken: ParsedUserToken = jwtDecode(baseUser.token);

const allowedRoles = decodedToken['https://hasura.io/jwt/claims']['x-hasura-allowed-roles'];
const defaultRole = decodedToken['https://hasura.io/jwt/claims']['x-hasura-default-role'];
activeRole ??= defaultRole;
const user: User = {
...baseUser,
activeRole,
allowedRoles,
defaultRole,
permissibleQueries: null,
rolePermissions: null,
};
const permissibleQueries = await effects.getUserQueries(user);

const rolePermissions = await effects.getRolePermissions(user);
return {
...user,
permissibleQueries,
rolePermissions,
};
}

0 comments on commit 6b3a794

Please sign in to comment.