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
81 changes: 81 additions & 0 deletions packages/openauth/src/issuer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,24 @@ export interface IssuerInput<
*/
retention?: number
}
/**
* Control when provider-side persistence should be committed.
*
* - `immediate`: commit during provider success callback.
* - `lazy`: defer commit until authorization code is exchanged.
*
* In `lazy` mode, providers should pass commit payload using
* `ctx.success(..., { commit })` and implement `provider.finalize()`.
*
* This only affects provider-side persistence timing, not token lifetime.
*
* Docs: /docs/concepts/lazy-registration
*
* @default "immediate"
*/
persistence?: {
registration?: "immediate" | "lazy"
}
/**
* Optionally, configure the UI that's displayed when the user visits the root URL of the
* of the OpenAuth server.
Expand Down Expand Up @@ -466,6 +484,8 @@ export function issuer<
const ttlRefresh = input.ttl?.refresh ?? 60 * 60 * 24 * 365
const ttlRefreshReuse = input.ttl?.reuse ?? 60
const ttlRefreshRetention = input.ttl?.retention ?? 0
const registrationPersistence =
input.persistence?.registration ?? "immediate"
if (input.theme) {
setTheme(input.theme)
}
Expand Down Expand Up @@ -517,13 +537,32 @@ export function issuer<
{
async subject(type, properties, subjectOpts) {
const authorization = await getAuthorization(ctx)
const providerName = ctx.get("provider") as string
const provider = input.providers[providerName]
const subject = subjectOpts?.subject
? subjectOpts.subject
: await resolveSubject(type, properties)
await successOpts?.invalidate?.(
await resolveSubject(type, properties),
)
const commit = successOpts?.commit
async function commitProvider() {
if (!commit) return
if (!provider?.finalize) {
throw new Error(
`Provider ${providerName} does not support lazy registration`,
)
}
await provider.finalize({
storage: storage!,
subject,
type: type as string,
properties,
data: commit,
})
}
if (authorization.response_type === "token") {
await commitProvider()
const location = new URL(authorization.redirect_uri)
const tokens = await generateTokens(ctx, {
subject,
Expand All @@ -545,10 +584,14 @@ export function issuer<
}
if (authorization.response_type === "code") {
const code = crypto.randomUUID()
if (registrationPersistence === "immediate") {
await commitProvider()
}
await Storage.set(
storage,
["oauth:code", code],
{
provider: providerName,
type,
properties,
subject,
Expand All @@ -559,6 +602,10 @@ export function issuer<
access: subjectOpts?.ttl?.access ?? ttlAccess,
refresh: subjectOpts?.ttl?.refresh ?? ttlRefresh,
},
commit:
registrationPersistence === "lazy"
? commit
: undefined,
},
60,
)
Expand Down Expand Up @@ -810,6 +857,7 @@ export function issuer<
)
const key = ["oauth:code", code.toString()]
const payload = await Storage.get<{
provider?: string
type: string
properties: any
clientID: string
Expand All @@ -820,6 +868,7 @@ export function issuer<
refresh: number
}
pkce?: AuthorizationState["pkce"]
commit?: any
}>(storage, key)
if (!payload) {
return c.json(
Expand Down Expand Up @@ -877,6 +926,38 @@ export function issuer<
)
}
}
if (payload.commit !== undefined) {
const provider = payload.provider
? input.providers[payload.provider]
: undefined
if (!provider?.finalize) {
return c.json(
{
error: "server_error",
error_description:
"Provider does not support lazy registration",
},
500,
)
}
try {
await provider.finalize({
storage,
subject: payload.subject,
type: payload.type,
properties: payload.properties,
data: payload.commit,
})
} catch {
return c.json(
{
error: "server_error",
error_description: "Failed to commit registration",
},
500,
)
}
}
const tokens = await generateTokens(c, payload)
await Storage.remove(storage, key)
return c.json({
Expand Down
28 changes: 21 additions & 7 deletions packages/openauth/src/provider/password.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,15 @@ export function PasswordProvider(
}
return {
type: "password",
async finalize(input) {
if (input.data?.kind !== "password-register") return
const email = input.data.email?.toString()?.toLowerCase()
const password = input.data.password
if (!email || !password) return
const existing = await Storage.get(input.storage, ["email", email, "password"])
if (existing) return
await Storage.set(input.storage, ["email", email, "password"], password)
},
init(routes, ctx) {
routes.get("/authorize", async (c) =>
ctx.forward(c, await config.login(c.req.raw)),
Expand Down Expand Up @@ -408,14 +417,19 @@ export function PasswordProvider(
])
if (existing)
return transition({ type: "start" }, { type: "email_taken" })
await Storage.set(
ctx.storage,
["email", provider.email, "password"],
provider.password,
return ctx.success(
c,
{
email: provider.email,
},
{
commit: {
kind: "password-register",
email: provider.email,
password: provider.password,
},
},
)
return ctx.success(c, {
email: provider.email,
})
}

return transition({ type: "start" })
Expand Down
24 changes: 24 additions & 0 deletions packages/openauth/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,27 @@ import { StorageAdapter } from "../storage/storage.js"

export type ProviderRoute = Hono

/**
* Payload passed to `provider.finalize()` when lazy registration is enabled.
*/
export interface ProviderFinalizeInput {
storage: StorageAdapter
subject: string
type: string
properties: any
data: any
}

export interface Provider<Properties = any> {
type: string
init: (route: ProviderRoute, options: ProviderOptions<Properties>) => void
/**
* Finalize provider-side persistence at token exchange time.
*
* Called when `IssuerInput.persistence.registration = "lazy"` and the
* provider supplied commit payload during `success()`.
*/
finalize?: (input: ProviderFinalizeInput) => Promise<void>
client?: (input: {
clientID: string
clientSecret: string
Expand All @@ -20,6 +38,12 @@ export interface ProviderOptions<Properties> {
properties: Properties,
opts?: {
invalidate?: (subject: string) => Promise<void>
/**
* Arbitrary provider payload for lazy persistence.
*
* Saved into authorization-code state and passed to `finalize()`.
*/
commit?: any
},
) => Promise<Response>
forward: (ctx: Context, response: Response) => Response
Expand Down
Loading