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/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/),

### Added

- Introduced an experimental `signUp` flow for both the API and endpoint definitions. The new action enables user account creation within the authentication system and provides customizable payload validation through the supported schema. To enable this feature, developers must configure the `signUp` option when calling `createAuth`. [#183](https://github.com/aura-stack-ts/auth/pull/183)

- Added support for a custom `userInfo` function in OAuth provider configuration, enabling callers to perform the user info request themselves. The `userInfo` option continues to accept either a URL string or an object with a `url` and optional request options (for example, custom headers). [#182](https://github.com/aura-stack-ts/auth/pull/182)

---
Expand Down
19 changes: 19 additions & 0 deletions packages/core/src/@types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,3 +284,22 @@ export interface UpdateSessionAPIOptions<DefaultUser extends User = User>

/** Programmatic session update result with redirect metadata and `toResponse()`. */
export type UpdateSessionAPIReturn<DefaultUser extends User = User> = AuthActionAPIReturn<UpdateSessionReturnData<DefaultUser>>

export interface SignUpAPIOptions<Payload extends Record<string, any> = Record<string, any>>
extends APIOptionsWithRedirectTo, APIOptionsWithRequest {
payload: Payload
}

export type SignUpReturnData =
/** redirect: true & redirectTo: string */
| { success: true; redirect: true; redirectURL: null }
/** redirect: false & redirectTo: string */
| { success: true; redirect: false; redirectURL: string }
/** redirect: false & redirectTo: null | undefined (not set) */
/** redirect: true & redirectTo: null | undefined (not set) */
| { success: true; redirect: false; redirectURL: null }
/** Failed sign-up */
| { success: false; redirect: false; redirectURL: null }

/** Programmatic sign-up result with redirect metadata and `toResponse()`. */
export type SignUpAPIReturn = AuthActionAPIReturn<SignUpReturnData>
48 changes: 42 additions & 6 deletions packages/core/src/@types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import type { SerializeOptions } from "@aura-stack/router/cookie"
import type { ConfigSchema, FromShapeToObject, Prettify } from "@/@types/utility.ts"
import type { OAuthProviderCredentials, OAuthProviderRecord } from "@/@types/oauth.ts"
import type { JWTKey, SessionConfig, SessionStrategy, User } from "@/@types/session.ts"
import type { InferSchema } from "@aura-stack/router"
import type { ZodObject } from "zod"

/**
* Main configuration interface for Aura Auth.
* This is the user-facing configuration object passed to `createAuth()`.
*/
export type AuthConfig<Identity extends Identities> = {
export type AuthConfig<Identity extends Identities, SignUpSchema extends SchemaTypes = ZodObject<any>> = {
/**
* OAuth providers available in the authentication and authorization flows. It provides a type-inference
* for the OAuth providers that are supported by Aura Stack Auth; alternatively, you can provide a custom
Expand Down Expand Up @@ -157,6 +159,11 @@ export type AuthConfig<Identity extends Identities> = {
* Credentials provider for username/password or similar authentication.
*/
credentials?: CredentialsProvider<Identity>
/**
* Configuration for the signUp process, including the schema for validation
* and required callback for user creation.
*/
signUp?: SignUpConfig<Identity, SignUpSchema>
} & TrustedProxyHeadersConfig

// @todo Should trustedOrigins support subdomain wildcards like `https://*.example.com`?
Expand Down Expand Up @@ -337,7 +344,9 @@ export interface Logger {
* Programmatic auth API returned with the auth instance: `getSession`, `signIn`, `signInCredentials`, `signOut`, `updateSession`.
* Each method returns a result object plus `headers` and `toResponse()` for HTTP responses.
*/
export type AuthAPI<DefaultUser extends User = User> = ReturnType<typeof createAuthAPI<DefaultUser>>
export type AuthAPI<DefaultUser extends User = User, SignUpSchema extends SchemaTypes = ZodObject<any>> = ReturnType<
typeof createAuthAPI<DefaultUser, SignUpSchema>
>

/** JWT and crypto helpers bound to the configured identity schema (sign, verify, claims). */
export type JoseInstance<DefaultUser extends User = User> = ReturnType<typeof createJoseInstance<DefaultUser>>
Expand Down Expand Up @@ -403,7 +412,7 @@ export interface CredentialsProvider<Identity extends Identities> {
* Runtime context passed into auth actions and API handlers: OAuth map, cookies, JWT, session strategy, trusted origins, etc.
* This is the fully resolved configuration surface after `createAuth` initializes defaults.
*/
export interface RouterGlobalContext<DefaultUser extends User = User> {
export interface RouterGlobalContext<DefaultUser extends User = User, SignUpSchema extends SchemaTypes = ZodObject<any>> {
oauth: OAuthProviderRecord
credentials?: CredentialsProvider<any>
cookies: CookieStoreConfig
Expand All @@ -416,6 +425,7 @@ export interface RouterGlobalContext<DefaultUser extends User = User> {
logger?: InternalLogger
sessionStrategy: SessionStrategy<DefaultUser>
identity: SchemaRegistryContext
signUp?: SignUpConfig<DefaultUser, SignUpSchema>
}

export interface SchemaRegistryContext {
Expand All @@ -433,11 +443,11 @@ export type AuthRuntimeConfig<DefaultUser extends User = User> = RouterGlobalCon
/**
* Public auth instance: programmatic {@link AuthAPI}, {@link JoseInstance}, and HTTP {@link AuthClient} handlers.
*/
export interface AuthInstance<DefaultUser extends User = User> {
export interface AuthInstance<DefaultUser extends User = User, SignUpSchema extends SchemaTypes = ZodObject<any>> {
/**
* Programmatic API for authentication actions (getSession, signIn, signOut, etc.) that can be used in server-side contexts or API routes.
*/
api: AuthAPI<DefaultUser>
api: AuthAPI<DefaultUser, SignUpSchema>
/**
* JOSE helper functions for signin, encryption and verification of JWTs.
*/
Expand All @@ -456,9 +466,35 @@ export interface AuthInstance<DefaultUser extends User = User> {
/**
* Extended context used inside the library with both secure and standard cookie materializations.
*/
export type InternalContext<Identity extends Identities> = RouterGlobalContext<FromShapeToObject<Identity> & User> & {
export type InternalContext<Identity extends Identities, SignUpSchema extends SchemaTypes> = RouterGlobalContext<
FromShapeToObject<Identity>,
SignUpSchema
> & {
cookieConfig: {
secure: CookieStoreConfig
standard: CookieStoreConfig
}
}

export interface OnCreateUserContext<Schema extends SchemaTypes> {
payload: InferSchema<Schema>
}

/**
* Configuration for the signUp process, including the schema for validation
* and required callback for user creation.
*/
export interface SignUpConfig<Identity extends Identities, SignUpSchema extends SchemaTypes> {
/**
* Optional schema for validating the sign-up payload. It supports any
* Zod, Arktype, Valibot or Typebox schema.
*/
schema?: SignUpSchema
/**
* Callback function that is called when a new user signs up. It receives the validated
* sign-up payload and must handle the user creation.
*/
onCreateUser: (
context: OnCreateUserContext<SignUpSchema>
) => Promise<FromShapeToObject<Identity> | null> | FromShapeToObject<Identity> | null
}
1 change: 1 addition & 0 deletions packages/core/src/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export { sessionAction } from "@/actions/session/session.ts"
export { signOutAction } from "@/actions/signOut/signOut.ts"
export { csrfTokenAction } from "@/actions/csrfToken/csrfToken.ts"
export { updateSessionAction } from "@/actions/updateSession/updateSession.ts"
export { signUpAction } from "@/actions/signUp/signUp.ts"
4 changes: 2 additions & 2 deletions packages/core/src/actions/signIn/authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import { AuthInternalError } from "@/shared/errors.ts"
import { equals, extractPath, patternToRegex } from "@/shared/utils.ts"
import { isRelativeURL, isSameOrigin, isValidURL, isTrustedOrigin } from "@/shared/assert.ts"
import type { AuthConfig } from "@/@types/index.ts"
import type { Identities } from "@/shared/identity.ts"
import type { Identities, SchemaTypes } from "@/shared/identity.ts"
import type { GlobalContext } from "@aura-stack/router"

/**
* Resolves trusted origins from config (array or function).
*/
export const getTrustedOrigins = async (
request: Request,
trustedOrigins: AuthConfig<Identities>["trustedOrigins"]
trustedOrigins: AuthConfig<Identities, SchemaTypes>["trustedOrigins"]
): Promise<string[]> => {
if (!trustedOrigins) return []
const raw = typeof trustedOrigins === "function" ? await trustedOrigins(request) : trustedOrigins
Expand Down
40 changes: 40 additions & 0 deletions packages/core/src/actions/signUp/signUp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { z } from "zod"
import { signUp } from "@/api/signUp.ts"
import { createEndpoint, createEndpointConfig } from "@aura-stack/router"
import { RedirectOptionsSchema } from "@/schemas.ts"
import type { SignUpConfig } from "@/@types/config.ts"

const signUpConfig = (config: SignUpConfig<any, any>) => {
return createEndpointConfig({
schemas: {
body: config?.schema ?? z.object({}),
searchParams: RedirectOptionsSchema,
},
})
}

/**
* Handles the user sign-up process. It validates the incoming request against the provided schema,
* creates a new user using the `onCreateUser` callback.
*
* @returns The signed-up user's session
*/
export const signUpAction = (config: SignUpConfig<any, any>) => {
return createEndpoint(
"POST",
"/signUp",
async (ctx) => {
const payload = ctx.body
const { toResponse } = await signUp({
ctx: ctx.context,
payload,
request: ctx.request,
headers: ctx.request.headers,
redirect: ctx.searchParams.redirect,
redirectTo: ctx.searchParams.redirectTo,
})
return toResponse()
},
signUpConfig(config)
)
}
43 changes: 40 additions & 3 deletions packages/core/src/api/createApi.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getSession, signIn, signInCredentials, signOut, updateSession } from "@/api/index.ts"
import type { GlobalContext } from "@aura-stack/router"
import { getSession, signIn, signInCredentials, signOut, updateSession, signUp } from "@/api/index.ts"
import type { GlobalContext, InferSchema } from "@aura-stack/router"
import type {
BuiltInOAuthProvider,
LiteralUnion,
Expand All @@ -14,9 +14,22 @@ import type {
SignInCredentialsAPIReturn,
SignOutAPIReturn,
UpdateSessionAPIReturn,
SignUpAPIOptions,
SignUpAPIReturn,
Wrap,
} from "@/@types/index.ts"
import type { ZodObject } from "zod"
import type { SchemaTypes } from "@/shared/identity.ts"

export const createAuthAPI = <DefaultUser extends User = User>(ctx: GlobalContext) => {
type InferSignUp<T> = Wrap<RemoveIndexSignature<InferSchema<T>>>

type RemoveIndexSignature<T> = {
[K in keyof T as string extends K ? never : number extends K ? never : symbol extends K ? never : K]: T[K]
}

export const createAuthAPI = <DefaultUser extends User = User, SignUpSchema extends SchemaTypes = ZodObject<any>>(
ctx: GlobalContext
) => {
return {
/**
* Retrieves the current session data from the server-side.
Expand Down Expand Up @@ -64,6 +77,30 @@ export const createAuthAPI = <DefaultUser extends User = User>(ctx: GlobalContex
signInCredentials: async (options: SignInCredentialsAPIOptions): Promise<SignInCredentialsAPIReturn> => {
return signInCredentials({ ctx, ...options })
},
/**
* Signs up a new user on the server-side. It requires a `payload` with the necessary information for
* user creation and a callback function configured in `signUp.onCreateUser` to handle the actual user
* creation logic.
*
* @params options - Options for the API call, including the sign-up payload, headers, and redirect behavior.
* @return The object returned by the API call {@link SignUpAPIReturn}
* @example
* const response = await api.signUp({
* payload: {
* name: "John",
* lastName: "Doe",
* email: "john.doe@example.com",
* password: "1234567890"
* },
* redirectTo: "/dashboard",
* request: await getRequest()
* })
*/
signUp: async <Payload extends Record<string, any> = InferSignUp<SignUpSchema>>(
options: SignUpAPIOptions<Payload>
): Promise<SignUpAPIReturn> => {
return signUp({ ctx, ...options })
},
/**
* Updates the current session on the server-side. It allows partial updates to the session object, such as
* modifying user fields or extending the session expiry. It implements CSRF Protection by default, for
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export { signInCredentials } from "@/api/credentials.ts"
export { signOut } from "@/api/signOut.ts"
export { getSession } from "@/api/getSession.ts"
export { updateSession } from "@/api/updateSession.ts"
export { signUp } from "@/api/signUp.ts"
93 changes: 93 additions & 0 deletions packages/core/src/api/signUp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { createRedirectTo, getBaseURL, getOriginURL } from "@/actions/signIn/authorization.ts"
import type { FunctionAPIContext, SignUpAPIOptions, SignUpAPIReturn } from "@/@types/api.ts"
import { AuthValidationError, isAuthErrorWithCode } from "@/shared/errors.ts"
import { createCSRF } from "@/shared/crypto.ts"
import { HeadersBuilder } from "@aura-stack/router"
import { secureApiHeaders } from "@/shared/headers.ts"

export const signUp = async <Payload extends Record<string, unknown> = Record<string, unknown>>({
ctx,
payload,
headers: headersInit,
request: requestInit,
redirect = true,
redirectTo,
}: FunctionAPIContext<SignUpAPIOptions<Payload>>): Promise<SignUpAPIReturn> => {
const { signUp, cookies, sessionStrategy, logger } = ctx
try {
let request = requestInit
if (!request) {
const origin = await getBaseURL({ ctx, headers: headersInit })
const url = `${origin}${ctx.basePath}/signUp`
request = new Request(url, { headers: headersInit })
}
await getOriginURL(request, ctx)
const user = await signUp?.onCreateUser({
payload,
})
if (!user) {
throw new AuthValidationError("USER_CREATION_FAILED", "Failed to create user with the provided payload.")
}
const sessionToken = await sessionStrategy.createSession(user)
const csrfToken = await createCSRF(ctx.jose)
logger?.log("SIGN_UP_SUCCESS")

const headers = new HeadersBuilder(secureApiHeaders)
.setCookie(cookies.csrfToken.name, csrfToken, cookies.csrfToken.attributes)
.setCookie(cookies.sessionToken.name, sessionToken, cookies.sessionToken.attributes)

let redirectURL: string | null = await createRedirectTo(request, redirectTo, ctx)
redirectURL = redirectTo ? redirectURL : redirectURL === "/" ? null : redirectURL

if (redirect && redirectURL) {
headers.setHeader("Location", redirectURL)
}

const shouldRedirectServer = redirect && !!redirectURL
const toHeaders = headers.toHeaders()
return {
success: true,
redirect: shouldRedirectServer,
redirectURL: redirect ? null : redirectURL,
headers: toHeaders,
toResponse: () => {
return Response.json(
{
success: true,
redirect: shouldRedirectServer,
redirectURL: shouldRedirectServer ? null : redirectURL,
},
{ headers: toHeaders, status: shouldRedirectServer ? 302 : 200 }
)
},
} as SignUpAPIReturn
} catch (error) {
let code = "SIGN_UP_ERROR"
let message = "An error occurred during sign-up."
if (isAuthErrorWithCode(error)) {
code = error.code
message = error.message
}

return {
success: false,
error: {
code,
message,
},
redirect: false,
headers: new Headers(secureApiHeaders),
redirectURL: null,
toResponse: () => {
return Response.json(
{
success: false,
redirect: false,
redirectURL: null,
},
{ headers: secureApiHeaders, status: 400 }
)
},
Comment thread
halvaradop marked this conversation as resolved.
}
}
}
1 change: 1 addition & 0 deletions packages/core/src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export const createAuthClient = <DefaultUser extends User = User>(options: AuthC
const response = await client.patch("/session", {
// @ts-ignore - Fix type here - go to @aura-stack/router.
body: {
// @ts-ignore - Fix type here - go to @aura-stack/router.
user,
expires: session.expires ? new Date(session.expires) : undefined,
},
Expand Down
Loading
Loading