@ghuri/oauth 0.1.0-alpha.0 → 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -58,6 +58,7 @@ declare function fromB64url(value: string): Uint8Array<ArrayBuffer>;
|
|
|
58
58
|
declare function utf8(value: string): Uint8Array<ArrayBuffer>;
|
|
59
59
|
/** URL-safe random string with `bytes` bytes of entropy. */
|
|
60
60
|
declare function randomToken(bytes?: number): string;
|
|
61
|
+
declare function sha256(value: string): Promise<Uint8Array>;
|
|
61
62
|
/** PKCE: a verifier and its S256 challenge. */
|
|
62
63
|
declare function pkcePair(): Promise<{
|
|
63
64
|
verifier: string;
|
|
@@ -67,6 +68,7 @@ declare function pkcePair(): Promise<{
|
|
|
67
68
|
declare function seal(plaintext: string, purpose: string): Promise<string>;
|
|
68
69
|
/** Decrypt what `seal()` produced (also with `SECRET_KEY_FALLBACKS`). Null when it fails. */
|
|
69
70
|
declare function unseal(sealed: string, purpose: string): Promise<string | null>;
|
|
71
|
+
declare function timingSafeEqual(a: string, b: string): boolean;
|
|
70
72
|
|
|
71
73
|
declare class OAuthError extends GhuriError {
|
|
72
74
|
/** The provider's `error` code, when there is one. */
|
|
@@ -335,4 +337,4 @@ declare class OIDCStrategy extends OAuth2Strategy {
|
|
|
335
337
|
userProfile(tokens: OAuthTokens, c: Context, flow?: PendingFlow): Promise<Profile>;
|
|
336
338
|
}
|
|
337
339
|
|
|
338
|
-
export { AuthenticateOptions, BearerStrategy, type BearerVerify, type DecodedJwt, type DiscoveryDocument, EnvValue, type JwsAlgorithm, LocalStrategy, type LocalStrategyOptions, type LocalVerify, type MagicLinkOptions, MagicLinkStrategy, type MagicLinkVerify, type NamedResult, OAuth2Client, type OAuth2ClientConfig, OAuth2Strategy, type OAuth2StrategyOptions, OAuthError, OAuthTokens, OIDCStrategy, type OIDCStrategyOptions, type PendingFlow, Profile, type ProfileContext, SUPPORTED_ALGORITHMS, Strategy, StrategyRef, StrategyResult, StrategyUser, TokenValidationError, type VerifyFunction, type VerifyOptions, type VerifyParams, type VerifyResult, authenticate, b64url, clearDiscoveryCache, clearJwksCache, completeLogin, decodeJwt, discover, envValue, failureResponse, fromB64url, getJson, importPkcs8, loginBackendName, pkcePair, profileFromClaims, providerFetch, randomToken, readBody, runStrategy, savePending, seal, setProviderFetch, signJwt, stateSettings, takePending, toTokens, unseal, userInfo, utf8, verifyJwt };
|
|
340
|
+
export { AuthenticateOptions, BearerStrategy, type BearerVerify, type DecodedJwt, type DiscoveryDocument, EnvValue, type JwsAlgorithm, LocalStrategy, type LocalStrategyOptions, type LocalVerify, type MagicLinkOptions, MagicLinkStrategy, type MagicLinkVerify, type NamedResult, OAuth2Client, type OAuth2ClientConfig, OAuth2Strategy, type OAuth2StrategyOptions, OAuthError, OAuthTokens, OIDCStrategy, type OIDCStrategyOptions, type PendingFlow, Profile, type ProfileContext, SUPPORTED_ALGORITHMS, Strategy, StrategyRef, StrategyResult, StrategyUser, TokenValidationError, type VerifyFunction, type VerifyOptions, type VerifyParams, type VerifyResult, authenticate, b64url, clearDiscoveryCache, clearJwksCache, completeLogin, decodeJwt, discover, envValue, failureResponse, fromB64url, getJson, importPkcs8, loginBackendName, pkcePair, profileFromClaims, providerFetch, randomToken, readBody, runStrategy, savePending, seal, setProviderFetch, sha256, signJwt, stateSettings, takePending, timingSafeEqual, toTokens, unseal, userInfo, utf8, verifyJwt };
|
package/dist/index.js
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
savePending,
|
|
32
32
|
seal,
|
|
33
33
|
setProviderFetch,
|
|
34
|
+
sha256,
|
|
34
35
|
signJwt,
|
|
35
36
|
stateSettings,
|
|
36
37
|
takePending,
|
|
@@ -215,10 +216,12 @@ export {
|
|
|
215
216
|
savePending,
|
|
216
217
|
seal,
|
|
217
218
|
setProviderFetch,
|
|
219
|
+
sha256,
|
|
218
220
|
signJwt,
|
|
219
221
|
stateSettings,
|
|
220
222
|
strategies,
|
|
221
223
|
takePending,
|
|
224
|
+
timingSafeEqual,
|
|
222
225
|
toTokens,
|
|
223
226
|
unseal,
|
|
224
227
|
userInfo,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/local.ts"],"sourcesContent":["// Username/password, magic links and bearer tokens.\nimport { authenticate as authAuthenticate, getUserModel, readData, saltedHmac } from '@ghuri/auth'\nimport { getHost, isSecure, tryGetContext, wantsJson } from '@ghuri/core'\nimport type { Context } from 'hono'\nimport { b64url, fromB64url, fromUtf8, timingSafeEqual, utf8 } from './crypto'\nimport { Strategy } from './strategy'\nimport type { AuthenticateOptions, StrategyResult, StrategyUser } from './types'\n\n// ---- LocalStrategy ----\n\nexport interface LocalStrategyOptions {\n name?: string\n usernameField?: string\n passwordField?: string\n}\n\nexport type LocalVerify = (params: {\n username: string\n password: string\n c: Context\n}) => Promise<StrategyUser | null>\n\n/**\n * Username and password from a JSON or form POST. The default `verify` is `@ghuri/auth`'s `authenticate()`, so\n * `AUTHENTICATION_BACKENDS`, password hashers and `userLoginFailed` all apply.\n */\nexport class LocalStrategy extends Strategy {\n readonly name: string\n private readonly usernameField: string\n private readonly passwordField: string\n\n constructor(\n options: LocalStrategyOptions = {},\n readonly verify: LocalVerify = ({ username, password, c }) => authAuthenticate(c, { username, password }),\n ) {\n super()\n this.name = options.name ?? 'local'\n this.usernameField = options.usernameField ?? 'username'\n this.passwordField = options.passwordField ?? 'password'\n }\n\n async authenticate(c: Context): Promise<StrategyResult> {\n if (c.req.method !== 'POST') return this.pass()\n const data = await readData(c)\n const username = data[this.usernameField]\n const password = data[this.passwordField]\n if (typeof username !== 'string' || !username || typeof password !== 'string' || !password) {\n return this.fail('Missing credentials.', 400)\n }\n const user = await this.verify({ username, password, c })\n return user ? this.success(user) : this.fail('Invalid credentials.', 401)\n }\n}\n\n// ---- MagicLinkStrategy ----\n\nexport interface MagicLinkOptions {\n name?: string\n /** Deliver the link. Called only when `verify` found a user for the address. */\n send: (params: { email: string; url: string; user: StrategyUser; c: Context }) => Promise<void>\n /** Seconds a link stays valid (default 900). */\n maxAge?: number\n emailField?: string\n /** Where forms go after asking for a link (default: the same URL with `?sent=1`). */\n sentRedirect?: string\n}\n\nexport type MagicLinkVerify = (params: { email: string; c: Context }) => Promise<StrategyUser | null>\n\nconst MAGIC_SALT = 'ghuri.oauth.MagicLinkStrategy'\nconst EMAIL_RE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\nfunction hashValue(user: any, ts: number): string {\n const lastLogin = user.lastLogin ? new Date(user.lastLogin).toISOString() : ''\n const emailField = (user.constructor?.getEmailFieldName?.() as string | undefined) ?? 'email'\n return `${user.pk}${user.password ?? ''}${lastLogin}${ts}${user[emailField] ?? ''}`\n}\n\nasync function mac(user: any, ts: number, secret?: string): Promise<string> {\n return b64url(await saltedHmac(MAGIC_SALT, hashValue(user, ts), secret, 'sha256'))\n}\n\n/**\n * Passwordless login: `POST { email }` sends a link, `GET ?token=` logs in. Links expire after `maxAge` and stop\n * working after one login (the signature covers `lastLogin`) or a password/email change. The POST answers the\n * same whether or not the address has an account.\n */\nexport class MagicLinkStrategy extends Strategy {\n readonly name: string\n\n constructor(\n readonly options: MagicLinkOptions,\n readonly verify: MagicLinkVerify,\n ) {\n super()\n this.name = options.name ?? 'magic-link'\n }\n\n /** A login token for `user` (exposed for custom delivery). */\n async makeToken(user: any): Promise<string> {\n const ts = Math.floor(Date.now() / 1000)\n return `${b64url(utf8(String(user.pk)))}.${ts.toString(36)}.${await mac(user, ts)}`\n }\n\n async checkToken(token: string): Promise<StrategyUser | null> {\n const [pkPart, tsPart, sig] = token.split('.')\n if (!pkPart || !tsPart || !sig) return null\n const ts = Number.parseInt(tsPart, 36)\n if (\n !Number.isFinite(ts) ||\n Date.now() / 1000 - ts > (this.options.maxAge ?? 900) ||\n ts > Date.now() / 1000 + 60\n ) {\n return null\n }\n let pk: string\n try {\n pk = fromUtf8(fromB64url(pkPart))\n } catch {\n return null\n }\n const User = getUserModel()\n let value: unknown\n try {\n value = User._meta.pk.toPython(pk)\n } catch {\n return null\n }\n const user = await User.objects.filter({ pk: value }).first()\n if (!user) return null\n const secrets = [\n undefined,\n ...((tryGetContext()?.settings.SECRET_KEY_FALLBACKS as string[] | undefined) ?? []),\n ]\n for (const secret of secrets) {\n if (timingSafeEqual(await mac(user, ts, secret), sig)) return user\n }\n return null\n }\n\n async authenticate(c: Context, _options: AuthenticateOptions): Promise<StrategyResult> {\n if (c.req.method === 'POST') return this.request(c)\n const token = c.req.query('token')\n if (!token) return this.pass()\n const user = await this.checkToken(token)\n if (!user) return this.fail('This login link is invalid or has expired.', 400)\n return this.success(user, { next: c.req.query('next') || undefined })\n }\n\n private async request(c: Context): Promise<StrategyResult> {\n const data = await readData(c)\n const email = String(data[this.options.emailField ?? 'email'] ?? '').trim()\n if (!EMAIL_RE.test(email)) return this.fail('Enter a valid email address.', 400)\n const user = await this.verify({ email, c })\n if (user && user.isActive !== false) {\n const url = new URL(c.req.path, `${isSecure(c) ? 'https' : 'http'}://${getHost(c)}`)\n url.searchParams.set('token', await this.makeToken(user))\n const next = typeof data.next === 'string' ? data.next : c.req.query('next')\n if (next) url.searchParams.set('next', next)\n await this.options.send({ email, url: url.toString(), user, c })\n }\n const detail = 'If an account exists for that address, we sent it a login link.'\n if (wantsJson(c)) return { type: 'response', response: c.json({ detail }) }\n return { type: 'redirect', url: this.options.sentRedirect ?? `${c.req.path}?sent=1`, status: 302 }\n }\n}\n\n// ---- BearerStrategy ----\n\nexport type BearerVerify = (\n token: string,\n c: Context,\n) => Promise<StrategyUser | null | { user: StrategyUser; info?: Record<string, unknown> }>\n\n/** `Authorization: Bearer <token>` → `verify(token)`. No header → pass; unknown token → 401 with a challenge. */\nexport class BearerStrategy extends Strategy {\n readonly name: string\n private readonly realm: string\n\n constructor(\n readonly verify: BearerVerify,\n options: { name?: string; realm?: string } = {},\n ) {\n super()\n this.name = options.name ?? 'bearer'\n this.realm = options.realm ?? 'api'\n }\n\n async authenticate(c: Context): Promise<StrategyResult> {\n const header = c.req.header('authorization')\n if (!header) return this.pass()\n const m = /^Bearer\\s+(\\S+)\\s*$/i.exec(header)\n if (!m) return this.pass()\n const result = await this.verify(m[1], c)\n if (!result) {\n return this.fail('Invalid token.', 401, `Bearer realm=\"${this.realm}\", error=\"invalid_token\"`)\n }\n if (\n typeof result === 'object' &&\n 'user' in result &&\n Object.keys(result).every((k) => k === 'user' || k === 'info')\n ) {\n return this.success(result.user, result.info)\n }\n return this.success(result)\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AACA,SAAS,gBAAgB,kBAAkB,cAAc,UAAU,kBAAkB;AACrF,SAAS,SAAS,UAAU,eAAe,iBAAiB;AAwBrD,IAAM,gBAAN,cAA4B,SAAS;AAAA,EAK1C,YACE,UAAgC,CAAC,GACxB,SAAsB,CAAC,EAAE,UAAU,UAAU,EAAE,MAAM,iBAAiB,GAAG,EAAE,UAAU,SAAS,CAAC,GACxG;AACA,UAAM;AAFG;AAGT,SAAK,OAAO,QAAQ,QAAQ;AAC5B,SAAK,gBAAgB,QAAQ,iBAAiB;AAC9C,SAAK,gBAAgB,QAAQ,iBAAiB;AAAA,EAChD;AAAA,EANW;AAAA,EANF;AAAA,EACQ;AAAA,EACA;AAAA,EAYjB,MAAM,aAAa,GAAqC;AACtD,QAAI,EAAE,IAAI,WAAW,OAAQ,QAAO,KAAK,KAAK;AAC9C,UAAM,OAAO,MAAM,SAAS,CAAC;AAC7B,UAAM,WAAW,KAAK,KAAK,aAAa;AACxC,UAAM,WAAW,KAAK,KAAK,aAAa;AACxC,QAAI,OAAO,aAAa,YAAY,CAAC,YAAY,OAAO,aAAa,YAAY,CAAC,UAAU;AAC1F,aAAO,KAAK,KAAK,wBAAwB,GAAG;AAAA,IAC9C;AACA,UAAM,OAAO,MAAM,KAAK,OAAO,EAAE,UAAU,UAAU,EAAE,CAAC;AACxD,WAAO,OAAO,KAAK,QAAQ,IAAI,IAAI,KAAK,KAAK,wBAAwB,GAAG;AAAA,EAC1E;AACF;AAiBA,IAAM,aAAa;AACnB,IAAM,WAAW;AAEjB,SAAS,UAAU,MAAW,IAAoB;AAChD,QAAM,YAAY,KAAK,YAAY,IAAI,KAAK,KAAK,SAAS,EAAE,YAAY,IAAI;AAC5E,QAAM,aAAc,KAAK,aAAa,oBAAoB,KAA4B;AACtF,SAAO,GAAG,KAAK,EAAE,GAAG,KAAK,YAAY,EAAE,GAAG,SAAS,GAAG,EAAE,GAAG,KAAK,UAAU,KAAK,EAAE;AACnF;AAEA,eAAe,IAAI,MAAW,IAAY,QAAkC;AAC1E,SAAO,OAAO,MAAM,WAAW,YAAY,UAAU,MAAM,EAAE,GAAG,QAAQ,QAAQ,CAAC;AACnF;AAOO,IAAM,oBAAN,cAAgC,SAAS;AAAA,EAG9C,YACW,SACA,QACT;AACA,UAAM;AAHG;AACA;AAGT,SAAK,OAAO,QAAQ,QAAQ;AAAA,EAC9B;AAAA,EALW;AAAA,EACA;AAAA,EAJF;AAAA;AAAA,EAWT,MAAM,UAAU,MAA4B;AAC1C,UAAM,KAAK,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACvC,WAAO,GAAG,OAAO,KAAK,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,GAAG,SAAS,EAAE,CAAC,IAAI,MAAM,IAAI,MAAM,EAAE,CAAC;AAAA,EACnF;AAAA,EAEA,MAAM,WAAW,OAA6C;AAC5D,UAAM,CAAC,QAAQ,QAAQ,GAAG,IAAI,MAAM,MAAM,GAAG;AAC7C,QAAI,CAAC,UAAU,CAAC,UAAU,CAAC,IAAK,QAAO;AACvC,UAAM,KAAK,OAAO,SAAS,QAAQ,EAAE;AACrC,QACE,CAAC,OAAO,SAAS,EAAE,KACnB,KAAK,IAAI,IAAI,MAAO,MAAM,KAAK,QAAQ,UAAU,QACjD,KAAK,KAAK,IAAI,IAAI,MAAO,IACzB;AACA,aAAO;AAAA,IACT;AACA,QAAI;AACJ,QAAI;AACF,WAAK,SAAS,WAAW,MAAM,CAAC;AAAA,IAClC,QAAQ;AACN,aAAO;AAAA,IACT;AACA,UAAM,OAAO,aAAa;AAC1B,QAAI;AACJ,QAAI;AACF,cAAQ,KAAK,MAAM,GAAG,SAAS,EAAE;AAAA,IACnC,QAAQ;AACN,aAAO;AAAA,IACT;AACA,UAAM,OAAO,MAAM,KAAK,QAAQ,OAAO,EAAE,IAAI,MAAM,CAAC,EAAE,MAAM;AAC5D,QAAI,CAAC,KAAM,QAAO;AAClB,UAAM,UAAU;AAAA,MACd;AAAA,MACA,GAAK,cAAc,GAAG,SAAS,wBAAiD,CAAC;AAAA,IACnF;AACA,eAAW,UAAU,SAAS;AAC5B,UAAI,gBAAgB,MAAM,IAAI,MAAM,IAAI,MAAM,GAAG,GAAG,EAAG,QAAO;AAAA,IAChE;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,aAAa,GAAY,UAAwD;AACrF,QAAI,EAAE,IAAI,WAAW,OAAQ,QAAO,KAAK,QAAQ,CAAC;AAClD,UAAM,QAAQ,EAAE,IAAI,MAAM,OAAO;AACjC,QAAI,CAAC,MAAO,QAAO,KAAK,KAAK;AAC7B,UAAM,OAAO,MAAM,KAAK,WAAW,KAAK;AACxC,QAAI,CAAC,KAAM,QAAO,KAAK,KAAK,8CAA8C,GAAG;AAC7E,WAAO,KAAK,QAAQ,MAAM,EAAE,MAAM,EAAE,IAAI,MAAM,MAAM,KAAK,OAAU,CAAC;AAAA,EACtE;AAAA,EAEA,MAAc,QAAQ,GAAqC;AACzD,UAAM,OAAO,MAAM,SAAS,CAAC;AAC7B,UAAM,QAAQ,OAAO,KAAK,KAAK,QAAQ,cAAc,OAAO,KAAK,EAAE,EAAE,KAAK;AAC1E,QAAI,CAAC,SAAS,KAAK,KAAK,EAAG,QAAO,KAAK,KAAK,gCAAgC,GAAG;AAC/E,UAAM,OAAO,MAAM,KAAK,OAAO,EAAE,OAAO,EAAE,CAAC;AAC3C,QAAI,QAAQ,KAAK,aAAa,OAAO;AACnC,YAAM,MAAM,IAAI,IAAI,EAAE,IAAI,MAAM,GAAG,SAAS,CAAC,IAAI,UAAU,MAAM,MAAM,QAAQ,CAAC,CAAC,EAAE;AACnF,UAAI,aAAa,IAAI,SAAS,MAAM,KAAK,UAAU,IAAI,CAAC;AACxD,YAAM,OAAO,OAAO,KAAK,SAAS,WAAW,KAAK,OAAO,EAAE,IAAI,MAAM,MAAM;AAC3E,UAAI,KAAM,KAAI,aAAa,IAAI,QAAQ,IAAI;AAC3C,YAAM,KAAK,QAAQ,KAAK,EAAE,OAAO,KAAK,IAAI,SAAS,GAAG,MAAM,EAAE,CAAC;AAAA,IACjE;AACA,UAAM,SAAS;AACf,QAAI,UAAU,CAAC,EAAG,QAAO,EAAE,MAAM,YAAY,UAAU,EAAE,KAAK,EAAE,OAAO,CAAC,EAAE;AAC1E,WAAO,EAAE,MAAM,YAAY,KAAK,KAAK,QAAQ,gBAAgB,GAAG,EAAE,IAAI,IAAI,WAAW,QAAQ,IAAI;AAAA,EACnG;AACF;AAUO,IAAM,iBAAN,cAA6B,SAAS;AAAA,EAI3C,YACW,QACT,UAA6C,CAAC,GAC9C;AACA,UAAM;AAHG;AAIT,SAAK,OAAO,QAAQ,QAAQ;AAC5B,SAAK,QAAQ,QAAQ,SAAS;AAAA,EAChC;AAAA,EANW;AAAA,EAJF;AAAA,EACQ;AAAA,EAWjB,MAAM,aAAa,GAAqC;AACtD,UAAM,SAAS,EAAE,IAAI,OAAO,eAAe;AAC3C,QAAI,CAAC,OAAQ,QAAO,KAAK,KAAK;AAC9B,UAAM,IAAI,uBAAuB,KAAK,MAAM;AAC5C,QAAI,CAAC,EAAG,QAAO,KAAK,KAAK;AACzB,UAAM,SAAS,MAAM,KAAK,OAAO,EAAE,CAAC,GAAG,CAAC;AACxC,QAAI,CAAC,QAAQ;AACX,aAAO,KAAK,KAAK,kBAAkB,KAAK,iBAAiB,KAAK,KAAK,0BAA0B;AAAA,IAC/F;AACA,QACE,OAAO,WAAW,YAClB,UAAU,UACV,OAAO,KAAK,MAAM,EAAE,MAAM,CAAC,MAAM,MAAM,UAAU,MAAM,MAAM,GAC7D;AACA,aAAO,KAAK,QAAQ,OAAO,MAAM,OAAO,IAAI;AAAA,IAC9C;AACA,WAAO,KAAK,QAAQ,MAAM;AAAA,EAC5B;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/local.ts"],"sourcesContent":["// Username/password, magic links and bearer tokens.\nimport { authenticate as authAuthenticate, getUserModel, readData, saltedHmac } from '@ghuri/auth'\nimport { getHost, isSecure, tryGetContext, wantsJson } from '@ghuri/core'\nimport type { Context } from 'hono'\nimport { b64url, fromB64url, fromUtf8, timingSafeEqual, utf8 } from './crypto'\nimport { Strategy } from './strategy'\nimport type { AuthenticateOptions, StrategyResult, StrategyUser } from './types'\n\n// ---- LocalStrategy ----\n\nexport interface LocalStrategyOptions {\n name?: string\n usernameField?: string\n passwordField?: string\n}\n\nexport type LocalVerify = (params: {\n username: string\n password: string\n c: Context\n}) => Promise<StrategyUser | null>\n\n/**\n * Username and password from a JSON or form POST. The default `verify` is `@ghuri/auth`'s `authenticate()`, so\n * `AUTHENTICATION_BACKENDS`, password hashers and `userLoginFailed` all apply.\n */\nexport class LocalStrategy extends Strategy {\n readonly name: string\n private readonly usernameField: string\n private readonly passwordField: string\n\n constructor(\n options: LocalStrategyOptions = {},\n readonly verify: LocalVerify = ({ username, password, c }) => authAuthenticate(c, { username, password }),\n ) {\n super()\n this.name = options.name ?? 'local'\n this.usernameField = options.usernameField ?? 'username'\n this.passwordField = options.passwordField ?? 'password'\n }\n\n async authenticate(c: Context): Promise<StrategyResult> {\n if (c.req.method !== 'POST') return this.pass()\n const data = await readData(c)\n const username = data[this.usernameField]\n const password = data[this.passwordField]\n if (typeof username !== 'string' || !username || typeof password !== 'string' || !password) {\n return this.fail('Missing credentials.', 400)\n }\n const user = await this.verify({ username, password, c })\n return user ? this.success(user) : this.fail('Invalid credentials.', 401)\n }\n}\n\n// ---- MagicLinkStrategy ----\n\nexport interface MagicLinkOptions {\n name?: string\n /** Deliver the link. Called only when `verify` found a user for the address. */\n send: (params: { email: string; url: string; user: StrategyUser; c: Context }) => Promise<void>\n /** Seconds a link stays valid (default 900). */\n maxAge?: number\n emailField?: string\n /** Where forms go after asking for a link (default: the same URL with `?sent=1`). */\n sentRedirect?: string\n}\n\nexport type MagicLinkVerify = (params: { email: string; c: Context }) => Promise<StrategyUser | null>\n\nconst MAGIC_SALT = 'ghuri.oauth.MagicLinkStrategy'\nconst EMAIL_RE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\nfunction hashValue(user: any, ts: number): string {\n const lastLogin = user.lastLogin ? new Date(user.lastLogin).toISOString() : ''\n const emailField = (user.constructor?.getEmailFieldName?.() as string | undefined) ?? 'email'\n return `${user.pk}${user.password ?? ''}${lastLogin}${ts}${user[emailField] ?? ''}`\n}\n\nasync function mac(user: any, ts: number, secret?: string): Promise<string> {\n return b64url(await saltedHmac(MAGIC_SALT, hashValue(user, ts), secret, 'sha256'))\n}\n\n/**\n * Passwordless login: `POST { email }` sends a link, `GET ?token=` logs in. Links expire after `maxAge` and stop\n * working after one login (the signature covers `lastLogin`) or a password/email change. The POST answers the\n * same whether or not the address has an account.\n */\nexport class MagicLinkStrategy extends Strategy {\n readonly name: string\n\n constructor(\n readonly options: MagicLinkOptions,\n readonly verify: MagicLinkVerify,\n ) {\n super()\n this.name = options.name ?? 'magic-link'\n }\n\n /** A login token for `user` (exposed for custom delivery). */\n async makeToken(user: any): Promise<string> {\n const ts = Math.floor(Date.now() / 1000)\n return `${b64url(utf8(String(user.pk)))}.${ts.toString(36)}.${await mac(user, ts)}`\n }\n\n async checkToken(token: string): Promise<StrategyUser | null> {\n const [pkPart, tsPart, sig] = token.split('.')\n if (!pkPart || !tsPart || !sig) return null\n const ts = Number.parseInt(tsPart, 36)\n if (\n !Number.isFinite(ts) ||\n Date.now() / 1000 - ts > (this.options.maxAge ?? 900) ||\n ts > Date.now() / 1000 + 60\n ) {\n return null\n }\n let pk: string\n try {\n pk = fromUtf8(fromB64url(pkPart))\n } catch {\n return null\n }\n const User = getUserModel()\n let value: unknown\n try {\n value = User._meta.pk.toPython(pk)\n } catch {\n return null\n }\n const user = await User.objects.filter({ pk: value }).first()\n if (!user) return null\n const secrets = [\n undefined,\n ...((tryGetContext()?.settings.SECRET_KEY_FALLBACKS as string[] | undefined) ?? []),\n ]\n for (const secret of secrets) {\n if (timingSafeEqual(await mac(user, ts, secret), sig)) return user\n }\n return null\n }\n\n async authenticate(c: Context, _options: AuthenticateOptions): Promise<StrategyResult> {\n if (c.req.method === 'POST') return this.request(c)\n const token = c.req.query('token')\n if (!token) return this.pass()\n const user = await this.checkToken(token)\n if (!user) return this.fail('This login link is invalid or has expired.', 400)\n return this.success(user, { next: c.req.query('next') || undefined })\n }\n\n private async request(c: Context): Promise<StrategyResult> {\n const data = await readData(c)\n const email = String(data[this.options.emailField ?? 'email'] ?? '').trim()\n if (!EMAIL_RE.test(email)) return this.fail('Enter a valid email address.', 400)\n const user = await this.verify({ email, c })\n if (user && user.isActive !== false) {\n const url = new URL(c.req.path, `${isSecure(c) ? 'https' : 'http'}://${getHost(c)}`)\n url.searchParams.set('token', await this.makeToken(user))\n const next = typeof data.next === 'string' ? data.next : c.req.query('next')\n if (next) url.searchParams.set('next', next)\n await this.options.send({ email, url: url.toString(), user, c })\n }\n const detail = 'If an account exists for that address, we sent it a login link.'\n if (wantsJson(c)) return { type: 'response', response: c.json({ detail }) }\n return { type: 'redirect', url: this.options.sentRedirect ?? `${c.req.path}?sent=1`, status: 302 }\n }\n}\n\n// ---- BearerStrategy ----\n\nexport type BearerVerify = (\n token: string,\n c: Context,\n) => Promise<StrategyUser | null | { user: StrategyUser; info?: Record<string, unknown> }>\n\n/** `Authorization: Bearer <token>` → `verify(token)`. No header → pass; unknown token → 401 with a challenge. */\nexport class BearerStrategy extends Strategy {\n readonly name: string\n private readonly realm: string\n\n constructor(\n readonly verify: BearerVerify,\n options: { name?: string; realm?: string } = {},\n ) {\n super()\n this.name = options.name ?? 'bearer'\n this.realm = options.realm ?? 'api'\n }\n\n async authenticate(c: Context): Promise<StrategyResult> {\n const header = c.req.header('authorization')\n if (!header) return this.pass()\n const m = /^Bearer\\s+(\\S+)\\s*$/i.exec(header)\n if (!m) return this.pass()\n const result = await this.verify(m[1], c)\n if (!result) {\n return this.fail('Invalid token.', 401, `Bearer realm=\"${this.realm}\", error=\"invalid_token\"`)\n }\n if (\n typeof result === 'object' &&\n 'user' in result &&\n Object.keys(result).every((k) => k === 'user' || k === 'info')\n ) {\n return this.success(result.user, result.info)\n }\n return this.success(result)\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ghuri/oauth",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
4
|
"description": "Ghuri OAuth: Passport.js-style login strategies (OAuth2, OpenID Connect, local, magic link, bearer) for Workers and Node",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|