@adonisjs/auth
Version:
Official authentication provider for Adonis framework
390 lines (389 loc) • 14.1 kB
JavaScript
import { n as E_UNAUTHORIZED_ACCESS } from "../../errors-sGy-K8pd.js";
import "../../symbols-BQLDWwuQ.js";
import { RuntimeException } from "@adonisjs/core/exceptions";
import { inspect } from "node:util";
import { createHash } from "node:crypto";
import string from "@adonisjs/core/helpers/string";
import { Secret, base64, safeEqual } from "@adonisjs/core/helpers";
var SessionGuard = class {
#name;
#ctx;
#options;
#userProvider;
#emitter;
driverName = "session";
authenticationAttempted = false;
attemptedViaRemember = false;
isAuthenticated = false;
viaRemember = false;
isLoggedOut = false;
user;
get sessionKeyName() {
return `auth_${this.#name}`;
}
get rememberMeKeyName() {
return `remember_${this.#name}`;
}
constructor(name, ctx, options, emitter, userProvider) {
this.#name = name;
this.#ctx = ctx;
this.#options = {
rememberMeTokensAge: "2 years",
...options
};
this.#emitter = emitter;
this.#userProvider = userProvider;
}
#getSession() {
if (!("session" in this.#ctx)) throw new RuntimeException("Cannot authenticate user. Install and configure \"@adonisjs/session\" package");
return this.#ctx.session;
}
#authenticationFailed(sessionId) {
this.isAuthenticated = false;
this.viaRemember = false;
this.user = void 0;
this.isLoggedOut = false;
const error = new E_UNAUTHORIZED_ACCESS("Invalid or expired user session", { guardDriverName: this.driverName });
this.#emitter.emit("session_auth:authentication_failed", {
ctx: this.#ctx,
guardName: this.#name,
error,
sessionId
});
return error;
}
#authenticationSucceeded(sessionId, user, rememberMeToken) {
this.isAuthenticated = true;
this.viaRemember = !!rememberMeToken;
this.user = user;
this.isLoggedOut = false;
this.#emitter.emit("session_auth:authentication_succeeded", {
ctx: this.#ctx,
guardName: this.#name,
sessionId,
user,
rememberMeToken
});
}
#loginSucceeded(sessionId, user, rememberMeToken) {
this.user = user;
this.isLoggedOut = false;
this.#emitter.emit("session_auth:login_succeeded", {
ctx: this.#ctx,
guardName: this.#name,
sessionId,
user,
rememberMeToken
});
}
#createSessionForUser(userId) {
const session = this.#getSession();
session.put(this.sessionKeyName, userId);
session.regenerate();
}
#createRememberMeCookie(value) {
this.#ctx.response.encryptedCookie(this.rememberMeKeyName, value.release(), {
maxAge: this.#options.rememberMeTokensAge,
httpOnly: true
});
}
async #authenticateViaId(userId, sessionId) {
const providerUser = await this.#userProvider.findById(userId);
if (!providerUser) throw this.#authenticationFailed(sessionId);
this.#authenticationSucceeded(sessionId, providerUser.getOriginal());
return this.user;
}
async #authenticateViaRememberCookie(rememberMeCookie, sessionId) {
const userProvider = this.#userProvider;
const token = await userProvider.verifyRememberToken(new Secret(rememberMeCookie));
if (!token) throw this.#authenticationFailed(sessionId);
const providerUser = await userProvider.findById(token.tokenableId);
if (!providerUser) throw this.#authenticationFailed(sessionId);
const recycledToken = await userProvider.recycleRememberToken(providerUser.getOriginal(), token.identifier, this.#options.rememberMeTokensAge);
this.#createRememberMeCookie(recycledToken.value);
this.#createSessionForUser(providerUser.getId());
this.#authenticationSucceeded(sessionId, providerUser.getOriginal(), token);
return this.user;
}
getUserOrFail() {
if (!this.user) throw new E_UNAUTHORIZED_ACCESS("Invalid or expired user session", { guardDriverName: this.driverName });
return this.user;
}
async login(user, remember = false) {
const session = this.#getSession();
const providerUser = await this.#userProvider.createUserForGuard(user);
this.#emitter.emit("session_auth:login_attempted", {
ctx: this.#ctx,
user,
guardName: this.#name
});
let token;
if (remember) {
if (!this.#options.useRememberMeTokens) throw new RuntimeException("Cannot use \"rememberMe\" feature. It has been disabled");
token = await this.#userProvider.createRememberToken(providerUser.getOriginal(), this.#options.rememberMeTokensAge);
}
if (token) this.#createRememberMeCookie(token.value);
else this.#ctx.response.clearCookie(this.rememberMeKeyName);
this.#createSessionForUser(providerUser.getId());
this.#loginSucceeded(session.sessionId, providerUser.getOriginal(), token);
}
async logout() {
const session = this.#getSession();
const rememberMeCookie = this.#ctx.request.encryptedCookie(this.rememberMeKeyName);
session.forget(this.sessionKeyName);
this.#ctx.response.clearCookie(this.rememberMeKeyName);
if (this.user && rememberMeCookie && this.#options.useRememberMeTokens) {
const userProvider = this.#userProvider;
const token = await userProvider.verifyRememberToken(new Secret(rememberMeCookie));
if (token) await userProvider.deleteRemeberToken(this.user, token.identifier);
}
this.#emitter.emit("session_auth:logged_out", {
ctx: this.#ctx,
guardName: this.#name,
user: this.user || null,
sessionId: session.sessionId
});
this.user = void 0;
this.viaRemember = false;
this.isAuthenticated = false;
this.isLoggedOut = true;
}
async authenticate() {
if (this.authenticationAttempted) return this.getUserOrFail();
this.authenticationAttempted = true;
const session = this.#getSession();
this.#emitter.emit("session_auth:authentication_attempted", {
ctx: this.#ctx,
sessionId: session.sessionId,
guardName: this.#name
});
const authUserId = session.get(this.sessionKeyName);
if (authUserId) return this.#authenticateViaId(authUserId, session.sessionId);
const rememberMeCookie = this.#ctx.request.encryptedCookie(this.rememberMeKeyName);
if (rememberMeCookie && this.#options.useRememberMeTokens) {
this.attemptedViaRemember = true;
return this.#authenticateViaRememberCookie(rememberMeCookie, session.sessionId);
}
throw this.#authenticationFailed(session.sessionId);
}
async check() {
try {
await this.authenticate();
return true;
} catch (error) {
if (error instanceof E_UNAUTHORIZED_ACCESS) return false;
throw error;
}
}
async authenticateAsClient(user) {
const userId = (await this.#userProvider.createUserForGuard(user)).getId();
return { session: { [this.sessionKeyName]: userId } };
}
};
var RememberMeToken = class {
static decode(value) {
if (typeof value !== "string") return null;
if (!value) return null;
const [identifier, ...tokenValue] = value.split(".");
if (!identifier || tokenValue.length === 0) return null;
const decodedIdentifier = base64.urlDecode(identifier);
const decodedSecret = base64.urlDecode(tokenValue.join("."));
if (!decodedIdentifier || !decodedSecret) return null;
return {
identifier: decodedIdentifier,
secret: new Secret(decodedSecret)
};
}
static createTransientToken(userId, size, expiresIn) {
const expiresAt = /* @__PURE__ */ new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + string.seconds.parse(expiresIn));
return {
userId,
expiresAt,
...this.seed(size)
};
}
static seed(size) {
const secret = new Secret(string.random(size));
return {
secret,
hash: createHash("sha256").update(secret.release()).digest("hex")
};
}
identifier;
tokenableId;
value;
hash;
createdAt;
updatedAt;
expiresAt;
constructor(attributes) {
this.identifier = attributes.identifier;
this.tokenableId = attributes.tokenableId;
this.hash = attributes.hash;
this.createdAt = attributes.createdAt;
this.updatedAt = attributes.updatedAt;
this.expiresAt = attributes.expiresAt;
if (attributes.secret) this.value = new Secret(`${base64.urlEncode(String(this.identifier))}.${base64.urlEncode(attributes.secret.release())}`);
}
isExpired() {
return this.expiresAt < /* @__PURE__ */ new Date();
}
verify(secret) {
const newHash = createHash("sha256").update(secret.release()).digest("hex");
return safeEqual(this.hash, newHash);
}
};
var DbRememberMeTokensProvider = class DbRememberMeTokensProvider {
static forModel(model, options) {
return new DbRememberMeTokensProvider({
tokenableModel: model,
...options || {}
});
}
table;
tokenSecretLength;
constructor(options) {
this.options = options;
this.table = options.table || "remember_me_tokens";
this.tokenSecretLength = options.tokenSecretLength || 40;
}
#isObject(value) {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
#ensureIsPersisted(user) {
const model = this.options.tokenableModel;
if (user instanceof model === false) throw new RuntimeException(`Invalid user object. It must be an instance of the "${model.name}" model`);
if (!user.$primaryKeyValue) throw new RuntimeException(`Cannot use "${model.name}" model for managing remember me tokens. The value of column "${model.primaryKey}" is undefined or null`);
}
dbRowToRememberMeToken(dbRow) {
return new RememberMeToken({
identifier: dbRow.id,
tokenableId: dbRow.tokenable_id,
hash: dbRow.hash,
createdAt: typeof dbRow.created_at === "number" ? new Date(dbRow.created_at) : dbRow.created_at,
updatedAt: typeof dbRow.updated_at === "number" ? new Date(dbRow.updated_at) : dbRow.updated_at,
expiresAt: typeof dbRow.expires_at === "number" ? new Date(dbRow.expires_at) : dbRow.expires_at
});
}
async getDb() {
const model = this.options.tokenableModel;
return model.$adapter.query(model).client;
}
async create(user, expiresIn) {
this.#ensureIsPersisted(user);
const queryClient = await this.getDb();
const transientToken = RememberMeToken.createTransientToken(user.$primaryKeyValue, this.tokenSecretLength, expiresIn);
const dbRow = {
tokenable_id: transientToken.userId,
hash: transientToken.hash,
created_at: /* @__PURE__ */ new Date(),
updated_at: /* @__PURE__ */ new Date(),
expires_at: transientToken.expiresAt
};
const result = await queryClient.table(this.table).insert(dbRow).returning("id");
const id = this.#isObject(result[0]) ? result[0].id : result[0];
if (!id) throw new RuntimeException(`Cannot save access token. The result "${inspect(result)}" of insert query is unexpected`);
return new RememberMeToken({
identifier: id,
tokenableId: dbRow.tokenable_id,
secret: transientToken.secret,
hash: dbRow.hash,
createdAt: dbRow.created_at,
updatedAt: dbRow.updated_at,
expiresAt: dbRow.expires_at
});
}
async find(user, identifier) {
this.#ensureIsPersisted(user);
const dbRow = await (await this.getDb()).query().from(this.table).where({
id: identifier,
tokenable_id: user.$primaryKeyValue
}).limit(1).first();
if (!dbRow) return null;
return this.dbRowToRememberMeToken(dbRow);
}
async delete(user, identifier) {
this.#ensureIsPersisted(user);
return await (await this.getDb()).query().from(this.table).where({
id: identifier,
tokenable_id: user.$primaryKeyValue
}).del().exec();
}
async all(user) {
this.#ensureIsPersisted(user);
return (await (await this.getDb()).query().from(this.table).where({ tokenable_id: user.$primaryKeyValue }).orderBy("id", "desc").exec()).map((dbRow) => {
return this.dbRowToRememberMeToken(dbRow);
});
}
async verify(tokenValue) {
const decodedToken = RememberMeToken.decode(tokenValue.release());
if (!decodedToken) return null;
const dbRow = await (await this.getDb()).query().from(this.table).where({ id: decodedToken.identifier }).limit(1).first();
if (!dbRow) return null;
const rememberMeToken = this.dbRowToRememberMeToken(dbRow);
if (!rememberMeToken.verify(decodedToken.secret) || rememberMeToken.isExpired()) return null;
return rememberMeToken;
}
async recycle(user, identifier, expiresIn) {
await this.delete(user, identifier);
return this.create(user, expiresIn);
}
};
var SessionLucidUserProvider = class {
model;
constructor(options) {
this.options = options;
}
async getModel() {
if (this.model && !("hot" in import.meta)) return this.model;
this.model = (await this.options.model()).default;
return this.model;
}
async getTokensProvider() {
const model = await this.getModel();
if (!model.rememberMeTokens) throw new RuntimeException(`Cannot use "${model.name}" model for verifying remember me tokens. Make sure to assign a token provider to the model.`);
return model.rememberMeTokens;
}
async createUserForGuard(user) {
const model = await this.getModel();
if (user instanceof model === false) throw new RuntimeException(`Invalid user object. It must be an instance of the "${model.name}" model`);
return {
getId() {
if (!user.$primaryKeyValue) throw new RuntimeException(`Cannot use "${model.name}" model for authentication. The value of column "${model.primaryKey}" is undefined or null`);
return user.$primaryKeyValue;
},
getOriginal() {
return user;
}
};
}
async findById(identifier) {
const user = await (await this.getModel()).find(identifier);
if (!user) return null;
return this.createUserForGuard(user);
}
async createRememberToken(user, expiresIn) {
return (await this.getTokensProvider()).create(user, expiresIn);
}
async verifyRememberToken(tokenValue) {
return (await this.getTokensProvider()).verify(tokenValue);
}
async deleteRemeberToken(user, identifier) {
return (await this.getTokensProvider()).delete(user, identifier);
}
async recycleRememberToken(user, identifier, expiresIn) {
return (await this.getTokensProvider()).recycle(user, identifier, expiresIn);
}
};
function sessionGuard(config) {
return { async resolver(name, app) {
const emitter = await app.container.make("emitter");
const provider = "resolver" in config.provider ? await config.provider.resolver(app) : config.provider;
return (ctx) => new SessionGuard(name, ctx, config, emitter, provider);
} };
}
function sessionUserProvider(config) {
return new SessionLucidUserProvider(config);
}
export { DbRememberMeTokensProvider, RememberMeToken, SessionGuard, SessionLucidUserProvider, sessionGuard, sessionUserProvider };