Skip to content
Merged
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/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"scripts": {
"dev": "tsup --watch",
"build": "tsup",
"lint": "oxlint",
"lint:fix": "oxlint --fix",
"test": "vitest --run",
"test:watch": "vitest",
"test:coverage": "vitest --run --coverage",
Expand Down
46 changes: 19 additions & 27 deletions packages/core/src/@types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@ import { z } from "zod"
import { createLogEntry } from "@/logger.ts"
import { OAuthAccessTokenErrorResponse, OAuthAuthorizationErrorResponse, OAuthEnvSchema } from "@/schemas.ts"
import { createJoseInstance, type JWTPayload } from "@/jose.ts"
import type { SerializeOptions } from "@aura-stack/router/cookie"
import type { BuiltInOAuthProvider } from "@/oauth/index.ts"
import type { LiteralUnion, Prettify } from "@/@types/utility.ts"
import type { createAuthInstance } from "@/createAuth.ts"
import type { createAuthAPI } from "@/api/createApi.ts"
import type { ClientOptions } from "@aura-stack/router"
import type { createAuthInstance } from "@/createAuth.ts"
import type { BuiltInOAuthProvider } from "@/oauth/index.ts"
import type { LiteralUnion, Prettify } from "@/@types/utility.ts"
import type { SerializeOptions } from "@aura-stack/router/cookie"
import type { JWTKey, Session, SessionConfig, SessionStrategy, User } from "@/@types/session.ts"

export * from "./utility.ts"
export type * from "./utility.ts"
export type { BuiltInOAuthProvider } from "@/oauth/index.ts"
export type * from "@/@types/session.ts"
export type { TypedJWTPayload } from "@aura-stack/jose"

/**
* Standard JWT claims that are managed internally by the token system.
Expand All @@ -23,25 +26,6 @@ export type JWTStandardClaims = Pick<JWTPayload, "exp" | "iat" | "jti" | "nbf" |
*/
export type JWTPayloadWithToken = JWTPayload & { token: string }

/**
* Standardized user profile returned by OAuth providers after fetching user information
* and mapping the response to this format by default or via the `profile` custom function.
*/
export interface User extends Record<string, unknown> {
sub: string
name?: string | null
email?: string | null
image?: string | null
}

/**
* Session data returned by the session endpoint.
*/
export interface Session {
user: User
expires: string
}

export type AuthorizeParams = LiteralUnion<
"clientId" | "prompt" | "scope" | "responseMode" | "audience" | "loginHint" | "nonce" | "display"
>
Expand Down Expand Up @@ -210,7 +194,7 @@ export interface AuthConfig {
* If not provided, it will load from the environment variable `AURA_AUTH_SECRET` or `AUTH_SECRET`, but if it
* doesn't exist, it will throw an error during the initialization of the Auth module.
*/
secret?: string
secret?: JWTKey
Comment thread
halvaradop marked this conversation as resolved.
/**
* Base URL of the application, used to construct the incoming request's origin.
*/
Expand All @@ -236,7 +220,10 @@ export interface AuthConfig {
* @experimental
*/
trustedProxyHeaders?: boolean

/**
* Logger configuration for handling authentication-related logs and errors. It can be set to `true`,
* `DEBUG=true`, `LOG_LEVEL=debug`, or a custom logger. It implements the syslog format.
*/
logger?: boolean | Logger
/**
* Defines trusted origins for your application to prevent open redirect attacks.
Expand All @@ -255,6 +242,10 @@ export interface AuthConfig {
* }
*/
trustedOrigins?: TrustedOrigin[] | ((request: Request) => Promise<TrustedOrigin[]> | TrustedOrigin[])
/**
* Defines the session management strategy for Aura Auth. It determines how sessions are created, stored, and validated.
*/
session?: SessionConfig
}

/**
Expand Down Expand Up @@ -283,12 +274,13 @@ export interface RouterGlobalContext {
oauth: OAuthProviderRecord
cookies: CookieStoreConfig
jose: JoseInstance
secret?: string
secret?: JWTKey
baseURL?: string
basePath: string
trustedProxyHeaders: boolean
trustedOrigins?: TrustedOrigin[] | ((request: Request) => Promise<TrustedOrigin[]> | TrustedOrigin[])
logger?: InternalLogger
session: SessionStrategy
}

/**
Expand Down
199 changes: 199 additions & 0 deletions packages/core/src/@types/session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import type { JoseInstance, CookieStoreConfig, InternalLogger } from "@/@types/index.ts"

/**
* Standardized user profile returned by OAuth providers after fetching user information
* and mapping the response to this format by default or via the `profile` custom function.
*/
export interface User extends Record<string, unknown> {
sub: string
name?: string | null
email?: string | null
image?: string | null
}

/**
* Session data returned by the session endpoint.
*/
export interface Session {
user: User
expires: string
}

/**
* A symmetric secret or asymmetric key pair used for JWT operations.
*
* - string / Uint8Array: used as-is for HMAC (signed) or AES (encrypted)
* - CryptoKey: Web Crypto API key, for environments that support it
* - KeyPair: asymmetric signing (RS256, ES256, EdDSA, etc.)
*/
export type SecretKey = string | Uint8Array | CryptoKey

export interface KeyPair {
privateKey: CryptoKey
publicKey: CryptoKey
}

/**
* @todo: add key rotation support for "SecretKey | KeyPair | [SecretKey | KeyPair, ...(SecretKey | KeyPair)[]]"
*/
export type JWTKey = SecretKey

/**
* - "signed" → standard JWS (e.g. HS256, RS256, ES256).
* - "encrypted" → JWE only. (e.g. A256GCM with RSA-OAEP key wrapping).
* - "sealed" → JWS nested inside JWE (signed then encrypted).
*/
export type JWTMode = "signed" | "encrypted" | "sealed"

/**
* Signing algorithms for "signed" and "sealed" modes.
* Symmetric: HS256 | HS384 | HS512
* Asymmetric: RS256 | RS384 | RS512 | ES256 | ES384 | ES512 | EdDSA | PS256
*/
export type JWTSigningAlgorithm =
| "HS256"
| "HS384"
| "HS512"
| "RS256"
| "RS384"
| "RS512"
| "ES256"
| "ES384"
| "ES512"
| "EdDSA"
| "PS256"

/**
* Key-wrapping algorithms for "encrypted" and "sealed" modes.
* Symmetric: A128KW | A192KW | A256KW | dir (direct)
* ECDH: ECDH-ES | ECDH-ES+A128KW | ECDH-ES+A256KW
* RSA: RSA-OAEP | RSA-OAEP-256
*/
export type JWTKeyAlgorithm =
| "A128KW"
| "A192KW"
| "A256KW"
| "dir"
| "ECDH-ES"
| "ECDH-ES+A128KW"
| "ECDH-ES+A256KW"
| "RSA-OAEP"
| "RSA-OAEP-256"

/** Content-encryption algorithms for JWE. */
export type JWTEncryptionAlgorithm = "A128CBC-HS256" | "A192CBC-HS384" | "A256CBC-HS512" | "A128GCM" | "A192GCM" | "A256GCM"

/** Signed JWT mode configuration. */
export type JWTSignedMode = {
mode: "signed"
signingAlgorithm?: JWTSigningAlgorithm
}

/** Encrypted JWT mode configuration. */
export type JWTEncryptedMode = {
mode: "encrypted"
keyAlgorithm?: JWTKeyAlgorithm
encryptionAlgorithm?: JWTEncryptionAlgorithm
}

/** Signed and Encrypted JWT mode configuration. */
export type JWTSealedMode = {
mode?: "sealed"
signingAlgorithm?: JWTSigningAlgorithm
keyAlgorithm?: JWTKeyAlgorithm
encryptionAlgorithm?: JWTEncryptionAlgorithm
}

export type JWTConfigBase = JWTSignedMode | JWTEncryptedMode | JWTSealedMode

export type JWTConfig = {
/**
* Token lifetime.
*/
maxAge?: number
/**
* JWT `iss` (issuer) claim. Set this to your app's canonical URL.
* @example "https://auth.example.com"
*/
issuer?: string
/**
* JWT `aud` claim. Single value or array for multi-audience tokens.
* @example ["https://api.example.com", "https://app.example.com"]
*/
audience?: string | string[]
} & JWTConfigBase

/**
* Stateless JWT strategy.
* No database required. Tokens are self-contained and cannot be revoked
* before they expire — keep `jwt.maxAge` short or enable refresh tokens.
*
* @example
* {
* strategy: "jwt",
* jwt: { mode: "sealed", maxAge: "15m", issuer: "https://auth.example.com" },
* refreshToken: { enabled: true, maxAge: "7d" },
* }
*/
export type StatelessStrategyConfig = {
strategy?: "jwt"
jwt?: JWTConfig
}
Comment thread
halvaradop marked this conversation as resolved.

/**
* The session strategy. Determines which fields below are required.
*
* - "jwt": stateless. No database needed. JWTs are self-contained.
* - "database": stateful. Every request hits the DB to validate the session.
* - "hybrid": JWT transport + DB revocation. Best of both for most apps.
*
* @default "jwt"
*/
export type SessionConfig = StatelessStrategyConfig

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(request: Headers): Promise<Session | null>

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

/**
* Attempt to refresh using the refresh token cookie.
* Returns null session + cookie-clearing response on any failure.
*/
refreshSession(request: Headers): Promise<Session | null>
Comment thread
halvaradop marked this conversation as resolved.

/**
* 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(request: Headers, skipCSRFCheck?: boolean): Promise<Headers>
}

export interface CreateSessionStrategyOptions {
config?: SessionConfig
jose: JoseInstance
cookies: () => CookieStoreConfig
logger?: InternalLogger
}

export interface JWTStrategyOptions {
config?: StatelessStrategyConfig
jose: JoseInstance
logger?: InternalLogger
cookies: () => CookieStoreConfig
}
6 changes: 3 additions & 3 deletions packages/core/src/actions/callback/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { OAuthAuthorizationErrorResponse } from "@/schemas.ts"
import { AuthSecurityError, OAuthProtocolError } from "@/errors.ts"
import { getOriginURL, getTrustedOrigins } from "@/actions/signIn/authorization.ts"
import { createAccessToken } from "@/actions/callback/access-token.ts"
import { createSessionCookie, getCookie, expiredCookieAttributes } from "@/cookie.ts"
import { getCookie, expiredCookieAttributes } from "@/cookie.ts"
import type { OAuthProviderRecord } from "@/@types/index.ts"

const callbackConfig = (oauth: OAuthProviderRecord) => {
Expand Down Expand Up @@ -108,7 +108,7 @@ export const callbackAction = (oauth: OAuthProviderRecord) => {
}

const userInfo = await getUserInfo(oauthConfig, accessToken.access_token, logger)
const sessionCookie = await createSessionCookie(jose, userInfo)
const session = await context.session.createSession(userInfo)
const csrfToken = await createCSRF(jose)

logger?.log("OAUTH_CALLBACK_SUCCESS", {
Expand All @@ -119,7 +119,7 @@ export const callbackAction = (oauth: OAuthProviderRecord) => {

const headers = new HeadersBuilder(cacheControl)
.setHeader("Location", cookieRedirectTo)
.setCookie(cookies.sessionToken.name, sessionCookie, cookies.sessionToken.attributes)
.setCookie(cookies.sessionToken.name, session, cookies.sessionToken.attributes)
.setCookie(cookies.csrfToken.name, csrfToken, cookies.csrfToken.attributes)
.setCookie(cookies.state.name, "", expiredCookieAttributes)
.setCookie(cookies.redirectURI.name, "", expiredCookieAttributes)
Expand Down
14 changes: 4 additions & 10 deletions packages/core/src/api/getSession.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
import { getCookie } from "@/cookie.ts"
import { getErrorName, toISOString } from "@/utils.ts"
import { getErrorName } from "@/utils.ts"
import type { FunctionAPIContext, GetSessionAPIOptions, SessionResponse } from "@/@types/index.ts"

export const getSession = async ({ ctx, headers }: FunctionAPIContext<GetSessionAPIOptions>): Promise<SessionResponse> => {
try {
const session = getCookie(new Headers(headers), ctx.cookies.sessionToken.name)
const decoded = await ctx.jose.decodeJWT(session)
ctx?.logger?.log("AUTH_SESSION_VALID")
const { exp, iat: _iat, jti: _jti, nbf: _nbf, aud: _aud, iss: _iss, ...user } = decoded
const session = await ctx.session.getSession(new Headers(headers))
if (!session) return { session: null, authenticated: false }
return {
session: {
user,
expires: toISOString(exp! * 1000),
},
session,
authenticated: true,
}
} catch (error) {
Expand Down
Loading
Loading