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
5 changes: 5 additions & 0 deletions packages/core/src/@types/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,11 @@ export interface SessionStrategy<DefaultUser extends User = User> {
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>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
signIn(
oauth: string,
request: Request,
Expand Down
15 changes: 12 additions & 3 deletions packages/core/src/api/signUp.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { createCSRF } from "@/shared/crypto.ts"
import { HeadersBuilder } from "@aura-stack/router"
import { secureApiHeaders } from "@/shared/headers.ts"
import { createCSRF } from "@/shared/crypto.ts"
import { getErrorName } from "@/shared/utils.ts"
import { AuraAuthError } from "@/shared/errors.ts"
import { secureApiHeaders } from "@/shared/headers.ts"
import { createValidation, handleApiError, resolveApiRedirect } from "@/shared/utils/api.ts"
import type { FunctionAPIContext, SignUpAPIOptions, SignUpAPIReturn } from "@/@types/api.ts"

Expand Down Expand Up @@ -33,7 +34,8 @@ export const signUp = async <Payload extends Record<string, unknown> = Record<st
if (!user) {
throw new AuraAuthError({ code: "USER_CREATION_FAILED" })
}
const sessionToken = await sessionStrategy.createSession(user, request)

const sessionToken = await sessionStrategy.signUp(user, request)
const csrfToken = await createCSRF(ctx.jose)
logger?.log("SIGN_UP_SUCCESS")

Expand Down Expand Up @@ -67,6 +69,13 @@ export const signUp = async <Payload extends Record<string, unknown> = Record<st
},
} as SignUpAPIReturn
} catch (error) {
logger?.log("SIGN_UP_ERROR", {
structuredData: {
error_type: getErrorName(error),
error_code: error instanceof AuraAuthError ? error.code : "UNKNOWN_ERROR",
error_message: error instanceof Error ? error.message : String(error),
},
})
Comment thread
halvaradop marked this conversation as resolved.
const { code, message, statusCode } = handleApiError(error, "SIGN_UP_ERROR", "An error occurred during sign-up.")

return {
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/session/stateful/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { signIn } from "@/session/stateful/signIn.ts"
import { signUp } from "@/session/stateful/signUp.ts"
import { getSession } from "@/session/stateful/getSession.ts"
import { revokeToken } from "@/session/stateful/revokeToken.ts"
import { createSession } from "@/session/stateful/createSession.ts"
Expand Down Expand Up @@ -26,5 +27,6 @@ export const createStatefulStrategy = <DefaultUser extends User = User>(
isProviderConnected: isProviderConnected(ctx),
signIn: signIn(ctx),
oauthCallback: oauthCallback(ctx),
signUp: signUp(ctx),
}
}
144 changes: 144 additions & 0 deletions packages/core/src/session/stateful/signUp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { AuraAuthError } from "@/shared/errors.ts"
import { createHash, createSecretValue, hashPassword } from "@/shared/crypto.ts"
import { createDevice as __createDevice } from "@/shared/utils/session-strategy.ts"
import type { InternalStatefulContext } from "@/@types/config.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) => {
const { logger, sessionConfig } = ctx
const createDevice = __createDevice({ ctx, cookies, cookieManager })

return async (payload: Record<string, unknown>, request: Request): Promise<string> => {
logger?.log("STATEFUL_CREATE_SESSION_START", {
structuredData: {
strategy: "stateful",
operation: "signUp",
},
})

if (ctx.identity.skipValidation) {
logger?.log("IDENTITY_VALIDATION_DISABLED", {
structuredData: {
identity_validation_disabled: true,
},
})
}

/**
* @todo fix wrong logic from identity.schema (User schema) and signUp.schema (SignUpPayload schema)
*/
const { password } = payload
Comment thread
halvaradop marked this conversation as resolved.
const validatedPayload = ctx.identity.skipValidation ? payload : await ctx.identity.schemaRegistry.parse(payload)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
logger?.log("STATEFUL_PAYLOAD_VALIDATION", {
structuredData: {
validation_skipped: ctx.identity.skipValidation || false,
has_email: Boolean(validatedPayload.email) || false,
},
})

const { sub: _sub, email: rawEmail, name, image, ...attributes } = validatedPayload
const email = typeof rawEmail === "string" ? rawEmail.trim().toLowerCase() : rawEmail

if (email) {
const getEmail = await sessionConfig.adapter.getUserByEmail(email)
if (getEmail) {
throw new AuraAuthError({ code: "EMAIL_ALREADY_REGISTERED" })
}
}
Comment thread
halvaradop marked this conversation as resolved.

const userId = createSecretValue(32)
const user = await sessionConfig.adapter.createUser({
id: userId,
name,
email,
image,
attributes,
status: "active",
mfaEnabled: false,
mfaPreferredMethod: null,
emailVerifiedAt: null,
})
logger?.log("STATEFUL_USER_CREATED", {
structuredData: {
user_id: user.id,
has_email: Boolean(user.email),
},
})

const account = await sessionConfig.adapter.createAccount({
id: createSecretValue(32),
userId: user.id,
provider: "credentials",
providerUserId: user.id,
type: "credentials",
status: "active",
})

if (password !== undefined && password !== null) {
if (typeof password !== "string" || password.length === 0) {
throw new AuraAuthError({ code: "AUTH_CREDENTIALS_INVALID" })
}
const passwordHash = await hashPassword(password)
await sessionConfig.adapter.createCredentialAccount({
accountId: account.id,
passwordHash,
})
}

const device = await createDevice(user.id, request)
const secretValue = createSecretValue(64)
logger?.log("STATEFUL_TOKEN_GENERATED", {
structuredData: {
token_length: secretValue.length,
},
})

const tokenHash = await createHash(secretValue)
logger?.log("STATEFUL_TOKEN_HASHED", {
structuredData: {
hash_length: tokenHash.length,
},
})

const expiresAt = new Date(Date.now() + 60 * 60 * 24 * 15 * 1000)
logger?.log("STATEFUL_SESSION_EXPIRATION_SET", {
structuredData: {
expires_at: expiresAt?.toISOString(),
max_age_days: 15,
},
})

const dbSession = await sessionConfig.adapter.createSession({
id: createSecretValue(32),
userId: user.id,
deviceId: device.id,
authenticatedWith: "credentials",
status: "active",
mfaState: "none",
tokenHash,
expiresAt,
metadata: null,
})
Comment thread
halvaradop marked this conversation as resolved.

logger?.log("STATEFUL_SESSION_CREATED", {
structuredData: {
session_id: dbSession.id,
user_id: dbSession.userId,
status: dbSession.status,
expires_at: dbSession?.expiresAt?.toISOString(),
},
})

logger?.log("STATEFUL_CREATE_SESSION_SUCCESS", {
structuredData: {
session_id: dbSession.id,
user_id: dbSession.userId,
token_returned: true,
},
})

return secretValue
}
}
2 changes: 2 additions & 0 deletions packages/core/src/session/stateless/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { signIn } from "@/session/stateless/signIn.ts"
import { signUp } from "@/session/stateless/signUp.ts"
import { getSession } from "@/session/stateless/getSession.ts"
import { revokeToken } from "@/session/stateless/revokeToken.ts"
import { oauthCallback } from "@/session/stateless/oauthCallback.ts"
Expand Down Expand Up @@ -31,5 +32,6 @@ export const createStatelessStrategy = <DefaultUser extends User = User>(
destroySession: destroySession(ctx),
signIn: signIn(ctx),
oauthCallback: oauthCallback(ctx),
signUp: signUp(ctx),
}
}
10 changes: 10 additions & 0 deletions packages/core/src/session/stateless/signUp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createSession as __createSession } from "@/session/stateless/createSession.ts"
import type { InternalStatelessContext, TypedJWTPayload, User } from "@/@types/index.ts"

