From 23c6368be610df69393a0b81418318a06b13df02 Mon Sep 17 00:00:00 2001 From: Hernan Alvarado Date: Sun, 8 Nov 2026 17:29:30 -0500 Subject: [PATCH 1/2] refactor(core): simplify stateful session strategy functions --- packages/core/src/@types/internal.ts | 5 +- packages/core/src/api/get-provider-tokens.ts | 34 +-- packages/core/src/config/logger/catalog.ts | 12 - packages/core/src/errors/catalog.ts | 7 + .../src/session/stateful/create-session.ts | 11 +- .../src/session/stateful/destroy-session.ts | 13 +- .../session/stateful/get-provider-tokens.ts | 233 +++++++----------- .../session/stateful/is-provider-connected.ts | 73 +++--- .../core/src/session/stateful/revoke-token.ts | 116 ++++----- .../session/stateful/sign-in-credentials.ts | 11 +- packages/core/src/session/stateful/sign-up.ts | 11 +- .../providers/connected/stateful.test.ts | 24 +- .../providers/tokens/tokens/stateful.test.ts | 4 +- .../test/api/stateful/getAccessToken.test.ts | 2 +- .../api/stateful/getProviderTokens.test.ts | 2 +- 15 files changed, 198 insertions(+), 360 deletions(-) diff --git a/packages/core/src/@types/internal.ts b/packages/core/src/@types/internal.ts index 8157c362..e1cd8cf2 100644 --- a/packages/core/src/@types/internal.ts +++ b/packages/core/src/@types/internal.ts @@ -240,10 +240,7 @@ export interface SessionStrategy { request: Request provider: RuntimeOAuthProvider sessionByToken: SessionWithUserEntity - }): Promise< - | { success: true; tokens: OAuthTokenPayload; headers: Headers } - | { success: false; tokens: null; error: { code: string; message: string }; headers: Headers; statusCode: number } - > + }): Promise<{ tokens: OAuthTokenPayload; headers: Headers }> /** * Attempt to refresh using the refresh token cookie. * Returns null session + cookie-clearing response on any failure. diff --git a/packages/core/src/api/get-provider-tokens.ts b/packages/core/src/api/get-provider-tokens.ts index d61bfa48..883a6b07 100644 --- a/packages/core/src/api/get-provider-tokens.ts +++ b/packages/core/src/api/get-provider-tokens.ts @@ -15,7 +15,13 @@ export const getProviderTokens = async ( ): Promise => { const initialHeaders = toStandardizedHeaders(headersInit ?? requestInit?.headers ?? {}) try { - const { request, rateLimit, provider, session } = await createValidation(ctx, initialHeaders) + const { + request, + rateLimit, + provider, + session, + headers: resolvedHeaders, + } = await createValidation(ctx, initialHeaders) .verifyOAuthProvider(oauthId) .buildRequest(requestInit, `/providers/${oauthId}/tokens`) .verifyRateLimit("getProviderTokens") @@ -25,33 +31,19 @@ export const getProviderTokens = async ( if (rateLimit) { return rateLimit as unknown as GetProviderTokensAPIReturn } - const getTokens = await ctx.session.strategy.getProviderTokens({ + const { tokens, headers } = await ctx.session.strategy.getProviderTokens({ oauthId, request, provider, sessionByToken: session!, }) - if (getTokens.success) { - const { success, tokens, headers } = getTokens - return { - success, - tokens, - headers, - toResponse: () => { - return Response.json({ success, tokens }, { status: success ? 200 : 400, headers }) - }, - } - } + const updatedHeaders = toUnionHeaders(resolvedHeaders, headers) return { - success: false, - tokens: null, - headers: getTokens.headers, - error: getTokens.error, + success: true, + tokens: tokens, + headers: updatedHeaders, toResponse: () => { - return Response.json( - { success: false, tokens: null }, - { status: getTokens.statusCode, headers: getTokens.headers } - ) + return Response.json({ success: true, tokens: tokens }, { headers: updatedHeaders }) }, } } catch (error) { diff --git a/packages/core/src/config/logger/catalog.ts b/packages/core/src/config/logger/catalog.ts index dc18e338..c9955b1c 100644 --- a/packages/core/src/config/logger/catalog.ts +++ b/packages/core/src/config/logger/catalog.ts @@ -449,12 +449,6 @@ export const LOG_MESSAGES = { msgId: "STATEFUL_SESSION_EXPIRATION_SET", message: "Set session expiration time", }, - STATEFUL_SESSION_CREATED: { - facility: 4, - severity: "info", - msgId: "STATEFUL_SESSION_CREATED", - message: "Session created in database", - }, STATEFUL_CREATE_SESSION_SUCCESS: { facility: 4, severity: "info", @@ -743,12 +737,6 @@ export const LOG_MESSAGES = { msgId: "STATELESS_GET_PROVIDER_TOKENS_PROVIDER_NOT_FOUND", message: "OAuth provider not configured during stateless getProviderTokens", }, - STATELESS_GET_PROVIDER_TOKENS_PROVIDER_FOUND: { - facility: 4, - severity: "debug", - msgId: "STATELESS_GET_PROVIDER_TOKENS_PROVIDER_FOUND", - message: "OAuth provider found during stateless getProviderTokens", - }, STATELESS_GET_PROVIDER_TOKENS_NO_COOKIE: { facility: 4, severity: "warning", diff --git a/packages/core/src/errors/catalog.ts b/packages/core/src/errors/catalog.ts index 88cb07a9..ae06524c 100644 --- a/packages/core/src/errors/catalog.ts +++ b/packages/core/src/errors/catalog.ts @@ -1015,6 +1015,13 @@ export const ERROR_CATALOG = { message: "Sign-up is not allowed for magic link authentication. The user must already exist.", userMessage: "Sign-up is not allowed for magic link authentication. Please sign in with an existing account.", }, + OAUTH_ACCOUNT_NOT_FOUND: { + type: "AUTH_FLOW", + statusCode: 404, + name: "OAuthError", + message: "The requested OAuth account could not be found for the current user.", + userMessage: "The specified identity provider is not connected to your account.", + }, } satisfies Record export type AuraErrorCode = keyof typeof ERROR_CATALOG diff --git a/packages/core/src/session/stateful/create-session.ts b/packages/core/src/session/stateful/create-session.ts index 01275f79..26d946f1 100644 --- a/packages/core/src/session/stateful/create-session.ts +++ b/packages/core/src/session/stateful/create-session.ts @@ -123,20 +123,11 @@ export const createSession = (ctx: InternalContextForStateful): SessionStrategy[ expiresAt, }) - logger?.log("STATEFUL_SESSION_CREATED", { - structuredData: { - session_id: dbSession.id, - user_id: dbSession.userId, - status: dbSession.status, - expires_at: dbSession?.expiresAt?.toISOString(), - }, - }) - logger?.log("STATEFUL_CREATE_SESSION_SUCCESS", { structuredData: { session_id: dbSession.id, user_id: dbSession.userId, - token_returned: true, + status: dbSession.status, }, }) diff --git a/packages/core/src/session/stateful/destroy-session.ts b/packages/core/src/session/stateful/destroy-session.ts index 60ce97ba..fed97fce 100644 --- a/packages/core/src/session/stateful/destroy-session.ts +++ b/packages/core/src/session/stateful/destroy-session.ts @@ -4,19 +4,10 @@ export const destroySession = (ctx: InternalContextForStateful): SessionStrategy const { logger, session } = ctx return async ({ sessionByToken }) => { - logger?.log("STATEFUL_DESTROY_SESSION_START", { - structuredData: { - strategy: "stateful", - operation: "destroySession", - }, - }) + logger?.log("STATEFUL_DESTROY_SESSION_START") await session.config.adapter.revokeSession(sessionByToken.id, "user_logout") const clearedHeaders = ctx.cookies.manager.clearAll() - logger?.log("STATEFUL_DESTROY_SESSION_SUCCESS", { - structuredData: { - cookies_cleared: true, - }, - }) + logger?.log("STATEFUL_DESTROY_SESSION_SUCCESS") return clearedHeaders } } diff --git a/packages/core/src/session/stateful/get-provider-tokens.ts b/packages/core/src/session/stateful/get-provider-tokens.ts index 0f83fe07..ed88f00d 100644 --- a/packages/core/src/session/stateful/get-provider-tokens.ts +++ b/packages/core/src/session/stateful/get-provider-tokens.ts @@ -1,14 +1,13 @@ import { shouldRefresh } from "@/shared/utils.ts" -import { handleApiError } from "@/shared/utils/api.ts" -import { AuraAuthError, getErrorName } from "@/errors/aura-error.ts" +import { AuraAuthError } from "@/errors/aura-error.ts" +import { secureApiHeaders } from "@/shared/http/headers.ts" import { refreshProviderToken } from "@/shared/utils/refresh-tokens.ts" -import { secureApiHeaders, toUnionHeaders } from "@/shared/http/headers.ts" import type { InternalContextForStateful, SessionStrategy } from "@/@types/internal.ts" export const getProviderTokens = (ctx: InternalContextForStateful): SessionStrategy["getProviderTokens"] => { const { logger, session } = ctx - return async ({ oauthId, provider, request, sessionByToken }) => { + return async ({ oauthId, provider, sessionByToken }) => { logger?.log("STATEFUL_GET_PROVIDER_TOKENS_START", { structuredData: { strategy: "stateful", @@ -17,171 +16,113 @@ export const getProviderTokens = (ctx: InternalContextForStateful): SessionStrat }, }) - try { - const isExpired = Date.now() > sessionByToken.expiresAt.getTime() - if (sessionByToken.status !== "active" || isExpired) { - if (isExpired) { - await session.config.adapter.revokeSession(sessionByToken.id, "user_logout") - } - const { code, message, statusCode } = handleApiError( - new AuraAuthError({ code: "SESSION_NOT_FOUND" }), - "PROVIDER_TOKENS_ERROR", - "Failed to get provider tokens" - ) - return { - success: false, - error: { code, message }, - tokens: null, - headers: ctx.cookies.manager.clear(), - statusCode, - } + const isExpired = Date.now() > sessionByToken.expiresAt.getTime() + if (sessionByToken.status !== "active" || isExpired) { + if (isExpired) { + await session.config.adapter.revokeSession(sessionByToken.id, "user_logout") } + throw new AuraAuthError({ code: "SESSION_NOT_FOUND" }) + } - logger?.log("STATEFUL_GET_PROVIDER_TOKENS_SESSION_FOUND", { - structuredData: { - user_id: sessionByToken.userId, - session_id: sessionByToken.id, - }, - }) + logger?.log("STATEFUL_GET_PROVIDER_TOKENS_SESSION_FOUND", { + structuredData: { + user_id: sessionByToken.userId, + session_id: sessionByToken.id, + }, + }) - const accounts = await session.config.adapter.getAccountsByUserId(sessionByToken.userId) - const getAccount = accounts.find((account) => account.provider === oauthId && account.status === "active") - const oauthAccount = getAccount ? await session.config.adapter.getOAuthAccount(getAccount?.id ?? "") : null - if (!oauthAccount) { - logger?.log("STATEFUL_GET_PROVIDER_TOKENS_OAUTH_ACCOUNT_NOT_FOUND", { - structuredData: { - oauth_id: oauthId, - user_id: sessionByToken.userId, - reason: "oauth_account_not_found", - }, - }) - const { code, message, statusCode } = handleApiError( - new AuraAuthError({ code: "OAUTH_UNLINKED_ACCOUNT_ERROR" }), - "PROVIDER_TOKENS_ERROR", - "Failed to get provider tokens" - ) - const headers = toUnionHeaders(request.headers, secureApiHeaders) - return { success: false, error: { code, message }, tokens: null, headers, statusCode } - } + const accounts = await session.config.adapter.getAccountsByUserId(sessionByToken.userId) + const getAccount = accounts.find((account) => account.provider === oauthId && account.status === "active") + const oauthAccount = getAccount ? await session.config.adapter.getOAuthAccount(getAccount?.id ?? "") : null + if (!oauthAccount) { + throw new AuraAuthError({ code: "OAUTH_ACCOUNT_NOT_FOUND" }) + } - logger?.log("STATEFUL_GET_PROVIDER_TOKENS_OAUTH_ACCOUNT_FOUND", { - structuredData: { - oauth_id: oauthId, - account_id: oauthAccount.accountId, - }, - }) + logger?.log("STATEFUL_GET_PROVIDER_TOKENS_OAUTH_ACCOUNT_FOUND", { + structuredData: { + oauth_id: oauthId, + account_id: oauthAccount.accountId, + }, + }) - const tokens = { - accessToken: oauthAccount.accessToken, - refreshToken: oauthAccount.refreshToken || undefined, - idToken: oauthAccount.idToken || undefined, - tokenType: (oauthAccount.tokenType || "Bearer") as "Bearer", - scopes: oauthAccount.scopes ? oauthAccount.scopes.split(" ") : [], - expiresAt: oauthAccount.accessTokenExpiresAt ? Math.floor(oauthAccount.accessTokenExpiresAt.getTime() / 1000) : 0, - refreshTokenExpiresAt: oauthAccount.refreshTokenExpiresAt - ? Math.floor(oauthAccount.refreshTokenExpiresAt.getTime() / 1000) - : undefined, - issuedAt: Math.floor(oauthAccount.updatedAt.getTime() / 1000), - } + const tokens = { + accessToken: oauthAccount.accessToken, + refreshToken: oauthAccount.refreshToken || undefined, + idToken: oauthAccount.idToken || undefined, + tokenType: (oauthAccount.tokenType || "Bearer") as "Bearer", + scopes: oauthAccount.scopes ? oauthAccount.scopes.split(" ") : [], + expiresAt: oauthAccount.accessTokenExpiresAt ? Math.floor(oauthAccount.accessTokenExpiresAt.getTime() / 1000) : 0, + refreshTokenExpiresAt: oauthAccount.refreshTokenExpiresAt + ? Math.floor(oauthAccount.refreshTokenExpiresAt.getTime() / 1000) + : undefined, + issuedAt: Math.floor(oauthAccount.updatedAt.getTime() / 1000), + } - logger?.log("STATEFUL_GET_PROVIDER_TOKENS_TOKENS_EXTRACTED", { - structuredData: { - has_access_token: Boolean(tokens.accessToken), - has_refresh_token: Boolean(tokens.refreshToken), - expires_at: tokens.expiresAt, - }, - }) + logger?.log("STATEFUL_GET_PROVIDER_TOKENS_TOKENS_EXTRACTED", { + structuredData: { + has_access_token: Boolean(tokens.accessToken), + has_refresh_token: Boolean(tokens.refreshToken), + expires_at: tokens.expiresAt, + }, + }) - const refreshWindow = provider.refreshWindow ?? 300 - const needsRefresh = shouldRefresh(tokens as any, refreshWindow) + const refreshWindow = provider.refreshWindow ?? 300 + const needsRefresh = shouldRefresh(tokens as any, refreshWindow) - logger?.log("STATEFUL_GET_PROVIDER_TOKENS_REFRESH_CHECK", { + logger?.log("STATEFUL_GET_PROVIDER_TOKENS_REFRESH_CHECK", { + structuredData: { + needs_refresh: needsRefresh, + refresh_window: refreshWindow, + expires_at: tokens.expiresAt, + }, + }) + + if (needsRefresh) { + logger?.log("STATEFUL_GET_PROVIDER_TOKENS_REFRESHING", { structuredData: { - needs_refresh: needsRefresh, - refresh_window: refreshWindow, - expires_at: tokens.expiresAt, + oauth_id: oauthId, + reason: "token_expired_or_in_refresh_window", }, }) - if (needsRefresh) { - logger?.log("STATEFUL_GET_PROVIDER_TOKENS_REFRESHING", { - structuredData: { - oauth_id: oauthId, - reason: "token_expired_or_in_refresh_window", - }, - }) - - try { - const refreshedTokens = await refreshProviderToken(tokens as any, provider) - - logger?.log("STATEFUL_GET_PROVIDER_TOKENS_REFRESH_SUCCESS", { - structuredData: { - oauth_id: oauthId, - new_expires_at: refreshedTokens.expiresAt, - }, - }) - - await session.config.adapter.updateOAuthTokens(getAccount?.id as string, { - accountId: oauthAccount.accountId, - accessToken: refreshedTokens.accessToken, - refreshToken: refreshedTokens.refreshToken, - idToken: refreshedTokens.idToken, - tokenType: refreshedTokens.tokenType, - scopes: refreshedTokens.scopes ? refreshedTokens.scopes.join(" ") : null, - accessTokenExpiresAt: refreshedTokens.expiresAt ? new Date(refreshedTokens.expiresAt * 1000) : null, - refreshTokenExpiresAt: refreshedTokens.refreshTokenExpiresAt - ? new Date(refreshedTokens.refreshTokenExpiresAt * 1000) - : null, - }) - - logger?.log("STATEFUL_GET_PROVIDER_TOKENS_OAUTH_ACCOUNT_UPDATED", { - structuredData: { - oauth_id: oauthId, - account_id: oauthAccount.accountId, - }, - }) - - const headers = toUnionHeaders(request.headers, secureApiHeaders) - return { success: true, tokens: refreshedTokens, headers } - } catch (refreshError) { - logger?.log("STATEFUL_GET_PROVIDER_TOKENS_REFRESH_ERROR", { - structuredData: { - oauth_id: oauthId, - error_type: getErrorName(refreshError), - error_message: refreshError instanceof Error ? refreshError.message : String(refreshError), - }, - }) - - const { code, message, statusCode } = handleApiError( - refreshError, - "PROVIDER_TOKENS_ERROR", - "Failed to get provider tokens" - ) - const headers = toUnionHeaders(request.headers, secureApiHeaders) - return { success: false, error: { code, message }, tokens: null, headers, statusCode } - } - } + const refreshedTokens = await refreshProviderToken(tokens as any, provider) - logger?.log("STATEFUL_GET_PROVIDER_TOKENS_SUCCESS", { + logger?.log("STATEFUL_GET_PROVIDER_TOKENS_REFRESH_SUCCESS", { structuredData: { oauth_id: oauthId, - tokens_returned: true, + new_expires_at: refreshedTokens.expiresAt, }, }) - const headers = toUnionHeaders(request.headers, secureApiHeaders) - return { success: true, tokens: tokens as any, headers } - } catch (error) { - logger?.log("STATEFUL_GET_PROVIDER_TOKENS_ERROR", { + await session.config.adapter.updateOAuthTokens(getAccount?.id as string, { + accountId: oauthAccount.accountId, + accessToken: refreshedTokens.accessToken, + refreshToken: refreshedTokens.refreshToken, + idToken: refreshedTokens.idToken, + tokenType: refreshedTokens.tokenType, + scopes: refreshedTokens.scopes ? refreshedTokens.scopes.join(" ") : null, + accessTokenExpiresAt: refreshedTokens.expiresAt ? new Date(refreshedTokens.expiresAt * 1000) : null, + refreshTokenExpiresAt: refreshedTokens.refreshTokenExpiresAt + ? new Date(refreshedTokens.refreshTokenExpiresAt * 1000) + : null, + }) + + logger?.log("STATEFUL_GET_PROVIDER_TOKENS_OAUTH_ACCOUNT_UPDATED", { structuredData: { oauth_id: oauthId, - error_type: getErrorName(error), - error_message: error instanceof Error ? error.message : String(error), + account_id: oauthAccount.accountId, }, }) - const { code, message, statusCode } = handleApiError(error, "PROVIDER_TOKENS_ERROR", "Failed to get provider tokens") - return { success: false, error: { code, message }, tokens: null, headers: request.headers, statusCode } + return { tokens: refreshedTokens, headers: new Headers(secureApiHeaders) } } + + logger?.log("STATEFUL_GET_PROVIDER_TOKENS_SUCCESS", { + structuredData: { + oauth_id: oauthId, + }, + }) + + return { tokens: tokens, headers: new Headers(secureApiHeaders) } } } diff --git a/packages/core/src/session/stateful/is-provider-connected.ts b/packages/core/src/session/stateful/is-provider-connected.ts index b5f9dd43..c00e2e83 100644 --- a/packages/core/src/session/stateful/is-provider-connected.ts +++ b/packages/core/src/session/stateful/is-provider-connected.ts @@ -1,4 +1,3 @@ -import { getErrorName } from "@/errors/aura-error.ts" import type { InternalContextForStateful, SessionStrategy } from "@/@types/internal.ts" export const isProviderConnected = (ctx: InternalContextForStateful): SessionStrategy["isProviderConnected"] => { @@ -11,61 +10,47 @@ export const isProviderConnected = (ctx: InternalContextForStateful): SessionStr operation: "isProviderConnected", }, }) - try { - const isExpired = Date.now() > sessionByToken.expiresAt.getTime() - if (sessionByToken.status !== "active" || isExpired) { - if (isExpired) { - await session.config.adapter.revokeSession(sessionByToken.id, "account_suspended") - } - logger?.log("AUTH_SESSION_INVALID", { - structuredData: { - reason: "session_expired_or_inactive", - }, - }) - return false + const isExpired = Date.now() > sessionByToken.expiresAt.getTime() + if (sessionByToken.status !== "active" || isExpired) { + if (isExpired) { + await session.config.adapter.revokeSession(sessionByToken.id, "account_suspended") } - - logger?.log("AUTH_SESSION_VALID", { + logger?.log("AUTH_SESSION_INVALID", { structuredData: { - user_id: sessionByToken.userId, - session_id: sessionByToken.id, + reason: "session_expired_or_inactive", }, }) + return false + } - const accounts = await session.config.adapter.getAccountsByUserId(sessionByToken.userId) - const account = accounts.find((acc) => acc.provider === oauthId) - - if (!account) { - logger?.log("OAUTH_ACCESS_TOKEN_REQUEST_INITIATED", { - structuredData: { - provider: oauthId, - reason: "account_not_found_for_user", - }, - }) - return false - } + logger?.log("AUTH_SESSION_VALID", { + structuredData: { + user_id: sessionByToken.userId, + session_id: sessionByToken.id, + }, + }) - const isConnected = account.status === "active" - logger?.log("OAUTH_ACCESS_TOKEN_SUCCESS", { - structuredData: { - provider: oauthId, - connected: isConnected, - }, - }) + const accounts = await session.config.adapter.getAccountsByUserId(sessionByToken.userId) + const account = accounts.find((acc) => acc.provider === oauthId) - return isConnected - } catch (error) { - /** - * @todo returns the error to the user in a structured way. - */ - logger?.log("OAUTH_ACCESS_TOKEN_ERROR", { + if (!account) { + logger?.log("OAUTH_ACCESS_TOKEN_REQUEST_INITIATED", { structuredData: { provider: oauthId, - error_type: getErrorName(error), - error_message: error instanceof Error ? error.message : String(error), + reason: "account_not_found_for_user", }, }) return false } + + const isConnected = account.status === "active" + logger?.log("OAUTH_ACCESS_TOKEN_SUCCESS", { + structuredData: { + provider: oauthId, + connected: isConnected, + }, + }) + + return isConnected } } diff --git a/packages/core/src/session/stateful/revoke-token.ts b/packages/core/src/session/stateful/revoke-token.ts index d57782c7..737f6aa1 100644 --- a/packages/core/src/session/stateful/revoke-token.ts +++ b/packages/core/src/session/stateful/revoke-token.ts @@ -1,8 +1,8 @@ import { HeadersBuilder } from "@aura-stack/router" +import { AuraAuthError } from "@/errors/aura-error.ts" import { getExpiredCookie } from "@/shared/http/cookie.ts" -import { AuraAuthError, getErrorName } from "@/errors/aura-error.ts" -import { secureApiHeaders, toUnionHeaders } from "@/shared/http/headers.ts" import { revokeProviderToken } from "@/shared/utils/revoke-token.ts" +import { secureApiHeaders, toUnionHeaders } from "@/shared/http/headers.ts" import type { InternalContextForStateful, SessionStrategy } from "@/@types/internal.ts" export const revokeToken = (ctx: InternalContextForStateful): SessionStrategy["revokeToken"] => { @@ -17,85 +17,57 @@ export const revokeToken = (ctx: InternalContextForStateful): SessionStrategy["r }, }) - try { - const isExpired = Date.now() > sessionByToken.expiresAt.getTime() - if (sessionByToken.status !== "active" || isExpired) { - if (isExpired) { - await session.config.adapter.revokeSession(sessionByToken.id, "user_logout") - } - logger?.log("AUTH_SESSION_INVALID", { - structuredData: { - reason: "session_expired_or_inactive", - }, - }) - throw new AuraAuthError({ code: "SESSION_NOT_FOUND" }) - } - - logger?.log("AUTH_SESSION_VALID", { - structuredData: { - user_id: sessionByToken.userId, - session_id: sessionByToken.id, - }, - }) - - const accounts = await session.config.adapter.getAccountsByUserId(sessionByToken.userId) - const account = accounts.find((account) => account.provider === oauthId) - if (!account) { - logger?.log("OAUTH_UNLINKED_ACCOUNT_ERROR", { - structuredData: { - provider: oauthId, - reason: "oauth_account_not_found", - }, - }) - throw new AuraAuthError({ code: "OAUTH_UNLINKED_ACCOUNT_ERROR" }) + const isExpired = Date.now() > sessionByToken.expiresAt.getTime() + if (sessionByToken.status !== "active" || isExpired) { + if (isExpired) { + await session.config.adapter.revokeSession(sessionByToken.id, "user_logout") } + throw new AuraAuthError({ code: "SESSION_NOT_FOUND" }) + } - const oauthAccount = await session.config.adapter.getOAuthAccount(account?.id) - if (!oauthAccount) { - logger?.log("OAUTH_UNLINKED_ACCOUNT_ERROR", { - structuredData: { - provider: oauthId, - reason: "oauth_account_not_found", - }, - }) - throw new AuraAuthError({ code: "OAUTH_UNLINKED_ACCOUNT_ERROR" }) - } + logger?.log("AUTH_SESSION_VALID", { + structuredData: { + user_id: sessionByToken.userId, + session_id: sessionByToken.id, + }, + }) - if (!disconnect) { - logger?.log("OAUTH_ACCESS_TOKEN_REQUEST_INITIATED", { - structuredData: { provider: oauthId, hasAccessToken: !!oauthAccount.accessToken }, - }) + const accounts = await session.config.adapter.getAccountsByUserId(sessionByToken.userId) + const account = accounts.find((account) => account.provider === oauthId) + if (!account) { + throw new AuraAuthError({ code: "OAUTH_UNLINKED_ACCOUNT_ERROR" }) + } - await revokeProviderToken(provider, oauthAccount.accessToken) + const oauthAccount = await session.config.adapter.getOAuthAccount(account?.id) + if (!oauthAccount) { + throw new AuraAuthError({ code: "OAUTH_UNLINKED_ACCOUNT_ERROR" }) + } - logger?.log("OAUTH_ACCESS_TOKEN_SUCCESS", { - structuredData: { provider: oauthId }, - }) - } + if (!disconnect) { + logger?.log("OAUTH_ACCESS_TOKEN_REQUEST_INITIATED", { + structuredData: { provider: oauthId, hasAccessToken: !!oauthAccount.accessToken }, + }) - await session.config.adapter.updateAccountStatus(oauthAccount.accountId, "unlinked") + await revokeProviderToken(provider, oauthAccount.accessToken) logger?.log("OAUTH_ACCESS_TOKEN_SUCCESS", { - structuredData: { - provider: oauthId, - account_unlinked: true, - }, + structuredData: { provider: oauthId }, }) - - const cookieName = `${cookies.store.accessToken.name}.${oauthId}` - const builder = new HeadersBuilder(secureApiHeaders) - .setCookie(cookieName, "", getExpiredCookie(cookies.store.accessToken.attributes)) - .toHeaders() - return toUnionHeaders(builder, headers) - } catch (error) { - logger?.log("OAUTH_ACCESS_TOKEN_ERROR", { - structuredData: { - provider: oauthId, - error_type: getErrorName(error), - error_message: error instanceof Error ? error.message : String(error), - }, - }) - throw error } + + await session.config.adapter.updateAccountStatus(oauthAccount.accountId, "unlinked") + + logger?.log("OAUTH_ACCESS_TOKEN_SUCCESS", { + structuredData: { + provider: oauthId, + account_unlinked: true, + }, + }) + + const cookieName = `${cookies.store.accessToken.name}.${oauthId}` + const builder = new HeadersBuilder(secureApiHeaders) + .setCookie(cookieName, "", getExpiredCookie(cookies.store.accessToken.attributes)) + .toHeaders() + return toUnionHeaders(builder, headers) } } diff --git a/packages/core/src/session/stateful/sign-in-credentials.ts b/packages/core/src/session/stateful/sign-in-credentials.ts index 03ebd81c..ba66b31c 100644 --- a/packages/core/src/session/stateful/sign-in-credentials.ts +++ b/packages/core/src/session/stateful/sign-in-credentials.ts @@ -73,20 +73,11 @@ export const signInCredentials = (ctx: InternalContextForStateful): SessionStrat expiresAt, }) - logger?.log("STATEFUL_SESSION_CREATED", { - structuredData: { - session_id: dbSession.id, - user_id: dbSession.userId, - status: dbSession.status, - expires_at: dbSession?.expiresAt?.toISOString(), - }, - }) - logger?.log("STATEFUL_SIGN_IN_CREDENTIALS_SUCCESS", { structuredData: { session_id: dbSession.id, user_id: dbSession.userId, - token_returned: true, + status: dbSession.status, }, }) diff --git a/packages/core/src/session/stateful/sign-up.ts b/packages/core/src/session/stateful/sign-up.ts index 708f0987..7e62f0a7 100644 --- a/packages/core/src/session/stateful/sign-up.ts +++ b/packages/core/src/session/stateful/sign-up.ts @@ -120,20 +120,11 @@ export const signUp = (ctx: InternalContextForStateful): SessionStrategy["signUp expiresAt, }) - logger?.log("STATEFUL_SESSION_CREATED", { - structuredData: { - session_id: dbSession.id, - user_id: dbSession.userId, - status: dbSession.status, - expires_at: dbSession?.expiresAt?.toISOString(), - }, - }) - logger?.log("STATEFUL_CREATE_SESSION_SUCCESS", { structuredData: { session_id: dbSession.id, user_id: dbSession.userId, - token_returned: true, + status: dbSession.status, }, }) diff --git a/packages/core/test/actions/providers/connected/stateful.test.ts b/packages/core/test/actions/providers/connected/stateful.test.ts index 2f8fbed6..d153d66a 100644 --- a/packages/core/test/actions/providers/connected/stateful.test.ts +++ b/packages/core/test/actions/providers/connected/stateful.test.ts @@ -130,10 +130,8 @@ describe("connectedAction", () => { }) test("returns connected: false when provider token cookie is malformed", async () => { - const getSessionByTokenMock = vi.fn() - const getAccountsByUserIdMock = vi.fn() - - getSessionByTokenMock.mockResolvedValueOnce(sessionEntityWithUser) + const getSessionByTokenMock = vi.fn().mockResolvedValueOnce(sessionEntityWithUser) + const getAccountsByUserIdMock = vi.fn().mockResolvedValue([]) const { handlers: { GET }, @@ -161,10 +159,8 @@ describe("connectedAction", () => { }) test("returns connected: false when provider token cookie is expired", async () => { - const getSessionByTokenMock = vi.fn() - const getAccountsByUserIdMock = vi.fn() - - getSessionByTokenMock.mockResolvedValueOnce(sessionEntityWithUser) + const getSessionByTokenMock = vi.fn().mockResolvedValueOnce(sessionEntityWithUser) + const getAccountsByUserIdMock = vi.fn().mockResolvedValue([]) const { handlers: { GET }, @@ -244,10 +240,8 @@ describe("connectedAction", () => { }) test("handles empty cookie value", async () => { - const getSessionByTokenMock = vi.fn() - const getAccountsByUserIdMock = vi.fn() - - getSessionByTokenMock.mockResolvedValueOnce(sessionEntityWithUser) + const getSessionByTokenMock = vi.fn().mockResolvedValueOnce(sessionEntityWithUser) + const getAccountsByUserIdMock = vi.fn().mockResolvedValue([]) const { handlers: { GET }, @@ -275,10 +269,8 @@ describe("connectedAction", () => { }) test("handles multiple providers - checks correct provider", async () => { - const getSessionByTokenMock = vi.fn() - const getAccountsByUserIdMock = vi.fn() - - getSessionByTokenMock.mockResolvedValueOnce(sessionEntityWithUser) + const getSessionByTokenMock = vi.fn().mockResolvedValueOnce(sessionEntityWithUser) + const getAccountsByUserIdMock = vi.fn().mockResolvedValue([]) const { handlers: { GET }, diff --git a/packages/core/test/actions/providers/tokens/tokens/stateful.test.ts b/packages/core/test/actions/providers/tokens/tokens/stateful.test.ts index 42e4d43d..46f0e6cb 100644 --- a/packages/core/test/actions/providers/tokens/tokens/stateful.test.ts +++ b/packages/core/test/actions/providers/tokens/tokens/stateful.test.ts @@ -99,7 +99,7 @@ describe("tokensAction (Stateful)", async () => { expect(updateOAuthTokensMock).not.toHaveBeenCalled() }) - test("should return 401 if OAuth account does not exist", async () => { + test("should return 404 if OAuth account does not exist", async () => { const getSessionByTokenMock = vi.fn().mockResolvedValue(sessionEntityWithUser) const getAccountsByUserIdMock = vi.fn().mockResolvedValue([]) const getOAuthAccountMock = vi.fn().mockResolvedValue(null) @@ -125,7 +125,7 @@ describe("tokensAction (Stateful)", async () => { }, }) ) - expect(response.status).toBe(400) + expect(response.status).toBe(404) expect(await response.json()).toEqual({ success: false, tokens: null, diff --git a/packages/core/test/api/stateful/getAccessToken.test.ts b/packages/core/test/api/stateful/getAccessToken.test.ts index 9699710f..99b555cb 100644 --- a/packages/core/test/api/stateful/getAccessToken.test.ts +++ b/packages/core/test/api/stateful/getAccessToken.test.ts @@ -135,7 +135,7 @@ describe("getAccessToken API (Stateful)", () => { success: false, accessToken: null, error: { - code: "OAUTH_UNLINKED_ACCOUNT_ERROR", + code: "OAUTH_ACCOUNT_NOT_FOUND", message: "The specified identity provider is not connected to your account.", }, headers: expect.any(Headers), diff --git a/packages/core/test/api/stateful/getProviderTokens.test.ts b/packages/core/test/api/stateful/getProviderTokens.test.ts index a8cc2562..9b32ebfc 100644 --- a/packages/core/test/api/stateful/getProviderTokens.test.ts +++ b/packages/core/test/api/stateful/getProviderTokens.test.ts @@ -135,7 +135,7 @@ describe("getProviderTokens API (Stateful)", () => { success: false, tokens: null, error: { - code: "OAUTH_UNLINKED_ACCOUNT_ERROR", + code: "OAUTH_ACCOUNT_NOT_FOUND", message: "The specified identity provider is not connected to your account.", }, headers: expect.any(Headers), From d6273008f9ce5a2bb345dd13ea21fff72a66708d Mon Sep 17 00:00:00 2001 From: Hernan Alvarado Date: Sun, 8 Nov 2026 17:29:56 -0500 Subject: [PATCH 2/2] refactor(core): simplify stateless session strategy functions --- .../session/stateless/get-provider-tokens.ts | 158 ++++++------------ .../stateless/is-provider-connected.ts | 24 +-- .../src/session/stateless/refresh-userInfo.ts | 3 +- 3 files changed, 54 insertions(+), 131 deletions(-) diff --git a/packages/core/src/session/stateless/get-provider-tokens.ts b/packages/core/src/session/stateless/get-provider-tokens.ts index 56133c47..8531605e 100644 --- a/packages/core/src/session/stateless/get-provider-tokens.ts +++ b/packages/core/src/session/stateless/get-provider-tokens.ts @@ -1,9 +1,6 @@ import { HeadersBuilder } from "@aura-stack/router" import { shouldRefresh } from "@/shared/utils.ts" import { getCookie } from "@/shared/http/cookie.ts" -import { getErrorName } from "@/errors/aura-error.ts" -import { handleApiError } from "@/shared/utils/api.ts" -import { AuraAuthError } from "@/errors/aura-error.ts" import { secureApiHeaders } from "@/shared/http/headers.ts" import { refreshProviderToken } from "@/shared/utils/refresh-tokens.ts" import type { InternalContextForStateless, SessionStrategy } from "@/@types/internal.ts" @@ -20,139 +17,78 @@ export const getProviderTokens = (ctx: InternalContextForStateless): SessionStra }, }) - try { - logger?.log("STATELESS_GET_PROVIDER_TOKENS_PROVIDER_FOUND", { - structuredData: { - oauth_id: oauthId, - }, - }) + const cookieName = `${ctx.cookies.store.accessToken.name}.${oauthId}` + const cookie = getCookie(request, cookieName) - const cookieName = `${ctx.cookies.store.accessToken.name}.${oauthId}` - const cookie = getCookie(request, cookieName) + const decodedToken = await jose.manager.verifyToken(cookie) + const tokens = await identity.schemaRegistry.parseOAuthTokens(decodedToken) - if (!cookie) { - logger?.log("STATELESS_GET_PROVIDER_TOKENS_NO_COOKIE", { - structuredData: { - oauth_id: oauthId, - cookie_name: cookieName, - reason: "no_access_token_cookie", - }, - }) - const { code, message, statusCode } = handleApiError( - new AuraAuthError({ code: "COOKIE_INVALID_VALUE" }), - "PROVIDER_TOKENS_ERROR", - "Failed to get provider tokens" - ) - return { success: false, error: { code, message }, tokens: null, headers: request.headers, statusCode } - } + logger?.log("STATELESS_GET_PROVIDER_TOKENS_TOKENS_DECODED", { + structuredData: { + oauth_id: oauthId, + has_access_token: Boolean(tokens.accessToken), + has_refresh_token: Boolean(tokens.refreshToken), + expires_at: tokens.expiresAt, + }, + }) - logger?.log("STATELESS_GET_PROVIDER_TOKENS_COOKIE_FOUND", { - structuredData: { - oauth_id: oauthId, - cookie_name: cookieName, - }, - }) + const refreshWindow = provider.refreshWindow ?? 300 + const needsRefresh = shouldRefresh(tokens, refreshWindow) - const decodedToken = await jose.manager.verifyToken(cookie) - const tokens = await identity.schemaRegistry.parseOAuthTokens(decodedToken) + logger?.log("STATELESS_GET_PROVIDER_TOKENS_REFRESH_CHECK", { + structuredData: { + oauth_id: oauthId, + needs_refresh: needsRefresh, + refresh_window: refreshWindow, + expires_at: tokens.expiresAt, + }, + }) - logger?.log("STATELESS_GET_PROVIDER_TOKENS_TOKENS_DECODED", { + if (needsRefresh) { + logger?.log("STATELESS_GET_PROVIDER_TOKENS_REFRESHING", { structuredData: { oauth_id: oauthId, - has_access_token: Boolean(tokens.accessToken), - has_refresh_token: Boolean(tokens.refreshToken), - expires_at: tokens.expiresAt, + reason: "token_expired_or_in_refresh_window", }, }) - const refreshWindow = provider.refreshWindow ?? 300 - const needsRefresh = shouldRefresh(tokens, refreshWindow) + const refreshedTokens = await refreshProviderToken(tokens, provider!) - logger?.log("STATELESS_GET_PROVIDER_TOKENS_REFRESH_CHECK", { + logger?.log("STATELESS_GET_PROVIDER_TOKENS_REFRESH_SUCCESS", { structuredData: { oauth_id: oauthId, - needs_refresh: needsRefresh, - refresh_window: refreshWindow, - expires_at: tokens.expiresAt, + new_expires_at: refreshedTokens.expiresAt, }, }) - if (needsRefresh) { - logger?.log("STATELESS_GET_PROVIDER_TOKENS_REFRESHING", { - structuredData: { - oauth_id: oauthId, - reason: "token_expired_or_in_refresh_window", - }, - }) + const encodedTokens = await jose.manager.createToken(refreshedTokens as any) + const builder = new HeadersBuilder(secureApiHeaders) + .setCookie(cookieName, encodedTokens, ctx.cookies.store.accessToken.attributes) + .toHeaders() - try { - const refreshedTokens = await refreshProviderToken(tokens, provider!) - - logger?.log("STATELESS_GET_PROVIDER_TOKENS_REFRESH_SUCCESS", { - structuredData: { - oauth_id: oauthId, - new_expires_at: refreshedTokens.expiresAt, - }, - }) - - const encodedTokens = await jose.manager.createToken(refreshedTokens as any) - const builder = new HeadersBuilder(secureApiHeaders) - .setCookie(cookieName, encodedTokens, ctx.cookies.store.accessToken.attributes) - .toHeaders() - - logger?.log("STATELESS_GET_PROVIDER_TOKENS_COOKIE_UPDATED", { - structuredData: { - oauth_id: oauthId, - cookie_name: cookieName, - }, - }) - - return { - success: true, - tokens: refreshedTokens, - headers: builder, - } - } catch (refreshError) { - logger?.log("STATELESS_GET_PROVIDER_TOKENS_REFRESH_ERROR", { - structuredData: { - oauth_id: oauthId, - error_type: getErrorName(refreshError), - error_message: refreshError instanceof Error ? refreshError.message : String(refreshError), - }, - }) - - const { code, message, statusCode } = handleApiError( - refreshError, - "PROVIDER_TOKENS_ERROR", - "Failed to get provider tokens" - ) - return { success: false, error: { code, message }, tokens: null, headers: request.headers, statusCode } - } - } - - logger?.log("STATELESS_GET_PROVIDER_TOKENS_SUCCESS", { + logger?.log("STATELESS_GET_PROVIDER_TOKENS_COOKIE_UPDATED", { structuredData: { oauth_id: oauthId, - tokens_returned: true, + cookie_name: cookieName, }, }) return { - success: true, - tokens, - headers: request.headers, + tokens: refreshedTokens, + headers: builder, } - } catch (error) { - logger?.log("STATELESS_GET_PROVIDER_TOKENS_ERROR", { - structuredData: { - oauth_id: oauthId, - error_type: getErrorName(error), - error_message: error instanceof Error ? error.message : String(error), - }, - }) + } + + logger?.log("STATELESS_GET_PROVIDER_TOKENS_SUCCESS", { + structuredData: { + oauth_id: oauthId, + tokens_returned: true, + }, + }) - const { code, message, statusCode } = handleApiError(error, "PROVIDER_TOKENS_ERROR", "Failed to get provider tokens") - return { success: false, error: { code, message }, tokens: null, headers: request.headers, statusCode } + return { + tokens, + headers: new Headers(secureApiHeaders), } } } diff --git a/packages/core/src/session/stateless/is-provider-connected.ts b/packages/core/src/session/stateless/is-provider-connected.ts index d5db95ae..80a0335c 100644 --- a/packages/core/src/session/stateless/is-provider-connected.ts +++ b/packages/core/src/session/stateless/is-provider-connected.ts @@ -1,27 +1,15 @@ -import { getCookie } from "@/shared/http/cookie.ts" -import { getErrorName } from "@/errors/aura-error.ts" +import { getOptionalCookie } from "@/shared/http/cookie.ts" import type { InternalContextForStateless, SessionStrategy } from "@/@types/internal.ts" export const isProviderConnected = (ctx: InternalContextForStateless): SessionStrategy["isProviderConnected"] => { - const { logger, jose } = ctx - return async ({ oauthId, headers }) => { - const cookieName = `${ctx.cookies.store.accessToken.name}.${oauthId}` - let cookieValue: string try { - cookieValue = getCookie(headers, cookieName) + const cookieName = `${ctx.cookies.store.accessToken.name}.${oauthId}` + const token = getOptionalCookie(headers, cookieName) + if (!token) return false + await ctx.jose.manager.verifyToken(token) + return true } catch { - logger?.log("OAUTH_ACCESS_TOKEN_REQUEST_INITIATED", { - structuredData: { provider: oauthId, hasCookie: false }, - }) - return false - } - - try { - const decodedToken = await jose.manager.verifyToken(cookieValue) - return !!decodedToken - } catch (error) { - logger?.log("AUTH_SESSION_INVALID", { structuredData: { error_type: getErrorName(error) } }) return false } } diff --git a/packages/core/src/session/stateless/refresh-userInfo.ts b/packages/core/src/session/stateless/refresh-userInfo.ts index 9e858dce..aa3aa4e5 100644 --- a/packages/core/src/session/stateless/refresh-userInfo.ts +++ b/packages/core/src/session/stateless/refresh-userInfo.ts @@ -7,7 +7,6 @@ export const refreshUserInfo = ( ): SessionStrategy["refreshUserInfo"] => { const refreshSession = refreshSessionHelper(ctx) return async ({ headers, user }) => { - const value = await refreshSession({ headers, session: { user } }) - return value + return await refreshSession({ headers, session: { user } }) } }