@stacksjs/auth 0.70.363 → 0.70.365
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/authentication.d.ts +1 -0
- package/dist/authentication.js +1 -1
- package/dist/cookie-auth.d.ts +21 -87
- package/dist/cookie-auth.js +1 -1
- package/dist/cookie.d.ts +84 -0
- package/dist/cookie.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/middleware.d.ts +8 -1
- package/dist/middleware.js +1 -1
- package/dist/request-token.d.ts +16 -0
- package/dist/request-token.js +1 -0
- package/package.json +3 -3
package/dist/authentication.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { requestToken } from './request-token';
|
|
1
2
|
import { User } from '@stacksjs/orm';
|
|
2
3
|
import type { AuthCredentials, AuthToken, NewAccessToken, PersonalAccessToken, TokenCreateOptions } from '@stacksjs/types';
|
|
3
4
|
declare type UserModel = NonNullable<Awaited<ReturnType<typeof User.find>>>;
|
package/dist/authentication.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{config}from"@stacksjs/config";import{db,parseSqlDateTime}from"@stacksjs/database";import{HttpError}from"@stacksjs/error-handling";import{formatDate,User}from"@stacksjs/orm";import{getCurrentRequest,request}from"@stacksjs/router";import{Buffer}from"node:buffer";import{createHash,randomBytes,timingSafeEqual}from"node:crypto";import{decrypt,encrypt,verifyHash}from"@stacksjs/security";import{log}from"@stacksjs/logging";import{DUMMY_BCRYPT_HASH}from"./internal-constants";import{RateLimiter}from"./rate-limiter";const REQUEST_AUTH_STATE_KEY=Symbol.for("stacks.requestAuthState");function authStateOrNull(){const req=getCurrentRequest();if(!req)return null;let state=req[REQUEST_AUTH_STATE_KEY];if(!state){state={};req[REQUEST_AUTH_STATE_KEY]=state}return state}function hashToken(token){return createHash("sha256").update(token).digest("hex")}import{createToken as createRawToken,getPasswordChangedAt,isIssuedBeforePasswordChange,parseScopes}from"./tokens";export class Auth{static getBearerToken(){let bearerToken=request.bearerToken?.();if(!bearerToken){const authHeader=request.headers?.get?.("authorization")||request.headers?.get?.("Authorization");if(authHeader&&authHeader.startsWith("Bearer "))bearerToken=authHeader.substring(7)}return bearerToken||null}static parseToken(token){const firstColonIndex=token.indexOf(":");if(firstColonIndex===-1)return null;const plainToken=token.substring(0,firstColonIndex),encryptedId=token.substring(firstColonIndex+1);if(!plainToken||!encryptedId)return null;return{plainToken,encryptedId}}static async getClientSecret(){const state=authStateOrNull();if(state?.clientSecret)return state.clientSecret;const client=await this.getPersonalAccessClient();if(state)state.clientSecret=client.secret;return client.secret}static async encryptTokenId(id){return await encrypt(String(id))}static async decryptTokenId(encryptedId){try{return await decrypt(encryptedId)}catch{try{const clientSecret=await this.getClientSecret();return await decrypt(encryptedId,clientSecret)}catch{return null}}}static async getPersonalAccessClient(){try{const client=await db.selectFrom("oauth_clients").where("personal_access_client","=",!0).where("revoked","=",!1).selectAll().executeTakeFirst();if(!client)throw new HttpError(500,"No personal access client found. Please run `./buddy auth:setup` first.");return client}catch(error){if(error instanceof Error&&error.message.includes("does not exist"))throw new HttpError(500,"OAuth tables not found. Please run `./buddy auth:setup` first.");throw error}}static async validateClient(clientId,clientSecret){const client=await db.selectFrom("oauth_clients").where("id","=",clientId).where("revoked","=",!1).selectAll().executeTakeFirst(),provided=Buffer.from(clientSecret);if(!client?.secret){const dummy=Buffer.alloc(Math.max(provided.length,1)),padded=provided.length>0?provided:Buffer.alloc(1);timingSafeEqual(dummy,padded);return!1}const stored=String(client.secret);if(stored.startsWith("$2"))return await verifyHash(clientSecret,stored);const storedBuf=Buffer.from(stored);if(storedBuf.length!==provided.length){timingSafeEqual(storedBuf,storedBuf);return!1}return timingSafeEqual(storedBuf,provided)}static async getTokenFromId(tokenId){const result=await db.selectFrom("oauth_access_tokens").where("id","=",tokenId).selectAll().executeTakeFirst();if(!result)return null;const token=result;return{id:token.id,userId:token.user_id,clientId:token.oauth_client_id,name:token.name||"auth-token",scopes:parseScopes(token.scopes),abilities:parseScopes(token.scopes),expiresAt:parseSqlDateTime(token.expires_at),createdAt:token.created_at?new Date(String(token.created_at)):new Date,updatedAt:token.updated_at?new Date(String(token.updated_at)):new Date,revoked:!!token.revoked}}static async attempt(credentials){const username=config.auth.username||"email",password=config.auth.password||"password",email=credentials[username];if(!email)return!1;const isRateLimited=await RateLimiter.isRateLimited(email),user=await User.where("email","=",email).first(),authPass=credentials[password]||"",hashToVerify=user?.password||DUMMY_BCRYPT_HASH,hashCheck=await verifyHash(authPass,hashToVerify);if(isRateLimited)return!1;if(hashCheck&&user){await RateLimiter.resetAttempts(email);const state=authStateOrNull();if(state)state.authUser=user;return!0}await RateLimiter.recordFailedAttempt(email);return!1}static async validate(credentials){const username=config.auth.username||"email",password=config.auth.password||"password",email=credentials[username];if(!email)return!1;const user=await User.where("email","=",email).first(),authPass=credentials[password]||"",hashToVerify=user?.password||DUMMY_BCRYPT_HASH;return await verifyHash(authPass,hashToVerify)&&!!user}static async login(credentials,options){if(!await this.attempt(credentials))return null;const username=config.auth.username||"email",authedUser=authStateOrNull()?.authUser??await User.where(username,"=",credentials[username]).first();if(!authedUser)return null;const{plainTextToken,refreshToken,expiresIn}=await this.createTokenForUser(authedUser,options);return{user:authedUser,token:plainTextToken,refreshToken,expiresIn}}static async loginUsingId(userId,options){const user=await User.find(userId);if(!user)return null;const state=authStateOrNull();if(state)state.authUser=user;const{plainTextToken,refreshToken,expiresIn}=await this.createTokenForUser(user,options);return{user,token:plainTextToken,refreshToken,expiresIn}}static async logout(){const bearerToken=this.getBearerToken();if(bearerToken){const accessToken=await db.selectFrom("oauth_access_tokens").where("token","=",hashToken(bearerToken)).select(["id"]).executeTakeFirst();if(accessToken)await db.updateTable("oauth_refresh_tokens").set({revoked:!0}).where("access_token_id","=",Number(accessToken.id)).execute();await this.revokeToken(bearerToken)}const state=authStateOrNull();if(state){state.authUser=void 0;state.currentToken=void 0}}static async user(){const state=authStateOrNull();if(state?.authUser)return state.authUser;const bearerToken=this.getBearerToken();if(!bearerToken)return;const user=await this.getUserFromToken(bearerToken);if(user&&state)state.authUser=user;return user}static async check(){return await this.user()!==void 0}static async guest(){return!await this.check()}static async id(){return(await this.user())?.id}static setUser(user){const state=authStateOrNull();if(state)state.authUser=user}static async createTokenForUser(user,options){const name=options?.name??config.auth.defaultTokenName??"auth-token",abilities=options?.abilities??options?.scopes??config.auth.defaultAbilities??["*"],accessTtlMs=options?.expiresInMinutes!==void 0?options.expiresInMinutes*60*1000:config.auth.tokenExpiry??3600000,expiresAt=options?.expiresAt??new Date(Date.now()+accessTtlMs),expiresInMinutes=Math.max(1,Math.floor((expiresAt.getTime()-Date.now())/60000)),refreshExpiresInDays=options?.refreshExpiresInDays??Math.max(1,Math.round((config.auth.refreshTokenExpiry??2592000000)/86400000));log.debug(`[auth] Creating token for user#${user.id}: ${name}`);const result=await createRawToken(user.id,name,abilities,{expiresInMinutes,withRefreshToken:options?.withRefreshToken!==!1,refreshExpiresInDays,userAgent:options?.userAgent??null,ipAddress:options?.ipAddress??null}),plainTextToken=result.plainTextToken;return{accessToken:{id:result.accessToken.id,userId:result.accessToken.userId,clientId:result.accessToken.clientId,name:result.accessToken.name,scopes:result.accessToken.scopes,abilities,expiresAt:result.accessToken.expiresAt??expiresAt,createdAt:result.accessToken.createdAt,updatedAt:result.accessToken.updatedAt,revoked:result.accessToken.revoked,plainTextToken},plainTextToken,refreshToken:result.refreshToken,expiresIn:result.expiresIn}}static async createToken(user,name=config.auth.defaultTokenName||"auth-token",abilities=config.auth.defaultAbilities||["*"]){const{plainTextToken}=await this.createTokenForUser(user,{name,abilities});return plainTextToken}static async requestToken(credentials,clientId,clientSecret){if(!await this.validateClient(clientId,clientSecret))throw new HttpError(401,"Invalid client credentials");const isValid=await this.attempt(credentials),authedUser=authStateOrNull()?.authUser;if(!isValid||!authedUser)return null;return{token:await this.createToken(authedUser,"user-auth-token")}}static async validateToken(token){const hashedPlainToken=hashToken(token),accessToken=await db.selectFrom("oauth_access_tokens").where("token","=",hashedPlainToken).selectAll().executeTakeFirst();if(!accessToken)return!1;log.debug(`[auth] Token validated for token#${accessToken.id}`);if(accessToken.expires_at&&(parseSqlDateTime(accessToken.expires_at)??new Date(0))<new Date){await db.deleteFrom("oauth_access_tokens").where("id","=",accessToken.id).execute();return!1}if(accessToken.revoked)return!1;if(isIssuedBeforePasswordChange(accessToken.created_at,await getPasswordChangedAt(accessToken.user_id)))return!1;await db.updateTable("oauth_access_tokens").set({updated_at:formatDate(new Date)}).where("id","=",accessToken.id).execute();return!0}static async getUserFromToken(token){const hashedPlainToken=hashToken(token),accessToken=await db.selectFrom("oauth_access_tokens").where("token","=",hashedPlainToken).selectAll().executeTakeFirst();if(!accessToken)return;if(accessToken.expires_at&&(parseSqlDateTime(accessToken.expires_at)??new Date(0))<new Date){await db.deleteFrom("oauth_access_tokens").where("id","=",accessToken.id).execute();return}if(accessToken.revoked)return;const idleMs=config.auth?.idleTimeout??0;if(idleMs>0){const lastSeen=parseSqlDateTime(accessToken.updated_at??accessToken.created_at);if(lastSeen&&Date.now()-lastSeen.getTime()>idleMs){await db.updateTable("oauth_access_tokens").set({revoked:!0}).where("id","=",accessToken.id).execute();return}}const stateForToken=authStateOrNull();if(stateForToken)stateForToken.currentToken=await this.getTokenFromId(accessToken.id)??void 0;await db.updateTable("oauth_access_tokens").set({updated_at:formatDate(new Date)}).where("id","=",accessToken.id).execute();if(!accessToken?.user_id)return;const user=await User.find(accessToken.user_id);if(isIssuedBeforePasswordChange(accessToken.created_at,await getPasswordChangedAt(accessToken.user_id)))return;return user}static async currentAccessToken(){const state=authStateOrNull();if(state?.currentToken)return state.currentToken;const bearerToken=this.getBearerToken();if(!bearerToken)return;const accessToken=await db.selectFrom("oauth_access_tokens").where("token","=",hashToken(bearerToken)).select(["id"]).executeTakeFirst();if(!accessToken)return;const token=await this.getTokenFromId(Number(accessToken.id));if(token&&state)state.currentToken=token;return token??void 0}static async tokenCan(ability){const token=await this.currentAccessToken();if(!token)return!1;if(token.abilities.includes("*"))return!0;return token.abilities.includes(ability)}static async tokenCant(ability){return!await this.tokenCan(ability)}static async tokenAbilities(){return(await this.currentAccessToken())?.abilities??[]}static async tokenCanAll(abilities){const token=await this.currentAccessToken();if(!token)return!1;if(token.abilities.includes("*"))return!0;return abilities.every((a)=>token.abilities.includes(a))}static async tokenCanAny(abilities){const token=await this.currentAccessToken();if(!token)return!1;if(token.abilities.includes("*"))return!0;return abilities.some((a)=>token.abilities.includes(a))}static async tokens(userId){const uid=userId??await this.id();if(!uid)return[];return(await db.selectFrom("oauth_access_tokens").where("user_id","=",uid).where("revoked","=",!1).selectAll().execute()).map((token)=>({id:Number(token.id),userId:Number(token.user_id),clientId:Number(token.oauth_client_id),name:String(token.name||"auth-token"),scopes:parseScopes(String(token.scopes??"")),abilities:parseScopes(String(token.scopes??"")),expiresAt:parseSqlDateTime(token.expires_at),createdAt:token.created_at?new Date(String(token.created_at)):new Date,updatedAt:token.updated_at?new Date(String(token.updated_at)):new Date,revoked:!!token.revoked}))}static async revokeToken(token){await db.updateTable("oauth_access_tokens").set({revoked:!0,updated_at:formatDate(new Date)}).where("token","=",hashToken(token)).execute()}static async revokeTokenById(tokenId){await db.updateTable("oauth_access_tokens").set({revoked:!0,updated_at:formatDate(new Date)}).where("id","=",tokenId).execute()}static async revokeAllTokens(userId){const uid=userId??await this.id();if(!uid)return;await db.updateTable("oauth_access_tokens").set({revoked:!0,updated_at:formatDate(new Date)}).where("user_id","=",uid).execute()}static async revokeOtherTokens(userId){const uid=userId??await this.id();if(!uid)return;const currentToken=await this.currentAccessToken();if(!currentToken)return;await db.updateTable("oauth_access_tokens").set({revoked:!0,updated_at:formatDate(new Date)}).where("user_id","=",uid).where("id","!=",currentToken.id).execute()}static async pruneExpiredTokens(){const result=await db.deleteFrom("oauth_access_tokens").where("expires_at","<",formatDate(new Date)).executeTakeFirst();return Number(result?.numDeletedRows)||0}static async pruneRevokedTokens(){const result=await db.deleteFrom("oauth_access_tokens").where("revoked","=",!0).executeTakeFirst();return Number(result?.numDeletedRows)||0}static async rotateToken(oldToken){const{findToken:findRawToken,revokeToken:revokeRawToken}=await import("./tokens"),existing=await findRawToken(oldToken);if(!existing)return null;const remainingMs=existing.expiresAt?existing.expiresAt.getTime()-Date.now():config.auth.tokenExpiry??3600000,expiresInMinutes=Math.max(1,Math.floor(remainingMs/60000));await revokeRawToken(oldToken);const user=await User.find(existing.userId);if(!user)return null;return(await this.createTokenForUser(user,{name:existing.name,abilities:existing.scopes??["*"],expiresInMinutes,withRefreshToken:!1})).plainTextToken}static async findToken(tokenId){return this.getTokenFromId(tokenId)}static async once(credentials){const username=config.auth.username||"email",password=config.auth.password||"password",email=credentials[username];if(!email)return!1;const user=await User.where("email","=",email).first(),authPass=credentials[password]||"",hashToVerify=user?.password||DUMMY_BCRYPT_HASH;if(await verifyHash(authPass,hashToVerify)&&user){const state=authStateOrNull();if(state)state.authUser=user;return!0}return!1}static guard(_name){return this}static viaRemember(){return!1}static clearState(){const state=authStateOrNull();if(state){state.authUser=void 0;state.currentToken=void 0;state.clientSecret=void 0}}}
|
|
1
|
+
import{config}from"@stacksjs/config";import{db,parseSqlDateTime}from"@stacksjs/database";import{HttpError}from"@stacksjs/error-handling";import{formatDate,User}from"@stacksjs/orm";import{getCurrentRequest,request}from"@stacksjs/router";import{requestToken}from"./request-token";import{Buffer}from"node:buffer";import{createHash,randomBytes,timingSafeEqual}from"node:crypto";import{decrypt,encrypt,verifyHash}from"@stacksjs/security";import{log}from"@stacksjs/logging";import{DUMMY_BCRYPT_HASH}from"./internal-constants";import{RateLimiter}from"./rate-limiter";const REQUEST_AUTH_STATE_KEY=Symbol.for("stacks.requestAuthState");function authStateOrNull(){const req=getCurrentRequest();if(!req)return null;let state=req[REQUEST_AUTH_STATE_KEY];if(!state){state={};req[REQUEST_AUTH_STATE_KEY]=state}return state}function hashToken(token){return createHash("sha256").update(token).digest("hex")}import{createToken as createRawToken,getPasswordChangedAt,isIssuedBeforePasswordChange,parseScopes}from"./tokens";export class Auth{static getBearerToken(){return requestToken(request)}static parseToken(token){const firstColonIndex=token.indexOf(":");if(firstColonIndex===-1)return null;const plainToken=token.substring(0,firstColonIndex),encryptedId=token.substring(firstColonIndex+1);if(!plainToken||!encryptedId)return null;return{plainToken,encryptedId}}static async getClientSecret(){const state=authStateOrNull();if(state?.clientSecret)return state.clientSecret;const client=await this.getPersonalAccessClient();if(state)state.clientSecret=client.secret;return client.secret}static async encryptTokenId(id){return await encrypt(String(id))}static async decryptTokenId(encryptedId){try{return await decrypt(encryptedId)}catch{try{const clientSecret=await this.getClientSecret();return await decrypt(encryptedId,clientSecret)}catch{return null}}}static async getPersonalAccessClient(){try{const client=await db.selectFrom("oauth_clients").where("personal_access_client","=",!0).where("revoked","=",!1).selectAll().executeTakeFirst();if(!client)throw new HttpError(500,"No personal access client found. Please run `./buddy auth:setup` first.");return client}catch(error){if(error instanceof Error&&error.message.includes("does not exist"))throw new HttpError(500,"OAuth tables not found. Please run `./buddy auth:setup` first.");throw error}}static async validateClient(clientId,clientSecret){const client=await db.selectFrom("oauth_clients").where("id","=",clientId).where("revoked","=",!1).selectAll().executeTakeFirst(),provided=Buffer.from(clientSecret);if(!client?.secret){const dummy=Buffer.alloc(Math.max(provided.length,1)),padded=provided.length>0?provided:Buffer.alloc(1);timingSafeEqual(dummy,padded);return!1}const stored=String(client.secret);if(stored.startsWith("$2"))return await verifyHash(clientSecret,stored);const storedBuf=Buffer.from(stored);if(storedBuf.length!==provided.length){timingSafeEqual(storedBuf,storedBuf);return!1}return timingSafeEqual(storedBuf,provided)}static async getTokenFromId(tokenId){const result=await db.selectFrom("oauth_access_tokens").where("id","=",tokenId).selectAll().executeTakeFirst();if(!result)return null;const token=result;return{id:token.id,userId:token.user_id,clientId:token.oauth_client_id,name:token.name||"auth-token",scopes:parseScopes(token.scopes),abilities:parseScopes(token.scopes),expiresAt:parseSqlDateTime(token.expires_at),createdAt:token.created_at?new Date(String(token.created_at)):new Date,updatedAt:token.updated_at?new Date(String(token.updated_at)):new Date,revoked:!!token.revoked}}static async attempt(credentials){const username=config.auth.username||"email",password=config.auth.password||"password",email=credentials[username];if(!email)return!1;const isRateLimited=await RateLimiter.isRateLimited(email),user=await User.where("email","=",email).first(),authPass=credentials[password]||"",hashToVerify=user?.password||DUMMY_BCRYPT_HASH,hashCheck=await verifyHash(authPass,hashToVerify);if(isRateLimited)return!1;if(hashCheck&&user){await RateLimiter.resetAttempts(email);const state=authStateOrNull();if(state)state.authUser=user;return!0}await RateLimiter.recordFailedAttempt(email);return!1}static async validate(credentials){const username=config.auth.username||"email",password=config.auth.password||"password",email=credentials[username];if(!email)return!1;const user=await User.where("email","=",email).first(),authPass=credentials[password]||"",hashToVerify=user?.password||DUMMY_BCRYPT_HASH;return await verifyHash(authPass,hashToVerify)&&!!user}static async login(credentials,options){if(!await this.attempt(credentials))return null;const username=config.auth.username||"email",authedUser=authStateOrNull()?.authUser??await User.where(username,"=",credentials[username]).first();if(!authedUser)return null;const{plainTextToken,refreshToken,expiresIn}=await this.createTokenForUser(authedUser,options);return{user:authedUser,token:plainTextToken,refreshToken,expiresIn}}static async loginUsingId(userId,options){const user=await User.find(userId);if(!user)return null;const state=authStateOrNull();if(state)state.authUser=user;const{plainTextToken,refreshToken,expiresIn}=await this.createTokenForUser(user,options);return{user,token:plainTextToken,refreshToken,expiresIn}}static async logout(){const bearerToken=this.getBearerToken();if(bearerToken)await this.revokeToken(bearerToken);const state=authStateOrNull();if(state){state.authUser=void 0;state.currentToken=void 0}}static async user(){const state=authStateOrNull();if(state?.authUser)return state.authUser;const bearerToken=this.getBearerToken();if(!bearerToken)return;const user=await this.getUserFromToken(bearerToken);if(user&&state)state.authUser=user;return user}static async check(){return await this.user()!==void 0}static async guest(){return!await this.check()}static async id(){return(await this.user())?.id}static setUser(user){const state=authStateOrNull();if(state)state.authUser=user}static async createTokenForUser(user,options){const name=options?.name??config.auth.defaultTokenName??"auth-token",abilities=options?.abilities??options?.scopes??config.auth.defaultAbilities??["*"],accessTtlMs=options?.expiresInMinutes!==void 0?options.expiresInMinutes*60*1000:config.auth.tokenExpiry??3600000,expiresAt=options?.expiresAt??new Date(Date.now()+accessTtlMs),expiresInMinutes=Math.max(1,Math.floor((expiresAt.getTime()-Date.now())/60000)),refreshExpiresInDays=options?.refreshExpiresInDays??Math.max(1,Math.round((config.auth.refreshTokenExpiry??2592000000)/86400000));log.debug(`[auth] Creating token for user#${user.id}: ${name}`);const result=await createRawToken(user.id,name,abilities,{expiresInMinutes,withRefreshToken:options?.withRefreshToken!==!1,refreshExpiresInDays,userAgent:options?.userAgent??null,ipAddress:options?.ipAddress??null}),plainTextToken=result.plainTextToken;return{accessToken:{id:result.accessToken.id,userId:result.accessToken.userId,clientId:result.accessToken.clientId,name:result.accessToken.name,scopes:result.accessToken.scopes,abilities,expiresAt:result.accessToken.expiresAt??expiresAt,createdAt:result.accessToken.createdAt,updatedAt:result.accessToken.updatedAt,revoked:result.accessToken.revoked,plainTextToken},plainTextToken,refreshToken:result.refreshToken,expiresIn:result.expiresIn}}static async createToken(user,name=config.auth.defaultTokenName||"auth-token",abilities=config.auth.defaultAbilities||["*"]){const{plainTextToken}=await this.createTokenForUser(user,{name,abilities});return plainTextToken}static async requestToken(credentials,clientId,clientSecret){if(!await this.validateClient(clientId,clientSecret))throw new HttpError(401,"Invalid client credentials");const isValid=await this.attempt(credentials),authedUser=authStateOrNull()?.authUser;if(!isValid||!authedUser)return null;return{token:await this.createToken(authedUser,"user-auth-token")}}static async validateToken(token){const hashedPlainToken=hashToken(token),accessToken=await db.selectFrom("oauth_access_tokens").where("token","=",hashedPlainToken).selectAll().executeTakeFirst();if(!accessToken)return!1;log.debug(`[auth] Token validated for token#${accessToken.id}`);if(accessToken.expires_at&&(parseSqlDateTime(accessToken.expires_at)??new Date(0))<new Date){await db.deleteFrom("oauth_access_tokens").where("id","=",accessToken.id).execute();return!1}if(accessToken.revoked)return!1;if(isIssuedBeforePasswordChange(accessToken.created_at,await getPasswordChangedAt(accessToken.user_id)))return!1;await db.updateTable("oauth_access_tokens").set({updated_at:formatDate(new Date)}).where("id","=",accessToken.id).execute();return!0}static async getUserFromToken(token){const hashedPlainToken=hashToken(token),accessToken=await db.selectFrom("oauth_access_tokens").where("token","=",hashedPlainToken).selectAll().executeTakeFirst();if(!accessToken)return;if(accessToken.expires_at&&(parseSqlDateTime(accessToken.expires_at)??new Date(0))<new Date){await db.deleteFrom("oauth_access_tokens").where("id","=",accessToken.id).execute();return}if(accessToken.revoked)return;const idleMs=config.auth?.idleTimeout??0;if(idleMs>0){const lastSeen=parseSqlDateTime(accessToken.updated_at??accessToken.created_at);if(lastSeen&&Date.now()-lastSeen.getTime()>idleMs){await db.updateTable("oauth_access_tokens").set({revoked:!0}).where("id","=",accessToken.id).execute();return}}const stateForToken=authStateOrNull();if(stateForToken)stateForToken.currentToken=await this.getTokenFromId(accessToken.id)??void 0;await db.updateTable("oauth_access_tokens").set({updated_at:formatDate(new Date)}).where("id","=",accessToken.id).execute();if(!accessToken?.user_id)return;const user=await User.find(accessToken.user_id);if(isIssuedBeforePasswordChange(accessToken.created_at,await getPasswordChangedAt(accessToken.user_id)))return;return user}static async currentAccessToken(){const state=authStateOrNull();if(state?.currentToken)return state.currentToken;const bearerToken=this.getBearerToken();if(!bearerToken)return;const accessToken=await db.selectFrom("oauth_access_tokens").where("token","=",hashToken(bearerToken)).select(["id"]).executeTakeFirst();if(!accessToken)return;const token=await this.getTokenFromId(Number(accessToken.id));if(token&&state)state.currentToken=token;return token??void 0}static async tokenCan(ability){const token=await this.currentAccessToken();if(!token)return!1;if(token.abilities.includes("*"))return!0;return token.abilities.includes(ability)}static async tokenCant(ability){return!await this.tokenCan(ability)}static async tokenAbilities(){return(await this.currentAccessToken())?.abilities??[]}static async tokenCanAll(abilities){const token=await this.currentAccessToken();if(!token)return!1;if(token.abilities.includes("*"))return!0;return abilities.every((a)=>token.abilities.includes(a))}static async tokenCanAny(abilities){const token=await this.currentAccessToken();if(!token)return!1;if(token.abilities.includes("*"))return!0;return abilities.some((a)=>token.abilities.includes(a))}static async tokens(userId){const uid=userId??await this.id();if(!uid)return[];return(await db.selectFrom("oauth_access_tokens").where("user_id","=",uid).where("revoked","=",!1).selectAll().execute()).map((token)=>({id:Number(token.id),userId:Number(token.user_id),clientId:Number(token.oauth_client_id),name:String(token.name||"auth-token"),scopes:parseScopes(String(token.scopes??"")),abilities:parseScopes(String(token.scopes??"")),expiresAt:parseSqlDateTime(token.expires_at),createdAt:token.created_at?new Date(String(token.created_at)):new Date,updatedAt:token.updated_at?new Date(String(token.updated_at)):new Date,revoked:!!token.revoked}))}static async revokeToken(token){const accessToken=await db.selectFrom("oauth_access_tokens").where("token","=",hashToken(token)).select(["id"]).executeTakeFirst();if(accessToken)await this.revokeRefreshTokensFor(Number(accessToken.id));await db.updateTable("oauth_access_tokens").set({revoked:!0,updated_at:formatDate(new Date)}).where("token","=",hashToken(token)).execute()}static async revokeTokenById(tokenId){await this.revokeRefreshTokensFor(tokenId);await db.updateTable("oauth_access_tokens").set({revoked:!0,updated_at:formatDate(new Date)}).where("id","=",tokenId).execute()}static async revokeRefreshTokensFor(accessTokenId){await db.updateTable("oauth_refresh_tokens").set({revoked:!0}).where("access_token_id","=",accessTokenId).execute()}static async revokeAllTokens(userId){const uid=userId??await this.id();if(!uid)return;const accessTokens=await db.selectFrom("oauth_access_tokens").where("user_id","=",uid).select(["id"]).execute();for(const accessToken of accessTokens)await this.revokeRefreshTokensFor(Number(accessToken.id));await db.updateTable("oauth_access_tokens").set({revoked:!0,updated_at:formatDate(new Date)}).where("user_id","=",uid).execute()}static async revokeOtherTokens(userId){const uid=userId??await this.id();if(!uid)return;const currentToken=await this.currentAccessToken();if(!currentToken)return;await db.updateTable("oauth_access_tokens").set({revoked:!0,updated_at:formatDate(new Date)}).where("user_id","=",uid).where("id","!=",currentToken.id).execute()}static async pruneExpiredTokens(){const result=await db.deleteFrom("oauth_access_tokens").where("expires_at","<",formatDate(new Date)).executeTakeFirst();return Number(result?.numDeletedRows)||0}static async pruneRevokedTokens(){const result=await db.deleteFrom("oauth_access_tokens").where("revoked","=",!0).executeTakeFirst();return Number(result?.numDeletedRows)||0}static async rotateToken(oldToken){const{findToken:findRawToken,revokeToken:revokeRawToken}=await import("./tokens"),existing=await findRawToken(oldToken);if(!existing)return null;const remainingMs=existing.expiresAt?existing.expiresAt.getTime()-Date.now():config.auth.tokenExpiry??3600000,expiresInMinutes=Math.max(1,Math.floor(remainingMs/60000));await revokeRawToken(oldToken);const user=await User.find(existing.userId);if(!user)return null;return(await this.createTokenForUser(user,{name:existing.name,abilities:existing.scopes??["*"],expiresInMinutes,withRefreshToken:!1})).plainTextToken}static async findToken(tokenId){return this.getTokenFromId(tokenId)}static async once(credentials){const username=config.auth.username||"email",password=config.auth.password||"password",email=credentials[username];if(!email)return!1;const user=await User.where("email","=",email).first(),authPass=credentials[password]||"",hashToVerify=user?.password||DUMMY_BCRYPT_HASH;if(await verifyHash(authPass,hashToVerify)&&user){const state=authStateOrNull();if(state)state.authUser=user;return!0}return!1}static guard(_name){return this}static viaRemember(){return!1}static clearState(){const state=authStateOrNull();if(state){state.authUser=void 0;state.currentToken=void 0;state.clientSecret=void 0}}}
|
package/dist/cookie-auth.d.ts
CHANGED
|
@@ -1,69 +1,29 @@
|
|
|
1
1
|
import { Auth } from './authentication';
|
|
2
|
+
import { authCookieToken, clearAuthCookie } from './cookie';
|
|
3
|
+
import type { AuthCookieOptions } from './cookie';
|
|
2
4
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* There used to be two. `authCookie()` wrote `stacks_auth` (via
|
|
6
|
-
* `config.auth.cookie.name`, a key that did not exist on `AuthOptions`, so no
|
|
7
|
-
* app using `satisfies AuthConfig` could even set it), while the Auth
|
|
8
|
-
* middleware, `team.ts` and the stx page gate all read
|
|
9
|
-
* `config.auth.defaultTokenName` — `auth-token`. A cookie the framework wrote
|
|
10
|
-
* was never one the framework read, which is why apps ended up hand-writing a
|
|
11
|
-
* token pack into `localStorage` from an inline script instead
|
|
12
|
-
* (stacksjs/stacks#2236).
|
|
13
|
-
*
|
|
14
|
-
* Resolution order:
|
|
15
|
-
* 1. an explicit `options.name`
|
|
16
|
-
* 2. `config.auth.cookie.name` — the supported key
|
|
17
|
-
* 3. `config.auth.defaultTokenName` — DEPRECATED, honoured so an app that
|
|
18
|
-
* had renamed it (and thereby renamed the cookie those readers wanted)
|
|
19
|
-
* keeps working. Ignored with a warning when it is not a legal cookie
|
|
20
|
-
* name, which it very often is not: it is a human-readable token label
|
|
21
|
-
* like `Web Session`.
|
|
22
|
-
* 4. `auth-token`
|
|
23
|
-
*/
|
|
24
|
-
export declare function authCookieName(options?: AuthCookieOptions): string;
|
|
25
|
-
/**
|
|
26
|
-
* Whether the auth cookie should carry `Secure`, decided from what the app
|
|
27
|
-
* demonstrably is rather than what its environment is called.
|
|
28
|
-
*
|
|
29
|
-
* The old rule was "Secure unless APP_ENV looks development-ish", and it
|
|
30
|
-
* failed open: `.env.example` ships `APP_ENV=development`, so an HTTPS
|
|
31
|
-
* deployment that never changed the env name served its session token
|
|
32
|
-
* without `Secure` (stacksjs/stacks#2275). Now the URL decides:
|
|
33
|
-
*
|
|
34
|
-
* - an `https://` app URL is always Secure
|
|
35
|
-
* - a plain-HTTP or scheme-less URL drops Secure only on a loopback host
|
|
36
|
-
* (localhost, `*.localhost`, 127.0.0.1) — the one place plain HTTP is a
|
|
37
|
-
* development reality rather than a misconfiguration
|
|
38
|
-
* - with no URL configured at all, only the unambiguous `local` / `dev`
|
|
39
|
-
* environment names opt out; `development` no longer does
|
|
40
|
-
*
|
|
41
|
-
* Exported for tests; `authCookie()` feeds it the live config.
|
|
42
|
-
*/
|
|
43
|
-
export declare function shouldSecureAuthCookie(app?: { url?: unknown, env?: unknown }): boolean;
|
|
44
|
-
/**
|
|
45
|
-
* The `Set-Cookie` value that signs a browser in.
|
|
5
|
+
* Cookie-carried access tokens, for server-rendered pages.
|
|
46
6
|
*
|
|
47
|
-
*
|
|
7
|
+
* The token system assumes an API client that can hold a bearer token and put
|
|
8
|
+
* it in a header. A server-rendered page has no such client: the browser posts
|
|
9
|
+
* a form, follows a redirect, and comes back with nothing but cookies. So
|
|
10
|
+
* those apps ended up either inventing their own cookie format or reaching for
|
|
11
|
+
* `SessionAuth`, whose in-memory map drops every session when the process
|
|
12
|
+
* restarts and cannot be shared across workers.
|
|
48
13
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* status: 303,
|
|
53
|
-
* headers: { 'Location': '/account', 'Set-Cookie': authCookie(result.token) },
|
|
54
|
-
* })
|
|
55
|
-
* ```
|
|
56
|
-
*/
|
|
57
|
-
export declare function authCookie(token: string, options?: AuthCookieOptions): string;
|
|
58
|
-
/**
|
|
59
|
-
* The `Set-Cookie` value that signs a browser out.
|
|
14
|
+
* This carries the same personal access token the API uses in an httpOnly
|
|
15
|
+
* cookie: one source of truth for who is signed in, revocable through the
|
|
16
|
+
* usual token calls, and durable because the token lives in the database.
|
|
60
17
|
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
18
|
+
* The cookie is httpOnly (a page script never needs it), SameSite=Lax (so a
|
|
19
|
+
* link from an email still arrives signed in, while a cross-site POST does
|
|
20
|
+
* not), and Secure everywhere except a plain-HTTP loopback app URL — see
|
|
21
|
+
* `shouldSecureAuthCookie` for exactly how that is decided.
|
|
63
22
|
*/
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
23
|
+
// The cookie value itself — naming, attributes, parsing — lives in ./cookie so
|
|
24
|
+
// authentication.ts can read it without importing this module back. Re-exported
|
|
25
|
+
// here because this is the documented entry point for cookie auth.
|
|
26
|
+
export type { AuthCookieOptions } from './cookie';
|
|
67
27
|
/**
|
|
68
28
|
* The signed-in user for a server-rendered request, or undefined.
|
|
69
29
|
*
|
|
@@ -82,32 +42,6 @@ export declare function cookieCheck(request: Request | { headers: Headers }, opt
|
|
|
82
42
|
* the key".
|
|
83
43
|
*/
|
|
84
44
|
export declare function logoutCookie(request: Request | { headers: Headers }, options?: AuthCookieOptions): Promise<string>;
|
|
85
|
-
/**
|
|
86
|
-
* Cookie-carried access tokens, for server-rendered pages.
|
|
87
|
-
*
|
|
88
|
-
* The token system assumes an API client that can hold a bearer token and put
|
|
89
|
-
* it in a header. A server-rendered page has no such client: the browser posts
|
|
90
|
-
* a form, follows a redirect, and comes back with nothing but cookies. So
|
|
91
|
-
* those apps ended up either inventing their own cookie format or reaching for
|
|
92
|
-
* `SessionAuth`, whose in-memory map drops every session when the process
|
|
93
|
-
* restarts and cannot be shared across workers.
|
|
94
|
-
*
|
|
95
|
-
* This carries the same personal access token the API uses in an httpOnly
|
|
96
|
-
* cookie: one source of truth for who is signed in, revocable through the
|
|
97
|
-
* usual token calls, and durable because the token lives in the database.
|
|
98
|
-
*
|
|
99
|
-
* The cookie is httpOnly (a page script never needs it), SameSite=Lax (so a
|
|
100
|
-
* link from an email still arrives signed in, while a cross-site POST does
|
|
101
|
-
* not), and Secure everywhere except a plain-HTTP loopback app URL — see
|
|
102
|
-
* `shouldSecureAuthCookie` for exactly how that is decided.
|
|
103
|
-
*/
|
|
104
|
-
export declare interface AuthCookieOptions {
|
|
105
|
-
name?: string
|
|
106
|
-
maxAge?: number
|
|
107
|
-
path?: string
|
|
108
|
-
domain?: string
|
|
109
|
-
secure?: boolean
|
|
110
|
-
sameSite?: 'Strict' | 'Lax' | 'None'
|
|
111
|
-
}
|
|
112
45
|
/** Whatever the token layer resolves a user to, so the two cannot drift. */
|
|
113
46
|
declare type AuthenticatedUser = Awaited<ReturnType<typeof Auth.getUserFromToken>>;
|
|
47
|
+
export { authCookie, authCookieName, authCookieToken, clearAuthCookie, shouldSecureAuthCookie } from './cookie';
|
package/dist/cookie-auth.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{authCookieToken,clearAuthCookie}from"./cookie";import{Auth}from"./authentication";export{authCookie,authCookieName,authCookieToken,clearAuthCookie,shouldSecureAuthCookie}from"./cookie";export async function userFromCookie(request,options={}){const token=authCookieToken(request,options);if(!token)return;return Auth.getUserFromToken(token)}export async function cookieCheck(request,options={}){return Boolean(await userFromCookie(request,options))}export async function logoutCookie(request,options={}){const token=authCookieToken(request,options);if(token)try{await Auth.revokeToken(token)}catch{}return clearAuthCookie(options)}
|
package/dist/cookie.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one name the auth cookie has.
|
|
3
|
+
*
|
|
4
|
+
* There used to be two. `authCookie()` wrote `stacks_auth` (via
|
|
5
|
+
* `config.auth.cookie.name`, a key that did not exist on `AuthOptions`, so no
|
|
6
|
+
* app using `satisfies AuthConfig` could even set it), while the Auth
|
|
7
|
+
* middleware, `team.ts` and the stx page gate all read
|
|
8
|
+
* `config.auth.defaultTokenName` — `auth-token`. A cookie the framework wrote
|
|
9
|
+
* was never one the framework read, which is why apps ended up hand-writing a
|
|
10
|
+
* token pack into `localStorage` from an inline script instead
|
|
11
|
+
* (stacksjs/stacks#2236).
|
|
12
|
+
*
|
|
13
|
+
* Resolution order:
|
|
14
|
+
* 1. an explicit `options.name`
|
|
15
|
+
* 2. `config.auth.cookie.name` — the supported key
|
|
16
|
+
* 3. `config.auth.defaultTokenName` — DEPRECATED, honoured so an app that
|
|
17
|
+
* had renamed it (and thereby renamed the cookie those readers wanted)
|
|
18
|
+
* keeps working. Ignored with a warning when it is not a legal cookie
|
|
19
|
+
* name, which it very often is not: it is a human-readable token label
|
|
20
|
+
* like `Web Session`.
|
|
21
|
+
* 4. `auth-token`
|
|
22
|
+
*/
|
|
23
|
+
export declare function authCookieName(options?: AuthCookieOptions): string;
|
|
24
|
+
/**
|
|
25
|
+
* Whether the auth cookie should carry `Secure`, decided from what the app
|
|
26
|
+
* demonstrably is rather than what its environment is called.
|
|
27
|
+
*
|
|
28
|
+
* The old rule was "Secure unless APP_ENV looks development-ish", and it
|
|
29
|
+
* failed open: `.env.example` ships `APP_ENV=development`, so an HTTPS
|
|
30
|
+
* deployment that never changed the env name served its session token
|
|
31
|
+
* without `Secure` (stacksjs/stacks#2275). Now the URL decides:
|
|
32
|
+
*
|
|
33
|
+
* - an `https://` app URL is always Secure
|
|
34
|
+
* - a plain-HTTP or scheme-less URL drops Secure only on a loopback host
|
|
35
|
+
* (localhost, `*.localhost`, 127.0.0.1) — the one place plain HTTP is a
|
|
36
|
+
* development reality rather than a misconfiguration
|
|
37
|
+
* - with no URL configured at all, only the unambiguous `local` / `dev`
|
|
38
|
+
* environment names opt out; `development` no longer does
|
|
39
|
+
*
|
|
40
|
+
* Exported for tests; `authCookie()` feeds it the live config.
|
|
41
|
+
*/
|
|
42
|
+
export declare function shouldSecureAuthCookie(app?: { url?: unknown, env?: unknown }): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* The `Set-Cookie` value that signs a browser in.
|
|
45
|
+
*
|
|
46
|
+
* Pair it with the token from `Auth.login()`:
|
|
47
|
+
*
|
|
48
|
+
* ```ts
|
|
49
|
+
* const result = await Auth.login({ email, password })
|
|
50
|
+
* return new Response(null, {
|
|
51
|
+
* status: 303,
|
|
52
|
+
* headers: { 'Location': '/account', 'Set-Cookie': authCookie(result.token) },
|
|
53
|
+
* })
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export declare function authCookie(token: string, options?: AuthCookieOptions): string;
|
|
57
|
+
/**
|
|
58
|
+
* The `Set-Cookie` value that signs a browser out.
|
|
59
|
+
*
|
|
60
|
+
* Every attribute except the value has to match the cookie being replaced, or
|
|
61
|
+
* the browser keeps the original alongside the expired one.
|
|
62
|
+
*/
|
|
63
|
+
export declare function clearAuthCookie(options?: AuthCookieOptions): string;
|
|
64
|
+
/** The raw token in a request's auth cookie, if it carries one. */
|
|
65
|
+
export declare function authCookieToken(request: Request | { headers: Headers }, options?: AuthCookieOptions): string | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* Auth cookie values: naming, attributes, and parsing.
|
|
68
|
+
*
|
|
69
|
+
* Split out of `cookie-auth.ts` so `authentication.ts` can read the cookie
|
|
70
|
+
* without importing it back. Nothing here touches `Auth`, which is what makes
|
|
71
|
+
* that safe; the parts that resolve a token to a user stay in `cookie-auth.ts`.
|
|
72
|
+
*
|
|
73
|
+
* The prose explaining WHY the cookie exists, and why it is httpOnly +
|
|
74
|
+
* SameSite=Lax + conditionally Secure, lives on the re-export in
|
|
75
|
+
* `cookie-auth.ts`.
|
|
76
|
+
*/
|
|
77
|
+
export declare interface AuthCookieOptions {
|
|
78
|
+
name?: string
|
|
79
|
+
maxAge?: number
|
|
80
|
+
path?: string
|
|
81
|
+
domain?: string
|
|
82
|
+
secure?: boolean
|
|
83
|
+
sameSite?: 'Strict' | 'Lax' | 'None'
|
|
84
|
+
}
|
package/dist/cookie.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{config}from"@stacksjs/config";import{log}from"@stacksjs/logging";const COOKIE_NAME_RE=/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;export function authCookieName(options){if(options?.name)return options.name;const configured=config.auth?.cookie?.name;if(typeof configured==="string"&&configured.length>0)return configured;const legacy=config.auth?.defaultTokenName;if(typeof legacy==="string"&&legacy.length>0&&legacy!=="auth-token"){if(COOKIE_NAME_RE.test(legacy))return legacy;console.warn(`[auth] config.auth.defaultTokenName ("${legacy}") is not a valid cookie name and is being ignored for cookie naming; using "auth-token". defaultTokenName is a personal access token label, not a `+"cookie name \u2014 set config.auth.cookie.name instead (stacksjs/stacks#2236).")}return"auth-token"}function cookieName(options){return authCookieName(options)}function defaultMaxAge(){const milliseconds=Number(config.auth?.tokenExpiry??3600000);if(!Number.isFinite(milliseconds)||milliseconds<=0)return 3600;return Math.max(60,Math.round(milliseconds/1000))}function isLoopbackHost(hostname){const host=hostname.toLowerCase();return host==="localhost"||host.endsWith(".localhost")||host==="127.0.0.1"||host==="0.0.0.0"||host==="::1"||host==="[::1]"}export function shouldSecureAuthCookie(app=config.app??{}){const rawUrl=String(app?.url??process.env.APP_URL??"").trim();if(rawUrl)try{const url=new URL(rawUrl.includes("://")?rawUrl:`http://${rawUrl}`);if(url.protocol==="https:")return!0;return!isLoopbackHost(url.hostname)}catch{}const environment=String(app?.env??process.env.APP_ENV??"");return!(environment==="local"||environment==="dev")}let warnedInsecureOverride=!1;export function authCookie(token,options={}){const parts=[`${cookieName(options)}=${encodeURIComponent(token)}`,`Path=${options.path??"/"}`,`Max-Age=${options.maxAge??defaultMaxAge()}`,"HttpOnly",`SameSite=${options.sameSite??"Lax"}`];if(options.domain)parts.push(`Domain=${options.domain}`);if(options.secure??shouldSecureAuthCookie())parts.push("Secure");else if(options.secure===!1&&shouldSecureAuthCookie()&&!warnedInsecureOverride){warnedInsecureOverride=!0;log.warn("[auth] authCookie() was asked for secure: false while the app URL is HTTPS \u2014 the session cookie will also travel over plain HTTP.")}return parts.join("; ")}export function clearAuthCookie(options={}){return authCookie("",{...options,maxAge:0})}export function authCookieToken(request,options={}){const header=request.headers.get("cookie");if(!header)return;const wanted=cookieName(options);for(const pair of header.split(";")){const index=pair.indexOf("=");if(index===-1)continue;if(pair.slice(0,index).trim()!==wanted)continue;const value=decodeURIComponent(pair.slice(index+1).trim());return value.length>0?value:void 0}return}
|
package/dist/index.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export * from './email-verification';
|
|
|
26
26
|
export * from './session-auth';
|
|
27
27
|
// Cookie-carried access tokens, for server-rendered pages.
|
|
28
28
|
export * from './cookie-auth';
|
|
29
|
+
export * from './request-token';
|
|
29
30
|
// The stx page gate (`middleware: ['auth' | 'guest']`), token-validating.
|
|
30
31
|
export * from './page-gate';
|
|
31
32
|
// Social sign-in: which local user a provider identity resolves to.
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export*from"./authentication";export*from"./authenticator";export*from"./client";export*from"./middleware";export*from"./rate-limiter";export*from"./passkey";export*from"./password/reset";export*from"./register";export*from"./user";export*from"./tokens";export*from"./gate";export*from"./policy";export*from"./authorizable";export*from"./permissions";export*from"./rbac";export{createBqbRbacStore}from"./rbac-store-bqb";export{DEFAULT_ROLE_PACKS,seedDefaultRoles}from"./rbac-seed";export*from"./email-verification";export*from"./session-auth";export*from"./cookie-auth";export*from"./page-gate";export*from"./socials";export{generateTOTP,verifyTOTP,generateTOTPSecret,totpKeyUri}from"@stacksjs/ts-auth";export*from"./two-factor";export*from"./team";
|
|
1
|
+
export*from"./authentication";export*from"./authenticator";export*from"./client";export*from"./middleware";export*from"./rate-limiter";export*from"./passkey";export*from"./password/reset";export*from"./register";export*from"./user";export*from"./tokens";export*from"./gate";export*from"./policy";export*from"./authorizable";export*from"./permissions";export*from"./rbac";export{createBqbRbacStore}from"./rbac-store-bqb";export{DEFAULT_ROLE_PACKS,seedDefaultRoles}from"./rbac-seed";export*from"./email-verification";export*from"./session-auth";export*from"./cookie-auth";export*from"./request-token";export*from"./page-gate";export*from"./socials";export{generateTOTP,verifyTOTP,generateTOTPSecret,totpKeyUri}from"@stacksjs/ts-auth";export*from"./two-factor";export*from"./team";
|
package/dist/middleware.d.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Built-in auth middleware handler
|
|
3
|
-
* Validates
|
|
3
|
+
* Validates the request's access token and sets the authenticated user on Auth
|
|
4
|
+
*
|
|
5
|
+
* Resolution is shared with `Auth.getBearerToken()` via `requestToken`, which
|
|
6
|
+
* checks the Authorization header and then the auth cookie. This used to be a
|
|
7
|
+
* second hand-rolled copy that stopped at the header, so a browser signed in
|
|
8
|
+
* by cookie — the whole point of `cookie-auth.ts`, and what
|
|
9
|
+
* `SocialCallbackAction` produces — was rejected here with 401 on every
|
|
10
|
+
* protected route (#2306).
|
|
4
11
|
*/
|
|
5
12
|
export declare function authMiddleware(request: any): Promise<void>;
|
|
6
13
|
/**
|
package/dist/middleware.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Auth}from"./authentication";export async function authMiddleware(request){
|
|
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};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The access token a request carries, from the Authorization header or the
|
|
3
|
+
* auth cookie.
|
|
4
|
+
*
|
|
5
|
+
* One function because there used to be two copies of it — `authMiddleware`
|
|
6
|
+
* and `Auth.getBearerToken()` each extracted the bearer by hand, and both
|
|
7
|
+
* stopped at the header. That is what made cookie auth half-real: the cookie
|
|
8
|
+
* was written by `SocialCallbackAction` and read by `userFromCookie`, but the
|
|
9
|
+
* middleware that actually gates routes never looked at it, so a browser
|
|
10
|
+
* signed in by cookie got 401 "No authentication token provided" on every
|
|
11
|
+
* protected route, and `Auth.logout()` found no token, revoked nothing, and
|
|
12
|
+
* still answered 200 (#2306).
|
|
13
|
+
*
|
|
14
|
+
* The header is checked first, so an API client behaves exactly as before.
|
|
15
|
+
*/
|
|
16
|
+
export declare function requestToken(request: any): string | null;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{authCookieToken}from"./cookie";export function requestToken(request){let token=request?.bearerToken?.();if(!token){const header=request?.headers?.get?.("authorization")||request?.headers?.get?.("Authorization");if(typeof header==="string"&&header.startsWith("Bearer "))token=header.substring(7)}if(!token&&request?.headers)token=authCookieToken(request);return token||null}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/auth",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.70.
|
|
5
|
+
"version": "0.70.365",
|
|
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.17",
|
|
64
|
-
"@stacksjs/error-handling": "0.70.
|
|
65
|
-
"@stacksjs/router": "0.70.
|
|
64
|
+
"@stacksjs/error-handling": "0.70.365",
|
|
65
|
+
"@stacksjs/router": "0.70.365"
|
|
66
66
|
}
|
|
67
67
|
}
|