This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266// SPDX-License-Identifier: AGPL-3.0-or-later
import {BotUserAuthSessionCreationDeniedError} from '@fluxer/errors/src/domains/auth/BotUserAuthSessionCreationDeniedError';import {RegistrationPendingApprovalError} from '@fluxer/errors/src/domains/auth/RegistrationPendingApprovalError';import {RegistrationRejectedError} from '@fluxer/errors/src/domains/auth/RegistrationRejectedError';import {SessionTokenMismatchError} from '@fluxer/errors/src/domains/auth/SessionTokenMismatchError';import {InvalidTokenError} from '@fluxer/errors/src/domains/core/InvalidTokenError';import {UnknownUserError} from '@fluxer/errors/src/domains/user/UnknownUserError';import {requireClientIp} from '@fluxer/ip_utils/src/ClientIp';import type {AuthSessionResponse} from '@fluxer/schema/src/domains/auth/AuthSchemas';import type {ApiContext} from '../ApiContext';import type {UserID} from '../BrandedTypes';import {REGISTRATION_PENDING_APPROVAL_TRAIT, REGISTRATION_REJECTED_TRAIT} from '../instance/InstanceConfigRepository';import {Logger} from '../Logger';import type {AuthSession} from '../models/AuthSession';import type {User} from '../models/User';import {lookupGeoip} from '../utils/IpUtils';import {mapAuthSessionsToResponse} from './AuthModel';import * as AuthUtility from './AuthUtility';
interface CreateAuthSessionParams { user: User; request: Request;}
interface LogoutAuthSessionsParams { user: User; sessionIdHashes: Array<string>;}
interface UpdateUserActivityParams { userId: UserID; clientIp: string; user?: User; action?: 'session_authenticated' | 'bearer_fallback_session_authenticated' | 'unknown'; tokenType?: 'session' | 'bearer'; sessionId?: string;}
interface DispatchAuthSessionChangeParams { userId: UserID; oldAuthSessionIdHash: string; newAuthSessionIdHash: string; newToken: string;}
interface ReplaceCurrentAuthSessionParams { user: User; currentAuthSession: AuthSession; request: Request;}
interface ReplaceCurrentAuthSessionResult { token: string; authSession: AuthSession; oldAuthSessionIdHash: string; newAuthSessionIdHash: string;}
interface CreateAdditionalAuthSessionFromTokenParams { token: string; expectedUserId?: string; request: Request;}
export async function createAuthSession( ctx: ApiContext, {user, request}: CreateAuthSessionParams,): Promise<[token: string, AuthSession]> { const {users, config} = ctx.services; if (user.isBot) throw new BotUserAuthSessionCreationDeniedError(); if (user.traits.has(REGISTRATION_PENDING_APPROVAL_TRAIT)) throw new RegistrationPendingApprovalError(); if (user.traits.has(REGISTRATION_REJECTED_TRAIT)) throw new RegistrationRejectedError(); const now = new Date(); const token = await AuthUtility.generateAuthToken(ctx); const ip = requireClientIp(request, { trustClientIpHeader: config.proxy.trust_client_ip_header, clientIpHeaderName: config.proxy.client_ip_header, }); const platformHeader = request.headers.get('x-fluxer-platform')?.trim().toLowerCase() ?? null; const uaRaw = request.headers.get('user-agent') ?? ''; const isDesktopClient = platformHeader === 'desktop'; let clientCountry: string | null = null; try { const geoip = await lookupGeoip(ip); clientCountry = geoip.countryCode ? geoip.countryCode.toUpperCase() : null; } catch (error) { Logger.warn({userId: user.id.toString(), error}, 'GeoIP lookup failed at session creation'); } const authSession = await users.createAuthSession({ user_id: user.id, session_id_hash: Buffer.from(AuthUtility.getTokenIdHash(ctx, token)), created_at: now, approx_last_used_at: now, client_ip: ip, client_user_agent: uaRaw || null, client_is_desktop: isDesktopClient, client_os: null, client_platform: null, client_country: clientCountry, version: 1, }); return [token, authSession];}
export async function createAdditionalAuthSessionFromToken( ctx: ApiContext, {token, expectedUserId, request}: CreateAdditionalAuthSessionFromTokenParams,): Promise<{ token: string; userId: string;}> { const existingSession = await getAuthSessionByToken(ctx, token); if (!existingSession) { throw new InvalidTokenError(); } const {users} = ctx.services; const user = await users.findUnique(existingSession.userId); if (!user) { throw new UnknownUserError(); } if (expectedUserId && user.id.toString() !== expectedUserId) { throw new SessionTokenMismatchError(); } const [newToken] = await createAuthSession(ctx, {user, request}); return {token: newToken, userId: user.id.toString()};}
export async function getAuthSessionByToken(ctx: ApiContext, token: string): Promise<AuthSession | null> { const {users} = ctx.services; return users.getAuthSessionByToken(Buffer.from(AuthUtility.getTokenIdHash(ctx, token)));}
export async function getAuthSessions(ctx: ApiContext, userId: UserID): Promise<Array<AuthSessionResponse>> { const {users} = ctx.services; const authSessions = await users.listAuthSessions(userId); return await mapAuthSessionsToResponse({authSessions});}
export async function updateAuthSessionLastUsed(ctx: ApiContext, tokenHash: Uint8Array): Promise<void> { await ctx.services.userActivityBuffer.recordAuthSessionActivity(Buffer.from(tokenHash), new Date());}
export async function updateUserActivity(ctx: ApiContext, {userId, clientIp}: UpdateUserActivityParams): Promise<void> { const {users} = ctx.services; await users.updateUserActivity(userId, clientIp);}
export async function revokeToken(ctx: ApiContext, token: string): Promise<void> { const {users, gateway} = ctx.services; const tokenHash = Buffer.from(AuthUtility.getTokenIdHash(ctx, token)); const authSession = await users.getAuthSessionByToken(tokenHash); if (!authSession) return; const sessionIdHash = Buffer.from(authSession.sessionIdHash).toString('base64url'); await users.deletePushSubscriptionsForAuthSessions(authSession.userId, [sessionIdHash], { deleteUnboundSubscriptions: true, }); await gateway.invalidatePushSubscriptions({userId: authSession.userId}); await users.revokeAuthSession(tokenHash); await gateway.terminateSession({ userId: authSession.userId, sessionIdHashes: [sessionIdHash], });}
export async function logoutAuthSessions( ctx: ApiContext, {user, sessionIdHashes}: LogoutAuthSessionsParams,): Promise<void> { const {users, gateway} = ctx.services; const hashes = sessionIdHashes.map((hash) => Buffer.from(hash, 'base64url')); await users.deletePushSubscriptionsForAuthSessions(user.id, sessionIdHashes, { deleteUnboundSubscriptions: true, }); await gateway.invalidatePushSubscriptions({userId: user.id}); await users.deleteAuthSessions(user.id, hashes); await gateway.terminateSession({ userId: user.id, sessionIdHashes, });}
export async function terminateAllUserSessions(ctx: ApiContext, userId: UserID): Promise<void> { const {users, gateway} = ctx.services; const authSessions = await users.listAuthSessions(userId); await users.deleteAllPushSubscriptions(userId); await gateway.invalidatePushSubscriptions({userId}); if (authSessions.length === 0) return; const hashes = authSessions.map((s) => s.sessionIdHash); await users.deleteAuthSessions(userId, hashes); await gateway.terminateSession({ userId, sessionIdHashes: authSessions.map((s) => Buffer.from(s.sessionIdHash).toString('base64url')), });}
export async function replaceCurrentAuthSession( ctx: ApiContext, {user, currentAuthSession, request}: ReplaceCurrentAuthSessionParams,): Promise<ReplaceCurrentAuthSessionResult> { const {users} = ctx.services; const oldAuthSessionIdHash = encodeSessionIdHash(currentAuthSession.sessionIdHash); const authSessions = await users.listAuthSessions(user.id); const otherAuthSessions = authSessions.filter( (authSession) => !authSession.sessionIdHash.equals(currentAuthSession.sessionIdHash), ); await deleteAndTerminateAuthSessions(ctx, user.id, otherAuthSessions); const [newToken, newAuthSession] = await createAuthSession(ctx, {user, request}); const newAuthSessionIdHash = encodeSessionIdHash(newAuthSession.sessionIdHash); await dispatchAuthSessionChange(ctx, { userId: user.id, oldAuthSessionIdHash, newAuthSessionIdHash, newToken, }); await deleteAndTerminateAuthSessions(ctx, user.id, [currentAuthSession]); return { token: newToken, authSession: newAuthSession, oldAuthSessionIdHash, newAuthSessionIdHash, };}
async function deleteAndTerminateAuthSessions( ctx: ApiContext, userId: UserID, authSessions: ReadonlyArray<AuthSession>,): Promise<void> { if (authSessions.length === 0) { return; } const {users, gateway} = ctx.services; const sessionIdHashes = authSessions.map((authSession) => encodeSessionIdHash(authSession.sessionIdHash)); await users.deletePushSubscriptionsForAuthSessions(userId, sessionIdHashes, { deleteUnboundSubscriptions: true, }); await gateway.invalidatePushSubscriptions({userId}); await users.deleteAuthSessions( userId, authSessions.map((authSession) => authSession.sessionIdHash), ); await gateway.terminateSession({ userId, sessionIdHashes, });}
function encodeSessionIdHash(sessionIdHash: Uint8Array): string { return Buffer.from(sessionIdHash).toString('base64url');}
async function dispatchAuthSessionChange(ctx: ApiContext, params: DispatchAuthSessionChangeParams): Promise<void> { const {gateway} = ctx.services; const {userId, oldAuthSessionIdHash, newAuthSessionIdHash, newToken} = params; await gateway.dispatchPresence({ userId, event: 'AUTH_SESSION_CHANGE', data: { old_auth_session_id_hash: oldAuthSessionIdHash, new_auth_session_id_hash: newAuthSessionIdHash, new_token: newToken, }, });}