From ef0a731c3d1449bf01379609ecaa15db3936f0d8 Mon Sep 17 00:00:00 2001 From: Juha Paananen Date: Tue, 2 Jan 2024 21:00:35 +0200 Subject: [PATCH] Use the "hd" claim for checking whether a user is part of a domain --- backend/src/generic-oidc-auth.ts | 15 ++++++++------- backend/src/google-auth.ts | 1 + backend/src/http-session.ts | 7 ++++++- backend/src/websocket-sessions.ts | 4 +++- common/src/authenticated-user.ts | 1 + common/src/domain.ts | 11 +++++------ frontend/src/store/board-store.ts | 5 ++++- 7 files changed, 28 insertions(+), 16 deletions(-) diff --git a/backend/src/generic-oidc-auth.ts b/backend/src/generic-oidc-auth.ts index 63afd20b3..655f0aa23 100644 --- a/backend/src/generic-oidc-auth.ts +++ b/backend/src/generic-oidc-auth.ts @@ -1,15 +1,13 @@ -import { isLeft, Left, left } from "fp-ts/lib/Either" +import { Request, Response } from "express" import * as t from "io-ts" -import { PathReporter } from "io-ts/lib/PathReporter" import JWT from "jsonwebtoken" import { OAuthAuthenticatedUser } from "../../common/src/authenticated-user" +import { optional } from "../../common/src/domain" +import { decodeOrThrow } from "./decodeOrThrow" import { getEnv } from "./env" -import { AuthProvider } from "./oauth" import { ROOT_URL } from "./host-config" -import { optional } from "../../common/src/domain" +import { AuthProvider } from "./oauth" import { REQUIRE_AUTH } from "./require-auth" -import { Request, Response } from "express" -import { decodeOrThrow } from "./decodeOrThrow" type GenericOAuthConfig = { OIDC_CONFIG_URL: string @@ -51,12 +49,13 @@ export function GenericOIDCAuthProvider(config: GenericOAuthConfig): AuthProvide const body = await response.json() const idToken = JWT.decode(body.id_token) - console.log(JSON.stringify(idToken, null, 2)) + //console.log(JSON.stringify(idToken, null, 2)) const user = decodeOrThrow(IdToken, idToken) return { email: user.email, name: "name" in user ? user.name : user.preferred_username, picture: user.picture ?? undefined, + domain: user.hd ?? null, } } @@ -113,10 +112,12 @@ const IdToken = t.union([ email: t.string, name: t.string, picture: optional(t.string), + hd: optional(t.string), }), t.type({ email: t.string, preferred_username: t.string, picture: optional(t.string), + hd: optional(t.string), }), ]) diff --git a/backend/src/google-auth.ts b/backend/src/google-auth.ts index ff9bd564b..e78412495 100644 --- a/backend/src/google-auth.ts +++ b/backend/src/google-auth.ts @@ -58,6 +58,7 @@ export const GoogleAuthProvider = (googleConfig: GoogleConfig): AuthProvider => name: idToken.name, email, picture: idToken.picture, + domain: idToken.hd ?? null, } } diff --git a/backend/src/http-session.ts b/backend/src/http-session.ts index 68407f9c9..a9e4fd018 100644 --- a/backend/src/http-session.ts +++ b/backend/src/http-session.ts @@ -23,7 +23,12 @@ export function getAuthenticatedUser(req: IncomingMessage): LoginInfo | null { export function getAuthenticatedUserFromJWT(jwt: string): LoginInfo | null { try { JWT.verify(jwt, secret) - return JWT.decode(jwt) as LoginInfo + const loginInfo = JWT.decode(jwt) as LoginInfo + if (loginInfo.domain === undefined) { + console.log("Rejecting legacy token without domain") + return null + } + return loginInfo } catch (e) { console.warn("Token verification failed", jwt, e) } diff --git a/backend/src/websocket-sessions.ts b/backend/src/websocket-sessions.ts index f25310ec5..625acc83b 100644 --- a/backend/src/websocket-sessions.ts +++ b/backend/src/websocket-sessions.ts @@ -22,6 +22,7 @@ import { } from "../../common/src/domain" import { maybeGetBoard, ServerSideBoardState } from "./board-state" import { getBoardHistory } from "./board-store" +import { LoginInfo } from "./http-session" import { randomProfession } from "./professions" import { getUserIdForEmail } from "./user-store" import { StringifyCache, WsWrapper } from "./ws-wrapper" @@ -266,7 +267,7 @@ export function setNicknameForSession(event: SetNickname, origin: WsWrapper) { } export async function setVerifiedUserForSession( - event: UserLoggedIn | OAuthAuthenticatedUser, + event: OAuthAuthenticatedUser, session: UserSession, ): Promise { const userId = await getUserIdForEmail(event.email) @@ -276,6 +277,7 @@ export async function setVerifiedUserForSession( name: event.name, email: event.email, picture: event.picture, + domain: event.domain, userId, } if (session.boardSession) { diff --git a/common/src/authenticated-user.ts b/common/src/authenticated-user.ts index b098553f7..5795fa8ee 100644 --- a/common/src/authenticated-user.ts +++ b/common/src/authenticated-user.ts @@ -2,4 +2,5 @@ export type OAuthAuthenticatedUser = { name: string email: string picture?: string + domain: string | null } diff --git a/common/src/domain.ts b/common/src/domain.ts index 189805137..e2d079ad8 100644 --- a/common/src/domain.ts +++ b/common/src/domain.ts @@ -62,10 +62,6 @@ export type BoardAccessPolicyDefined = t.TypeOf -export type AuthorizedParty = AuthorizedByEmailAddress | AuthorizedByDomain -export type AuthorizedByEmailAddress = { email: string } -export type AuthorizedByDomain = { domain: string } - export type EventUserInfo = UnidentifiedUserInfo | SystemUserInfo | EventUserInfoAuthenticated export type UnidentifiedUserInfo = { nickname: string; userType: "unidentified" } @@ -88,6 +84,7 @@ export type SessionUserInfoAuthenticated = { email: string picture: string | undefined userId: string + domain: string | null } export type UserSessionInfo = SessionUserInfo & { @@ -607,7 +604,7 @@ export function getBoardAttributes(board: Board, userInfo?: EventUserInfo): Boar export const BOARD_ITEM_BORDER_MARGIN = 0.5 -export function checkBoardAccess(accessPolicy: BoardAccessPolicy | undefined, userInfo: EventUserInfo): AccessLevel { +export function checkBoardAccess(accessPolicy: BoardAccessPolicy | undefined, userInfo: SessionUserInfo): AccessLevel { if (!accessPolicy) return "read-write" let accessLevel: AccessLevel = accessPolicy.publicWrite ? "read-write" @@ -618,12 +615,14 @@ export function checkBoardAccess(accessPolicy: BoardAccessPolicy | undefined, us return accessLevel } const email = userInfo.email + const domain = userInfo.domain + const defaultAccess = "read-write" for (let entry of accessPolicy.allowList) { const nextLevel = "email" in entry && entry.email === email ? entry.access || defaultAccess - : "domain" in entry && email.endsWith(entry.domain) + : "domain" in entry && domain === entry.domain ? entry.access || defaultAccess : "none" accessLevel = combineAccessLevels(accessLevel, nextLevel) diff --git a/frontend/src/store/board-store.ts b/frontend/src/store/board-store.ts index d1eaa3cf3..f27321fe0 100644 --- a/frontend/src/store/board-store.ts +++ b/frontend/src/store/board-store.ts @@ -26,6 +26,7 @@ import { newISOTimeStamp, PersistableBoardItemEvent, ServerConfig, + SessionUserInfo, TransientBoardItemEvent, UIEvent, UserSessionInfo, @@ -512,7 +513,7 @@ export function BoardStore( } } -export function sessionState2UserInfo(state: UserSessionState): EventUserInfo { +export function sessionState2UserInfo(state: UserSessionState): SessionUserInfo { if (state.status === "logged-in") { return { userType: "authenticated", @@ -520,6 +521,8 @@ export function sessionState2UserInfo(state: UserSessionState): EventUserInfo { nickname: state.nickname, name: state.name, userId: state.userId, + domain: state.domain, + picture: state.picture, } } else { return {