@stacksjs/auth 0.72.5 → 0.72.7
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/middleware.d.ts +20 -0
- package/dist/middleware.js +1 -1
- package/dist/passkey.d.ts +50 -0
- package/dist/passkey.js +1 -1
- package/package.json +3 -3
package/dist/middleware.d.ts
CHANGED
|
@@ -10,6 +10,26 @@
|
|
|
10
10
|
* protected route (#2306).
|
|
11
11
|
*/
|
|
12
12
|
export declare function authMiddleware(request: any): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* The authenticated user, for a middleware or action that needs to reason
|
|
15
|
+
* about one.
|
|
16
|
+
*
|
|
17
|
+
* Every authorization middleware in the scaffold used to open with the same
|
|
18
|
+
* line:
|
|
19
|
+
*
|
|
20
|
+
* const user = request.user || request._user || request._authenticatedUser
|
|
21
|
+
*
|
|
22
|
+
* Two things were wrong with it. `_user` is assigned nowhere in the framework,
|
|
23
|
+
* so it was a dead term. And `user` is a lazily-resolving MACRO - a function -
|
|
24
|
+
* so on any request carrying it, `user` WAS the function: truthy enough to
|
|
25
|
+
* pass the "is anyone signed in" check, then missing every field the caller
|
|
26
|
+
* went on to read, which surfaces as a confusing 403 rather than an honest
|
|
27
|
+
* 401.
|
|
28
|
+
*
|
|
29
|
+
* Resolving it here means every caller agrees on what "the user" is, and the
|
|
30
|
+
* answer is a user rather than a callable.
|
|
31
|
+
*/
|
|
32
|
+
export declare function authenticatedUser(request: any): Promise<any | undefined>;
|
|
13
33
|
/**
|
|
14
34
|
* Auth middleware object with handle method (for compatibility with middleware loader)
|
|
15
35
|
* @defaultValue `{ name: 'auth' }`
|
package/dist/middleware.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Auth}from"./authentication";import{requestToken}from"./request-token";export async function authMiddleware(request){const bearerToken=requestToken(request);if(!bearerToken){const error=Error("No authentication token provided.");error.statusCode=401;throw error}const user=await Auth.getUserFromToken(bearerToken);if(!user){const error=Error("Invalid or expired authentication token.");error.statusCode=401;throw error}Auth.setUser(user);request._authenticatedUser=user;const accessToken=await Auth.currentAccessToken();request._currentAccessToken=accessToken}export const authMiddlewareHandler={name:"auth",handle:authMiddleware};
|
|
1
|
+
import{Auth}from"./authentication";import{requestToken}from"./request-token";export async function authMiddleware(request){const bearerToken=requestToken(request);if(!bearerToken){const error=Error("No authentication token provided.");error.statusCode=401;throw error}const user=await Auth.getUserFromToken(bearerToken);if(!user){const error=Error("Invalid or expired authentication token.");error.statusCode=401;throw error}Auth.setUser(user);request._authenticatedUser=user;const accessToken=await Auth.currentAccessToken();request._currentAccessToken=accessToken}export const authMiddlewareHandler={name:"auth",handle:authMiddleware};export async function authenticatedUser(request){const cached=request?._authenticatedUser;if(cached&&typeof cached==="object")return cached;const macro=request?.user;if(typeof macro==="function"){const resolved=await macro();return resolved&&typeof resolved==="object"?resolved:void 0}if(macro&&typeof macro==="object")return macro;return}
|
package/dist/passkey.d.ts
CHANGED
|
@@ -12,6 +12,17 @@ export type {
|
|
|
12
12
|
RegistrationOptions,
|
|
13
13
|
AuthenticationOptions,
|
|
14
14
|
} from '@stacksjs/ts-auth';
|
|
15
|
+
/**
|
|
16
|
+
* Turn a user's stored passkeys into credential descriptors for the options a
|
|
17
|
+
* server hands the browser.
|
|
18
|
+
*
|
|
19
|
+
* Both `GenerateRegistrationAction` (as `excludeCredentials`) and
|
|
20
|
+
* `GenerateAuthenticationAction` (as `allowCredentials`) need the same three
|
|
21
|
+
* facts: the stored base64url id, the `type: 'public-key'` the spec requires,
|
|
22
|
+
* and the transports. Building it in one place is also where the
|
|
23
|
+
* ArrayBuffer-vs-JSON boundary gets explained once rather than at each call.
|
|
24
|
+
*/
|
|
25
|
+
export declare function passkeyDescriptors(passkeys: readonly PasskeyAttribute[]): PublicKeyCredentialDescriptorJSON[];
|
|
15
26
|
export declare function getUserPasskeys(userId: number): Promise<PasskeyAttribute[]>;
|
|
16
27
|
export declare function getUserPasskey(userId: number, passkeyId: string): Promise<PasskeyAttribute | undefined>;
|
|
17
28
|
/**
|
|
@@ -51,6 +62,45 @@ export declare function storeWebAuthnChallenge(userId: number, challenge: string
|
|
|
51
62
|
* failure.
|
|
52
63
|
*/
|
|
53
64
|
export declare function consumeWebAuthnChallenge(userId: number, purpose: WebAuthnChallengePurpose): Promise<Uint8Array | null>;
|
|
65
|
+
/**
|
|
66
|
+
* A credential descriptor as it crosses the wire.
|
|
67
|
+
*
|
|
68
|
+
* `ts-auth`'s `PublicKeyCredentialRequestOptions` describes the shape the
|
|
69
|
+
* BROWSER wants: `id` as an ArrayBuffer. A server handing those options to a
|
|
70
|
+
* client has to send JSON, and an ArrayBuffer serializes to `{}` - so ids
|
|
71
|
+
* travel as the base64url strings the passkey rows already store, and the
|
|
72
|
+
* client turns them back into buffers before calling `navigator.credentials`.
|
|
73
|
+
*
|
|
74
|
+
* The passkey actions were already written against these names; they simply
|
|
75
|
+
* had nowhere to import them from, so the whole file typechecked as `any`.
|
|
76
|
+
*/
|
|
77
|
+
export declare interface PublicKeyCredentialDescriptorJSON {
|
|
78
|
+
id: string
|
|
79
|
+
type: 'public-key'
|
|
80
|
+
transports?: Array<'ble' | 'internal' | 'nfc' | 'usb' | 'hybrid'>
|
|
81
|
+
}
|
|
82
|
+
export declare interface PublicKeyCredentialRequestOptionsJSON {
|
|
83
|
+
challenge: Uint8Array
|
|
84
|
+
rpId?: string
|
|
85
|
+
allowCredentials?: PublicKeyCredentialDescriptorJSON[]
|
|
86
|
+
userVerification?: 'required' | 'preferred' | 'discouraged'
|
|
87
|
+
timeout?: number
|
|
88
|
+
}
|
|
89
|
+
export declare interface PublicKeyCredentialCreationOptionsJSON {
|
|
90
|
+
challenge: Uint8Array
|
|
91
|
+
rp: { name: string, id?: string }
|
|
92
|
+
user: { id: Uint8Array | string, name: string, displayName: string }
|
|
93
|
+
pubKeyCredParams: Array<{ alg: number, type: 'public-key' }>
|
|
94
|
+
timeout?: number
|
|
95
|
+
attestation?: string
|
|
96
|
+
authenticatorSelection?: {
|
|
97
|
+
authenticatorAttachment?: 'platform' | 'cross-platform'
|
|
98
|
+
requireResidentKey?: boolean
|
|
99
|
+
residentKey?: 'discouraged' | 'preferred' | 'required'
|
|
100
|
+
userVerification?: 'required' | 'preferred' | 'discouraged'
|
|
101
|
+
}
|
|
102
|
+
excludeCredentials?: PublicKeyCredentialDescriptorJSON[]
|
|
103
|
+
}
|
|
54
104
|
export declare interface PasskeyAttribute {
|
|
55
105
|
id: string
|
|
56
106
|
cred_public_key: string
|
package/dist/passkey.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Buffer}from"node:buffer";import{db,sqlDateTime,parseSqlDateTime}from"@stacksjs/database";import{User}from"@stacksjs/orm";export{generateRegistrationOptions,generateAuthenticationOptions,verifyRegistrationResponse,verifyAuthenticationResponse,startRegistration,startAuthentication,browserSupportsWebAuthn,browserSupportsWebAuthnAutofill,platformAuthenticatorIsAvailable}from"@stacksjs/ts-auth";export async function getUserPasskeys(userId){return await db.selectFrom("passkeys").selectAll().where("user_id","=",userId).execute()}export async function getUserPasskey(userId,passkeyId){return await db.selectFrom("passkeys").selectAll().where("id","=",passkeyId).where("user_id","=",userId).executeTakeFirst()}export async function updatePasskeyCounter(userId,passkeyId,newCounter){const passkey=await getUserPasskey(userId,passkeyId);if(!passkey)return!1;const stored=Number(passkey.counter??0);if(newCounter<=stored&&!(newCounter===0&&stored===0))return!1;await db.updateTable("passkeys").set({counter:newCounter,last_used_at:formatDateTime()}).where("id","=",passkeyId).where("user_id","=",userId).execute();return!0}export async function setCurrentRegistrationOptions(user,verified){const credentialId=verified.registrationInfo?.credential.id,credentialPublicKey=verified.registrationInfo?.credential.publicKey;if(!credentialId)throw Error("[auth/passkey] WebAuthn registration response is missing credential.id");if(!credentialPublicKey)throw Error("[auth/passkey] WebAuthn registration response is missing credential.publicKey");const passkeyData={id:credentialId,cred_public_key:JSON.stringify(credentialPublicKey),user_id:user.id,webauthn_user_id:user.email||"",counter:verified.registrationInfo?.credential.counter||0,credential_type:verified.registrationInfo?.credentialType||"",device_type:verified.registrationInfo?.credentialDeviceType||"",backup_eligible:!1,backup_status:verified.registrationInfo?.credentialBackedUp||!1,transports:JSON.stringify(["internal"]),last_used_at:formatDateTime()};await db.insertInto("passkeys").values(passkeyData).executeTakeFirstOrThrow()}function formatDateTime(){const date=new Date,pad=(num)=>String(num).padStart(2,"0"),year=date.getFullYear(),month=pad(date.getMonth()+1),day=pad(date.getDate()),hours=pad(date.getHours()),minutes=pad(date.getMinutes()),seconds=pad(date.getSeconds());return`${year}-${month}-${day} ${hours}:${minutes}:${seconds}`}const DEFAULT_CHALLENGE_TTL_SECONDS=300;export async function storeWebAuthnChallenge(userId,challenge,purpose,ttlSeconds=DEFAULT_CHALLENGE_TTL_SECONDS){const expiresAt=sqlDateTime(new Date(Date.now()+ttlSeconds*1000)),encodedChallenge=typeof challenge==="string"?challenge:Buffer.from(challenge).toString("base64url");await db.deleteFrom("webauthn_challenges").where("user_id","=",userId).where("purpose","=",purpose).execute();await db.insertInto("webauthn_challenges").values({user_id:userId,challenge:encodedChallenge,purpose,expires_at:expiresAt}).execute()}export async function consumeWebAuthnChallenge(userId,purpose){const row=await db.selectFrom("webauthn_challenges").where("user_id","=",userId).where("purpose","=",purpose).selectAll().executeTakeFirst();if(!row)return null;await db.deleteFrom("webauthn_challenges").where("user_id","=",userId).where("purpose","=",purpose).execute();const expiresAt=parseSqlDateTime(row.expires_at)?.getTime()??0;if(Date.now()>expiresAt)return null;return Buffer.from(String(row.challenge),"base64url")}
|
|
1
|
+
import{Buffer}from"node:buffer";import{db,sqlDateTime,parseSqlDateTime}from"@stacksjs/database";import{User}from"@stacksjs/orm";export{generateRegistrationOptions,generateAuthenticationOptions,verifyRegistrationResponse,verifyAuthenticationResponse,startRegistration,startAuthentication,browserSupportsWebAuthn,browserSupportsWebAuthnAutofill,platformAuthenticatorIsAvailable}from"@stacksjs/ts-auth";export function passkeyDescriptors(passkeys){return passkeys.map((passkey)=>({id:passkey.id,type:"public-key",transports:parseTransports(passkey.transports)}))}function parseTransports(stored){if(!stored)return["internal"];try{const parsed=JSON.parse(stored);return Array.isArray(parsed)&&parsed.length?parsed:["internal"]}catch{return["internal"]}}export async function getUserPasskeys(userId){return await db.selectFrom("passkeys").selectAll().where("user_id","=",userId).execute()}export async function getUserPasskey(userId,passkeyId){return await db.selectFrom("passkeys").selectAll().where("id","=",passkeyId).where("user_id","=",userId).executeTakeFirst()}export async function updatePasskeyCounter(userId,passkeyId,newCounter){const passkey=await getUserPasskey(userId,passkeyId);if(!passkey)return!1;const stored=Number(passkey.counter??0);if(newCounter<=stored&&!(newCounter===0&&stored===0))return!1;await db.updateTable("passkeys").set({counter:newCounter,last_used_at:formatDateTime()}).where("id","=",passkeyId).where("user_id","=",userId).execute();return!0}export async function setCurrentRegistrationOptions(user,verified){const credentialId=verified.registrationInfo?.credential.id,credentialPublicKey=verified.registrationInfo?.credential.publicKey;if(!credentialId)throw Error("[auth/passkey] WebAuthn registration response is missing credential.id");if(!credentialPublicKey)throw Error("[auth/passkey] WebAuthn registration response is missing credential.publicKey");const passkeyData={id:credentialId,cred_public_key:JSON.stringify(credentialPublicKey),user_id:user.id,webauthn_user_id:user.email||"",counter:verified.registrationInfo?.credential.counter||0,credential_type:verified.registrationInfo?.credentialType||"",device_type:verified.registrationInfo?.credentialDeviceType||"",backup_eligible:!1,backup_status:verified.registrationInfo?.credentialBackedUp||!1,transports:JSON.stringify(["internal"]),last_used_at:formatDateTime()};await db.insertInto("passkeys").values(passkeyData).executeTakeFirstOrThrow()}function formatDateTime(){const date=new Date,pad=(num)=>String(num).padStart(2,"0"),year=date.getFullYear(),month=pad(date.getMonth()+1),day=pad(date.getDate()),hours=pad(date.getHours()),minutes=pad(date.getMinutes()),seconds=pad(date.getSeconds());return`${year}-${month}-${day} ${hours}:${minutes}:${seconds}`}const DEFAULT_CHALLENGE_TTL_SECONDS=300;export async function storeWebAuthnChallenge(userId,challenge,purpose,ttlSeconds=DEFAULT_CHALLENGE_TTL_SECONDS){const expiresAt=sqlDateTime(new Date(Date.now()+ttlSeconds*1000)),encodedChallenge=typeof challenge==="string"?challenge:Buffer.from(challenge).toString("base64url");await db.deleteFrom("webauthn_challenges").where("user_id","=",userId).where("purpose","=",purpose).execute();await db.insertInto("webauthn_challenges").values({user_id:userId,challenge:encodedChallenge,purpose,expires_at:expiresAt}).execute()}export async function consumeWebAuthnChallenge(userId,purpose){const row=await db.selectFrom("webauthn_challenges").where("user_id","=",userId).where("purpose","=",purpose).selectAll().executeTakeFirst();if(!row)return null;await db.deleteFrom("webauthn_challenges").where("user_id","=",userId).where("purpose","=",purpose).execute();const expiresAt=parseSqlDateTime(row.expires_at)?.getTime()??0;if(Date.now()>expiresAt)return null;return Buffer.from(String(row.challenge),"base64url")}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/auth",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.72.
|
|
5
|
+
"version": "0.72.7",
|
|
6
6
|
"description": "A more simplistic way to authenticate.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"better-dx": "^0.2.23",
|
|
64
|
-
"@stacksjs/error-handling": "0.72.
|
|
65
|
-
"@stacksjs/router": "0.72.
|
|
64
|
+
"@stacksjs/error-handling": "0.72.7",
|
|
65
|
+
"@stacksjs/router": "0.72.7"
|
|
66
66
|
}
|
|
67
67
|
}
|