export const signUp = <DefaultUser extends User = User>(ctx: InternalStatelessContext) => {
const createSession = __createSession<DefaultUser>(ctx)

return async (payload: Record<string, unknown>, _request: Request): Promise<string> => {
return await createSession(payload as TypedJWTPayload<DefaultUser>)
}
}
1 change: 0 additions & 1 deletion packages/core/src/session/strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ export const createSessionStrategy = <Identity extends Identities>(
const cookieManager = createCookieManager(config.cookies)
const ctx = { ...config, cookieManager }

console.log("isStateles: ", isStatelessStrategy(config?.ctx?.sessionConfig))
if (!isStatelessStrategy(config?.ctx?.sessionConfig) && !config?.ctx?.sessionConfig?.adapter) {
throw new AuraAuthError({ code: "MISSING_ADAPTER_IN_STATEFUL_STRATEGY" })
}
Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/shared/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ export const AuraErrorCode = {
OAUTH_UNLINKED_ACCOUNT_ERROR: "OAUTH_UNLINKED_ACCOUNT_ERROR",
OAUTH_ACCOUNT_USER_MISMATCH: "OAUTH_ACCOUNT_USER_MISMATCH",
MISSING_ADAPTER_IN_STATEFUL_STRATEGY: "MISSING_ADAPTER_IN_STATEFUL_STRATEGY",
EMAIL_ALREADY_REGISTERED: "EMAIL_ALREADY_REGISTERED",
} as const

export type AuraErrorCode = (typeof AuraErrorCode)[keyof typeof AuraErrorCode]
Expand Down Expand Up @@ -927,6 +928,14 @@ export const ERROR_CATALOG: Record<AuraErrorCode, CatalogEntry> = {
userMessage:
"Internal library configuration error. Database session strategy requires an adapter instance to be configured.",
},
EMAIL_ALREADY_REGISTERED: {
type: "AUTH_FLOW",
statusCode: 409,
name: "AuthError",
message:
"The registration request was rejected because the provided email address is already associated with an existing user record in the system.",
userMessage: "This email address is already registered. Please sign in or use a different email.",
},
}

export interface AuraErrorOptions extends ErrorOptions {
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/shared/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,12 @@ export const logMessages = {
msgId: "STATELESS_REVOKE_SESSION_NOOP",
message: "Stateless session revocation is a no-op (no server-side state to revoke)",
},
SIGN_UP_ERROR: {
facility: 4,
severity: "error",
msgId: "SIGN_UP_ERROR",
message: "Error occurred during user sign-up process",
},
} as const

export const createLogEntry = <T extends keyof typeof logMessages>(key: T, overrides?: Partial<SyslogOptions>): SyslogOptions => {
Expand Down
Loading
Loading