Comparing version 2.7.3 to 3.0.0-beta.0
@@ -1,15 +0,27 @@ | ||
export type KeySchema = { | ||
import type { DatabaseSessionAttributes, DatabaseUserAttributes } from "../index.js"; | ||
export interface Adapter { | ||
getSessionAndUser(sessionId: string): Promise<[session: DatabaseSession | null, user: DatabaseUser | null]>; | ||
getUserSessions(userId: string): Promise<DatabaseSession[]>; | ||
setSession(value: DatabaseSession): Promise<void>; | ||
updateSession(sessionId: string, value: Partial<DatabaseSession>): Promise<void>; | ||
deleteSession(sessionId: string): Promise<void>; | ||
deleteUserSessions(userId: string): Promise<void>; | ||
} | ||
export interface SessionAdapter { | ||
getSession(sessionId: string): Promise<DatabaseSession | null>; | ||
getUserSessions(userId: string): Promise<DatabaseSession[]>; | ||
setSession(value: DatabaseSession): Promise<void>; | ||
updateSession(sessionId: string, value: Partial<DatabaseSession>): Promise<void>; | ||
deleteSession(sessionId: string): Promise<void>; | ||
deleteUserSessions(userId: string): Promise<void>; | ||
} | ||
export interface DatabaseUser { | ||
id: string; | ||
hashed_password: string | null; | ||
user_id: string; | ||
}; | ||
export type UserSchema = { | ||
attributes: DatabaseUserAttributes; | ||
} | ||
export interface DatabaseSession { | ||
sessionId: string; | ||
expiresAt: Date; | ||
id: string; | ||
} & Lucia.DatabaseUserAttributes; | ||
export type SessionSchema = { | ||
id: string; | ||
active_expires: number; | ||
idle_expires: number; | ||
user_id: string; | ||
} & Lucia.DatabaseSessionAttributes; | ||
export declare const createKeyId: (providerId: string, providerUserId: string) => string; | ||
attributes: DatabaseSessionAttributes; | ||
} |
@@ -1,6 +0,1 @@ | ||
export const createKeyId = (providerId, providerUserId) => { | ||
if (providerId.includes(":")) { | ||
throw new TypeError("Provider id must not include any colons (:)"); | ||
} | ||
return `${providerId}:${providerUserId}`; | ||
}; | ||
export {}; |
import { AuthRequest } from "./request.js"; | ||
import { lucia as defaultMiddleware } from "../middleware/index.js"; | ||
import type { Cookie, SessionCookieConfiguration } from "./cookie.js"; | ||
import type { UserSchema, SessionSchema, KeySchema } from "./database.js"; | ||
import type { Adapter, SessionAdapter, InitializeAdapter } from "./adapter.js"; | ||
import type { Middleware } from "./request.js"; | ||
export type Session = Readonly<{ | ||
user: User; | ||
sessionId: string; | ||
activePeriodExpiresAt: Date; | ||
idlePeriodExpiresAt: Date; | ||
state: "idle" | "active"; | ||
import { TimeSpan } from "oslo"; | ||
import type { SessionCookie } from "oslo/session"; | ||
import type { Adapter } from "./database.js"; | ||
import type { DatabaseSessionAttributes, DatabaseUserAttributes, RegisteredLucia } from "../index.js"; | ||
type SessionAttributes = RegisteredLucia extends Lucia<any, infer _SessionAttributes> ? _SessionAttributes : {}; | ||
type UserAttributes = RegisteredLucia extends Lucia<any, any, infer _UserAttributes> ? _UserAttributes : {}; | ||
export interface Session extends SessionAttributes { | ||
id: string; | ||
expiresAt: Date; | ||
fresh: boolean; | ||
}> & ReturnType<Lucia.Auth["getSessionAttributes"]>; | ||
export type Key = Readonly<{ | ||
userId: string; | ||
providerId: string; | ||
providerUserId: string; | ||
passwordDefined: boolean; | ||
}>; | ||
export type Env = "DEV" | "PROD"; | ||
export type User = { | ||
userId: string; | ||
} & ReturnType<Lucia.Auth["getUserAttributes"]>; | ||
export declare const lucia: <_Configuration extends Configuration<{}, {}>>(config: _Configuration) => Auth<_Configuration>; | ||
export declare class Auth<_Configuration extends Configuration = any> { | ||
} | ||
export interface User extends UserAttributes { | ||
id: string; | ||
} | ||
export declare class Lucia<_Middleware extends Middleware = Middleware<[RequestContext]>, _SessionAttributes extends {} = Record<never, never>, _UserAttributes extends {} = Record<never, never>> { | ||
private adapter; | ||
private sessionCookieConfig; | ||
private sessionExpiresIn; | ||
private sessionController; | ||
private sessionCookieController; | ||
private csrfProtection; | ||
private env; | ||
private passwordHash; | ||
protected middleware: _Configuration["middleware"] extends Middleware ? _Configuration["middleware"] : ReturnType<typeof defaultMiddleware>; | ||
private middleware; | ||
private experimental; | ||
constructor(config: _Configuration); | ||
protected getUserAttributes: (databaseUser: UserSchema) => _Configuration extends Configuration<infer _UserAttributes> ? _UserAttributes : never; | ||
protected getSessionAttributes: (databaseSession: SessionSchema) => _Configuration extends Configuration<any, infer _SessionAttributes> ? _SessionAttributes : never; | ||
transformDatabaseUser: (databaseUser: UserSchema) => User; | ||
transformDatabaseKey: (databaseKey: KeySchema) => Key; | ||
transformDatabaseSession: (databaseSession: SessionSchema, context: { | ||
private getSessionAttributes; | ||
private getUserAttributes; | ||
constructor(adapter: Adapter, options?: { | ||
middleware?: _Middleware; | ||
csrfProtection?: boolean | CSRFProtectionOptions; | ||
sessionExpiresIn?: TimeSpan; | ||
sessionCookie?: SessionCookieOptions; | ||
getSessionAttributes?: (databaseSessionAttributes: DatabaseSessionAttributes) => _SessionAttributes; | ||
getUserAttributes?: (databaseUserAttributes: DatabaseUserAttributes) => _UserAttributes; | ||
experimental?: ExperimentalOptions; | ||
}); | ||
getUserSessions(userId: string): Promise<Session[]>; | ||
validateSession(sessionId: string): Promise<{ | ||
user: User; | ||
fresh: boolean; | ||
}) => Session; | ||
private getDatabaseUser; | ||
private getDatabaseSession; | ||
private getDatabaseSessionAndUser; | ||
private validateSessionIdArgument; | ||
private getNewSessionExpiration; | ||
getUser: (userId: string) => Promise<User>; | ||
createUser: (options: { | ||
userId?: string; | ||
key: { | ||
providerId: string; | ||
providerUserId: string; | ||
password: string | null; | ||
} | null; | ||
attributes: Lucia.DatabaseUserAttributes; | ||
}) => Promise<User>; | ||
updateUserAttributes: (userId: string, attributes: Partial<Lucia.DatabaseUserAttributes>) => Promise<User>; | ||
deleteUser: (userId: string) => Promise<void>; | ||
useKey: (providerId: string, providerUserId: string, password: string | null) => Promise<Key>; | ||
getSession: (sessionId: string) => Promise<Session>; | ||
getAllUserSessions: (userId: string) => Promise<Session[]>; | ||
validateSession: (sessionId: string) => Promise<Session>; | ||
createSession: (options: { | ||
sessionId?: string; | ||
userId: string; | ||
attributes: Lucia.DatabaseSessionAttributes; | ||
}) => Promise<Session>; | ||
updateSessionAttributes: (sessionId: string, attributes: Partial<Lucia.DatabaseSessionAttributes>) => Promise<Session>; | ||
invalidateSession: (sessionId: string) => Promise<void>; | ||
invalidateAllUserSessions: (userId: string) => Promise<void>; | ||
deleteDeadUserSessions: (userId: string) => Promise<void>; | ||
/** | ||
* @deprecated To be removed in next major release | ||
*/ | ||
validateRequestOrigin: (request: { | ||
url: string | null; | ||
method: string | null; | ||
headers: { | ||
origin: string | null; | ||
}; | ||
}) => void; | ||
readSessionCookie: (cookieHeader: string | null | undefined) => string | null; | ||
readBearerToken: (authorizationHeader: string | null | undefined) => string | null; | ||
handleRequest: (...args: (_Configuration["middleware"] extends Middleware ? _Configuration["middleware"] : Middleware<[import("./request.js").RequestContext]>) extends Middleware<infer Args extends any[]> ? Args : never) => AuthRequest<Lucia.Auth>; | ||
createSessionCookie: (session: Session | null) => Cookie; | ||
createKey: (options: { | ||
userId: string; | ||
providerId: string; | ||
providerUserId: string; | ||
password: string | null; | ||
}) => Promise<Key>; | ||
deleteKey: (providerId: string, providerUserId: string) => Promise<void>; | ||
getKey: (providerId: string, providerUserId: string) => Promise<Key>; | ||
getAllUserKeys: (userId: string) => Promise<Key[]>; | ||
updateKeyPassword: (providerId: string, providerUserId: string, password: string | null) => Promise<Key>; | ||
session: Session; | ||
} | { | ||
user: null; | ||
session: null; | ||
}>; | ||
createSession(userId: string, attributes: DatabaseSessionAttributes): Promise<Session>; | ||
invalidateSession(sessionId: string): Promise<void>; | ||
invalidateUserSessions(userId: string): Promise<void>; | ||
readSessionCookie(cookieHeader: string): string | null; | ||
readBearerToken(authorizationHeader: string): string | null; | ||
handleRequest(...args: _Middleware extends Middleware<infer _Args> ? _Args : []): AuthRequest<typeof this>; | ||
private verifyRequestOrigin; | ||
createSessionCookie(sessionId: string): SessionCookie; | ||
createBlankSessionCookie(): SessionCookie; | ||
} | ||
type MaybePromise<T> = T | Promise<T>; | ||
export type Configuration<_UserAttributes extends Record<string, any> = {}, _SessionAttributes extends Record<string, any> = {}> = { | ||
adapter: InitializeAdapter<Adapter> | { | ||
user: InitializeAdapter<Adapter>; | ||
session: InitializeAdapter<SessionAdapter>; | ||
}; | ||
env: Env; | ||
middleware?: Middleware; | ||
csrfProtection?: boolean | { | ||
host?: string; | ||
hostHeader?: string; | ||
allowedSubDomains?: string[] | "*"; | ||
}; | ||
sessionExpiresIn?: { | ||
activePeriod: number; | ||
idlePeriod: number; | ||
}; | ||
sessionCookie?: SessionCookieConfiguration; | ||
getSessionAttributes?: (databaseSession: SessionSchema) => _SessionAttributes; | ||
getUserAttributes?: (databaseUser: UserSchema) => _UserAttributes; | ||
passwordHash?: { | ||
generate: (password: string) => MaybePromise<string>; | ||
validate: (password: string, hash: string) => MaybePromise<boolean>; | ||
}; | ||
experimental?: { | ||
debugMode?: boolean; | ||
}; | ||
}; | ||
export interface SessionCookieOptions { | ||
name?: string; | ||
expires?: boolean; | ||
sameSite?: "lax" | "strict"; | ||
domain?: string; | ||
path?: string; | ||
secure?: boolean; | ||
} | ||
export interface CSRFProtectionOptions { | ||
allowedDomains?: string[]; | ||
hostHeader?: string; | ||
} | ||
export interface ExperimentalOptions { | ||
debugMode?: boolean; | ||
} | ||
export interface LuciaRequest { | ||
method: string; | ||
url?: string; | ||
headers: Pick<Headers, "get">; | ||
} | ||
export interface RequestContext { | ||
sessionCookie?: string | null; | ||
request: LuciaRequest; | ||
setCookie: (cookie: SessionCookie) => void; | ||
} | ||
export type Middleware<Args extends any[] = any> = (context: { | ||
args: Args; | ||
sessionCookieName: string; | ||
}) => RequestContext; | ||
export {}; |
@@ -1,365 +0,135 @@ | ||
import { DEFAULT_SESSION_COOKIE_NAME, createSessionCookie } from "./cookie.js"; | ||
import { logError } from "../utils/log.js"; | ||
import { generateScryptHash, validateScryptHash } from "../utils/crypto.js"; | ||
import { generateRandomString } from "../utils/crypto.js"; | ||
import { LuciaError } from "./error.js"; | ||
import { parseCookie } from "../utils/cookie.js"; | ||
import { isValidDatabaseSession } from "./session.js"; | ||
import { AuthRequest, transformRequestContext } from "./request.js"; | ||
import { AuthRequest } from "./request.js"; | ||
import { lucia as defaultMiddleware } from "../middleware/index.js"; | ||
import { debug } from "../utils/debug.js"; | ||
import { isWithinExpiration } from "../utils/date.js"; | ||
import { createAdapter } from "./adapter.js"; | ||
import { createKeyId } from "./database.js"; | ||
import { isAllowedOrigin, safeParseUrl } from "../utils/url.js"; | ||
export const lucia = (config) => { | ||
return new Auth(config); | ||
}; | ||
const validateConfiguration = (config) => { | ||
const adapterProvided = config.adapter; | ||
if (!adapterProvided) { | ||
logError('Adapter is not defined in configuration ("config.adapter")'); | ||
process.exit(1); | ||
} | ||
}; | ||
export class Auth { | ||
import { SessionController, SessionCookieController } from "oslo/session"; | ||
import { TimeSpan, isWithinExpirationDate } from "oslo"; | ||
import { generateRandomString, alphabet } from "oslo/random"; | ||
import { verifyRequestOrigin } from "oslo/request"; | ||
export class Lucia { | ||
adapter; | ||
sessionCookieConfig; | ||
sessionExpiresIn; | ||
sessionController; | ||
sessionCookieController; | ||
csrfProtection; | ||
env; | ||
passwordHash = { | ||
generate: generateScryptHash, | ||
validate: validateScryptHash | ||
}; | ||
middleware = defaultMiddleware(); | ||
middleware; | ||
experimental; | ||
constructor(config) { | ||
validateConfiguration(config); | ||
this.adapter = createAdapter(config.adapter); | ||
this.env = config.env; | ||
this.sessionExpiresIn = { | ||
activePeriod: config.sessionExpiresIn?.activePeriod ?? 1000 * 60 * 60 * 24, | ||
idlePeriod: config.sessionExpiresIn?.idlePeriod ?? 1000 * 60 * 60 * 24 * 14 | ||
getSessionAttributes; | ||
getUserAttributes; | ||
constructor(adapter, options) { | ||
this.adapter = adapter; | ||
this.middleware = options?.middleware ?? defaultMiddleware(); | ||
// we have to use `any` here since TS can't do conditional return types | ||
this.getUserAttributes = (databaseUserAttributes) => { | ||
if (options && options.getUserAttributes) { | ||
return options.getUserAttributes(databaseUserAttributes); | ||
} | ||
return {}; | ||
}; | ||
this.getUserAttributes = (databaseUser) => { | ||
const defaultTransform = () => { | ||
return {}; | ||
}; | ||
const transform = config.getUserAttributes ?? defaultTransform; | ||
return transform(databaseUser); | ||
this.getSessionAttributes = (databaseSessionAttributes) => { | ||
if (options && options.getSessionAttributes) { | ||
return options.getSessionAttributes(databaseSessionAttributes); | ||
} | ||
return {}; | ||
}; | ||
this.getSessionAttributes = (databaseSession) => { | ||
const defaultTransform = () => { | ||
return {}; | ||
}; | ||
const transform = config.getSessionAttributes ?? defaultTransform; | ||
return transform(databaseSession); | ||
}; | ||
this.csrfProtection = config.csrfProtection ?? true; | ||
this.sessionCookieConfig = config.sessionCookie ?? {}; | ||
if (config.passwordHash) { | ||
this.passwordHash = config.passwordHash; | ||
this.sessionController = new SessionController(options?.sessionExpiresIn ?? new TimeSpan(30, "d")); | ||
this.sessionCookieController = new SessionCookieController(options?.sessionCookie?.name ?? "auth_session", this.sessionController.expiresIn, options?.sessionCookie); | ||
this.csrfProtection = options?.csrfProtection ?? true; | ||
if (options?.middleware) { | ||
this.middleware = options.middleware; | ||
} | ||
if (config.middleware) { | ||
this.middleware = config.middleware; | ||
} | ||
this.experimental = { | ||
debugMode: config.experimental?.debugMode ?? false | ||
debugMode: options?.experimental?.debugMode ?? false | ||
}; | ||
debug.init(this.experimental.debugMode); | ||
} | ||
getUserAttributes; | ||
getSessionAttributes; | ||
transformDatabaseUser = (databaseUser) => { | ||
const attributes = this.getUserAttributes(databaseUser); | ||
return { | ||
...attributes, | ||
userId: databaseUser.id | ||
}; | ||
}; | ||
transformDatabaseKey = (databaseKey) => { | ||
const [providerId, ...providerUserIdSegments] = databaseKey.id.split(":"); | ||
const providerUserId = providerUserIdSegments.join(":"); | ||
const userId = databaseKey.user_id; | ||
const isPasswordDefined = !!databaseKey.hashed_password; | ||
return { | ||
providerId, | ||
providerUserId, | ||
userId, | ||
passwordDefined: isPasswordDefined | ||
}; | ||
}; | ||
transformDatabaseSession = (databaseSession, context) => { | ||
const attributes = this.getSessionAttributes(databaseSession); | ||
const active = isWithinExpiration(databaseSession.active_expires); | ||
return { | ||
...attributes, | ||
user: context.user, | ||
sessionId: databaseSession.id, | ||
activePeriodExpiresAt: new Date(Number(databaseSession.active_expires)), | ||
idlePeriodExpiresAt: new Date(Number(databaseSession.idle_expires)), | ||
state: active ? "active" : "idle", | ||
fresh: context.fresh | ||
}; | ||
}; | ||
getDatabaseUser = async (userId) => { | ||
const databaseUser = await this.adapter.getUser(userId); | ||
if (!databaseUser) { | ||
throw new LuciaError("AUTH_INVALID_USER_ID"); | ||
async getUserSessions(userId) { | ||
const databaseSessions = await this.adapter.getUserSessions(userId); | ||
const sessions = []; | ||
for (const databaseSession of databaseSessions) { | ||
if (!isWithinExpirationDate(databaseSession.expiresAt)) { | ||
continue; | ||
} | ||
sessions.push({ | ||
id: databaseSession.sessionId, | ||
expiresAt: databaseSession.expiresAt, | ||
userId: databaseSession.id, | ||
fresh: false, | ||
...this.getSessionAttributes(databaseSession) | ||
}); | ||
} | ||
return databaseUser; | ||
}; | ||
getDatabaseSession = async (sessionId) => { | ||
const databaseSession = await this.adapter.getSession(sessionId); | ||
return sessions; | ||
} | ||
async validateSession(sessionId) { | ||
const [databaseSession, databaseUser] = await this.adapter.getSessionAndUser(sessionId); | ||
if (!databaseSession) { | ||
debug.session.fail("Session not found", sessionId); | ||
throw new LuciaError("AUTH_INVALID_SESSION_ID"); | ||
return { session: null, user: null }; | ||
} | ||
if (!isValidDatabaseSession(databaseSession)) { | ||
debug.session.fail(`Session expired at ${new Date(Number(databaseSession.idle_expires))}`, sessionId); | ||
throw new LuciaError("AUTH_INVALID_SESSION_ID"); | ||
if (!databaseUser) { | ||
await this.adapter.deleteSession(databaseSession.sessionId); | ||
debug.session.fail("Session not found", sessionId); | ||
return { session: null, user: null }; | ||
} | ||
return databaseSession; | ||
}; | ||
getDatabaseSessionAndUser = async (sessionId) => { | ||
if (this.adapter.getSessionAndUser) { | ||
const [databaseSession, databaseUser] = await this.adapter.getSessionAndUser(sessionId); | ||
if (!databaseSession) { | ||
debug.session.fail("Session not found", sessionId); | ||
throw new LuciaError("AUTH_INVALID_SESSION_ID"); | ||
} | ||
if (!isValidDatabaseSession(databaseSession)) { | ||
debug.session.fail(`Session expired at ${new Date(Number(databaseSession.idle_expires))}`, sessionId); | ||
throw new LuciaError("AUTH_INVALID_SESSION_ID"); | ||
} | ||
return [databaseSession, databaseUser]; | ||
const sessionState = this.sessionController.getSessionState(databaseSession.expiresAt); | ||
if (sessionState === "expired") { | ||
debug.session.fail("Session expired", sessionId); | ||
await this.adapter.deleteSession(databaseSession.sessionId); | ||
return { session: null, user: null }; | ||
} | ||
const databaseSession = await this.getDatabaseSession(sessionId); | ||
const databaseUser = await this.getDatabaseUser(databaseSession.user_id); | ||
return [databaseSession, databaseUser]; | ||
}; | ||
validateSessionIdArgument = (sessionId) => { | ||
if (!sessionId) { | ||
debug.session.fail("Empty session id"); | ||
throw new LuciaError("AUTH_INVALID_SESSION_ID"); | ||
let expiresAt = databaseSession.expiresAt; | ||
let fresh = false; | ||
if (sessionState === "idle") { | ||
expiresAt = this.sessionController.createExpirationDate(); | ||
await this.adapter.updateSession(databaseSession.sessionId, { | ||
expiresAt | ||
}); | ||
fresh = true; | ||
} | ||
}; | ||
getNewSessionExpiration = (sessionExpiresIn) => { | ||
const activePeriodExpiresAt = new Date(new Date().getTime() + | ||
(sessionExpiresIn?.activePeriod ?? this.sessionExpiresIn.activePeriod)); | ||
const idlePeriodExpiresAt = new Date(activePeriodExpiresAt.getTime() + | ||
(sessionExpiresIn?.idlePeriod ?? this.sessionExpiresIn.idlePeriod)); | ||
return { activePeriodExpiresAt, idlePeriodExpiresAt }; | ||
}; | ||
getUser = async (userId) => { | ||
const databaseUser = await this.getDatabaseUser(userId); | ||
const user = this.transformDatabaseUser(databaseUser); | ||
return user; | ||
}; | ||
createUser = async (options) => { | ||
const userId = options.userId ?? generateRandomString(15); | ||
const userAttributes = options.attributes ?? {}; | ||
const databaseUser = { | ||
...userAttributes, | ||
id: userId | ||
const session = { | ||
id: databaseSession.sessionId, | ||
userId: databaseSession.id, | ||
fresh, | ||
expiresAt, | ||
...this.getSessionAttributes(databaseSession.attributes) | ||
}; | ||
if (options.key === null) { | ||
await this.adapter.setUser(databaseUser, null); | ||
return this.transformDatabaseUser(databaseUser); | ||
} | ||
const keyId = createKeyId(options.key.providerId, options.key.providerUserId); | ||
const password = options.key.password; | ||
const hashedPassword = password === null ? null : await this.passwordHash.generate(password); | ||
await this.adapter.setUser(databaseUser, { | ||
id: keyId, | ||
user_id: userId, | ||
hashed_password: hashedPassword | ||
const user = { | ||
...this.getUserAttributes(databaseUser), | ||
id: databaseUser.id | ||
}; | ||
return { user, session }; | ||
} | ||
async createSession(userId, attributes) { | ||
const sessionId = generateRandomString(40, alphabet("0-9", "a-z")); | ||
const sessionExpiresAt = this.sessionController.createExpirationDate(); | ||
await this.adapter.setSession({ | ||
sessionId, | ||
id: userId, | ||
expiresAt: sessionExpiresAt, | ||
attributes | ||
}); | ||
return this.transformDatabaseUser(databaseUser); | ||
}; | ||
updateUserAttributes = async (userId, attributes) => { | ||
await this.adapter.updateUser(userId, attributes); | ||
return await this.getUser(userId); | ||
}; | ||
deleteUser = async (userId) => { | ||
await this.adapter.deleteSessionsByUserId(userId); | ||
await this.adapter.deleteKeysByUserId(userId); | ||
await this.adapter.deleteUser(userId); | ||
}; | ||
useKey = async (providerId, providerUserId, password) => { | ||
const keyId = createKeyId(providerId, providerUserId); | ||
const databaseKey = await this.adapter.getKey(keyId); | ||
if (!databaseKey) { | ||
debug.key.fail("Key not found", keyId); | ||
throw new LuciaError("AUTH_INVALID_KEY_ID"); | ||
} | ||
const hashedPassword = databaseKey.hashed_password; | ||
if (hashedPassword !== null) { | ||
debug.key.info("Key includes password"); | ||
if (!password) { | ||
debug.key.fail("Key password not provided", keyId); | ||
throw new LuciaError("AUTH_INVALID_PASSWORD"); | ||
} | ||
const validPassword = await this.passwordHash.validate(password, hashedPassword); | ||
if (!validPassword) { | ||
debug.key.fail("Incorrect key password", password); | ||
throw new LuciaError("AUTH_INVALID_PASSWORD"); | ||
} | ||
debug.key.notice("Validated key password"); | ||
} | ||
else { | ||
if (password !== null) { | ||
debug.key.fail("Incorrect key password", password); | ||
throw new LuciaError("AUTH_INVALID_PASSWORD"); | ||
} | ||
debug.key.info("No password included in key"); | ||
} | ||
debug.key.success("Validated key", keyId); | ||
return this.transformDatabaseKey(databaseKey); | ||
}; | ||
getSession = async (sessionId) => { | ||
this.validateSessionIdArgument(sessionId); | ||
const [databaseSession, databaseUser] = await this.getDatabaseSessionAndUser(sessionId); | ||
const user = this.transformDatabaseUser(databaseUser); | ||
return this.transformDatabaseSession(databaseSession, { | ||
user, | ||
fresh: false | ||
}); | ||
}; | ||
getAllUserSessions = async (userId) => { | ||
const [user, databaseSessions] = await Promise.all([ | ||
this.getUser(userId), | ||
await this.adapter.getSessionsByUserId(userId) | ||
]); | ||
const validStoredUserSessions = databaseSessions | ||
.filter((databaseSession) => { | ||
return isValidDatabaseSession(databaseSession); | ||
}) | ||
.map((databaseSession) => { | ||
return this.transformDatabaseSession(databaseSession, { | ||
user, | ||
fresh: false | ||
}); | ||
}); | ||
return validStoredUserSessions; | ||
}; | ||
validateSession = async (sessionId) => { | ||
this.validateSessionIdArgument(sessionId); | ||
const [databaseSession, databaseUser] = await this.getDatabaseSessionAndUser(sessionId); | ||
const user = this.transformDatabaseUser(databaseUser); | ||
const session = this.transformDatabaseSession(databaseSession, { | ||
user, | ||
fresh: false | ||
}); | ||
if (session.state === "active") { | ||
debug.session.success("Validated session", session.sessionId); | ||
return session; | ||
} | ||
const { activePeriodExpiresAt, idlePeriodExpiresAt } = this.getNewSessionExpiration(); | ||
await this.adapter.updateSession(session.sessionId, { | ||
active_expires: activePeriodExpiresAt.getTime(), | ||
idle_expires: idlePeriodExpiresAt.getTime() | ||
}); | ||
const renewedDatabaseSession = { | ||
...session, | ||
idlePeriodExpiresAt, | ||
activePeriodExpiresAt, | ||
fresh: true | ||
}; | ||
return renewedDatabaseSession; | ||
}; | ||
createSession = async (options) => { | ||
const { activePeriodExpiresAt, idlePeriodExpiresAt } = this.getNewSessionExpiration(); | ||
const userId = options.userId; | ||
const sessionId = options?.sessionId ?? generateRandomString(40); | ||
const attributes = options.attributes; | ||
const databaseSession = { | ||
...attributes, | ||
const session = { | ||
id: sessionId, | ||
user_id: userId, | ||
active_expires: activePeriodExpiresAt.getTime(), | ||
idle_expires: idlePeriodExpiresAt.getTime() | ||
userId, | ||
fresh: true, | ||
expiresAt: sessionExpiresAt, | ||
...this.getSessionAttributes(attributes) | ||
}; | ||
const [user] = await Promise.all([ | ||
this.getUser(userId), | ||
this.adapter.setSession(databaseSession) | ||
]); | ||
return this.transformDatabaseSession(databaseSession, { | ||
user, | ||
fresh: false | ||
}); | ||
}; | ||
updateSessionAttributes = async (sessionId, attributes) => { | ||
this.validateSessionIdArgument(sessionId); | ||
await this.adapter.updateSession(sessionId, attributes); | ||
return this.getSession(sessionId); | ||
}; | ||
invalidateSession = async (sessionId) => { | ||
this.validateSessionIdArgument(sessionId); | ||
return session; | ||
} | ||
// public updateSessionAttributes = async ( | ||
// sessionId: string, | ||
// attributes: Partial<DatabaseSessionAttributes> | ||
// ): Promise<Session> => { | ||
// this.validateSessionIdArgument(sessionId); | ||
// await this.adapter.updateSession(sessionId, attributes); | ||
// return this.getSession(sessionId); | ||
// }; | ||
async invalidateSession(sessionId) { | ||
await this.adapter.deleteSession(sessionId); | ||
debug.session.notice("Invalidated session", sessionId); | ||
}; | ||
invalidateAllUserSessions = async (userId) => { | ||
await this.adapter.deleteSessionsByUserId(userId); | ||
}; | ||
deleteDeadUserSessions = async (userId) => { | ||
const databaseSessions = await this.adapter.getSessionsByUserId(userId); | ||
const deadSessionIds = databaseSessions | ||
.filter((databaseSession) => { | ||
return !isValidDatabaseSession(databaseSession); | ||
}) | ||
.map((databaseSession) => databaseSession.id); | ||
await Promise.all(deadSessionIds.map((deadSessionId) => { | ||
this.adapter.deleteSession(deadSessionId); | ||
})); | ||
}; | ||
/** | ||
* @deprecated To be removed in next major release | ||
*/ | ||
validateRequestOrigin = (request) => { | ||
if (request.method === null) { | ||
debug.request.fail("Request method unavailable"); | ||
throw new LuciaError("AUTH_INVALID_REQUEST"); | ||
} | ||
if (request.url === null) { | ||
debug.request.fail("Request url unavailable"); | ||
throw new LuciaError("AUTH_INVALID_REQUEST"); | ||
} | ||
if (request.method.toUpperCase() !== "GET" && | ||
request.method.toUpperCase() !== "HEAD") { | ||
const requestOrigin = request.headers.origin; | ||
if (!requestOrigin) { | ||
debug.request.fail("No request origin available"); | ||
throw new LuciaError("AUTH_INVALID_REQUEST"); | ||
} | ||
try { | ||
const url = safeParseUrl(request.url); | ||
const allowedSubDomains = typeof this.csrfProtection === "object" | ||
? this.csrfProtection.allowedSubDomains ?? [] | ||
: []; | ||
if (url === null || | ||
!isAllowedOrigin(requestOrigin, url.origin, allowedSubDomains)) { | ||
throw new LuciaError("AUTH_INVALID_REQUEST"); | ||
} | ||
debug.request.info("Valid request origin", requestOrigin); | ||
} | ||
catch { | ||
debug.request.fail("Invalid origin string", requestOrigin); | ||
// failed to parse url | ||
throw new LuciaError("AUTH_INVALID_REQUEST"); | ||
} | ||
} | ||
else { | ||
debug.request.notice("Skipping CSRF check"); | ||
} | ||
}; | ||
readSessionCookie = (cookieHeader) => { | ||
if (!cookieHeader) { | ||
debug.request.info("No session cookie found"); | ||
return null; | ||
} | ||
const cookies = parseCookie(cookieHeader); | ||
const sessionCookieName = this.sessionCookieConfig.name ?? DEFAULT_SESSION_COOKIE_NAME; | ||
const sessionId = cookies[sessionCookieName] ?? null; | ||
} | ||
async invalidateUserSessions(userId) { | ||
await this.adapter.deleteUserSessions(userId); | ||
} | ||
readSessionCookie(cookieHeader) { | ||
const sessionId = this.sessionCookieController.parseCookies(cookieHeader); | ||
if (sessionId) { | ||
@@ -372,8 +142,4 @@ debug.request.info("Found session cookie", sessionId); | ||
return sessionId; | ||
}; | ||
readBearerToken = (authorizationHeader) => { | ||
if (!authorizationHeader) { | ||
debug.request.info("No token found in authorization header"); | ||
return null; | ||
} | ||
} | ||
readBearerToken(authorizationHeader) { | ||
const [authScheme, token] = authorizationHeader.split(" "); | ||
@@ -385,70 +151,64 @@ if (authScheme !== "Bearer") { | ||
return token ?? null; | ||
}; | ||
handleRequest = ( | ||
// cant reference middleware type with Lucia.Auth | ||
...args) => { | ||
} | ||
handleRequest(...args) { | ||
const middleware = this.middleware; | ||
const sessionCookieName = this.sessionCookieConfig.name ?? DEFAULT_SESSION_COOKIE_NAME; | ||
return new AuthRequest(this, { | ||
csrfProtection: this.csrfProtection, | ||
requestContext: transformRequestContext(middleware({ | ||
args, | ||
env: this.env, | ||
sessionCookieName: sessionCookieName | ||
})) | ||
const requestContext = middleware({ | ||
args, | ||
sessionCookieName: this.sessionCookieController.cookieName | ||
}); | ||
}; | ||
createSessionCookie = (session) => { | ||
return createSessionCookie(session, { | ||
env: this.env, | ||
cookie: this.sessionCookieConfig | ||
}); | ||
}; | ||
createKey = async (options) => { | ||
const keyId = createKeyId(options.providerId, options.providerUserId); | ||
let hashedPassword = null; | ||
if (options.password !== null) { | ||
hashedPassword = await this.passwordHash.generate(options.password); | ||
debug.request.init(requestContext.request.method, requestContext.request.url ?? "(url unknown)"); | ||
const authorizationHeader = requestContext.request.headers.get("Authorization"); | ||
let bearerToken = authorizationHeader; | ||
if (authorizationHeader) { | ||
const parts = authorizationHeader.split(" "); | ||
if (parts.length === 2 && parts[0] === "Bearer") { | ||
bearerToken = parts[1]; | ||
} | ||
} | ||
const userId = options.userId; | ||
await this.adapter.setKey({ | ||
id: keyId, | ||
user_id: userId, | ||
hashed_password: hashedPassword | ||
}); | ||
return { | ||
providerId: options.providerId, | ||
providerUserId: options.providerUserId, | ||
passwordDefined: !!options.password, | ||
userId | ||
}; | ||
}; | ||
deleteKey = async (providerId, providerUserId) => { | ||
const keyId = createKeyId(providerId, providerUserId); | ||
await this.adapter.deleteKey(keyId); | ||
}; | ||
getKey = async (providerId, providerUserId) => { | ||
const keyId = createKeyId(providerId, providerUserId); | ||
const databaseKey = await this.adapter.getKey(keyId); | ||
if (!databaseKey) { | ||
throw new LuciaError("AUTH_INVALID_KEY_ID"); | ||
if (this.csrfProtection !== false) { | ||
const options = this.csrfProtection === true ? {} : this.csrfProtection; | ||
const validRequestOrigin = this.verifyRequestOrigin(requestContext, options); | ||
if (!validRequestOrigin) { | ||
return new AuthRequest(this, null, bearerToken, requestContext.setCookie); | ||
} | ||
} | ||
const key = this.transformDatabaseKey(databaseKey); | ||
return key; | ||
}; | ||
getAllUserKeys = async (userId) => { | ||
const [databaseKeys] = await Promise.all([ | ||
await this.adapter.getKeysByUserId(userId), | ||
this.getUser(userId) | ||
]); | ||
return databaseKeys.map((databaseKey) => this.transformDatabaseKey(databaseKey)); | ||
}; | ||
updateKeyPassword = async (providerId, providerUserId, password) => { | ||
const keyId = createKeyId(providerId, providerUserId); | ||
const hashedPassword = password === null ? null : await this.passwordHash.generate(password); | ||
await this.adapter.updateKey(keyId, { | ||
hashed_password: hashedPassword | ||
}); | ||
return await this.getKey(providerId, providerUserId); | ||
}; | ||
const sessionCookie = requestContext.sessionCookie ?? | ||
this.sessionCookieController.parseCookies(requestContext.request.headers.get("Cookie") ?? ""); | ||
return new AuthRequest(this, sessionCookie, bearerToken, requestContext.setCookie); | ||
} | ||
verifyRequestOrigin(requestContext, options) { | ||
const whitelist = ["GET", "HEAD", "OPTIONS", "TRACE"]; | ||
const allowedMethod = whitelist.some((val) => val === requestContext.request.method.toUpperCase()); | ||
if (allowedMethod) { | ||
return true; | ||
} | ||
const requestOrigin = requestContext.request.headers.get("Origin"); | ||
if (!requestOrigin) { | ||
debug.request.fail("Origin header unavailable"); | ||
return false; | ||
} | ||
const allowedDomains = options.allowedDomains ?? []; | ||
const hostHeader = requestContext.request.headers.get(options.hostHeader ?? "Host"); | ||
if (hostHeader) { | ||
allowedDomains.push(hostHeader); | ||
} | ||
if (requestContext.request.url !== undefined) { | ||
allowedDomains.push(requestContext.request.url); | ||
} | ||
debug.request.info("Allowed domains", allowedDomains.join(", ")); | ||
debug.request.info("Origin", requestOrigin ?? "(Origin unknown)"); | ||
const validOrigin = verifyRequestOrigin(requestOrigin, allowedDomains); | ||
if (validOrigin) { | ||
debug.request.info("Valid request origin"); | ||
return true; | ||
} | ||
debug.request.info("Invalid request origin"); | ||
return false; | ||
} | ||
createSessionCookie(sessionId) { | ||
return this.sessionCookieController.createSessionCookie(sessionId); | ||
} | ||
createBlankSessionCookie() { | ||
return this.sessionCookieController.createBlankSessionCookie(); | ||
} | ||
} |
@@ -1,57 +0,28 @@ | ||
import type { Auth, Env, Session } from "./index.js"; | ||
import type { Cookie } from "./cookie.js"; | ||
export type LuciaRequest = { | ||
method: string; | ||
url?: string; | ||
headers: Pick<Headers, "get">; | ||
}; | ||
export type RequestContext = { | ||
sessionCookie?: string | null; | ||
request: LuciaRequest; | ||
setCookie: (cookie: Cookie) => void; | ||
}; | ||
export type Middleware<Args extends any[] = any> = (context: { | ||
args: Args; | ||
env: Env; | ||
sessionCookieName: string; | ||
}) => MiddlewareRequestContext; | ||
type MiddlewareRequestContext = Omit<RequestContext, "request"> & { | ||
sessionCookie?: string | null; | ||
request: { | ||
method: string; | ||
url?: string; | ||
headers: Pick<Headers, "get"> | { | ||
origin: string | null; | ||
cookie: string | null; | ||
authorization: string | null; | ||
}; | ||
storedSessionCookie?: string | null; | ||
}; | ||
setCookie: (cookie: Cookie) => void; | ||
}; | ||
export type CSRFProtectionConfiguration = { | ||
host?: string; | ||
hostHeader?: string; | ||
allowedSubDomains?: string[] | "*"; | ||
}; | ||
export declare class AuthRequest<_Auth extends Auth = any> { | ||
import type { SessionCookie } from "oslo/session"; | ||
import type { Lucia, Session, User } from "./index.js"; | ||
export declare class AuthRequest<_Lucia extends Lucia = Lucia> { | ||
private auth; | ||
private requestContext; | ||
constructor(auth: _Auth, config: { | ||
requestContext: RequestContext; | ||
csrfProtection: boolean | CSRFProtectionConfiguration; | ||
}); | ||
private sessionCookie; | ||
private bearerToken; | ||
private setCookie; | ||
constructor(auth: _Lucia, sessionCookie: string | null, bearerToken: string | null, setCookie: (cookie: SessionCookie) => void); | ||
private validatePromise; | ||
private validateBearerTokenPromise; | ||
private storedSessionId; | ||
private bearerToken; | ||
setSession: (session: Session | null) => void; | ||
private maybeSetSession; | ||
private setSessionCookie; | ||
validate: () => Promise<Session | null>; | ||
validateBearerToken: () => Promise<Session | null>; | ||
setSessionCookie(sessionId: string): void; | ||
deleteSessionCookie(): void; | ||
validate(): Promise<{ | ||
user: User; | ||
session: Session; | ||
} | { | ||
user: null; | ||
session: null; | ||
}>; | ||
validateBearerToken(): Promise<{ | ||
user: User; | ||
session: Session; | ||
} | { | ||
user: null; | ||
session: null; | ||
}>; | ||
invalidate(): void; | ||
private isValidRequestOrigin; | ||
} | ||
export declare const transformRequestContext: ({ request, setCookie, sessionCookie }: MiddlewareRequestContext) => RequestContext; | ||
export {}; |
@@ -1,105 +0,54 @@ | ||
import { debug } from "../utils/debug.js"; | ||
import { LuciaError } from "./error.js"; | ||
import { createHeadersFromObject } from "../utils/request.js"; | ||
import { isAllowedOrigin, safeParseUrl } from "../utils/url.js"; | ||
export class AuthRequest { | ||
auth; | ||
requestContext; | ||
constructor(auth, config) { | ||
debug.request.init(config.requestContext.request.method, config.requestContext.request.url ?? "(url unknown)"); | ||
sessionCookie; | ||
bearerToken; | ||
setCookie; | ||
constructor(auth, sessionCookie, bearerToken, setCookie) { | ||
this.auth = auth; | ||
this.requestContext = config.requestContext; | ||
const csrfProtectionConfig = typeof config.csrfProtection === "object" ? config.csrfProtection : {}; | ||
const csrfProtectionEnabled = config.csrfProtection !== false; | ||
if (!csrfProtectionEnabled || | ||
this.isValidRequestOrigin(csrfProtectionConfig)) { | ||
this.storedSessionId = | ||
this.requestContext.sessionCookie ?? | ||
auth.readSessionCookie(this.requestContext.request.headers.get("Cookie")); | ||
} | ||
else { | ||
this.storedSessionId = null; | ||
} | ||
this.bearerToken = auth.readBearerToken(this.requestContext.request.headers.get("Authorization")); | ||
this.sessionCookie = sessionCookie; | ||
this.bearerToken = bearerToken; | ||
this.setCookie = setCookie; | ||
} | ||
validatePromise = null; | ||
validateBearerTokenPromise = null; | ||
storedSessionId; | ||
bearerToken; | ||
setSession = (session) => { | ||
const sessionId = session?.sessionId ?? null; | ||
if (this.storedSessionId === sessionId) | ||
setSessionCookie(sessionId) { | ||
if (this.sessionCookie !== sessionId) { | ||
this.validatePromise = null; | ||
} | ||
this.setCookie(this.auth.createSessionCookie(sessionId)); | ||
} | ||
deleteSessionCookie() { | ||
if (this.sessionCookie === null) | ||
return; | ||
this.sessionCookie = null; | ||
this.validatePromise = null; | ||
this.setSessionCookie(session); | ||
}; | ||
maybeSetSession = (session) => { | ||
try { | ||
this.setSession(session); | ||
} | ||
catch { | ||
// ignore error | ||
// some middleware throw error | ||
} | ||
}; | ||
setSessionCookie = (session) => { | ||
const sessionId = session?.sessionId ?? null; | ||
if (this.storedSessionId === sessionId) | ||
return; | ||
this.storedSessionId = sessionId; | ||
this.requestContext.setCookie(this.auth.createSessionCookie(session)); | ||
if (session) { | ||
debug.request.notice("Session cookie stored", session.sessionId); | ||
} | ||
else { | ||
debug.request.notice("Session cookie deleted"); | ||
} | ||
}; | ||
validate = async () => { | ||
if (this.validatePromise) { | ||
debug.request.info("Using cached result for session validation"); | ||
return this.validatePromise; | ||
} | ||
this.validatePromise = new Promise(async (resolve, reject) => { | ||
if (!this.storedSessionId) | ||
return resolve(null); | ||
try { | ||
const session = await this.auth.validateSession(this.storedSessionId); | ||
if (session.fresh) { | ||
this.maybeSetSession(session); | ||
this.setCookie(this.auth.createBlankSessionCookie()); | ||
} | ||
async validate() { | ||
if (!this.validatePromise) { | ||
this.validatePromise = new Promise(async (resolve) => { | ||
if (!this.sessionCookie) { | ||
return resolve({ session: null, user: null }); | ||
} | ||
return resolve(session); | ||
} | ||
catch (e) { | ||
if (e instanceof LuciaError && | ||
e.message === "AUTH_INVALID_SESSION_ID") { | ||
this.maybeSetSession(null); | ||
return resolve(null); | ||
const result = await this.auth.validateSession(this.sessionCookie); | ||
if (result.session && result.session.fresh) { | ||
const sessionCookie = this.auth.createSessionCookie(result.session.id); | ||
this.setCookie(sessionCookie); | ||
} | ||
return reject(e); | ||
} | ||
}); | ||
return resolve(result); | ||
}); | ||
} | ||
return await this.validatePromise; | ||
}; | ||
validateBearerToken = async () => { | ||
if (this.validateBearerTokenPromise) { | ||
debug.request.info("Using cached result for bearer token validation"); | ||
return this.validatePromise; | ||
} | ||
async validateBearerToken() { | ||
if (!this.validateBearerTokenPromise) { | ||
this.validateBearerTokenPromise = new Promise(async (resolve, reject) => { | ||
if (!this.bearerToken) { | ||
return resolve({ session: null, user: null }); | ||
} | ||
return await this.auth.validateSession(this.bearerToken); | ||
}); | ||
} | ||
this.validatePromise = new Promise(async (resolve, reject) => { | ||
if (!this.bearerToken) | ||
return resolve(null); | ||
try { | ||
const session = await this.auth.validateSession(this.bearerToken); | ||
return resolve(session); | ||
} | ||
catch (e) { | ||
if (e instanceof LuciaError) { | ||
return resolve(null); | ||
} | ||
return reject(e); | ||
} | ||
}); | ||
return await this.validatePromise; | ||
}; | ||
return await this.validateBearerTokenPromise; | ||
} | ||
invalidate() { | ||
@@ -109,47 +58,2 @@ this.validatePromise = null; | ||
} | ||
isValidRequestOrigin = (config) => { | ||
const request = this.requestContext.request; | ||
const whitelist = ["GET", "HEAD", "OPTIONS", "TRACE"]; | ||
if (whitelist.some((val) => val === request.method.toUpperCase())) { | ||
return true; | ||
} | ||
const requestOrigin = request.headers.get("Origin"); | ||
if (!requestOrigin) | ||
return false; | ||
if (!requestOrigin) { | ||
debug.request.fail("No request origin available"); | ||
return false; | ||
} | ||
let host = null; | ||
if (config.host !== undefined) { | ||
host = config.host ?? null; | ||
} | ||
else if (request.url !== null && request.url !== undefined) { | ||
host = safeParseUrl(request.url)?.host ?? null; | ||
} | ||
else { | ||
host = request.headers.get(config.hostHeader ?? "Host"); | ||
} | ||
debug.request.info("Host", host ?? "(Host unknown)"); | ||
if (host !== null && | ||
isAllowedOrigin(requestOrigin, host, config.allowedSubDomains ?? [])) { | ||
debug.request.info("Valid request origin", requestOrigin); | ||
return true; | ||
} | ||
debug.request.info("Invalid request origin", requestOrigin); | ||
return false; | ||
}; | ||
} | ||
export const transformRequestContext = ({ request, setCookie, sessionCookie }) => { | ||
return { | ||
request: { | ||
url: request.url, | ||
method: request.method, | ||
headers: "authorization" in request.headers | ||
? createHeadersFromObject(request.headers) | ||
: request.headers | ||
}, | ||
setCookie, | ||
sessionCookie: sessionCookie ?? request.storedSessionCookie | ||
}; | ||
}; |
@@ -1,13 +0,18 @@ | ||
export { lucia } from "./auth/index.js"; | ||
export { DEFAULT_SESSION_COOKIE_NAME } from "./auth/cookie.js"; | ||
export { LuciaError } from "./auth/error.js"; | ||
export { createKeyId } from "./auth/database.js"; | ||
export type GlobalAuth = Lucia.Auth; | ||
export type GlobalDatabaseUserAttributes = Lucia.DatabaseUserAttributes; | ||
export type GlobalDatabaseSessionAttributes = Lucia.DatabaseSessionAttributes; | ||
export type { User, Key, Session, Configuration, Env, Auth } from "./auth/index.js"; | ||
export type { Adapter, InitializeAdapter, UserAdapter, SessionAdapter } from "./auth/adapter.js"; | ||
export type { UserSchema, KeySchema, SessionSchema } from "./auth/database.js"; | ||
export type { RequestContext, Middleware, AuthRequest } from "./auth/request.js"; | ||
export type { Cookie } from "./auth/cookie.js"; | ||
export type { LuciaErrorConstructor } from "./auth/error.js"; | ||
export { Lucia } from "./auth/index.js"; | ||
export { AuthRequest } from "./auth/request.js"; | ||
export { generateScryptHash as generateLegacyLuciaPasswordHash, verifyScryptHash as verifyLegacyLuciaPasswordHash } from "./utils/crypto.js"; | ||
export { TimeSpan } from "oslo"; | ||
export type { User, Session, ExperimentalOptions, SessionCookieOptions, CSRFProtectionOptions, RequestContext, Middleware } from "./auth/index.js"; | ||
export type { DatabaseSession, DatabaseUser, Adapter, SessionAdapter } from "./auth/database.js"; | ||
export interface Register { | ||
} | ||
import type { Lucia } from "./auth/index.js"; | ||
export type RegisteredLucia = Register extends { | ||
Lucia: infer _Lucia; | ||
} ? _Lucia extends Lucia ? _Lucia : Lucia : Lucia; | ||
export type DatabaseUserAttributes = Register extends { | ||
DatabaseUserAttributes: {}; | ||
} ? Register["DatabaseUserAttributes"] : {}; | ||
export type DatabaseSessionAttributes = Register extends { | ||
DatabaseSessionAttributes: {}; | ||
} ? Register["DatabaseSessionAttributes"] : {}; |
@@ -1,4 +0,4 @@ | ||
export { lucia } from "./auth/index.js"; | ||
export { DEFAULT_SESSION_COOKIE_NAME } from "./auth/cookie.js"; | ||
export { LuciaError } from "./auth/error.js"; | ||
export { createKeyId } from "./auth/database.js"; | ||
export { Lucia } from "./auth/index.js"; | ||
export { AuthRequest } from "./auth/request.js"; | ||
export { generateScryptHash as generateLegacyLuciaPasswordHash, verifyScryptHash as verifyLegacyLuciaPasswordHash } from "./utils/crypto.js"; | ||
export { TimeSpan } from "oslo"; |
@@ -1,11 +0,11 @@ | ||
import type { CookieAttributes } from "../utils/cookie.js"; | ||
import type { Middleware, RequestContext } from "../auth/request.js"; | ||
type NodeIncomingMessage = { | ||
import type { CookieAttributes } from "oslo/cookie"; | ||
import type { Middleware, RequestContext } from "../auth/index.js"; | ||
interface NodeIncomingMessage { | ||
method?: string; | ||
headers: Record<string, string | string[] | undefined>; | ||
}; | ||
type NodeOutGoingMessage = { | ||
} | ||
interface NodeOutGoingMessage { | ||
getHeader: (name: string) => string | string[] | number | undefined; | ||
setHeader: (name: string, value: string | number | readonly string[]) => void; | ||
}; | ||
} | ||
export declare const node: () => Middleware<[ | ||
@@ -15,19 +15,19 @@ NodeIncomingMessage, | ||
]>; | ||
type ExpressRequest = { | ||
interface ExpressRequest { | ||
method: string; | ||
headers: Record<string, string | string[] | undefined>; | ||
}; | ||
type ExpressResponse = { | ||
} | ||
interface ExpressResponse { | ||
cookie: (name: string, val: string, options?: CookieAttributes) => void; | ||
}; | ||
} | ||
export declare const express: () => Middleware<[ExpressRequest, ExpressResponse]>; | ||
type FastifyRequest = { | ||
interface FastifyRequest { | ||
method: string; | ||
headers: Record<string, string | string[] | undefined>; | ||
}; | ||
type FastifyReply = { | ||
} | ||
interface FastifyReply { | ||
header: (name: string, val: any) => void; | ||
}; | ||
} | ||
export declare const fastify: () => Middleware<[FastifyRequest, FastifyReply]>; | ||
type SvelteKitRequestEvent = { | ||
interface SvelteKitRequestEvent { | ||
request: Request; | ||
@@ -38,3 +38,3 @@ cookies: { | ||
}; | ||
}; | ||
} | ||
export declare const sveltekit: () => Middleware<[SvelteKitRequestEvent]>; | ||
@@ -51,3 +51,3 @@ type AstroAPIContext = { | ||
export declare const astro: () => Middleware<[AstroAPIContext]>; | ||
type QwikRequestEvent = { | ||
interface QwikRequestEvent { | ||
request: Request; | ||
@@ -60,5 +60,5 @@ cookie: { | ||
}; | ||
}; | ||
} | ||
export declare const qwik: () => Middleware<[QwikRequestEvent]>; | ||
type ElysiaContext = { | ||
interface ElysiaContext { | ||
request: Request; | ||
@@ -70,10 +70,10 @@ set: { | ||
}; | ||
}; | ||
} | ||
export declare const elysia: () => Middleware<[ElysiaContext]>; | ||
export declare const lucia: () => Middleware<[RequestContext]>; | ||
export declare const web: () => Middleware<[Request]>; | ||
type NextJsPagesServerContext = { | ||
interface NextJsPagesServerContext { | ||
req: NodeIncomingMessage; | ||
res?: NodeOutGoingMessage; | ||
}; | ||
} | ||
type NextCookie = { | ||
@@ -90,20 +90,17 @@ name: string; | ||
}; | ||
type NextRequest = Request & { | ||
interface NextRequest extends Request { | ||
cookies: { | ||
get: (name: string) => NextCookie; | ||
}; | ||
}; | ||
type NextJsAppServerContext = { | ||
} | ||
interface NextJsAppServerContext { | ||
cookies: NextCookiesFunction; | ||
request: NextRequest | null; | ||
}; | ||
export declare const nextjs: () => Middleware<[ | ||
NextJsPagesServerContext | NextJsAppServerContext | NextRequest | ||
]>; | ||
type NextJsAppServerContext_V3 = { | ||
} | ||
interface NextJsAppServerContext { | ||
headers: NextHeadersFunction; | ||
cookies: NextCookiesFunction; | ||
}; | ||
export declare const nextjs_future: () => Middleware<[NextJsPagesServerContext] | [NextRequest] | [requestMethod: string, context: NextJsAppServerContext_V3]>; | ||
type H3Event = { | ||
} | ||
export declare const nextjs: () => Middleware<[NextJsPagesServerContext] | [NextRequest] | [requestMethod: string, context: NextJsAppServerContext]>; | ||
interface H3Event { | ||
node: { | ||
@@ -113,5 +110,5 @@ req: NodeIncomingMessage; | ||
}; | ||
}; | ||
} | ||
export declare const h3: () => Middleware<[H3Event]>; | ||
type HonoContext = { | ||
interface HonoContext { | ||
req: { | ||
@@ -123,4 +120,4 @@ url: string; | ||
header: (name: string, value: string) => void; | ||
}; | ||
} | ||
export declare const hono: () => Middleware<[HonoContext]>; | ||
export {}; |
@@ -133,60 +133,2 @@ import { createHeadersFromObject } from "../utils/request.js"; | ||
export const nextjs = () => { | ||
return ({ args, sessionCookieName, env }) => { | ||
const [serverContext] = args; | ||
if ("cookies" in serverContext) { | ||
// for some reason `"request" in NextRequest` returns true??? | ||
const request = typeof serverContext.cookies === "function" | ||
? serverContext.request | ||
: serverContext; | ||
const readonlyCookieStore = typeof serverContext.cookies === "function" | ||
? serverContext.cookies() | ||
: serverContext.cookies; | ||
const sessionCookie = readonlyCookieStore.get(sessionCookieName)?.value ?? null; | ||
const requestContext = { | ||
request: request ?? { | ||
method: "GET", | ||
headers: new Headers() | ||
}, | ||
sessionCookie, | ||
setCookie: (cookie) => { | ||
if (typeof serverContext.cookies !== "function") | ||
return; | ||
const cookieStore = serverContext.cookies(); | ||
if (!cookieStore.set) | ||
return; | ||
try { | ||
cookieStore.set(cookie.name, cookie.value, cookie.attributes); | ||
} | ||
catch { | ||
// ignore - set() is not available | ||
} | ||
} | ||
}; | ||
return requestContext; | ||
} | ||
const req = "req" in serverContext ? serverContext.req : serverContext; | ||
const res = "res" in serverContext ? serverContext.res : null; | ||
const request = { | ||
method: req.method ?? "", | ||
headers: createHeadersFromObject(req.headers) | ||
}; | ||
return { | ||
request, | ||
setCookie: (cookie) => { | ||
if (!res) | ||
return; | ||
const setCookieHeaderValues = res | ||
.getHeader("Set-Cookie") | ||
?.toString() | ||
.split(",") | ||
.filter((val) => val) ?? []; | ||
res.setHeader("Set-Cookie", [ | ||
cookie.serialize(), | ||
...setCookieHeaderValues | ||
]); | ||
} | ||
}; | ||
}; | ||
}; | ||
export const nextjs_future = () => { | ||
return ({ args, sessionCookieName }) => { | ||
@@ -247,8 +189,7 @@ if (args.length === 2) { | ||
const nodeMiddleware = node(); | ||
return ({ args, sessionCookieName, env }) => { | ||
return ({ args, sessionCookieName }) => { | ||
const [context] = args; | ||
return nodeMiddleware({ | ||
args: [context.node.req, context.node.res], | ||
sessionCookieName, | ||
env | ||
sessionCookieName | ||
}); | ||
@@ -255,0 +196,0 @@ }; |
@@ -1,4 +0,2 @@ | ||
export declare const generateRandomString: (length: number, alphabet?: string) => string; | ||
export declare const generateScryptHash: (s: string) => Promise<string>; | ||
export declare const validateScryptHash: (s: string, hash: string) => Promise<boolean>; | ||
export declare const convertUint8ArrayToHex: (arr: Uint8Array) => string; | ||
export declare const verifyScryptHash: (s: string, hash: string) => Promise<boolean>; |
@@ -1,15 +0,10 @@ | ||
import { LuciaError } from "../auth/error.js"; | ||
import { scryptAsync as scrypt } from "@noble/hashes/scrypt"; | ||
import { customAlphabet } from "nanoid"; | ||
export const generateRandomString = (length, alphabet) => { | ||
const DEFAULT_ALPHABET = "abcdefghijklmnopqrstuvwxyz1234567890"; | ||
const customNanoid = customAlphabet(alphabet ?? DEFAULT_ALPHABET); | ||
return customNanoid(length); | ||
}; | ||
import { encodeHex, decodeHex } from "oslo/encoding"; | ||
import { constantTimeEqual } from "oslo/crypto"; | ||
import { scrypt } from "../scrypt/index.js"; | ||
export const generateScryptHash = async (s) => { | ||
const salt = generateRandomString(16); | ||
const key = await hashWithScrypt(s.normalize("NFKC"), salt); | ||
return `s2:${salt}:${key}`; | ||
const salt = encodeHex(crypto.getRandomValues(new Uint8Array(16))); | ||
const key = await generateScryptKey(s.normalize("NFKC"), salt); | ||
return `s2:${salt}:${encodeHex(key)}`; | ||
}; | ||
const hashWithScrypt = async (s, salt, blockSize = 16) => { | ||
const generateScryptKey = async (s, salt, blockSize = 16) => { | ||
const keyUint8Array = await scrypt(new TextEncoder().encode(s), new TextEncoder().encode(salt), { | ||
@@ -21,16 +16,10 @@ N: 16384, | ||
}); | ||
return convertUint8ArrayToHex(keyUint8Array); | ||
return keyUint8Array; | ||
}; | ||
export const validateScryptHash = async (s, hash) => { | ||
// detect bcrypt hash | ||
// lucia used bcrypt in one of the beta versions | ||
// TODO: remove in v3 | ||
if (hash.startsWith("$2a")) { | ||
throw new LuciaError("AUTH_OUTDATED_PASSWORD"); | ||
} | ||
export const verifyScryptHash = async (s, hash) => { | ||
const arr = hash.split(":"); | ||
if (arr.length === 2) { | ||
const [salt, key] = arr; | ||
const targetKey = await hashWithScrypt(s.normalize("NFKC"), salt, 8); | ||
const result = constantTimeEqual(targetKey, key); | ||
const targetKey = await generateScryptKey(s.normalize("NFKC"), salt, 8); | ||
const result = constantTimeEqual(targetKey, decodeHex(key)); | ||
return result; | ||
@@ -42,22 +31,6 @@ } | ||
if (version === "s2") { | ||
const targetKey = await hashWithScrypt(s.normalize("NFKC"), salt); | ||
const result = constantTimeEqual(targetKey, key); | ||
return result; | ||
const targetKey = await generateScryptKey(s.normalize("NFKC"), salt); | ||
return constantTimeEqual(targetKey, decodeHex(key)); | ||
} | ||
return false; | ||
}; | ||
const constantTimeEqual = (a, b) => { | ||
if (a.length !== b.length) { | ||
return false; | ||
} | ||
const aUint8Array = new TextEncoder().encode(a); | ||
const bUint8Array = new TextEncoder().encode(b); | ||
let c = 0; | ||
for (let i = 0; i < a.length; i++) { | ||
c |= aUint8Array[i] ^ bUint8Array[i]; // ^: XOR operator | ||
} | ||
return c === 0; | ||
}; | ||
export const convertUint8ArrayToHex = (arr) => { | ||
return [...arr].map((x) => x.toString(16).padStart(2, "0")).join(""); | ||
}; |
{ | ||
"name": "lucia", | ||
"version": "2.7.3", | ||
"version": "3.0.0-beta.0", | ||
"description": "A simple and flexible authentication library", | ||
@@ -23,4 +23,3 @@ "main": "dist/index.js", | ||
"./middleware": "./dist/middleware/index.js", | ||
"./polyfill/node": "./dist/polyfill/node.js", | ||
"./utils": "./dist/utils/index.js" | ||
"./polyfill/node": "./dist/polyfill/node.js" | ||
}, | ||
@@ -34,5 +33,2 @@ "typesVersions": { | ||
"dist/polyfill/node.d.ts" | ||
], | ||
"utils": [ | ||
"dist/utils/index.d.ts" | ||
] | ||
@@ -54,4 +50,3 @@ } | ||
"dependencies": { | ||
"@noble/hashes": "1.3.2", | ||
"nanoid": "5.0.1" | ||
"oslo": "^0.19.0" | ||
}, | ||
@@ -58,0 +53,0 @@ "scripts": { |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
1
61762
26
1171
2
1
+ Addedoslo@^0.19.0
+ Added@emnapi/core@1.3.1(transitive)
+ Added@emnapi/runtime@1.3.1(transitive)
+ Added@emnapi/wasi-threads@1.0.1(transitive)
+ Added@napi-rs/wasm-runtime@0.2.6(transitive)
+ Added@node-rs/argon2@1.8.3(transitive)
+ Added@node-rs/argon2-android-arm-eabi@1.8.3(transitive)
+ Added@node-rs/argon2-android-arm64@1.8.3(transitive)
+ Added@node-rs/argon2-darwin-arm64@1.8.3(transitive)
+ Added@node-rs/argon2-darwin-x64@1.8.3(transitive)
+ Added@node-rs/argon2-freebsd-x64@1.8.3(transitive)
+ Added@node-rs/argon2-linux-arm-gnueabihf@1.8.3(transitive)
+ Added@node-rs/argon2-linux-arm64-gnu@1.8.3(transitive)
+ Added@node-rs/argon2-linux-arm64-musl@1.8.3(transitive)
+ Added@node-rs/argon2-linux-x64-gnu@1.8.3(transitive)
+ Added@node-rs/argon2-linux-x64-musl@1.8.3(transitive)
+ Added@node-rs/argon2-wasm32-wasi@1.8.3(transitive)
+ Added@node-rs/argon2-win32-arm64-msvc@1.8.3(transitive)
+ Added@node-rs/argon2-win32-ia32-msvc@1.8.3(transitive)
+ Added@node-rs/argon2-win32-x64-msvc@1.8.3(transitive)
+ Added@node-rs/bcrypt@1.10.7(transitive)
+ Added@node-rs/bcrypt-android-arm-eabi@1.10.7(transitive)
+ Added@node-rs/bcrypt-android-arm64@1.10.7(transitive)
+ Added@node-rs/bcrypt-darwin-arm64@1.10.7(transitive)
+ Added@node-rs/bcrypt-darwin-x64@1.10.7(transitive)
+ Added@node-rs/bcrypt-freebsd-x64@1.10.7(transitive)
+ Added@node-rs/bcrypt-linux-arm-gnueabihf@1.10.7(transitive)
+ Added@node-rs/bcrypt-linux-arm64-gnu@1.10.7(transitive)
+ Added@node-rs/bcrypt-linux-arm64-musl@1.10.7(transitive)
+ Added@node-rs/bcrypt-linux-x64-gnu@1.10.7(transitive)
+ Added@node-rs/bcrypt-linux-x64-musl@1.10.7(transitive)
+ Added@node-rs/bcrypt-wasm32-wasi@1.10.7(transitive)
+ Added@node-rs/bcrypt-win32-arm64-msvc@1.10.7(transitive)
+ Added@node-rs/bcrypt-win32-ia32-msvc@1.10.7(transitive)
+ Added@node-rs/bcrypt-win32-x64-msvc@1.10.7(transitive)
+ Added@tybys/wasm-util@0.9.0(transitive)
+ Addedoslo@0.19.0(transitive)
+ Addedtslib@2.8.1(transitive)
- Removed@noble/hashes@1.3.2
- Removednanoid@5.0.1
- Removed@noble/hashes@1.3.2(transitive)
- Removednanoid@5.0.1(transitive)