@adonisjs/auth
Version:
Official authentication provider for Adonis framework
754 lines (748 loc) • 22.3 kB
JavaScript
import {
E_UNAUTHORIZED_ACCESS
} from "../../chunk-MUPAP5IP.js";
import "../../chunk-UXA4FHST.js";
// modules/session_guard/remember_me_token.ts
import { createHash } from "crypto";
import string from "@adonisjs/core/helpers/string";
import { Secret, base64, safeEqual } from "@adonisjs/core/helpers";
var RememberMeToken = class {
/**
* Decodes a publicly shared token and return the series
* and the token value from it.
*
* Returns null when unable to decode the token because of
* invalid format or encoding.
*/
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)
};
}
/**
* Creates a transient token that can be shared with the persistence
* layer.
*/
static createTransientToken(userId, size, expiresIn) {
const expiresAt = /* @__PURE__ */ new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + string.seconds.parse(expiresIn));
return {
userId,
expiresAt,
...this.seed(size)
};
}
/**
* Creates a secret opaque token and its hash.
*/
static seed(size) {
const seed = string.random(size);
const secret = new Secret(seed);
const hash = createHash("sha256").update(secret.release()).digest("hex");
return { secret, hash };
}
/**
* Identifer is a unique sequence to identify the
* token within database. It should be the
* primary/unique key
*/
identifier;
/**
* Reference to the user id for whom the token
* is generated.
*/
tokenableId;
/**
* The value is a public representation of a token. It is created
* by combining the "identifier"."secret"
*/
value;
/**
* Hash is computed from the seed to later verify the validity
* of seed
*/
hash;
/**
* Date/time when the token instance was created
*/
createdAt;
/**
* Date/time when the token was updated
*/
updatedAt;
/**
* Timestamp at which the token will expire
*/
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()
)}`
);
}
}
/**
* Check if the token has been expired. Verifies
* the "expiresAt" timestamp with the current
* date.
*/
isExpired() {
return this.expiresAt < /* @__PURE__ */ new Date();
}
/**
* Verifies the value of a token against the pre-defined hash
*/
verify(secret) {
const newHash = createHash("sha256").update(secret.release()).digest("hex");
return safeEqual(this.hash, newHash);
}
};
// modules/session_guard/guard.ts
import { Secret as Secret2 } from "@adonisjs/core/helpers";
import { RuntimeException } from "@adonisjs/core/exceptions";
var SessionGuard = class {
/**
* A unique name for the guard.
*/
#name;
/**
* Reference to the current HTTP context
*/
#ctx;
/**
* Options accepted by the session guard
*/
#options;
/**
* Provider to lookup user details
*/
#userProvider;
/**
* Emitter to emit events
*/
#emitter;
/**
* Driver name of the guard
*/
driverName = "session";
/**
* Whether or not the authentication has been attempted
* during the current request.
*/
authenticationAttempted = false;
/**
* A boolean to know if a remember me token was used in attempt
* to login a user.
*/
attemptedViaRemember = false;
/**
* A boolean to know if the current request has
* been authenticated
*/
isAuthenticated = false;
/**
* A boolean to know if the current request is authenticated
* using the "rememember_me" token.
*/
viaRemember = false;
/**
* Find if the user has been logged out during
* the current request
*/
isLoggedOut = false;
/**
* Reference to an instance of the authenticated user.
* The value only exists after calling one of the
* following methods.
*
* - authenticate
* - check
*
* You can use the "getUserOrFail" method to throw an exception if
* the request is not authenticated.
*/
user;
/**
* The key used to store the logged-in user id inside
* session
*/
get sessionKeyName() {
return `auth_${this.#name}`;
}
/**
* The key used to store the remember me token cookie
*/
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;
}
/**
* Returns the session instance for the given request,
* ensuring the property exists
*/
#getSession() {
if (!("session" in this.#ctx)) {
throw new RuntimeException(
'Cannot authenticate user. Install and configure "@adonisjs/session" package'
);
}
return this.#ctx.session;
}
/**
* Emits authentication failure, updates the local state,
* and returns an exception to end the authentication
* cycle.
*/
#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;
}
/**
* Emits the authentication succeeded event and updates
* the local state to reflect successful authentication
*/
#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
});
}
/**
* Emits the login succeeded event and updates the login
* state
*/
#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
});
}
/**
* Creates session for a given user by their user id.
*/
#createSessionForUser(userId) {
const session = this.#getSession();
session.put(this.sessionKeyName, userId);
session.regenerate();
}
/**
* Creates the remember me cookie
*/
#createRememberMeCookie(value) {
this.#ctx.response.encryptedCookie(this.rememberMeKeyName, value.release(), {
maxAge: this.#options.rememberMeTokensAge,
httpOnly: true
});
}
/**
* Authenticates the user using its id read from the session
* store.
*
* - We check the user exists in the db
* - If not, throw exception.
* - Otherwise, update local state to mark the user as logged-in
*/
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;
}
/**
* Authenticates user from the remember me cookie. Creates a fresh
* session for them and recycles the remember me token as well.
*/
async #authenticateViaRememberCookie(rememberMeCookie, sessionId) {
const userProvider = this.#userProvider;
const token = await userProvider.verifyRememberToken(new Secret2(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;
}
/**
* Returns an instance of the authenticated user. Or throws
* an exception if the request is not authenticated.
*/
getUserOrFail() {
if (!this.user) {
throw new E_UNAUTHORIZED_ACCESS("Invalid or expired user session", {
guardDriverName: this.driverName
});
}
return this.user;
}
/**
* Login user using sessions. Optionally, you can also create
* a remember me token to automatically login user when their
* session expires.
*/
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');
}
const userProvider = this.#userProvider;
token = await 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);
}
/**
* Logout a user by removing its state from the session
* store and delete the remember me cookie (if any).
*/
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 Secret2(rememberMeCookie));
if (token) {
await userProvider.deleteRemeberToken(this.user, token.identifier);
}
}
this.user = void 0;
this.viaRemember = false;
this.isAuthenticated = false;
this.isLoggedOut = true;
this.#emitter.emit("session_auth:logged_out", {
ctx: this.#ctx,
guardName: this.#name,
user: this.user || null,
sessionId: session.sessionId
});
}
/**
* Authenticate the current HTTP request by verifying the bearer
* token or fails with an exception
*/
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);
}
/**
* Silently check if the user is authenticated or not, without
* throwing any exceptions
*/
async check() {
try {
await this.authenticate();
return true;
} catch (error) {
if (error instanceof E_UNAUTHORIZED_ACCESS) {
return false;
}
throw error;
}
}
/**
* Returns the session info for the clients to send during
* an HTTP request to mark the user as logged-in.
*/
async authenticateAsClient(user) {
const providerUser = await this.#userProvider.createUserForGuard(user);
const userId = providerUser.getId();
return {
session: {
[this.sessionKeyName]: userId
}
};
}
};
// modules/session_guard/token_providers/db.ts
import { inspect } from "util";
import { RuntimeException as RuntimeException2 } from "@adonisjs/core/exceptions";
var DbRememberMeTokensProvider = class _DbRememberMeTokensProvider {
constructor(options) {
this.options = options;
this.table = options.table || "remember_me_tokens";
this.tokenSecretLength = options.tokenSecretLength || 40;
}
/**
* Create tokens provider instance for a given Lucid model
*/
static forModel(model, options) {
return new _DbRememberMeTokensProvider({
tokenableModel: model,
...options || {}
});
}
/**
* Database table to use for querying remember me tokens
*/
table;
/**
* The length for the token secret. A secret is a cryptographically
* secure random string.
*/
tokenSecretLength;
/**
* Check if value is an object
*/
#isObject(value) {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
/**
* Ensure the provided user is an instance of the user model and
* has a primary key
*/
#ensureIsPersisted(user) {
const model = this.options.tokenableModel;
if (user instanceof model === false) {
throw new RuntimeException2(
`Invalid user object. It must be an instance of the "${model.name}" model`
);
}
if (!user.$primaryKeyValue) {
throw new RuntimeException2(
`Cannot use "${model.name}" model for managing remember me tokens. The value of column "${model.primaryKey}" is undefined or null`
);
}
}
/**
* Maps a database row to an instance token instance
*/
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
});
}
/**
* Returns a query client instance from the parent model
*/
async getDb() {
const model = this.options.tokenableModel;
return model.$adapter.query(model).client;
}
/**
* Create a token for a user
*/
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 RuntimeException2(
`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
});
}
/**
* Find a token for a user by the token id
*/
async find(user, identifier) {
this.#ensureIsPersisted(user);
const queryClient = await this.getDb();
const dbRow = await queryClient.query().from(this.table).where({ id: identifier, tokenable_id: user.$primaryKeyValue }).limit(1).first();
if (!dbRow) {
return null;
}
return this.dbRowToRememberMeToken(dbRow);
}
/**
* Delete a token by its id
*/
async delete(user, identifier) {
this.#ensureIsPersisted(user);
const queryClient = await this.getDb();
const affectedRows = await queryClient.query().from(this.table).where({ id: identifier, tokenable_id: user.$primaryKeyValue }).del().exec();
return affectedRows;
}
/**
* Returns all the tokens a given user
*/
async all(user) {
this.#ensureIsPersisted(user);
const queryClient = await this.getDb();
const dbRows = await queryClient.query().from(this.table).where({ tokenable_id: user.$primaryKeyValue }).orderBy("id", "desc").exec();
return dbRows.map((dbRow) => {
return this.dbRowToRememberMeToken(dbRow);
});
}
/**
* Verifies a publicly shared remember me token and returns an
* RememberMeToken for it.
*
* Returns null when unable to verify the token or find it
* inside the storage
*/
async verify(tokenValue) {
const decodedToken = RememberMeToken.decode(tokenValue.release());
if (!decodedToken) {
return null;
}
const db = await this.getDb();
const dbRow = await db.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;
}
/**
* Recycles a remember me token by deleting the old one and
* creates a new one.
*
* Ideally, the recycle should update the existing token, but we
* skip that for now and come back to it later and handle race
* conditions as well.
*/
async recycle(user, identifier, expiresIn) {
await this.delete(user, identifier);
return this.create(user, expiresIn);
}
};
// modules/session_guard/user_providers/lucid.ts
import { RuntimeException as RuntimeException3 } from "@adonisjs/core/exceptions";
var SessionLucidUserProvider = class {
constructor(options) {
this.options = options;
}
/**
* Reference to the lazily imported model
*/
model;
/**
* Imports the model from the provider, returns and caches it
* for further operations.
*/
async getModel() {
if (this.model && !("hot" in import.meta)) {
return this.model;
}
const importedModel = await this.options.model();
this.model = importedModel.default;
return this.model;
}
/**
* Returns the tokens provider associated with the user model
*/
async getTokensProvider() {
const model = await this.getModel();
if (!model.rememberMeTokens) {
throw new RuntimeException3(
`Cannot use "${model.name}" model for verifying remember me tokens. Make sure to assign a token provider to the model.`
);
}
return model.rememberMeTokens;
}
/**
* Creates an adapter user for the guard
*/
async createUserForGuard(user) {
const model = await this.getModel();
if (user instanceof model === false) {
throw new RuntimeException3(
`Invalid user object. It must be an instance of the "${model.name}" model`
);
}
return {
getId() {
if (!user.$primaryKeyValue) {
throw new RuntimeException3(
`Cannot use "${model.name}" model for authentication. The value of column "${model.primaryKey}" is undefined or null`
);
}
return user.$primaryKeyValue;
},
getOriginal() {
return user;
}
};
}
/**
* Finds a user by their primary key value
*/
async findById(identifier) {
const model = await this.getModel();
const user = await model.find(identifier);
if (!user) {
return null;
}
return this.createUserForGuard(user);
}
/**
* Creates a remember token for a given user
*/
async createRememberToken(user, expiresIn) {
const tokensProvider = await this.getTokensProvider();
return tokensProvider.create(user, expiresIn);
}
/**
* Verify a token by its publicly shared value
*/
async verifyRememberToken(tokenValue) {
const tokensProvider = await this.getTokensProvider();
return tokensProvider.verify(tokenValue);
}
/**
* Delete a token for a user by the token identifier
*/
async deleteRemeberToken(user, identifier) {
const tokensProvider = await this.getTokensProvider();
return tokensProvider.delete(user, identifier);
}
/**
* Recycle a token for a user by the token identifier
*/
async recycleRememberToken(user, identifier, expiresIn) {
const tokensProvider = await this.getTokensProvider();
return tokensProvider.recycle(user, identifier, expiresIn);
}
};
// modules/session_guard/define_config.ts
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
};