Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>` instead of `Promise<void>`. 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)
Expand Down
161 changes: 159 additions & 2 deletions packages/core/src/@types/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -29,15 +29,17 @@ import type {
} from "@/@types/config.ts"
import type {
JWTKey,
OAuthTokenPayload,
Session,
SessionConfig,
SessionStrategy,
StatefulStrategyConfig,
StatelessStrategyConfig,
User,
} from "@/@types/session.ts"
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
Expand Down Expand Up @@ -214,3 +216,158 @@ export type OAuthProviderRecord<DefaultUser extends User = User> = Record<
export type JWTPayloadWithToken = JWTPayload & { token: string }

export type CookieManager = ReturnType<typeof createCookieManager>

// #region Session Strategy
/**
* Abstraction layer for session management.
*/
export interface SessionStrategy<DefaultUser extends User = User> {
/**
* 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<DefaultUser> | 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<string>
/**
* Get the OAuth access token for a specific provider, using the refresh token if necessary.
*/
getProviderTokens(options: {
oauthId: string
request: Request
provider: RuntimeOAuthProvider<any, DefaultUser>
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<Session<DefaultUser>> }): Promise<{
session: Session<DefaultUser> | 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<void>
/**
* Destroy the session attached to this request (logout).
* Returns a response that clears cookies.
*/
destroySession(options: { sessionByToken: SessionWithUserEntity }): Promise<Headers>
/**
* 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<any, DefaultUser>
headers: Headers
disconnect: boolean
sessionByToken: SessionWithUserEntity
}): Promise<Headers>
/**
* 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<boolean>
/**
* Refresh the user info in the session.
* @unstable This API is experimental and may change in future releases.
*/
refreshUserInfo(options: { user: DeepPartial<DefaultUser>; headers: Headers; skipCSRFCheck?: boolean }): Promise<{
session: Session<DefaultUser> | 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<string, unknown>; request: Request }): Promise<string>
/**
* 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<string, unknown>; request: Request; redirectTo?: string }): Promise<string>
/**
* Sign in a user with the given OAuth provider and request. Returns the session token on success.
*/
signIn(options: {
oauthId: string
provider: RuntimeOAuthProvider<any, DefaultUser>
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<Response>
/**
* 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<void>
/**
* 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<void>
/**
* Change the password for a user, given the current password and new password.
*/
changePassword(options: { userId: string; currentPassword: string; newPassword: string }): Promise<void>
/**
* 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<void>
/**
* Cancel the email change request using the token sent to the user.
*/
cancelEmailChange(options: { token: string; userId?: string } | { token?: string; userId: string }): Promise<void>
/**
* 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 }>
}
135 changes: 1 addition & 134 deletions packages/core/src/@types/session.ts
Original file line number Diff line number Diff line change
@@ -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<typeof identitySchema>
Expand Down Expand Up @@ -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<DefaultUser extends User = User> {
session: Session<DefaultUser> | null
headers: Headers
}

export type GetStatefulSessionReturn<DefaultUser extends User = User> = GetStatelessSessionReturn<DefaultUser>

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<DefaultUser extends User = User> {
/**
* 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<GetStatelessSessionReturn<DefaultUser>>

/**
* Create a session after successful authentication.
* Signs the JWT / writes the DB row / sets cookies.
*/
createSession(session: User, request: Request): Promise<string>

getProviderTokens(oauth: string, request: Request): Promise<GetProviderTokensStatefulReturn>

/**
* Attempt to refresh using the refresh token cookie.
* Returns null session + cookie-clearing response on any failure.
*/
refreshSession(
headers: Headers,
session: DeepPartial<Session<DefaultUser>>
): Promise<{
session: Session<DefaultUser> | 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<void>

/**
* Destroy the session attached to this request (logout).
* Returns a response that clears cookies.
*/
destroySession(headers: Headers): Promise<Headers>

/**
* 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<Headers>

/**
* 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<boolean>

/**
* Refresh the user info in the session.
* @unstable This API is experimental and may change in future releases.
*/
refreshUserInfo(
user: DeepPartial<DefaultUser>,
headers: Headers,
skipCSRFCheck?: boolean
): Promise<{
session: Session<DefaultUser> | 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<string, unknown>, request: Request): Promise<string>
/**
* 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<string, unknown>, request: Request, redirectTo?: string): Promise<string>
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<Response>

sendVerificationEmail(options: { email: string; origin: string }): Promise<{
token: string
url: string
}>

verifyEmail(options: { token: string }): Promise<void>

forgotPassword(options: { email: string; origin: string }): Promise<{
token: string
url: string
}>

resetPassword(options: { token: string; newPassword: string }): Promise<void>

changePassword(options: { userId: string; currentPassword: string; newPassword: string }): Promise<void>

verifyEmailChange(options: { token: string; userId: string; sessionId: string }): Promise<void>

cancelEmailChange(options: { token: string; userId?: string } | { token?: string; userId: string }): Promise<void>

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.
Expand Down
8 changes: 6 additions & 2 deletions packages/core/src/actions/callback/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
Expand Down
15 changes: 10 additions & 5 deletions packages/core/src/api/get-provider-tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,27 @@ import type {
} from "@/@types/index.ts"

export const getProviderTokens = async (
oauth: LiteralUnion<BuiltInOAuthProvider>,
oauthId: LiteralUnion<BuiltInOAuthProvider>,
{ ctx, request: requestInit, headers: headersInit }: FunctionAPIContext<GetProviderTokensAPIOptions>
): Promise<GetProviderTokensAPIReturn> => {
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()

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 {
Expand Down
Loading
Loading