diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index ebaaefaf..3d1a7abd 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -10,6 +10,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### BREAKING CHANGES +- Removed the `GetStatelessSessionReturn`, `GetStatefulSessionReturn`, `GetProviderTokensStatefulReturn`, and `SessionStrategy` types from the public API. These types, along with the interfaces related to `SessionStrategy`, are now internal and are no longer exported. [#301](https://github.com/aura-stack-ts/auth/pull/301) + - Updated `VerificationTokensAdapter`: custom adapters must now implement `revokeAllVerificationTokens(userId, type?)`, and `revokeToken(id)` must return `Promise` instead of `Promise`. The boolean indicates whether the token was found and successfully revoked. [#297](https://github.com/aura-stack-ts/auth/pull/297) - Changed the return types of the `deriveSecret` and `verifySecret` helpers in the public `CredentialsConfigContext` to `Awaitable`. Custom implementations can now return either synchronous or asynchronous values. Consumers must not call `.then()` directly on these results without first normalizing them with `Promise.resolve()`. [#297](https://github.com/aura-stack-ts/auth/pull/297) diff --git a/packages/core/src/@types/internal.ts b/packages/core/src/@types/internal.ts index fde7cc6e..fe13bad8 100644 --- a/packages/core/src/@types/internal.ts +++ b/packages/core/src/@types/internal.ts @@ -5,7 +5,7 @@ */ import type { Awaitable, InferSchema, Prettify, RoutePattern } from "@aura-stack/router" import type { ZodObject, infer as Infer } from "zod" -import type { LiteralUnion } from "@/@types/utility.ts" +import type { DeepPartial, LiteralUnion } from "@/@types/utility.ts" import type { identitySchema } from "@/identity/zod.ts" import type { createLogEntry } from "@/config/logger/create-logger.ts" import type { InferRules } from "@aura-stack/rate-limiter" @@ -29,8 +29,9 @@ import type { } from "@/@types/config.ts" import type { JWTKey, + OAuthTokenPayload, + Session, SessionConfig, - SessionStrategy, StatefulStrategyConfig, StatelessStrategyConfig, User, @@ -38,6 +39,7 @@ import type { import type { OAuthAccessTokenResponse, OIDCAccessTokenResponseSchema } from "@/shared/schemas/general.ts" import type { JWTPayload } from "@/jose.ts" import type { TypedJWTPayload } from "@aura-stack/jose" +import type { SessionWithUserEntity } from "@/@types/entities.ts" import type { createCookieManager } from "@/session/cookie-manager.ts" // #region API @@ -214,3 +216,158 @@ export type OAuthProviderRecord = Record< export type JWTPayloadWithToken = JWTPayload & { token: string } export type CookieManager = ReturnType + +// #region Session Strategy +/** + * Abstraction layer for session management. + */ +export interface SessionStrategy { + /** + * Read and validate the session from an incoming request. + * Returns null if absent, invalid, or expired. Never throws on auth failure. + */ + getSession(options: { headers: Headers }): Promise<{ + session: Session | null + headers: Headers + }> + /** + * Create a session after successful authentication. + * Signs the JWT / writes the DB row / sets cookies. + */ + createSession(options: { session: User; request: Request }): Promise + /** + * Get the OAuth access token for a specific provider, using the refresh token if necessary. + */ + getProviderTokens(options: { + oauthId: string + 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 } + > + /** + * Attempt to refresh using the refresh token cookie. + * Returns null session + cookie-clearing response on any failure. + */ + refreshSession(options: { headers: Headers; session: DeepPartial> }): Promise<{ + session: Session | null + headers: Headers + }> + /** + * Revoke a session by ID. + * JWT strategy: best-effort (clears cookies, no server state). + * Database / hybrid: marks row inactive. + */ + revokeSession(options: { sessionId: string }): Promise + /** + * Destroy the session attached to this request (logout). + * Returns a response that clears cookies. + */ + destroySession(options: { sessionByToken: SessionWithUserEntity }): Promise + /** + * Revoke the access token for a specific OAuth provider. + * @unstable This API is experimental and may change in future releases. + */ + revokeToken(options: { + oauthId: string + provider: RuntimeOAuthProvider + headers: Headers + disconnect: boolean + sessionByToken: SessionWithUserEntity + }): Promise + /** + * Check if the user is connected to a specific OAuth provider. + * @unstable This API is experimental and may change in future releases. + */ + isProviderConnected(options: { oauthId: string; headers: Headers; sessionByToken: SessionWithUserEntity }): Promise + /** + * Refresh the user info in the session. + * @unstable This API is experimental and may change in future releases. + */ + refreshUserInfo(options: { user: DeepPartial; headers: Headers; skipCSRFCheck?: boolean }): Promise<{ + session: Session | null + headers: Headers + }> + /** + * Sign up a new user with the given payload and request. Returns the session token on success. + * @unstable This API is experimental and may change in future releases. + */ + signUp(options: { payload: Record; request: Request }): Promise + /** + * Sign in a user with the given credentials and request. Returns the session token on success. + * @unstable This API is experimental and may change in future releases. + */ + signInCredentials(options: { payload: Record; request: Request; redirectTo?: string }): Promise + /** + * Sign in a user with the given OAuth provider and request. Returns the session token on success. + */ + signIn(options: { + oauthId: string + provider: RuntimeOAuthProvider + request: Request + redirectTo?: string + }): Promise<{ + success: boolean + headers: Headers + signInURL: string + }> + /** + * Handle the OAuth callback after the user authorizes the app. Returns a response that + * sets cookies and redirects to the original URL. + */ + oauthCallback(options: { oauthId: string; request: Request; params: { code: string; state: string } }): Promise + /** + * Send a verification email to the user with a token and URL. + */ + sendVerificationEmail(options: { email: string; origin: string }): Promise<{ + token: string + url: string + }> + /** + * Verify the email using the token sent to the user. + */ + verifyEmail(options: { token: string }): Promise + /** + * Initiate a password reset flow by sending an email with a token and URL. Returns the token and URL. + */ + forgotPassword(options: { email: string; origin: string }): Promise<{ + token: string + url: string + }> + /** + * Reset the password using the token sent to the user. + */ + resetPassword(options: { token: string; newPassword: string }): Promise + /** + * Change the password for a user, given the current password and new password. + */ + changePassword(options: { userId: string; currentPassword: string; newPassword: string }): Promise + /** + * Initiate an email change flow by sending a confirmation email to the new address + * and a cancellation email to the old address. + */ + changeEmail(options: { + userId: string + currentPassword: string + newEmail: string + origin: string + }): Promise<{ email: string; confirmToken: string; confirmURL: string; cancelToken: string; cancelURL: string }> + /** + * Verify the email change request using the token sent to the user. + */ + verifyEmailChange(options: { token: string; userId: string; sessionId: string }): Promise + /** + * Cancel the email change request using the token sent to the user. + */ + cancelEmailChange(options: { token: string; userId?: string } | { token?: string; userId: string }): Promise + /** + * Initiates the magic link sign-in or sign-up flow by sending an email with a token and URL. + */ + sendMagicLink(options: { email: string; allowSignUp: boolean; origin: string }): Promise<{ token: string; url: string }> + /** + * Verivy the magic link token + */ + verifyMagicLink(options: { token: string; request: Request }): Promise<{ sessionToken: string }> +} diff --git a/packages/core/src/@types/session.ts b/packages/core/src/@types/session.ts index 7be009a3..6a4ea7a0 100644 --- a/packages/core/src/@types/session.ts +++ b/packages/core/src/@types/session.ts @@ -1,8 +1,8 @@ import { identitySchema } from "@/identity/zod.ts" import type { JWK } from "@aura-stack/jose/jose" import type { infer as Infer } from "zod/v4/core" +import type { Prettify } from "@/@types/index.ts" import type { DatabaseAdapter } from "@/@types/adapter.ts" -import type { DeepPartial, Prettify } from "@/@types/index.ts" /** Application user type, inferred from the configured identity schema (defaults to the built-in user shape). */ export type User = Infer @@ -247,139 +247,6 @@ export interface StatefulStrategyConfig extends SessionConfigBase { */ export type SessionConfig = StatelessStrategyConfig | StatefulStrategyConfig -/** Result of reading a stateless (JWT) session from a request: session payload and outgoing header mutations. */ -export interface GetStatelessSessionReturn { - session: Session | null - headers: Headers -} - -export type GetStatefulSessionReturn = GetStatelessSessionReturn - -export type GetProviderTokensStatefulReturn = - | { success: true; tokens: OAuthTokenPayload; headers: Headers } - | { success: false; tokens: null; error: { code: string; message: string }; headers: Headers; statusCode: number } - -/** - * Abstraction layer for session management. - */ -export interface SessionStrategy { - /** - * Read and validate the session from an incoming request. - * Returns null if absent, invalid, or expired. Never throws on auth failure. - */ - getSession(headers: Headers): Promise> - - /** - * Create a session after successful authentication. - * Signs the JWT / writes the DB row / sets cookies. - */ - createSession(session: User, request: Request): Promise - - getProviderTokens(oauth: string, request: Request): Promise - - /** - * Attempt to refresh using the refresh token cookie. - * Returns null session + cookie-clearing response on any failure. - */ - refreshSession( - headers: Headers, - session: DeepPartial> - ): Promise<{ - session: Session | null - headers: Headers - }> - - /** - * Revoke a session by ID. - * JWT strategy: best-effort (clears cookies, no server state). - * Database / hybrid: marks row inactive. - */ - revokeSession(sessionId: string): Promise - - /** - * Destroy the session attached to this request (logout). - * Returns a response that clears cookies. - */ - destroySession(headers: Headers): Promise - - /** - * Revoke the access token for a specific OAuth provider. - * @unstable This API is experimental and may change in future releases. - */ - revokeToken(oauth: string, headers: Headers, disconnect: boolean): Promise - - /** - * Check if the user is connected to a specific OAuth provider. - * @unstable This API is experimental and may change in future releases. - */ - isProviderConnected(oauth: string, headers: Headers): Promise - - /** - * Refresh the user info in the session. - * @unstable This API is experimental and may change in future releases. - */ - refreshUserInfo( - user: DeepPartial, - headers: Headers, - skipCSRFCheck?: boolean - ): Promise<{ - session: Session | null - headers: Headers - }> - - /** - * Sign up a new user with the given payload and request. Returns the session token on success. - * @unstable This API is experimental and may change in future releases. - */ - signUp(payload: Record, request: Request): Promise - /** - * Sign in a user with the given credentials and request. Returns the session token on success. - * @unstable This API is experimental and may change in future releases. - */ - signInCredentials(payload: Record, request: Request, redirectTo?: string): Promise - signIn( - oauth: string, - request: Request, - redirectTo?: string - ): Promise<{ - success: boolean - headers: Headers - signInURL: string - }> - - oauthCallback(oauth: string, request: Request, { code, state }: { code: string; state: string }): Promise - - sendVerificationEmail(options: { email: string; origin: string }): Promise<{ - token: string - url: string - }> - - verifyEmail(options: { token: string }): Promise - - forgotPassword(options: { email: string; origin: string }): Promise<{ - token: string - url: string - }> - - resetPassword(options: { token: string; newPassword: string }): Promise - - changePassword(options: { userId: string; currentPassword: string; newPassword: string }): Promise - - verifyEmailChange(options: { token: string; userId: string; sessionId: string }): Promise - - cancelEmailChange(options: { token: string; userId?: string } | { token?: string; userId: string }): Promise - - sendMagicLink(options: { email: string; allowSignUp: boolean; origin: string }): Promise<{ token: string; url: string }> - - verifyMagicLink(options: { token: string; request: Request }): Promise<{ sessionToken: string }> - changeEmail(options: { - userId: string - currentPassword: string - newEmail: string - origin: string - }): Promise<{ email: string; confirmToken: string; confirmURL: string; cancelToken: string; cancelURL: string }> -} - export interface OAuthTokenPayload { /** * The raw access token string issued by the OAuth provider. diff --git a/packages/core/src/actions/callback/callback.ts b/packages/core/src/actions/callback/callback.ts index 01e35531..f1f57388 100644 --- a/packages/core/src/actions/callback/callback.ts +++ b/packages/core/src/actions/callback/callback.ts @@ -18,11 +18,15 @@ export const callbackAction = (oauth: OAuthProviderRecord) => { async (ctx) => { const { request, - params: { oauth }, + params: { oauth: oauthId }, searchParams: { code, state }, context, } = ctx - return await context.sessionStrategy.oauthCallback(oauth, request, { code, state }) + return await context.sessionStrategy.oauthCallback({ + oauthId, + request, + params: { code, state }, + }) }, callbackConfig(oauth) ) diff --git a/packages/core/src/api/get-provider-tokens.ts b/packages/core/src/api/get-provider-tokens.ts index 6fc3d690..905475ec 100644 --- a/packages/core/src/api/get-provider-tokens.ts +++ b/packages/core/src/api/get-provider-tokens.ts @@ -10,14 +10,14 @@ import type { } from "@/@types/index.ts" export const getProviderTokens = async ( - oauth: LiteralUnion, + oauthId: LiteralUnion, { ctx, request: requestInit, headers: headersInit }: FunctionAPIContext ): Promise => { const initialHeaders = toStandardizedHeaders(headersInit ?? requestInit?.headers ?? {}) try { - const { request, rateLimit } = await createValidation(ctx, initialHeaders) - .verifyOAuthProvider(oauth) - .buildRequest(requestInit, `/providers/${oauth}/tokens`) + const { request, rateLimit, provider, session } = await createValidation(ctx, initialHeaders) + .verifyOAuthProvider(oauthId) + .buildRequest(requestInit, `/providers/${oauthId}/tokens`) .verifyRateLimit("getProviderTokens") .verifySession() .execute() @@ -25,7 +25,12 @@ export const getProviderTokens = async ( if (rateLimit) { return rateLimit as unknown as GetProviderTokensAPIReturn } - const getTokens = await ctx.sessionStrategy.getProviderTokens(oauth, request) + const getTokens = await ctx.sessionStrategy.getProviderTokens({ + oauthId, + request, + provider, + sessionByToken: session!, + }) if (getTokens.success) { const { success, tokens, headers } = getTokens return { diff --git a/packages/core/src/api/get-session.ts b/packages/core/src/api/get-session.ts index 97215f1f..5142c534 100644 --- a/packages/core/src/api/get-session.ts +++ b/packages/core/src/api/get-session.ts @@ -25,7 +25,9 @@ export const getSession = async ({ toResponse: () => Response.json({ success: false, session: null }, { status: 401, headers }), } try { - const { session, headers } = await ctx.sessionStrategy.getSession(toStandardizedHeaders(headersInit)) + const { session, headers } = await ctx.sessionStrategy.getSession({ + headers: toStandardizedHeaders(headersInit), + }) if (!session) return unauthorized const newHeaders = toUnionHeaders(headers, secureApiHeaders) return { diff --git a/packages/core/src/api/is-provider-connected.ts b/packages/core/src/api/is-provider-connected.ts index 570b512b..49155d79 100644 --- a/packages/core/src/api/is-provider-connected.ts +++ b/packages/core/src/api/is-provider-connected.ts @@ -6,20 +6,20 @@ import type { FunctionAPIContext } from "@/@types/internal.ts" import type { ProviderConnectedAPIOptions, ProviderConnectedAPIReturn } from "@/@types/api.ts" export const isProviderConnected = async ( - oauth: LiteralUnion, + oauthId: LiteralUnion, { ctx, headers: headersInit, request: requestInit }: FunctionAPIContext ): Promise => { try { ctx.logger?.log("OAUTH_ACCESS_TOKEN_REQUEST_INITIATED", { - structuredData: { provider: oauth, operation: "check_connection" }, + structuredData: { provider: oauthId, operation: "check_connection" }, }) - const { headers, rateLimit } = await createValidation( + const { headers, rateLimit, session } = await createValidation( ctx, toStandardizedHeaders(headersInit ?? requestInit?.headers ?? {}) ) - .verifyOAuthProvider(oauth) - .buildRequest(requestInit, `/providers/${oauth}`) + .verifyOAuthProvider(oauthId) + .buildRequest(requestInit, `/providers/${oauthId}`) .verifyRateLimit("isProviderConnected") .verifySession() .execute() @@ -28,10 +28,14 @@ export const isProviderConnected = async ( return rateLimit } - const connected = await ctx.sessionStrategy.isProviderConnected(oauth, headers) + const connected = await ctx.sessionStrategy.isProviderConnected({ + oauthId, + headers, + sessionByToken: session!, + }) ctx.logger?.log("OAUTH_ACCESS_TOKEN_SUCCESS", { - structuredData: { provider: oauth, connected }, + structuredData: { provider: oauthId, connected }, }) return { diff --git a/packages/core/src/api/refresh-userInfo.ts b/packages/core/src/api/refresh-userInfo.ts index 451a6f0d..c73c0af1 100644 --- a/packages/core/src/api/refresh-userInfo.ts +++ b/packages/core/src/api/refresh-userInfo.ts @@ -82,7 +82,10 @@ export const refreshUserInfo = async ( structuredData: { provider: oauth, userId: userInfo.sub }, }) - const { session, headers: newHeaders } = await ctx.sessionStrategy.refreshUserInfo(userInfo, headers) + const { session, headers: newHeaders } = await ctx.sessionStrategy.refreshUserInfo({ + headers, + user: userInfo, + }) return { success: !!session, headers: newHeaders, diff --git a/packages/core/src/api/revoke-token.ts b/packages/core/src/api/revoke-token.ts index 011ce7a5..1d2bb80d 100644 --- a/packages/core/src/api/revoke-token.ts +++ b/packages/core/src/api/revoke-token.ts @@ -4,7 +4,7 @@ import type { FunctionAPIContext } from "@/@types/internal.ts" import type { RevokeTokenAPIOptions, RevokeTokenAPIReturn, LiteralUnion, BuiltInOAuthProvider } from "@/@types/index.ts" export const revokeToken = async ( - oauth: LiteralUnion, + oauthId: LiteralUnion, { ctx, headers: headersInit, @@ -17,19 +17,19 @@ export const revokeToken = async ( try { ctx.logger?.log("OAUTH_ACCESS_TOKEN_REQUEST_INITIATED", { structuredData: { - provider: oauth, + provider: oauthId, operation: disconnect ? "disconnect" : "revoke", skipCSRFCheck: skipCSRFCheck, doubleSubmitToken: doubleSubmitToken ? "provided" : "not_provided", }, }) - const { headers, rateLimit } = await createValidation( + const { provider, headers, rateLimit, session } = await createValidation( ctx, toStandardizedHeaders(headersInit ?? requestInit?.headers ?? {}) ) - .verifyOAuthProvider(oauth) - .buildRequest(requestInit, `/providers/${oauth}/tokens/revoke`) + .verifyOAuthProvider(oauthId) + .buildRequest(requestInit, `/providers/${oauthId}/tokens/revoke`) .verifyRateLimit("revokeToken") .verifySession() .verifyCSRFToken(skipCSRFCheck, doubleSubmitToken) @@ -37,12 +37,18 @@ export const revokeToken = async ( if (rateLimit) { ctx.logger?.log("INVALID_REQUEST", { - structuredData: { provider: oauth, reason: "rate_limit_exceeded" }, + structuredData: { provider: oauthId, reason: "rate_limit_exceeded" }, }) return rateLimit as RevokeTokenAPIReturn } - const revokeHeaders = await ctx.sessionStrategy.revokeToken(oauth, headers, disconnect) + const revokeHeaders = await ctx.sessionStrategy.revokeToken({ + oauthId, + provider, + headers, + disconnect, + sessionByToken: session!, + }) return { success: true, headers: revokeHeaders, diff --git a/packages/core/src/api/sign-in-credentials.ts b/packages/core/src/api/sign-in-credentials.ts index c165a076..d69fdef9 100644 --- a/packages/core/src/api/sign-in-credentials.ts +++ b/packages/core/src/api/sign-in-credentials.ts @@ -46,7 +46,10 @@ export const signInCredentials = async ({ if (!session) { throw new AuraAuthError({ code: "AUTH_CREDENTIALS_INVALID" }) } - const sessionToken = await sessionStrategy.signInCredentials(session, request) + const sessionToken = await sessionStrategy.signInCredentials({ + request, + payload: session, + }) const csrfToken = await createCSRF(ctx.jose) logger?.log("CREDENTIALS_SIGN_IN_SUCCESS") diff --git a/packages/core/src/api/sign-in.ts b/packages/core/src/api/sign-in.ts index 5e7fbce9..3484b785 100644 --- a/packages/core/src/api/sign-in.ts +++ b/packages/core/src/api/sign-in.ts @@ -8,16 +8,16 @@ import type { BuiltInOAuthProvider, LiteralUnion, SignInAPIOptions, SignInAPIRet * Initiates the sign-in flow on the server. Called when the client invokes the `signIn` API function. */ export const signIn = async ( - oauth: LiteralUnion, + oauthId: LiteralUnion, { ctx, request: requestInit, headers: headersInit, redirect, redirectTo }: FunctionAPIContext ): Promise => { try { - const { request, rateLimit } = await createValidation( + const { request, rateLimit, provider } = await createValidation( ctx, toStandardizedHeaders(headersInit ?? requestInit?.headers ?? {}) ) - .verifyOAuthProvider(oauth) - .buildRequest(requestInit, `/signIn/${oauth}`) + .verifyOAuthProvider(oauthId) + .buildRequest(requestInit, `/signIn/${oauthId}`) .verifyRateLimit("signIn") .execute() @@ -27,10 +27,10 @@ export const signIn = async ( if (redirect === false) { ctx?.logger?.log("SIGN_IN_INITIATED", { - structuredData: { oauth_provider: oauth }, + structuredData: { oauth_provider: oauthId }, }) - const signInURL = await createSignInURL({ request, oauth, ctx, redirectTo }) + const signInURL = await createSignInURL({ request, oauth: oauthId, ctx, redirectTo }) const headers = new Headers(secureApiHeaders) return { success: true, @@ -43,7 +43,12 @@ export const signIn = async ( } } - const { success, signInURL, headers } = await ctx.sessionStrategy.signIn(oauth, request, redirectTo) + const { success, signInURL, headers } = await ctx.sessionStrategy.signIn({ + oauthId, + provider, + request, + redirectTo, + }) return { success, diff --git a/packages/core/src/api/sign-out.ts b/packages/core/src/api/sign-out.ts index bccc14c5..cf3f38a6 100644 --- a/packages/core/src/api/sign-out.ts +++ b/packages/core/src/api/sign-out.ts @@ -22,7 +22,7 @@ export const signOut = async ({ }, }) - const { request, rateLimit } = await createValidation(ctx, responseHeaders) + const { request, rateLimit, session } = await createValidation(ctx, responseHeaders) .buildRequest(requestInit, "/signOut") .verifyRateLimit("signOut") .verifyCSRFToken(skipCSRFCheck, doubleSubmitToken) @@ -33,7 +33,9 @@ export const signOut = async ({ return rateLimit } - responseHeaders = await ctx.sessionStrategy.destroySession(responseHeaders) + responseHeaders = await ctx.sessionStrategy.destroySession({ + sessionByToken: session!, + }) const headersBuilder = new HeadersBuilder(responseHeaders) const { redirect: shouldRedirectServer, redirectURL } = await resolveApiRedirect( diff --git a/packages/core/src/api/sign-up.ts b/packages/core/src/api/sign-up.ts index fd931c7e..dbbcc04b 100644 --- a/packages/core/src/api/sign-up.ts +++ b/packages/core/src/api/sign-up.ts @@ -45,7 +45,10 @@ export const signUp = async = Record({ return rateLimit as UpdateSessionAPIReturn } - const { session, headers } = await ctx.sessionStrategy.refreshSession(validatedHeaders, sessionInit) + const { session, headers } = await ctx.sessionStrategy.refreshSession({ + session: sessionInit, + headers: validatedHeaders, + }) if (!session) { throw new AuraAuthError({ code: "UPDATE_SESSION_INVALID" }) } diff --git a/packages/core/src/session/stateful/cancel-email-change.ts b/packages/core/src/session/stateful/cancel-email-change.ts index b48c718b..834a09a9 100644 --- a/packages/core/src/session/stateful/cancel-email-change.ts +++ b/packages/core/src/session/stateful/cancel-email-change.ts @@ -1,10 +1,10 @@ import { AuraAuthError } from "@/errors/aura-error.ts" import { createHash } from "@/shared/crypto.ts" -import type { InternalStatefulContext } from "@/@types/internal.ts" +import type { InternalStatefulContext, SessionStrategy } from "@/@types/internal.ts" -export const cancelEmailChange = ({ ctx }: InternalStatefulContext) => { +export const cancelEmailChange = ({ ctx }: InternalStatefulContext): SessionStrategy["cancelEmailChange"] => { const { sessionConfig } = ctx - return async ({ token, userId }: { token?: string; userId?: string }): Promise => { + return async ({ token, userId }): Promise => { const now = new Date() if (token) { const tokenHash = await createHash(token) @@ -20,11 +20,11 @@ export const cancelEmailChange = ({ ctx }: InternalStatefulContext) => { } const userTokens = await sessionConfig.adapter.getTokensByUserId(verificationToken.userId) const pending = userTokens.filter( - (t) => - (t.type === "email_change" || t.type === "email_change_cancel") && - t.newIdentifier === verificationToken.newIdentifier && - !t.revokedAt && - t.expiresAt > now + (token) => + (token.type === "email_change" || token.type === "email_change_cancel") && + token.newIdentifier === verificationToken.newIdentifier && + !token.revokedAt && + token.expiresAt > now ) await Promise.all(pending.map((t) => sessionConfig.adapter.revokeToken(t.id))) return @@ -35,8 +35,11 @@ export const cancelEmailChange = ({ ctx }: InternalStatefulContext) => { } const userIdTokens = await sessionConfig.adapter.getTokensByUserId(userId) const pending = userIdTokens.filter( - (t) => (t.type === "email_change" || t.type === "email_change_cancel") && !t.revokedAt && t.expiresAt > now + (token) => + (token.type === "email_change" || token.type === "email_change_cancel") && + !token.revokedAt && + token.expiresAt > now ) - await Promise.all(pending.map((t) => sessionConfig.adapter.revokeToken(t.id))) + await Promise.all(pending.map((token) => sessionConfig.adapter.revokeToken(token.id))) } } diff --git a/packages/core/src/session/stateful/change-email.ts b/packages/core/src/session/stateful/change-email.ts index d64c4915..64e8e544 100644 --- a/packages/core/src/session/stateful/change-email.ts +++ b/packages/core/src/session/stateful/change-email.ts @@ -1,7 +1,6 @@ import { AuraAuthError } from "@/errors/aura-error.ts" import { createHash, createSecretValue, verifyPassword } from "@/shared/crypto.ts" -import type { SessionStrategy } from "@/@types/session.ts" -import type { InternalStatefulContext } from "@/@types/internal.ts" +import type { SessionStrategy, InternalStatefulContext } from "@/@types/internal.ts" export const changeEmail = ({ ctx }: InternalStatefulContext): SessionStrategy["changeEmail"] => { const { sessionConfig } = ctx @@ -57,7 +56,7 @@ export const changeEmail = ({ ctx }: InternalStatefulContext): SessionStrategy[" userId, expiresAt: new Date(Date.now() + expiresIn * 1000), identifier: user.email, - newIdentifier: newEmail, + newIdentifier: normalizedNewEmail, type: "email_change_cancel", tokenHash: cancelTokenHash, }) diff --git a/packages/core/src/session/stateful/change-password.ts b/packages/core/src/session/stateful/change-password.ts index b1afbfb3..7d98aa4a 100644 --- a/packages/core/src/session/stateful/change-password.ts +++ b/packages/core/src/session/stateful/change-password.ts @@ -1,7 +1,6 @@ import { AuraAuthError } from "@/errors/aura-error.ts" import { hashPassword, verifyPassword } from "@/shared/crypto.ts" -import type { SessionStrategy } from "@/@types/session.ts" -import type { InternalStatefulContext } from "@/@types/internal.ts" +import type { SessionStrategy, InternalStatefulContext } from "@/@types/internal.ts" export const changePassword = ({ ctx }: InternalStatefulContext): SessionStrategy["changePassword"] => { const { sessionConfig, credentials } = ctx diff --git a/packages/core/src/session/stateful/create-session.ts b/packages/core/src/session/stateful/create-session.ts index 96d235d8..7cfc0c80 100644 --- a/packages/core/src/session/stateful/create-session.ts +++ b/packages/core/src/session/stateful/create-session.ts @@ -1,15 +1,13 @@ import { AuraAuthError } from "@/errors/aura-error.ts" import { createHash, createSecretValue } from "@/shared/crypto.ts" import { createDevice as __createDevice } from "@/shared/utils/session-strategy.ts" -import type { TypedJWTPayload } from "@aura-stack/jose" -import type { User } from "@/@types/index.ts" -import type { InternalStatefulContext } from "@/@types/internal.ts" +import type { InternalStatefulContext, SessionStrategy } from "@/@types/internal.ts" -export const createSession = ({ ctx, cookies, cookieManager }: InternalStatefulContext) => { +export const createSession = ({ ctx, cookies, cookieManager }: InternalStatefulContext): SessionStrategy["createSession"] => { const { logger, sessionConfig } = ctx const createDevice = __createDevice({ ctx, cookies, cookieManager }) - return async (session: TypedJWTPayload, request: Request) => { + return async ({ session, request }) => { logger?.log("STATEFUL_CREATE_SESSION_START", { structuredData: { strategy: "stateful", diff --git a/packages/core/src/session/stateful/destroy-session.ts b/packages/core/src/session/stateful/destroy-session.ts index 2749af95..239bfdfb 100644 --- a/packages/core/src/session/stateful/destroy-session.ts +++ b/packages/core/src/session/stateful/destroy-session.ts @@ -1,76 +1,22 @@ -import { createHash } from "@/shared/crypto.ts" -import { getErrorName } from "@/errors/aura-error.ts" -import type { InternalStatefulContext } from "@/@types/internal.ts" +import type { InternalStatefulContext, SessionStrategy } from "@/@types/internal.ts" -export const destroySession = ({ ctx, cookieManager }: InternalStatefulContext) => { +export const destroySession = ({ ctx, cookieManager }: InternalStatefulContext): SessionStrategy["destroySession"] => { const { logger, sessionConfig } = ctx - return async (headers: Headers) => { + return async ({ sessionByToken }) => { logger?.log("STATEFUL_DESTROY_SESSION_START", { structuredData: { strategy: "stateful", operation: "destroySession", }, }) - - try { - const { sessionToken } = cookieManager.getCookie(headers) - logger?.log("STATEFUL_SESSION_TOKEN_EXTRACTED", { - structuredData: { - has_token: Boolean(sessionToken), - token_length: sessionToken?.length || 0, - }, - }) - - if (sessionToken) { - const tokenHash = await createHash(sessionToken) - const sessionByToken = await sessionConfig.adapter.getSessionByToken(tokenHash) - logger?.log("STATEFUL_SESSION_DB_LOOKUP", { - structuredData: { - session_found: Boolean(sessionByToken), - session_id: sessionByToken?.id || "", - }, - }) - - if (sessionByToken) { - await sessionConfig.adapter.revokeSession(sessionByToken.id, "user_logout") - logger?.log("STATEFUL_SESSION_REVOKED", { - structuredData: { - session_id: sessionByToken.id, - reason: "user_logout", - }, - }) - } else { - logger?.log("STATEFUL_SESSION_NOT_FOUND_FOR_DESTRUCTION", { - structuredData: { - reason: "session_not_found_in_database", - }, - }) - } - } else { - logger?.log("STATEFUL_NO_TOKEN_FOR_DESTRUCTION", { - structuredData: { - reason: "no_session_token_in_cookie", - }, - }) - } - } catch (error) { - logger?.log("STATEFUL_DESTROY_SESSION_ERROR", { - structuredData: { - error_type: getErrorName(error), - error_message: error instanceof Error ? error.message : String(error), - }, - }) - throw error - } - + await sessionConfig.adapter.revokeSession(sessionByToken.id, "user_logout") const clearedHeaders = cookieManager.clearAll() logger?.log("STATEFUL_DESTROY_SESSION_SUCCESS", { structuredData: { cookies_cleared: true, }, }) - return clearedHeaders } } diff --git a/packages/core/src/session/stateful/forgot-password.ts b/packages/core/src/session/stateful/forgot-password.ts index a2fb0b03..5be65521 100644 --- a/packages/core/src/session/stateful/forgot-password.ts +++ b/packages/core/src/session/stateful/forgot-password.ts @@ -1,7 +1,6 @@ import { AuraAuthError } from "@/errors/aura-error.ts" import { createHash, createSecretValue } from "@/shared/crypto.ts" -import type { SessionStrategy } from "@/@types/session.ts" -import type { InternalStatefulContext } from "@/@types/internal.ts" +import type { SessionStrategy, InternalStatefulContext } from "@/@types/internal.ts" export const forgotPassword = ({ ctx }: InternalStatefulContext): SessionStrategy["forgotPassword"] => { const { sessionConfig, passwordReset, basePath } = ctx diff --git a/packages/core/src/session/stateful/get-provider-tokens.ts b/packages/core/src/session/stateful/get-provider-tokens.ts index 0a2ddd49..d802584c 100644 --- a/packages/core/src/session/stateful/get-provider-tokens.ts +++ b/packages/core/src/session/stateful/get-provider-tokens.ts @@ -1,21 +1,14 @@ -import { createHash } from "@/shared/crypto.ts" import { shouldRefresh } from "@/shared/utils.ts" import { handleApiError } from "@/shared/utils/api.ts" import { AuraAuthError, getErrorName } from "@/errors/aura-error.ts" import { refreshProviderToken } from "@/shared/utils/refresh-tokens.ts" import { secureApiHeaders, toUnionHeaders } from "@/shared/http/headers.ts" -import type { InternalStatefulContext } from "@/@types/internal.ts" -import type { GetProviderTokensStatefulReturn } from "@/@types/index.ts" +import type { InternalStatefulContext, SessionStrategy } from "@/@types/internal.ts" -export const getProviderTokens = ({ ctx, cookieManager }: InternalStatefulContext) => { - const { oauth, logger, sessionConfig } = ctx - - return async (oauthId: string, request: Request): Promise => { - const provider = oauth[oauthId] - if (!provider) { - throw new AuraAuthError({ code: "UNSUPPORTED_OAUTH_CONFIGURATION" }) - } +export const getProviderTokens = ({ ctx, cookieManager }: InternalStatefulContext): SessionStrategy["getProviderTokens"] => { + const { logger, sessionConfig } = ctx + return async ({ oauthId, provider, request, sessionByToken }) => { logger?.log("STATEFUL_GET_PROVIDER_TOKENS_START", { structuredData: { strategy: "stateful", @@ -25,37 +18,6 @@ export const getProviderTokens = ({ ctx, cookieManager }: InternalStatefulContex }) try { - const { sessionToken } = cookieManager.getCookie(new Headers(request.headers)) - if (!sessionToken) { - logger?.log("STATEFUL_GET_PROVIDER_TOKENS_NO_SESSION", { - structuredData: { - reason: "no_session_token", - }, - }) - 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: cookieManager.clear(), statusCode } - } - - const tokenHash = await createHash(sessionToken) - const sessionByToken = await sessionConfig.adapter.getSessionByToken(tokenHash) - if (!sessionByToken || !sessionByToken.user) { - logger?.log("STATEFUL_GET_PROVIDER_TOKENS_SESSION_INVALID", { - structuredData: { - reason: "session_not_found_or_no_user", - }, - }) - 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: cookieManager.clear(), statusCode } - } - const isExpired = Date.now() > sessionByToken.expiresAt.getTime() if (sessionByToken.status !== "active" || isExpired) { if (isExpired) { diff --git a/packages/core/src/session/stateful/get-session.ts b/packages/core/src/session/stateful/get-session.ts index 2209fc04..553f4916 100644 --- a/packages/core/src/session/stateful/get-session.ts +++ b/packages/core/src/session/stateful/get-session.ts @@ -4,10 +4,13 @@ import { AuraAuthError } from "@/errors/aura-error.ts" import { secureApiHeaders } from "@/shared/http/headers.ts" import { isInvalidSlidingThreshold } from "@/shared/assert.ts" import { calcStatefulExpiration, verifyDebounceLastActivity } from "@/shared/utils/session-strategy.ts" -import type { InternalStatefulContext } from "@/@types/internal.ts" -import type { GetStatefulSessionReturn, User } from "@/@types/index.ts" +import type { InternalStatefulContext, SessionStrategy } from "@/@types/internal.ts" +import type { User } from "@/@types/index.ts" -export const getSession = ({ ctx, cookieManager }: InternalStatefulContext) => { +export const getSession = ({ + ctx, + cookieManager, +}: InternalStatefulContext): SessionStrategy["getSession"] => { const { logger, sessionConfig } = ctx const maxAge = sessionConfig?.maxAge ?? 60 * 60 * 24 * 15 const strategy = sessionConfig?.expirationStrategy ?? "absolute" @@ -17,7 +20,7 @@ export const getSession = ({ ctx, cookieManager }: Int throw new AuraAuthError({ code: "INVALID_SLIDING_THRESHOLD_CONFIG_VALUE" }) } - return async (headers: Headers): Promise> => { + return async ({ headers }) => { logger?.log("STATEFUL_GET_SESSION_START", { structuredData: { strategy: "stateful", diff --git a/packages/core/src/session/stateful/index.ts b/packages/core/src/session/stateful/index.ts index 152eed85..2408a618 100644 --- a/packages/core/src/session/stateful/index.ts +++ b/packages/core/src/session/stateful/index.ts @@ -21,8 +21,8 @@ import { verifyEmailChange } from "@/session/stateful/verify-email-change.ts" import { cancelEmailChange } from "@/session/stateful/cancel-email-change.ts" import { isProviderConnected } from "@/session/stateful/is-provider-connected.ts" import { sendVerificationEmail } from "@/session/stateful/send-verification-email.ts" -import type { SessionStrategy, User } from "@/@types/index.ts" -import type { InternalStatefulContext } from "@/@types/internal.ts" +import type { User } from "@/@types/index.ts" +import type { SessionStrategy, InternalStatefulContext } from "@/@types/internal.ts" export const createStatefulStrategy = ( ctx: InternalStatefulContext diff --git a/packages/core/src/session/stateful/is-provider-connected.ts b/packages/core/src/session/stateful/is-provider-connected.ts index b1662647..18ab13cf 100644 --- a/packages/core/src/session/stateful/is-provider-connected.ts +++ b/packages/core/src/session/stateful/is-provider-connected.ts @@ -1,40 +1,17 @@ import { getErrorName } from "@/errors/aura-error.ts" -import { createHash } from "@/shared/crypto.ts" -import type { InternalStatefulContext } from "@/@types/internal.ts" +import type { InternalStatefulContext, SessionStrategy } from "@/@types/internal.ts" -export const isProviderConnected = ({ ctx, cookieManager }: InternalStatefulContext) => { +export const isProviderConnected = ({ ctx }: InternalStatefulContext): SessionStrategy["isProviderConnected"] => { const { logger, sessionConfig } = ctx - return async (oauthId: string, headers: Headers): Promise => { + return async ({ oauthId, sessionByToken }) => { logger?.log("OAUTH_ACCESS_TOKEN_REQUEST_INITIATED", { structuredData: { provider: oauthId, operation: "isProviderConnected", }, }) - try { - const { sessionToken } = cookieManager.getCookie(headers) - if (!sessionToken) { - logger?.log("SESSION_TOKEN_MISSING", { - structuredData: { - reason: "no_session_token", - }, - }) - return false - } - - const tokenHash = await createHash(sessionToken) - const sessionByToken = await sessionConfig.adapter.getSessionByToken(tokenHash) - if (!sessionByToken || !sessionByToken.user) { - logger?.log("AUTH_SESSION_INVALID", { - structuredData: { - reason: "session_not_found_or_no_user", - }, - }) - return false - } - const isExpired = Date.now() > sessionByToken.expiresAt.getTime() if (sessionByToken.status !== "active" || isExpired) { if (isExpired) { diff --git a/packages/core/src/session/stateful/oauth-callback.ts b/packages/core/src/session/stateful/oauth-callback.ts index 9f1a1a04..5f8cb111 100644 --- a/packages/core/src/session/stateful/oauth-callback.ts +++ b/packages/core/src/session/stateful/oauth-callback.ts @@ -7,13 +7,13 @@ import { createCSRF, createHash, createSecretValue } from "@/shared/crypto.ts" import { createDevice as __createDevice } from "@/shared/utils/session-strategy.ts" import { isOIDCProvider, resolveOpenIDProvider } from "@/shared/oidc/resolve-provider.ts" import { getOriginURL, isRelativeURL, isSameOrigin, isTrustedOrigin } from "@/shared/utils/get-origin-url.ts" -import type { InternalStatefulContext } from "@/@types/internal.ts" +import type { InternalStatefulContext, SessionStrategy } from "@/@types/internal.ts" -export const oauthCallback = ({ ctx, cookies, cookieManager }: InternalStatefulContext) => { +export const oauthCallback = ({ ctx, cookies, cookieManager }: InternalStatefulContext): SessionStrategy["oauthCallback"] => { const { logger, jose, oauth, sessionConfig } = ctx const createDevice = __createDevice({ ctx, cookies, cookieManager }) - return async (oauthId: string, request: Request, { code, state }: { code: string; state: string }) => { + return async ({ oauthId, request, params: { state, code } }) => { const oauthConfig = oauth[oauthId] if (!oauthConfig) { throw new AuraAuthError({ code: "UNSUPPORTED_OAUTH_CONFIGURATION" }) diff --git a/packages/core/src/session/stateful/refresh-session.ts b/packages/core/src/session/stateful/refresh-session.ts index f244e6d8..f5066b87 100644 --- a/packages/core/src/session/stateful/refresh-session.ts +++ b/packages/core/src/session/stateful/refresh-session.ts @@ -1,19 +1,15 @@ import { createHash } from "@/shared/crypto.ts" import { getErrorName } from "@/errors/aura-error.ts" -import type { DeepPartial } from "@/@types/utility.ts" import type { Session, User } from "@/@types/index.ts" -import type { InternalStatefulContext } from "@/@types/internal.ts" +import type { InternalStatefulContext, SessionStrategy } from "@/@types/internal.ts" -export const refreshSession = ({ ctx, cookieManager }: InternalStatefulContext) => { +export const refreshSession = ({ + ctx, + cookieManager, +}: InternalStatefulContext): SessionStrategy["refreshSession"] => { const { logger, sessionConfig } = ctx - return async ( - headers: Headers, - session: DeepPartial> - ): Promise<{ - session: Session | null - headers: Headers - }> => { + return async ({ headers, session }) => { logger?.log("STATEFUL_REFRESH_SESSION_START", { structuredData: { strategy: "stateful", diff --git a/packages/core/src/session/stateful/refresh-userInfo.ts b/packages/core/src/session/stateful/refresh-userInfo.ts index a395754d..87297376 100644 --- a/packages/core/src/session/stateful/refresh-userInfo.ts +++ b/packages/core/src/session/stateful/refresh-userInfo.ts @@ -1,12 +1,14 @@ import { refreshSession as __refreshSession } from "@/session/stateful/refresh-session.ts" -import type { User } from "@/@types/index.ts" -import type { InternalStatefulContext } from "@/@types/internal.ts" +import type { User } from "@/@types/session.ts" +import type { InternalStatefulContext, SessionStrategy } from "@/@types/internal.ts" -export const refreshUserInfo = (ctx: InternalStatefulContext) => { - const refreshSession = __refreshSession(ctx) +export const refreshUserInfo = ( + ctx: InternalStatefulContext +): SessionStrategy["refreshUserInfo"] => { + const refreshSession = __refreshSession(ctx) - return async (userInfo: Partial, headers: Headers) => { - const value = await refreshSession(headers, { user: userInfo }) + return async ({ headers, user }) => { + const value = await refreshSession({ headers, session: { user } }) return value as any } } diff --git a/packages/core/src/session/stateful/reset-password.ts b/packages/core/src/session/stateful/reset-password.ts index eb2597dd..34da56cb 100644 --- a/packages/core/src/session/stateful/reset-password.ts +++ b/packages/core/src/session/stateful/reset-password.ts @@ -1,7 +1,6 @@ import { AuraAuthError } from "@/errors/aura-error.ts" import { createHash, hashPassword } from "@/shared/crypto.ts" -import type { SessionStrategy } from "@/@types/session.ts" -import type { InternalStatefulContext } from "@/@types/internal.ts" +import type { SessionStrategy, InternalStatefulContext } from "@/@types/internal.ts" export const resetPassword = ({ ctx }: InternalStatefulContext): SessionStrategy["resetPassword"] => { const { sessionConfig } = ctx diff --git a/packages/core/src/session/stateful/revoke-session.ts b/packages/core/src/session/stateful/revoke-session.ts index 3e4099fd..bca67a47 100644 --- a/packages/core/src/session/stateful/revoke-session.ts +++ b/packages/core/src/session/stateful/revoke-session.ts @@ -1,10 +1,13 @@ import { AuraAuthError } from "@/errors/aura-error.ts" -import type { InternalStatefulContext } from "@/@types/internal.ts" +import type { InternalStatefulContext, SessionStrategy } from "@/@types/internal.ts" -export const revokeSession = ({ ctx }: InternalStatefulContext) => { +/** + * Is the function beeng used ? + */ +export const revokeSession = ({ ctx }: InternalStatefulContext): SessionStrategy["revokeSession"] => { const { logger, sessionConfig } = ctx - return async (sessionId: string): Promise => { + return async ({ sessionId }) => { logger?.log("STATEFUL_REVOKE_SESSION_START", { structuredData: { strategy: "stateful", diff --git a/packages/core/src/session/stateful/revoke-token.ts b/packages/core/src/session/stateful/revoke-token.ts index e6550f0b..9eb599b3 100644 --- a/packages/core/src/session/stateful/revoke-token.ts +++ b/packages/core/src/session/stateful/revoke-token.ts @@ -1,15 +1,14 @@ import { HeadersBuilder } from "@aura-stack/router" -import { createHash } from "@/shared/crypto.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 type { InternalStatefulContext } from "@/@types/internal.ts" +import type { InternalStatefulContext, SessionStrategy } from "@/@types/internal.ts" -export const revokeToken = ({ ctx, cookieManager }: InternalStatefulContext) => { - const { oauth, logger, sessionConfig, cookies } = ctx +export const revokeToken = ({ ctx }: InternalStatefulContext): SessionStrategy["revokeToken"] => { + const { logger, sessionConfig, cookies } = ctx - return async (oauthId: string, headers: Headers, disconnect: boolean): Promise => { + return async ({ oauthId, provider, headers, disconnect, sessionByToken }) => { logger?.log("OAUTH_ACCESS_TOKEN_REQUEST_INITIATED", { structuredData: { provider: oauthId, @@ -19,27 +18,6 @@ export const revokeToken = ({ ctx, cookieManager }: InternalStatefulContext) => }) try { - const { sessionToken } = cookieManager.getCookie(headers) - if (!sessionToken) { - logger?.log("SESSION_TOKEN_MISSING", { - structuredData: { - reason: "no_session_token", - }, - }) - throw new AuraAuthError({ code: "SESSION_NOT_FOUND" }) - } - - const tokenHash = await createHash(sessionToken) - const sessionByToken = await sessionConfig.adapter.getSessionByToken(tokenHash) - if (!sessionByToken || !sessionByToken.user) { - logger?.log("AUTH_SESSION_INVALID", { - structuredData: { - reason: "session_not_found_or_no_user", - }, - }) - throw new AuraAuthError({ code: "SESSION_NOT_FOUND" }) - } - const isExpired = Date.now() > sessionByToken.expiresAt.getTime() if (sessionByToken.status !== "active" || isExpired) { if (isExpired) { @@ -83,17 +61,6 @@ export const revokeToken = ({ ctx, cookieManager }: InternalStatefulContext) => throw new AuraAuthError({ code: "OAUTH_UNLINKED_ACCOUNT_ERROR" }) } - const provider = oauth[oauthId] - if (!provider) { - logger?.log("INVALID_OAUTH_CONFIGURATION", { - structuredData: { - provider: oauthId, - reason: "provider_not_configured", - }, - }) - throw new AuraAuthError({ code: "UNSUPPORTED_OAUTH_CONFIGURATION" }) - } - if (!disconnect) { logger?.log("OAUTH_ACCESS_TOKEN_REQUEST_INITIATED", { structuredData: { provider: oauthId, hasAccessToken: !!oauthAccount.accessToken }, diff --git a/packages/core/src/session/stateful/send-magic-link.ts b/packages/core/src/session/stateful/send-magic-link.ts index 70852575..5bf62b99 100644 --- a/packages/core/src/session/stateful/send-magic-link.ts +++ b/packages/core/src/session/stateful/send-magic-link.ts @@ -1,7 +1,6 @@ import { AuraAuthError } from "@/errors/aura-error.ts" import { createHash, createSecretValue } from "@/shared/crypto.ts" -import type { SessionStrategy } from "@/@types/session.ts" -import type { InternalStatefulContext } from "@/@types/internal.ts" +import type { SessionStrategy, InternalStatefulContext } from "@/@types/internal.ts" export const sendMagicLink = ({ ctx }: InternalStatefulContext): SessionStrategy["sendMagicLink"] => { const { sessionConfig } = ctx diff --git a/packages/core/src/session/stateful/send-verification-email.ts b/packages/core/src/session/stateful/send-verification-email.ts index 36884bcb..e8e33b57 100644 --- a/packages/core/src/session/stateful/send-verification-email.ts +++ b/packages/core/src/session/stateful/send-verification-email.ts @@ -1,7 +1,6 @@ import { AuraAuthError } from "@/errors/aura-error.ts" import { createHash, createSecretValue } from "@/shared/crypto.ts" -import type { SessionStrategy } from "@/@types/session.ts" -import type { InternalStatefulContext } from "@/@types/internal.ts" +import type { SessionStrategy, InternalStatefulContext } from "@/@types/internal.ts" export const sendVerificationEmail = ({ ctx }: InternalStatefulContext): SessionStrategy["sendVerificationEmail"] => { const { sessionConfig, emailVerification, basePath } = ctx diff --git a/packages/core/src/session/stateful/sign-in-credentials.ts b/packages/core/src/session/stateful/sign-in-credentials.ts index f93b13a0..e38e8508 100644 --- a/packages/core/src/session/stateful/sign-in-credentials.ts +++ b/packages/core/src/session/stateful/sign-in-credentials.ts @@ -1,13 +1,17 @@ import { AuraAuthError } from "@/errors/aura-error.ts" import { createHash, createSecretValue } from "@/shared/crypto.ts" import { createDevice as __createDevice } from "@/shared/utils/session-strategy.ts" -import type { InternalStatefulContext } from "@/@types/internal.ts" +import type { InternalStatefulContext, SessionStrategy } from "@/@types/internal.ts" -export const signInCredentials = ({ ctx, cookies, cookieManager }: InternalStatefulContext) => { +export const signInCredentials = ({ + ctx, + cookies, + cookieManager, +}: InternalStatefulContext): SessionStrategy["signInCredentials"] => { const { logger, sessionConfig } = ctx const createDevice = __createDevice({ ctx, cookies, cookieManager }) - return async (payload: Record, request: Request): Promise => { + return async ({ payload, request }) => { logger?.log("STATEFUL_SIGN_IN_CREDENTIALS_START", { structuredData: { strategy: "stateful", diff --git a/packages/core/src/session/stateful/sign-in.ts b/packages/core/src/session/stateful/sign-in.ts index 0e114267..5f863641 100644 --- a/packages/core/src/session/stateful/sign-in.ts +++ b/packages/core/src/session/stateful/sign-in.ts @@ -1,21 +1,15 @@ import { HeadersBuilder } from "@aura-stack/router" -import { AuraAuthError } from "@/errors/aura-error.ts" import { secureApiHeaders } from "@/shared/http/headers.ts" import { createOIDCAuthorizationURL } from "@/shared/oidc/authorization-url.ts" import { createFingerprint, getDeviceInfo } from "@/shared/http/request-metadata.ts" import { isOIDCProvider, resolveOpenIDProvider } from "@/shared/oidc/resolve-provider.ts" import { createAuthorizationURL, createRedirectTo, createRedirectURI } from "@/shared/oauth2/authorization.ts" -import type { InternalStatefulContext } from "@/@types/internal.ts" +import type { InternalStatefulContext, SessionStrategy } from "@/@types/internal.ts" -export const signIn = ({ ctx }: InternalStatefulContext) => { - const { logger, oauth, sessionConfig } = ctx - - return async (oauthId: string, request: Request, redirectTo?: string) => { - const provider = oauth[oauthId] - if (!provider) { - throw new AuraAuthError({ code: "UNSUPPORTED_OAUTH_CONFIGURATION" }) - } +export const signIn = ({ ctx }: InternalStatefulContext): SessionStrategy["signIn"] => { + const { logger, sessionConfig } = ctx + return async ({ oauthId, provider, request, redirectTo }) => { const redirectURI = await createRedirectURI(request, oauthId, ctx) const redirectToValue = await createRedirectTo(request, redirectTo, ctx) diff --git a/packages/core/src/session/stateful/sign-up.ts b/packages/core/src/session/stateful/sign-up.ts index 2f0f079c..1a59a9bd 100644 --- a/packages/core/src/session/stateful/sign-up.ts +++ b/packages/core/src/session/stateful/sign-up.ts @@ -1,16 +1,16 @@ import { AuraAuthError } from "@/errors/aura-error.ts" import { createHash, createSecretValue, hashPassword } from "@/shared/crypto.ts" import { createDevice as __createDevice } from "@/shared/utils/session-strategy.ts" -import type { InternalStatefulContext } from "@/@types/internal.ts" +import type { InternalStatefulContext, SessionStrategy } from "@/@types/internal.ts" /** * @todo Add transaction support for the signUp process to ensure atomicity and rollback in case of errors. */ -export const signUp = ({ ctx, cookies, cookieManager }: InternalStatefulContext) => { +export const signUp = ({ ctx, cookies, cookieManager }: InternalStatefulContext): SessionStrategy["signUp"] => { const { logger, sessionConfig } = ctx const createDevice = __createDevice({ ctx, cookies, cookieManager }) - return async (payload: Record, request: Request): Promise => { + return async ({ payload, request }): Promise => { logger?.log("STATEFUL_CREATE_SESSION_START", { structuredData: { strategy: "stateful", diff --git a/packages/core/src/session/stateful/verify-email-change.ts b/packages/core/src/session/stateful/verify-email-change.ts index cabfa8f2..8816f3c4 100644 --- a/packages/core/src/session/stateful/verify-email-change.ts +++ b/packages/core/src/session/stateful/verify-email-change.ts @@ -1,7 +1,6 @@ import { AuraAuthError } from "@/errors/aura-error.ts" import { createHash, timingSafeEqual } from "@/shared/crypto.ts" -import type { InternalStatefulContext } from "@/@types/internal.ts" -import type { SessionStrategy } from "@/@types/session.ts" +import type { SessionStrategy, InternalStatefulContext } from "@/@types/internal.ts" export const verifyEmailChange = ({ ctx }: InternalStatefulContext): SessionStrategy["verifyEmailChange"] => { const { sessionConfig } = ctx diff --git a/packages/core/src/session/stateful/verify-email.ts b/packages/core/src/session/stateful/verify-email.ts index 45ca7fa3..03d743ae 100644 --- a/packages/core/src/session/stateful/verify-email.ts +++ b/packages/core/src/session/stateful/verify-email.ts @@ -1,7 +1,6 @@ import { createHash } from "@/shared/crypto.ts" import { AuraAuthError } from "@/errors/aura-error.ts" -import type { SessionStrategy } from "@/@types/session.ts" -import type { InternalStatefulContext } from "@/@types/internal.ts" +import type { SessionStrategy, InternalStatefulContext } from "@/@types/internal.ts" export const verifyEmail = ({ ctx }: InternalStatefulContext): SessionStrategy["verifyEmail"] => { const { sessionConfig } = ctx diff --git a/packages/core/src/session/stateful/verify-magic-link.ts b/packages/core/src/session/stateful/verify-magic-link.ts index c2d3f829..246e68b4 100644 --- a/packages/core/src/session/stateful/verify-magic-link.ts +++ b/packages/core/src/session/stateful/verify-magic-link.ts @@ -1,8 +1,7 @@ import { AuraAuthError } from "@/errors/aura-error.ts" import { createHash, createSecretValue } from "@/shared/crypto.ts" import { createDevice as createDeviceRegistry } from "@/shared/utils/session-strategy.ts" -import type { SessionStrategy } from "@/@types/session.ts" -import type { InternalStatefulContext } from "@/@types/internal.ts" +import type { SessionStrategy, InternalStatefulContext } from "@/@types/internal.ts" export const verifyMagicLink = ({ ctx, cookies, cookieManager }: InternalStatefulContext): SessionStrategy["verifyMagicLink"] => { const { sessionConfig } = ctx diff --git a/packages/core/src/session/stateless/create-session.ts b/packages/core/src/session/stateless/create-session.ts index 4fdf9dcf..582e41dd 100644 --- a/packages/core/src/session/stateless/create-session.ts +++ b/packages/core/src/session/stateless/create-session.ts @@ -1,10 +1,11 @@ -import type { TypedJWTPayload } from "@aura-stack/jose" import type { User } from "@/@types/index.ts" -import type { InternalStatelessContext } from "@/@types/internal.ts" +import type { InternalStatelessContext, SessionStrategy } from "@/@types/internal.ts" -export const createSession = ({ ctx }: InternalStatelessContext) => { +export const createSession = ({ + ctx, +}: InternalStatelessContext): SessionStrategy["createSession"] => { const { logger, identity, jwtManager } = ctx - return async (session: TypedJWTPayload) => { + return async ({ session }) => { if (identity.skipValidation) { logger?.log("IDENTITY_VALIDATION_DISABLED", { structuredData: { diff --git a/packages/core/src/session/stateless/destroy-session.ts b/packages/core/src/session/stateless/destroy-session.ts index a5001ddb..cd7b6771 100644 --- a/packages/core/src/session/stateless/destroy-session.ts +++ b/packages/core/src/session/stateless/destroy-session.ts @@ -1,7 +1,7 @@ -import type { InternalStatelessContext } from "@/@types/internal.ts" +import type { InternalStatelessContext, SessionStrategy } from "@/@types/internal.ts" -export const destroySession = ({ cookieManager }: InternalStatelessContext) => { - return async (_headers: Headers) => { +export const destroySession = ({ cookieManager }: InternalStatelessContext): SessionStrategy["destroySession"] => { + return async () => { return cookieManager.clearAll() } } diff --git a/packages/core/src/session/stateless/get-provider-tokens.ts b/packages/core/src/session/stateless/get-provider-tokens.ts index 7f4c336c..0318e8fa 100644 --- a/packages/core/src/session/stateless/get-provider-tokens.ts +++ b/packages/core/src/session/stateless/get-provider-tokens.ts @@ -6,13 +6,12 @@ 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 { InternalStatelessContext } from "@/@types/internal.ts" -import type { GetProviderTokensStatefulReturn } from "@/@types/index.ts" +import type { InternalStatelessContext, SessionStrategy } from "@/@types/internal.ts" -export const getProviderTokens = ({ ctx, cookies }: InternalStatelessContext) => { - const { oauth, logger, identity, jwtManager: jwt } = ctx +export const getProviderTokens = ({ ctx, cookies }: InternalStatelessContext): SessionStrategy["getProviderTokens"] => { + const { logger, identity, jwtManager: jwt } = ctx - return async (oauthId: string, request: Request): Promise => { + return async ({ oauthId, provider, request }) => { logger?.log("STATELESS_GET_PROVIDER_TOKENS_START", { structuredData: { strategy: "stateless", @@ -22,22 +21,6 @@ export const getProviderTokens = ({ ctx, cookies }: InternalStatelessContext) => }) try { - const provider = oauth[oauthId] - if (!provider) { - logger?.log("STATELESS_GET_PROVIDER_TOKENS_PROVIDER_NOT_FOUND", { - structuredData: { - oauth_id: oauthId, - reason: "provider_not_configured", - }, - }) - const { code, message, statusCode } = handleApiError( - new AuraAuthError({ code: "UNSUPPORTED_OAUTH_CONFIGURATION" }), - "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_PROVIDER_FOUND", { structuredData: { oauth_id: oauthId, diff --git a/packages/core/src/session/stateless/get-session.ts b/packages/core/src/session/stateless/get-session.ts index 54e1d196..a398a68b 100644 --- a/packages/core/src/session/stateless/get-session.ts +++ b/packages/core/src/session/stateless/get-session.ts @@ -3,10 +3,13 @@ import { AuraAuthError } from "@/errors/aura-error.ts" import { secureApiHeaders } from "@/shared/http/headers.ts" import { isInvalidSlidingThreshold } from "@/shared/assert.ts" import { calcStatelessExpiration } from "@/shared/utils/session-strategy.ts" -import type { GetStatelessSessionReturn, Session, User } from "@/@types/index.ts" -import type { InternalStatelessContext } from "@/@types/internal.ts" +import type { Session, User } from "@/@types/index.ts" +import type { InternalStatelessContext, SessionStrategy } from "@/@types/internal.ts" -export const getSession = ({ ctx, cookieManager }: InternalStatelessContext) => { +export const getSession = ({ + ctx, + cookieManager, +}: InternalStatelessContext): SessionStrategy["getSession"] => { const { logger, identity, jwtManager, sessionConfig } = ctx const maxAge = sessionConfig?.maxAge ?? sessionConfig?.jwt?.maxAge ?? 60 * 60 * 24 * 15 @@ -16,7 +19,7 @@ export const getSession = ({ ctx, cookieManager }: Int throw new AuraAuthError({ code: "INVALID_SLIDING_THRESHOLD_CONFIG_VALUE" }) } - return async (headers: Headers): Promise> => { + return async ({ headers }) => { const newHeaders = new Headers(secureApiHeaders) try { const { sessionToken } = cookieManager.getCookie(headers) diff --git a/packages/core/src/session/stateless/index.ts b/packages/core/src/session/stateless/index.ts index 65d64dfa..c5c5a142 100644 --- a/packages/core/src/session/stateless/index.ts +++ b/packages/core/src/session/stateless/index.ts @@ -23,8 +23,8 @@ import { sendMagicLink, verifyMagicLink, } from "@/session/stateless/unsupported.ts" -import type { SessionStrategy, User } from "@/@types/index.ts" -import type { InternalStatelessContext } from "@/@types/internal.ts" +import type { User } from "@/@types/index.ts" +import type { SessionStrategy, InternalStatelessContext } from "@/@types/internal.ts" export const createStatelessStrategy = ( ctx: InternalStatelessContext diff --git a/packages/core/src/session/stateless/is-provider-connected.ts b/packages/core/src/session/stateless/is-provider-connected.ts index 4dc62cfe..1c97064a 100644 --- a/packages/core/src/session/stateless/is-provider-connected.ts +++ b/packages/core/src/session/stateless/is-provider-connected.ts @@ -1,11 +1,11 @@ import { getCookie } from "@/shared/http/cookie.ts" import { getErrorName } from "@/errors/aura-error.ts" -import type { InternalStatelessContext } from "@/@types/internal.ts" +import type { InternalStatelessContext, SessionStrategy } from "@/@types/internal.ts" -export const isProviderConnected = ({ ctx, cookies }: InternalStatelessContext) => { +export const isProviderConnected = ({ ctx, cookies }: InternalStatelessContext): SessionStrategy["isProviderConnected"] => { const { logger, jwtManager: jwt } = ctx - return async (oauthId: string, headers: Headers): Promise => { + return async ({ oauthId, headers }) => { const cookieName = `${cookies().accessToken.name}.${oauthId}` let cookieValue: string try { diff --git a/packages/core/src/session/stateless/oauth-callback.ts b/packages/core/src/session/stateless/oauth-callback.ts index b90e2cb9..582ff5c0 100644 --- a/packages/core/src/session/stateless/oauth-callback.ts +++ b/packages/core/src/session/stateless/oauth-callback.ts @@ -9,10 +9,10 @@ import { createAccessToken, getUserInfo } from "@/shared/oauth2/index.ts" import { getCookie, getExpiredCookie, getOptionalCookie } from "@/shared/http/cookie.ts" import { isOIDCProvider, resolveOpenIDProvider } from "@/shared/oidc/resolve-provider.ts" import { getOriginURL, isRelativeURL, isSameOrigin, isTrustedOrigin } from "@/shared/utils/get-origin-url.ts" -import type { InternalStatelessContext } from "@/@types/internal.ts" +import type { InternalStatelessContext, SessionStrategy } from "@/@types/internal.ts" -export const oauthCallback = ({ ctx }: InternalStatelessContext) => { - return async (oauthId: string, request: Request, { code, state }: { code: string; state: string }) => { +export const oauthCallback = ({ ctx }: InternalStatelessContext): SessionStrategy["oauthCallback"] => { + return async ({ oauthId, request, params: { code, state } }) => { const { oauth: providers, cookies, jose, logger, trustedOrigins } = ctx const oauthConfig = providers[oauthId] @@ -95,7 +95,7 @@ export const oauthCallback = ({ ctx }: InternalStatelessContext) => { } const userInfo = await getUserInfo(resolvedConfig, accessToken, logger) - const session = await ctx.sessionStrategy.createSession(userInfo, request) + const session = await ctx.sessionStrategy.createSession({ session: userInfo, request }) const csrfToken = await createCSRF(jose) const tokenPayload = transformToTokenPayload(accessToken) const providerToken = await ctx.jwtManager.createToken(tokenPayload) diff --git a/packages/core/src/session/stateless/refresh-session.ts b/packages/core/src/session/stateless/refresh-session.ts index 72981181..47bb1339 100644 --- a/packages/core/src/session/stateless/refresh-session.ts +++ b/packages/core/src/session/stateless/refresh-session.ts @@ -1,22 +1,18 @@ import { getErrorName } from "@/errors/aura-error.ts" import { updateExpires } from "@/shared/utils/session-strategy.ts" -import type { DeepPartial } from "@/@types/utility.ts" import type { Session, User } from "@/@types/index.ts" -import type { InternalStatelessContext } from "@/@types/internal.ts" +import type { InternalStatelessContext, SessionStrategy } from "@/@types/internal.ts" -export const refreshSession = ({ ctx, cookieManager }: InternalStatelessContext) => { +export const refreshSession = ({ + ctx, + cookieManager, +}: InternalStatelessContext): SessionStrategy["refreshSession"] => { const { logger, jwtManager: jwt, identity, sessionConfig } = ctx const maxAge = sessionConfig?.maxAge ?? sessionConfig?.jwt?.maxAge ?? 60 * 60 * 24 * 15 const strategy = sessionConfig?.expirationStrategy ?? sessionConfig?.jwt?.expirationStrategy ?? "absolute" - return async ( - headers: Headers, - session: DeepPartial> - ): Promise<{ - session: Session | null - headers: Headers - }> => { + return async ({ session, headers }) => { try { const { sessionToken } = cookieManager.getCookie(headers) if (!sessionToken) { diff --git a/packages/core/src/session/stateless/refresh-userInfo.ts b/packages/core/src/session/stateless/refresh-userInfo.ts index 29247ef1..b46fa3e5 100644 --- a/packages/core/src/session/stateless/refresh-userInfo.ts +++ b/packages/core/src/session/stateless/refresh-userInfo.ts @@ -1,11 +1,15 @@ import { refreshSession as __refreshSession } from "@/session/stateless/refresh-session.ts" -import type { DeepPartial, User } from "@/@types/index.ts" -import type { InternalStatelessContext } from "@/@types/internal.ts" +import type { User } from "@/@types/index.ts" +import type { InternalStatelessContext, SessionStrategy } from "@/@types/internal.ts" -export const refreshUserInfo = ({ ctx, cookies, cookieManager }: InternalStatelessContext) => { +export const refreshUserInfo = ({ + ctx, + cookies, + cookieManager, +}: InternalStatelessContext): SessionStrategy["refreshUserInfo"] => { const refreshSession = __refreshSession({ ctx, cookies, cookieManager }) - return async (userInfo: DeepPartial, headers: Headers) => { - const value = await refreshSession(headers, { user: userInfo as DeepPartial }) + return async ({ headers, user }) => { + const value = await refreshSession({ headers, session: { user } }) return value } } diff --git a/packages/core/src/session/stateless/revoke-token.ts b/packages/core/src/session/stateless/revoke-token.ts index f75c02ba..ea2cc3e6 100644 --- a/packages/core/src/session/stateless/revoke-token.ts +++ b/packages/core/src/session/stateless/revoke-token.ts @@ -1,22 +1,16 @@ import { HeadersBuilder } from "@aura-stack/router" -import { AuraAuthError } from "@/errors/aura-error.ts" import { toUnionHeaders } from "@/shared/http/headers.ts" import { secureApiHeaders } from "@/shared/http/headers.ts" import { getCookie, getExpiredCookie } from "@/shared/http/cookie.ts" import { revokeProviderToken } from "@/shared/utils/revoke-token.ts" -import type { InternalStatelessContext } from "@/@types/internal.ts" +import type { InternalStatelessContext, SessionStrategy } from "@/@types/internal.ts" -export const revokeToken = ({ ctx, cookies }: InternalStatelessContext) => { - const { logger, identity, jwtManager: jwt, oauth } = ctx +export const revokeToken = ({ ctx, cookies }: InternalStatelessContext): SessionStrategy["revokeToken"] => { + const { logger, identity, jwtManager: jwt } = ctx - return async (oauthId: string, headers: Headers, disconnect: boolean): Promise => { + return async ({ oauthId, provider, disconnect, headers }) => { const cookieName = `${cookies().accessToken.name}.${oauthId}` const cookie = getCookie(headers, cookieName) - const provider = oauth[oauthId] - - if (!provider) { - throw new AuraAuthError({ code: "UNSUPPORTED_OAUTH_CONFIGURATION" }) - } if (!disconnect) { const decodedToken = await jwt.verifyToken(cookie) diff --git a/packages/core/src/session/stateless/sign-in-credentials.ts b/packages/core/src/session/stateless/sign-in-credentials.ts index 086f5caf..3405b793 100644 --- a/packages/core/src/session/stateless/sign-in-credentials.ts +++ b/packages/core/src/session/stateless/sign-in-credentials.ts @@ -1,11 +1,13 @@ import { createSession as __createSession } from "@/session/stateless/create-session.ts" -import type { TypedJWTPayload, User } from "@/@types/index.ts" -import type { InternalStatelessContext } from "@/@types/internal.ts" +import type { User } from "@/@types/index.ts" +import type { InternalStatelessContext, SessionStrategy } from "@/@types/internal.ts" -export const signInCredentials = (ctx: InternalStatelessContext) => { +export const signInCredentials = ( + ctx: InternalStatelessContext +): SessionStrategy["signInCredentials"] => { const createSession = __createSession(ctx) - return async (payload: Record, _request: Request): Promise => { - return await createSession(payload as TypedJWTPayload) + return async ({ payload, request }): Promise => { + return await createSession({ request, session: payload as any }) } } diff --git a/packages/core/src/session/stateless/sign-in.ts b/packages/core/src/session/stateless/sign-in.ts index c60f0e52..5b7257d7 100644 --- a/packages/core/src/session/stateless/sign-in.ts +++ b/packages/core/src/session/stateless/sign-in.ts @@ -1,20 +1,14 @@ import { HeadersBuilder } from "@aura-stack/router" -import { AuraAuthError } from "@/errors/aura-error.ts" import { cacheControl } from "@/shared/http/headers.ts" import { createOIDCAuthorizationURL } from "@/shared/oidc/authorization-url.ts" import { isOIDCProvider, resolveOpenIDProvider } from "@/shared/oidc/resolve-provider.ts" import { createAuthorizationURL, createRedirectTo, createRedirectURI } from "@/shared/oauth2/authorization.ts" -import type { InternalStatelessContext } from "@/@types/internal.ts" +import type { InternalStatelessContext, SessionStrategy } from "@/@types/internal.ts" -export const signIn = ({ ctx, cookies }: InternalStatelessContext) => { - const { oauth, logger } = ctx - - return async (oauthId: string, request: Request, redirectTo?: string) => { - const provider = oauth[oauthId] - if (!provider) { - throw new AuraAuthError({ code: "UNSUPPORTED_OAUTH_CONFIGURATION" }) - } +export const signIn = ({ ctx, cookies }: InternalStatelessContext): SessionStrategy["signIn"] => { + const { logger } = ctx + return async ({ oauthId, provider, request, redirectTo }) => { const redirectURI = await createRedirectURI(request, oauthId, ctx) const redirectToValue = await createRedirectTo(request, redirectTo, ctx) diff --git a/packages/core/src/session/stateless/sign-up.ts b/packages/core/src/session/stateless/sign-up.ts index 68a5f2e8..3276ff2a 100644 --- a/packages/core/src/session/stateless/sign-up.ts +++ b/packages/core/src/session/stateless/sign-up.ts @@ -1,11 +1,13 @@ import { createSession as __createSession } from "@/session/stateless/create-session.ts" -import type { TypedJWTPayload, User } from "@/@types/index.ts" -import type { InternalStatelessContext } from "@/@types/internal.ts" +import type { User } from "@/@types/index.ts" +import type { InternalStatelessContext, SessionStrategy } from "@/@types/internal.ts" -export const signUp = (ctx: InternalStatelessContext) => { +export const signUp = ( + ctx: InternalStatelessContext +): SessionStrategy["signUp"] => { const createSession = __createSession(ctx) - return async (payload: Record, _request: Request): Promise => { - return await createSession(payload as TypedJWTPayload) + return async ({ payload, request }) => { + return await createSession({ request, session: payload as any }) } } diff --git a/packages/core/src/session/stateless/unsupported.ts b/packages/core/src/session/stateless/unsupported.ts index 61b9cf5a..9269aa61 100644 --- a/packages/core/src/session/stateless/unsupported.ts +++ b/packages/core/src/session/stateless/unsupported.ts @@ -1,9 +1,8 @@ import { AuraAuthError } from "@/errors/aura-error.ts" -import type { SessionStrategy, User } from "@/@types/session.ts" -import type { InternalStatelessContext } from "@/@types/internal.ts" +import type { SessionStrategy, InternalStatelessContext } from "@/@types/internal.ts" -export const revokeSession = (_: InternalStatelessContext) => { - return async (_sessionId: string): Promise => { +export const revokeSession = (_: InternalStatelessContext): SessionStrategy["revokeSession"] => { + return async () => { throw new AuraAuthError({ code: "UNSUPPORTED_FUNCTION_IN_STATELESS_MODE", cause: "revokeSession is not supported in stateless mode.", @@ -74,8 +73,8 @@ export const verifyEmailChange = (_: InternalStatelessContext): SessionStrategy[ } } -export const cancelEmailChange = (_: InternalStatelessContext) => { - return async (_options: {}): Promise => { +export const cancelEmailChange = (_: InternalStatelessContext): SessionStrategy["cancelEmailChange"] => { + return async () => { throw new AuraAuthError({ code: "UNSUPPORTED_FUNCTION_IN_STATELESS_MODE", cause: "cancelEmailChange is not supported in stateless mode", @@ -83,8 +82,8 @@ export const cancelEmailChange = (_: InternalStatelessContext) => { } } -export const sendMagicLink = (_: InternalStatelessContext) => { - return async (_options: {}): ReturnType["sendMagicLink"]> => { +export const sendMagicLink = (_: InternalStatelessContext): SessionStrategy["sendMagicLink"] => { + return async () => { throw new AuraAuthError({ code: "UNSUPPORTED_FUNCTION_IN_STATELESS_MODE", cause: "sendMagicLink is not supported in stateless mode.", @@ -92,8 +91,8 @@ export const sendMagicLink = (_: InternalStatelessCont } } -export const verifyMagicLink = (_: InternalStatelessContext) => { - return async (_options: {}): ReturnType["verifyMagicLink"]> => { +export const verifyMagicLink = (_: InternalStatelessContext): SessionStrategy["verifyMagicLink"] => { + return async () => { throw new AuraAuthError({ code: "UNSUPPORTED_FUNCTION_IN_STATELESS_MODE", cause: "verifyMagicLink is not supported in stateless mode.", diff --git a/packages/core/src/session/strategy.ts b/packages/core/src/session/strategy.ts index 5f251da3..7f9f2994 100644 --- a/packages/core/src/session/strategy.ts +++ b/packages/core/src/session/strategy.ts @@ -3,8 +3,13 @@ import { isStatelessStrategy } from "@/shared/assert.ts" import { createCookieManager } from "@/session/cookie-manager.ts" import { createStatefulStrategy } from "@/session/stateful/index.ts" import { createStatelessStrategy } from "@/session/stateless/index.ts" -import type { SessionStrategy, User, FromShapeToObject, Identities } from "@/@types/index.ts" -import type { CreateSessionStrategyOptions, InternalStatefulContext, InternalStatelessContext } from "@/@types/internal.ts" +import type { User, FromShapeToObject, Identities } from "@/@types/index.ts" +import type { + SessionStrategy, + CreateSessionStrategyOptions, + InternalStatefulContext, + InternalStatelessContext, +} from "@/@types/internal.ts" export const createSessionStrategy = ( config: CreateSessionStrategyOptions diff --git a/packages/core/src/shared/utils/api.ts b/packages/core/src/shared/utils/api.ts index 1807134f..b0fe9e61 100644 --- a/packages/core/src/shared/utils/api.ts +++ b/packages/core/src/shared/utils/api.ts @@ -11,7 +11,7 @@ import { getCookie, getOptionalCookie } from "@/shared/http/cookie.ts" import { AuraAuthError, isAuraAuthError } from "@/errors/aura-error.ts" import { createCSRF, createHash, verifyCSRF } from "@/shared/crypto.ts" import type { LOG_MESSAGES } from "@/config/logger/catalog.ts" -import type { BuiltInOAuthProvider, LiteralUnion, RateLimiterConfig } from "@/@types/index.ts" +import type { BuiltInOAuthProvider, LiteralUnion, RateLimiterConfig, SessionWithUserEntity } from "@/@types/index.ts" import type { InternalLogger, RouterGlobalContext, RuntimeOAuthProvider } from "@/@types/internal.ts" export const createValidation = (ctx: RouterGlobalContext, headersInit?: HeadersInit) => { @@ -26,6 +26,7 @@ export const createValidation = (ctx: RouterGlobalContext, headersInit?: Headers userId?: string sessionId?: string origin?: string + session?: SessionWithUserEntity } = { headers } const builder = { @@ -63,6 +64,8 @@ export const createValidation = (ctx: RouterGlobalContext, headersInit?: Headers } output.userId = session.userId output.sessionId = session.id + // @unstable + output.session = session } }) return builder @@ -119,6 +122,7 @@ export const createValidation = (ctx: RouterGlobalContext, headersInit?: Headers userId?: string sessionId?: string origin: string + session: SessionWithUserEntity | undefined } }, } diff --git a/packages/core/test/actions/providers/connected/stateful.test.ts b/packages/core/test/actions/providers/connected/stateful.test.ts index ea0b6f92..2f8fbed6 100644 --- a/packages/core/test/actions/providers/connected/stateful.test.ts +++ b/packages/core/test/actions/providers/connected/stateful.test.ts @@ -61,11 +61,8 @@ describe("connectedAction", () => { }) test("returns connected: false when provider token cookie does not exist", async () => { - const getSessionByTokenMock = vi.fn() - const getAccountsByUserIdMock = vi.fn() - - getSessionByTokenMock.mockResolvedValueOnce(sessionEntityWithUser) - getSessionByTokenMock.mockResolvedValueOnce(null) + const getSessionByTokenMock = vi.fn().mockResolvedValueOnce(sessionEntityWithUser) + const getAccountsByUserIdMock = vi.fn().mockResolvedValue([]) const { handlers: { GET }, @@ -91,10 +88,9 @@ describe("connectedAction", () => { connected: false, }) - expect(getSessionByTokenMock).toHaveBeenCalledTimes(2) + expect(getSessionByTokenMock).toHaveBeenCalledTimes(1) expect(getSessionByTokenMock).toHaveBeenNthCalledWith(1, tokenHash) - expect(getSessionByTokenMock).toHaveBeenNthCalledWith(2, tokenHash) - expect(getAccountsByUserIdMock).not.toHaveBeenCalled() + expect(getAccountsByUserIdMock).toHaveBeenCalledWith("user-123") }) test("returns connected: true when provider token cookie exists and is valid", async () => { diff --git a/packages/core/test/actions/providers/tokens/revoke/stateful.test.ts b/packages/core/test/actions/providers/tokens/revoke/stateful.test.ts index c1d1c357..ce125a5b 100644 --- a/packages/core/test/actions/providers/tokens/revoke/stateful.test.ts +++ b/packages/core/test/actions/providers/tokens/revoke/stateful.test.ts @@ -153,7 +153,7 @@ describe("Revoke Action", () => { }, }) ) - expect(response.status).toBe(401) + expect(response.status).toBe(400) expect(await response.json()).toEqual({ success: false }) const tokenHash = await createHash("valid-token-hash") @@ -192,7 +192,7 @@ describe("Revoke Action", () => { }, }) ) - expect(response.status).toBe(401) + expect(response.status).toBe(400) expect(await response.json()).toEqual({ success: false }) expect(getSessionByTokenMock).toHaveBeenCalledWith(tokenHash) diff --git a/packages/core/test/actions/providers/user/refresh/stateful.test.ts b/packages/core/test/actions/providers/user/refresh/stateful.test.ts index 1230d4f6..3e7dca3c 100644 --- a/packages/core/test/actions/providers/user/refresh/stateful.test.ts +++ b/packages/core/test/actions/providers/user/refresh/stateful.test.ts @@ -307,7 +307,6 @@ describe("refreshUserInfo action", () => { expect(getAccountsByUserIdMock).toHaveBeenCalledWith("user-123") expect(getOAuthAccountMock).toHaveBeenCalledWith("account-123") expect(updateOAuthTokensMock).not.toHaveBeenCalled() - expect(getSessionByTokenMock).toHaveBeenNthCalledWith(4, tokenHash) expect(revokeSessionMock).not.toHaveBeenCalled() const { attributes, ...spreadUser } = userEntity @@ -712,7 +711,6 @@ describe("refreshUserInfo action", () => { accessTokenExpiresAt: expect.any(Date), refreshTokenExpiresAt: expect.any(Date), }) - expect(getSessionByTokenMock).toHaveBeenNthCalledWith(4, tokenHash) expect(revokeSessionMock).not.toHaveBeenCalled() const { attributes, ...spreadUser } = userEntity @@ -904,7 +902,6 @@ describe("refreshUserInfo action", () => { expect(getAccountsByUserIdMock).toHaveBeenCalledWith("user-123") expect(getOAuthAccountMock).toHaveBeenCalledWith("account-123") expect(updateOAuthTokensMock).not.toHaveBeenCalled() - expect(getSessionByTokenMock).toHaveBeenNthCalledWith(4, tokenHash) expect(revokeSessionMock).not.toHaveBeenCalled() const { attributes, ...spreadUser } = userEntity diff --git a/packages/core/test/api/stateful/isProviderConnected.test.ts b/packages/core/test/api/stateful/isProviderConnected.test.ts index d5805bb3..263de82d 100644 --- a/packages/core/test/api/stateful/isProviderConnected.test.ts +++ b/packages/core/test/api/stateful/isProviderConnected.test.ts @@ -23,7 +23,7 @@ describe("isProviderConnected (Stateful)", () => { test("returns connected: false when session not found in database", async () => { vi.stubEnv("BASE_URL", "https://example.com") - const getSessionByTokenMock = vi.fn().mockResolvedValueOnce(sessionEntityWithUser).mockResolvedValueOnce(null) + const getSessionByTokenMock = vi.fn().mockResolvedValueOnce(null) const getAccountsByUserIdMock = vi.fn() const { api } = authInstance({ @@ -41,8 +41,12 @@ describe("isProviderConnected (Stateful)", () => { }, }) expect(output).toEqual({ - success: true, + success: false, connected: false, + error: { + code: "SESSION_NOT_FOUND", + message: "The session token is not found. There is no active session.", + }, headers: expect.any(Headers), toResponse: expect.any(Function), }) diff --git a/packages/core/test/api/stateful/refreshUserInfo.test.ts b/packages/core/test/api/stateful/refreshUserInfo.test.ts index bac690ee..b04adac6 100644 --- a/packages/core/test/api/stateful/refreshUserInfo.test.ts +++ b/packages/core/test/api/stateful/refreshUserInfo.test.ts @@ -319,7 +319,6 @@ describe("refreshUserInfo API (Stateful)", () => { expect(getAccountsByUserIdMock).toHaveBeenCalledWith("user-123") expect(getOAuthAccountMock).toHaveBeenCalledWith("account-123") expect(updateOAuthTokensMock).not.toHaveBeenCalled() - expect(getSessionByTokenMock).toHaveBeenNthCalledWith(4, tokenHash) expect(revokeSessionMock).not.toHaveBeenCalled() const { attributes, ...spreadUser } = userEntity @@ -681,7 +680,6 @@ describe("refreshUserInfo API (Stateful)", () => { accessTokenExpiresAt: expect.any(Date), refreshTokenExpiresAt: expect.any(Date), }) - expect(getSessionByTokenMock).toHaveBeenNthCalledWith(4, tokenHash) expect(revokeSessionMock).not.toHaveBeenCalled() const { attributes, ...spreadUser } = userEntity @@ -906,7 +904,6 @@ describe("refreshUserInfo API (Stateful)", () => { expect(getAccountsByUserIdMock).toHaveBeenCalledWith("user-123") expect(getOAuthAccountMock).toHaveBeenCalledWith("account-123") expect(updateOAuthTokensMock).not.toHaveBeenCalled() - expect(getSessionByTokenMock).toHaveBeenNthCalledWith(4, tokenHash) expect(revokeSessionMock).not.toHaveBeenCalled() const { attributes, ...spreadUser } = userEntity @@ -1087,7 +1084,6 @@ describe("refreshUserInfo API (Stateful)", () => { expect(getAccountsByUserIdMock).toHaveBeenCalledWith("user-123") expect(getOAuthAccountMock).toHaveBeenCalledWith("account-123") expect(updateOAuthTokensMock).not.toHaveBeenCalled() - expect(getSessionByTokenMock).toHaveBeenNthCalledWith(4, tokenHash) expect(revokeSessionMock).not.toHaveBeenCalled() const { attributes, ...spreadUser } = userEntity @@ -1250,7 +1246,6 @@ describe("refreshUserInfo API (Stateful)", () => { expect(getAccountsByUserIdMock).toHaveBeenCalledWith("user-123") expect(getOAuthAccountMock).toHaveBeenCalledWith("account-123") expect(updateOAuthTokensMock).not.toHaveBeenCalled() - expect(getSessionByTokenMock).toHaveBeenNthCalledWith(4, tokenHash) expect(revokeSessionMock).not.toHaveBeenCalled() const { attributes, ...spreadUser } = userEntity diff --git a/packages/core/test/api/stateful/revokeToken.test.ts b/packages/core/test/api/stateful/revokeToken.test.ts index f6e0069e..a6d87530 100644 --- a/packages/core/test/api/stateful/revokeToken.test.ts +++ b/packages/core/test/api/stateful/revokeToken.test.ts @@ -112,7 +112,7 @@ describe("revokeToken (Stateful)", () => { headers: expect.any(Headers), toResponse: expect.any(Function), }) - expect(getSessionByTokenMock).toHaveBeenCalledTimes(2) + expect(getSessionByTokenMock).toHaveBeenCalledTimes(1) expect(getAccountsByUserIdMock).toHaveBeenCalledWith("user-123") expect(getOAuthAccountMock).not.toHaveBeenCalled() expect(updateAccountStatusMock).not.toHaveBeenCalled()