@adonisjs/auth
Version:
Official authentication provider for Adonis framework
1,425 lines (1,424 loc) • 36.6 kB
JavaScript
import { n as E_UNAUTHORIZED_ACCESS } from "../../errors-eDV8ejO0.js";
import "../../symbols-C5QEqFvJ.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";
//#region modules/access_tokens_guard/crc32.ts
/**
* We use CRC32 just to add a recognizable checksum to tokens. This helps
* secret scanning tools like https://docs.github.com/en/github/administering-a-repository/about-secret-scanning easily detect tokens generated by a given program.
*
* You can learn more about appending checksum to a hash here in this Github
* article. https://github.blog/2021-04-05-behind-githubs-new-authentication-token-formats/
*
* Code taken from:
* https://github.com/tsxper/crc32/blob/main/src/CRC32.ts
*/
/**
* CRC32 implementation for adding recognizable checksums to tokens.
* This helps secret scanning tools easily detect tokens generated by AdonisJS.
*
* @example
* const crc = new CRC32()
* const checksum = crc.calculate('my-secret-token')
* console.log('Checksum:', checksum)
*/
var CRC32 = class {
/**
* Lookup table calculated for 0xEDB88320 divisor
*/
#lookupTable = [
0,
1996959894,
3993919788,
2567524794,
124634137,
1886057615,
3915621685,
2657392035,
249268274,
2044508324,
3772115230,
2547177864,
162941995,
2125561021,
3887607047,
2428444049,
498536548,
1789927666,
4089016648,
2227061214,
450548861,
1843258603,
4107580753,
2211677639,
325883990,
1684777152,
4251122042,
2321926636,
335633487,
1661365465,
4195302755,
2366115317,
997073096,
1281953886,
3579855332,
2724688242,
1006888145,
1258607687,
3524101629,
2768942443,
901097722,
1119000684,
3686517206,
2898065728,
853044451,
1172266101,
3705015759,
2882616665,
651767980,
1373503546,
3369554304,
3218104598,
565507253,
1454621731,
3485111705,
3099436303,
671266974,
1594198024,
3322730930,
2970347812,
795835527,
1483230225,
3244367275,
3060149565,
1994146192,
31158534,
2563907772,
4023717930,
1907459465,
112637215,
2680153253,
3904427059,
2013776290,
251722036,
2517215374,
3775830040,
2137656763,
141376813,
2439277719,
3865271297,
1802195444,
476864866,
2238001368,
4066508878,
1812370925,
453092731,
2181625025,
4111451223,
1706088902,
314042704,
2344532202,
4240017532,
1658658271,
366619977,
2362670323,
4224994405,
1303535960,
984961486,
2747007092,
3569037538,
1256170817,
1037604311,
2765210733,
3554079995,
1131014506,
879679996,
2909243462,
3663771856,
1141124467,
855842277,
2852801631,
3708648649,
1342533948,
654459306,
3188396048,
3373015174,
1466479909,
544179635,
3110523913,
3462522015,
1591671054,
702138776,
2966460450,
3352799412,
1504918807,
783551873,
3082640443,
3233442989,
3988292384,
2596254646,
62317068,
1957810842,
3939845945,
2647816111,
81470997,
1943803523,
3814918930,
2489596804,
225274430,
2053790376,
3826175755,
2466906013,
167816743,
2097651377,
4027552580,
2265490386,
503444072,
1762050814,
4150417245,
2154129355,
426522225,
1852507879,
4275313526,
2312317920,
282753626,
1742555852,
4189708143,
2394877945,
397917763,
1622183637,
3604390888,
2714866558,
953729732,
1340076626,
3518719985,
2797360999,
1068828381,
1219638859,
3624741850,
2936675148,
906185462,
1090812512,
3747672003,
2825379669,
829329135,
1181335161,
3412177804,
3160834842,
628085408,
1382605366,
3423369109,
3138078467,
570562233,
1426400815,
3317316542,
2998733608,
733239954,
1555261956,
3268935591,
3050360625,
752459403,
1541320221,
2607071920,
3965973030,
1969922972,
40735498,
2617837225,
3943577151,
1913087877,
83908371,
2512341634,
3803740692,
2075208622,
213261112,
2463272603,
3855990285,
2094854071,
198958881,
2262029012,
4057260610,
1759359992,
534414190,
2176718541,
4139329115,
1873836001,
414664567,
2282248934,
4279200368,
1711684554,
285281116,
2405801727,
4167216745,
1634467795,
376229701,
2685067896,
3608007406,
1308918612,
956543938,
2808555105,
3495958263,
1231636301,
1047427035,
2932959818,
3654703836,
1088359270,
936918e3,
2847714899,
3736837829,
1202900863,
817233897,
3183342108,
3401237130,
1404277552,
615818150,
3134207493,
3453421203,
1423857449,
601450431,
3009837614,
3294710456,
1567103746,
711928724,
3020668471,
3272380065,
1510334235,
755167117
];
#initialCRC = 4294967295;
#calculateBytes(bytes, accumulator) {
let crc = accumulator || this.#initialCRC;
for (const byte of bytes) {
const tableIndex = (crc ^ byte) & 255;
const tableVal = this.#lookupTable[tableIndex];
crc = crc >>> 8 ^ tableVal;
}
return crc;
}
#crcToUint(crc) {
return this.#toUint32(crc ^ 4294967295);
}
#strToBytes(input) {
return new TextEncoder().encode(input);
}
#toUint32(num) {
if (num >= 0) return num;
return 4294967295 - num * -1 + 1;
}
/**
* Calculate CRC32 checksum for a string input
*
* @param input - The string to calculate checksum for
*
* @example
* const crc = new CRC32()
* const checksum = crc.calculate('hello-world')
* console.log('CRC32:', checksum)
*/
calculate(input) {
return this.forString(input);
}
/**
* Calculate CRC32 checksum for a string
*
* @param input - The string to process
*
* @example
* const crc = new CRC32()
* const result = crc.forString('test-string')
*/
forString(input) {
const bytes = this.#strToBytes(input);
return this.forBytes(bytes);
}
/**
* Calculate CRC32 checksum for byte array
*
* @param bytes - The byte array to process
* @param accumulator - Optional accumulator for chained calculations
*
* @example
* const crc = new CRC32()
* const bytes = new TextEncoder().encode('hello')
* const result = crc.forBytes(bytes)
*/
forBytes(bytes, accumulator) {
const crc = this.#calculateBytes(bytes, accumulator);
return this.#crcToUint(crc);
}
};
//#endregion
//#region modules/access_tokens_guard/access_token.ts
/**
* Access token represents a token created for a user to authenticate
* using the auth module.
*
* It encapsulates the logic of creating an opaque token, generating
* its hash and verifying its hash.
*
* @example
* const token = new AccessToken({
* identifier: 1,
* tokenableId: 123,
* type: 'api',
* hash: 'sha256hash',
* createdAt: new Date(),
* updatedAt: new Date(),
* lastUsedAt: null,
* expiresAt: null,
* name: 'API Token',
* abilities: ['read', 'write']
* })
*/
var AccessToken = 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.
*
* @param prefix - The token prefix to validate against
* @param value - The token value to decode
*
* @example
* const decoded = AccessToken.decode('oat_', 'oat_abc123.def456')
* if (decoded) {
* console.log('Token ID:', decoded.identifier)
* console.log('Secret:', decoded.secret.release())
* }
*/
static decode(prefix, value) {
/**
* Ensure value is a string and starts with the prefix.
*/
if (typeof value !== "string" || !value.startsWith(`${prefix}`)) return null;
/**
* Remove prefix from the rest of the token.
*/
const token = value.replace(new RegExp(`^${prefix}`), "");
if (!token) return null;
const [identifier, ...tokenValue] = token.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.
*
* @param userId - The ID of the user for whom the token is created
* @param size - The size of the random secret to generate
* @param expiresIn - Optional expiration time (seconds or duration string)
*
* @example
* const transientToken = AccessToken.createTransientToken(123, 32, '7d')
* // Store transientToken in database
*/
static createTransientToken(userId, size, expiresIn) {
let expiresAt;
if (expiresIn) {
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. The secret is
* suffixed with a crc32 checksum for secret scanning tools
* to easily identify the token.
*
* @param size - The size of the random string to generate
*
* @example
* const { secret, hash } = AccessToken.seed(32)
* console.log('Secret:', secret.release())
* console.log('Hash:', hash)
*/
static seed(size) {
const seed = string.random(size);
const secret = new Secret(`${seed}${new CRC32().calculate(seed)}`);
return {
secret,
hash: createHash("sha256").update(secret.release()).digest("hex")
};
}
/**
* 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;
/**
* Recognizable name for the token
*/
name;
/**
* A unique type to identify a bucket of tokens inside the
* storage layer.
*/
type;
/**
* 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 was used for authentication
*/
lastUsedAt;
/**
* Timestamp at which the token will expire
*/
expiresAt;
/**
* An array of abilities the token can perform. The abilities
* is an array of abritary string values
*/
abilities;
/**
* Creates a new AccessToken instance
*
* @param attributes - Token attributes including identifier, user ID, type, hash, etc.
*
* @example
* const token = new AccessToken({
* identifier: 1,
* tokenableId: 123,
* type: 'api',
* hash: 'sha256hash',
* createdAt: new Date(),
* updatedAt: new Date(),
* lastUsedAt: null,
* expiresAt: new Date(Date.now() + 86400000),
* name: 'Mobile App Token',
* abilities: ['read:posts', 'write:posts']
* })
*/
constructor(attributes) {
this.identifier = attributes.identifier;
this.tokenableId = attributes.tokenableId;
this.name = attributes.name;
this.hash = attributes.hash;
this.type = attributes.type;
this.createdAt = attributes.createdAt;
this.updatedAt = attributes.updatedAt;
this.expiresAt = attributes.expiresAt;
this.lastUsedAt = attributes.lastUsedAt;
this.abilities = attributes.abilities || ["*"];
/**
* Compute value when secret is provided
*/
if (attributes.secret) {
if (!attributes.prefix) throw new RuntimeException("Cannot compute token value without the prefix");
this.value = new Secret(`${attributes.prefix}${base64.urlEncode(String(this.identifier))}.${base64.urlEncode(attributes.secret.release())}`);
}
}
/**
* Check if the token allows the given ability.
*
* @param ability - The ability to check
*
* @example
* if (token.allows('read:posts')) {
* console.log('User can read posts')
* }
*/
allows(ability) {
return this.abilities.includes(ability) || this.abilities.includes("*");
}
/**
* Check if the token denies the ability.
*
* @param ability - The ability to check
*
* @example
* if (token.denies('delete:posts')) {
* console.log('User cannot delete posts')
* }
*/
denies(ability) {
return !this.abilities.includes(ability) && !this.abilities.includes("*");
}
/**
* Authorize ability access using the current access token
*
* @param ability - The ability to authorize
*
* @throws {E_UNAUTHORIZED_ACCESS} When the token denies the ability
*
* @example
* token.authorize('write:posts') // Throws if not allowed
* console.log('Authorization successful')
*/
authorize(ability) {
if (this.denies(ability)) throw new E_UNAUTHORIZED_ACCESS("Unauthorized access", { guardDriverName: "access_tokens" });
}
/**
* Check if the token has been expired. Verifies
* the "expiresAt" timestamp with the current
* date.
*
* Tokens with no expiry never expire
*
* @example
* if (token.isExpired()) {
* console.log('Token has expired')
* } else {
* console.log('Token is still valid')
* }
*/
isExpired() {
if (!this.expiresAt) return false;
return this.expiresAt < /* @__PURE__ */ new Date();
}
/**
* Verifies the value of a token against the pre-defined hash
*
* @param secret - The secret to verify against the stored hash
*
* @example
* const isValid = token.verify(new Secret('user-provided-secret'))
* if (isValid) {
* console.log('Token is valid')
* }
*/
verify(secret) {
const newHash = createHash("sha256").update(secret.release()).digest("hex");
return safeEqual(this.hash, newHash);
}
/**
* Converts the token to a JSON representation suitable for API responses
*
* @example
* const tokenData = token.toJSON()
* console.log(tokenData.type) // 'bearer'
* console.log(tokenData.token) // 'oat_abc123.def456'
*/
toJSON() {
return {
type: "bearer",
name: this.name,
token: this.value ? this.value.release() : void 0,
abilities: this.abilities,
lastUsedAt: this.lastUsedAt,
expiresAt: this.expiresAt
};
}
};
//#endregion
//#region modules/access_tokens_guard/guard.ts
/**
* Implementation of access tokens guard for the Auth layer. The heavy lifting
* of verifying tokens is done by the user provider. However, the guard is
* used to seamlessly integrate with the auth layer of the package.
*
* @template UserProvider - The user provider contract
*
* @example
* const guard = new AccessTokensGuard(
* 'api',
* ctx,
* emitter,
* userProvider
* )
*
* const user = await guard.authenticate()
* console.log('Authenticated user:', user.email)
*/
var AccessTokensGuard = class {
/**
* A unique name for the guard.
*/
#name;
/**
* Reference to the current HTTP context
*/
#ctx;
/**
* Provider to lookup user details
*/
#userProvider;
/**
* Emitter to emit events
*/
#emitter;
/**
* Driver name of the guard
*/
driverName = "access_tokens";
/**
* Whether or not the authentication has been attempted
* during the current request.
*/
authenticationAttempted = false;
/**
* A boolean to know if the current request has
* been authenticated
*/
isAuthenticated = 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;
/**
* Creates a new AccessTokensGuard instance
*
* @param name - Unique name for the guard instance
* @param ctx - HTTP context for the current request
* @param emitter - Event emitter for guard events
* @param userProvider - User provider for token verification
*
* @example
* const guard = new AccessTokensGuard(
* 'api',
* ctx,
* emitter,
* new TokenUserProvider()
* )
*/
constructor(name, ctx, emitter, userProvider) {
this.#name = name;
this.#ctx = ctx;
this.#emitter = emitter;
this.#userProvider = userProvider;
}
/**
* Emits authentication failure and returns an exception
* to end the authentication cycle.
*/
#authenticationFailed() {
const error = new E_UNAUTHORIZED_ACCESS("Unauthorized access", { guardDriverName: this.driverName });
this.#emitter.emit("access_tokens_auth:authentication_failed", {
ctx: this.#ctx,
guardName: this.#name,
error
});
return error;
}
/**
* Returns the bearer token from the request headers or fails
*/
#getBearerToken() {
const [type, token] = this.#ctx.request.header("authorization", "").split(" ");
if (!type || type.toLowerCase() !== "bearer" || !token) throw this.#authenticationFailed();
return token;
}
/**
* Returns an instance of the authenticated user. Or throws
* an exception if the request is not authenticated.
*
* @throws {E_UNAUTHORIZED_ACCESS} When user is not authenticated
*
* @example
* const user = guard.getUserOrFail()
* console.log('User ID:', user.id)
* console.log('Current token:', user.currentAccessToken.name)
*/
getUserOrFail() {
if (!this.user) throw new E_UNAUTHORIZED_ACCESS("Unauthorized access", { guardDriverName: this.driverName });
return this.user;
}
/**
* Authenticate the current HTTP request by verifying the bearer
* token or fails with an exception
*
* @throws {E_UNAUTHORIZED_ACCESS} When authentication fails
*
* @example
* try {
* const user = await guard.authenticate()
* console.log('Authenticated as:', user.email)
* console.log('Token abilities:', user.currentAccessToken.abilities)
* } catch (error) {
* console.log('Authentication failed')
* }
*/
async authenticate() {
/**
* Return early when authentication has already
* been attempted
*/
if (this.authenticationAttempted) return this.getUserOrFail();
/**
* Notify we begin to attempt the authentication
*/
this.authenticationAttempted = true;
this.#emitter.emit("access_tokens_auth:authentication_attempted", {
ctx: this.#ctx,
guardName: this.#name
});
/**
* Decode token or fail when unable to do so
*/
const bearerToken = new Secret(this.#getBearerToken());
/**
* Verify for token via the user provider
*/
const token = await this.#userProvider.verifyToken(bearerToken);
if (!token) throw this.#authenticationFailed();
/**
* Check if a user for the token exists. Otherwise abort
* authentication
*/
const providerUser = await this.#userProvider.findById(token.tokenableId);
if (!providerUser) throw this.#authenticationFailed();
/**
* Update local state
*/
this.isAuthenticated = true;
this.user = providerUser.getOriginal();
this.user.currentAccessToken = token;
/**
* Notify
*/
this.#emitter.emit("access_tokens_auth:authentication_succeeded", {
ctx: this.#ctx,
token,
guardName: this.#name,
user: this.user
});
return this.user;
}
/**
* Create a token for a user (sign in)
*
* @param user - The user to create a token for
* @param abilities - Optional array of abilities the token should have
* @param options - Optional token configuration
*
* @example
* const token = await guard.createToken(user, ['read', 'write'], {
* name: 'Mobile App',
* expiresIn: '7d'
* })
* console.log('Token:', token.value.release())
*/
async createToken(user, abilities, options) {
return await this.#userProvider.createToken(user, abilities, options);
}
/**
* Invalidates the currently authenticated token (sign out)
*
* @example
* await guard.invalidateToken()
* console.log('Token invalidated successfully')
*/
async invalidateToken() {
const bearerToken = new Secret(this.#getBearerToken());
return await this.#userProvider.invalidateToken(bearerToken);
}
/**
* Returns the Authorization header clients can use to authenticate
* the request.
*
* @param user - The user to authenticate as
* @param abilities - Optional array of abilities
* @param options - Optional token configuration
*
* @example
* const clientAuth = await guard.authenticateAsClient(user, ['read'])
* // Use clientAuth.headers.authorization in API tests
*/
async authenticateAsClient(user, abilities, options) {
return { headers: { authorization: `Bearer ${(await this.#userProvider.createToken(user, abilities, options)).value.release()}` } };
}
/**
* Silently check if the user is authenticated or not. The
* method is same as the "authenticate" method but does not
* throw any exceptions.
*
* @example
* const isAuthenticated = await guard.check()
* if (isAuthenticated) {
* const user = guard.user
* console.log('User is authenticated:', user.email)
* }
*/
async check() {
try {
await this.authenticate();
return true;
} catch (error) {
if (error instanceof E_UNAUTHORIZED_ACCESS) return false;
throw error;
}
}
};
//#endregion
//#region modules/access_tokens_guard/token_providers/db.ts
/**
* DbAccessTokensProvider uses lucid database service to fetch and
* persist tokens for a given user.
*
* The user must be an instance of the associated user model.
*
* @template TokenableModel - The Lucid model that can have tokens
*
* @example
* const provider = new DbAccessTokensProvider({
* tokenableModel: () => import('#models/user'),
* table: 'api_tokens',
* type: 'api_token',
* prefix: 'api_'
* })
*/
var DbAccessTokensProvider = class DbAccessTokensProvider {
/**
* Create tokens provider instance for a given Lucid model
*
* @param model - The tokenable model factory function
* @param options - Optional configuration options
*
* @example
* const provider = DbAccessTokensProvider.forModel(
* () => import('#models/user'),
* { prefix: 'api_', type: 'api_token' }
* )
*/
static forModel(model, options) {
return new DbAccessTokensProvider({
tokenableModel: model,
...options || {}
});
}
/**
* A unique type for the value. The type is used to identify a
* bucket of tokens within the storage layer.
*
* Defaults to auth_token
*/
type;
/**
* A unique prefix to append to the publicly shared token value.
*
* Defaults to oat
*/
prefix;
/**
* Database table to use for querying access tokens
*/
table;
/**
* The length for the token secret. A secret is a cryptographically
* secure random string.
*/
tokenSecretLength;
/**
* Creates a new DbAccessTokensProvider instance
*
* @param options - Configuration options for the provider
*
* @example
* const provider = new DbAccessTokensProvider({
* tokenableModel: () => import('#models/user'),
* table: 'auth_access_tokens',
* tokenSecretLength: 40,
* type: 'auth_token',
* prefix: 'oat_'
* })
*/
constructor(options) {
this.options = options;
this.table = options.table || "auth_access_tokens";
this.tokenSecretLength = options.tokenSecretLength || 40;
this.type = options.type || "auth_token";
this.prefix = options.prefix || "oat_";
}
/**
* 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 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 access tokens. The value of column "${model.primaryKey}" is undefined or null`);
}
/**
* Maps a database row to an AccessToken instance
*
* @param dbRow - The database row containing token data
*
* @example
* const token = provider.dbRowToAccessToken({
* id: 1,
* tokenable_id: 123,
* type: 'auth_token',
* hash: 'sha256hash',
* // ... other columns
* })
*/
dbRowToAccessToken(dbRow) {
return new AccessToken({
identifier: dbRow.id,
tokenableId: dbRow.tokenable_id,
type: dbRow.type,
name: dbRow.name,
hash: dbRow.hash,
abilities: JSON.parse(dbRow.abilities),
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,
lastUsedAt: typeof dbRow.last_used_at === "number" ? new Date(dbRow.last_used_at) : dbRow.last_used_at,
expiresAt: typeof dbRow.expires_at === "number" ? new Date(dbRow.expires_at) : dbRow.expires_at
});
}
/**
* Returns a query client instance from the parent model
*
* @example
* const db = await provider.getDb()
* const tokens = await db.from('auth_access_tokens').select('*')
*/
async getDb() {
const model = this.options.tokenableModel;
return model.$adapter.query(model).client;
}
/**
* Create a token for a user
*
* @param user - The user instance to create a token for
* @param abilities - Array of abilities the token should have
* @param options - Optional token configuration
*
* @example
* const token = await provider.create(user, ['read', 'write'], {
* name: 'Mobile App Token',
* expiresIn: '7d'
* })
* console.log('Token:', token.value.release())
*/
async create(user, abilities = ["*"], options) {
this.#ensureIsPersisted(user);
const queryClient = await this.getDb();
/**
* Creating a transient token. Transient token abstracts
* the logic of creating a random secure secret and its
* hash
*/
const transientToken = AccessToken.createTransientToken(user.$primaryKeyValue, this.tokenSecretLength, options?.expiresIn || this.options.expiresIn);
/**
* Row to insert inside the database. We expect exactly these
* columns to exist.
*/
const dbRow = {
tokenable_id: transientToken.userId,
type: this.type,
name: options?.name || null,
hash: transientToken.hash,
abilities: JSON.stringify(abilities),
created_at: /* @__PURE__ */ new Date(),
updated_at: /* @__PURE__ */ new Date(),
last_used_at: null,
expires_at: transientToken.expiresAt || null
};
/**
* Insert data to the database.
*/
const result = await queryClient.table(this.table).insert(dbRow).returning("id");
const id = this.#isObject(result[0]) ? result[0].id : result[0];
/**
* Throw error when unable to find id in the return value of
* the insert query
*/
if (!id) throw new RuntimeException(`Cannot save access token. The result "${inspect(result)}" of insert query is unexpected`);
/**
* Convert db row to an access token
*/
return new AccessToken({
identifier: id,
tokenableId: dbRow.tokenable_id,
type: dbRow.type,
prefix: this.prefix,
secret: transientToken.secret,
name: dbRow.name,
hash: dbRow.hash,
abilities: JSON.parse(dbRow.abilities),
createdAt: dbRow.created_at,
updatedAt: dbRow.updated_at,
lastUsedAt: dbRow.last_used_at,
expiresAt: dbRow.expires_at
});
}
/**
* Find a token for a user by the token id
*
* @param user - The user instance that owns the token
* @param identifier - The token identifier to search for
*
* @example
* const token = await provider.find(user, 123)
* if (token) {
* console.log('Found token:', token.name)
* }
*/
async find(user, identifier) {
this.#ensureIsPersisted(user);
const dbRow = await (await this.getDb()).query().from(this.table).where({
id: identifier,
tokenable_id: user.$primaryKeyValue,
type: this.type
}).limit(1).first();
if (!dbRow) return null;
return this.dbRowToAccessToken(dbRow);
}
/**
* Delete a token by its id
*
* @param user - The user instance that owns the token
* @param identifier - The token identifier to delete
*
* @example
* const deletedCount = await provider.delete(user, 123)
* console.log('Deleted tokens:', deletedCount)
*/
async delete(user, identifier) {
this.#ensureIsPersisted(user);
return await (await this.getDb()).query().from(this.table).where({
id: identifier,
tokenable_id: user.$primaryKeyValue,
type: this.type
}).del().exec();
}
/**
* Delete all tokens for a given user
*
* @param user - The user instance to delete tokens for
*
* @example
* const deletedCount = await provider.deleteAll(user)
* console.log('Deleted tokens:', deletedCount)
*/
async deleteAll(user) {
this.#ensureIsPersisted(user);
return await (await this.getDb()).query().from(this.table).where({
tokenable_id: user.$primaryKeyValue,
type: this.type
}).del().exec();
}
/**
* Returns all the tokens for a given user
*
* @param user - The user instance to get tokens for
*
* @example
* const tokens = await provider.all(user)
* console.log('User has', tokens.length, 'tokens')
* tokens.forEach(token => console.log(token.name))
*/
async all(user) {
this.#ensureIsPersisted(user);
return (await (await this.getDb()).query().from(this.table).where({
tokenable_id: user.$primaryKeyValue,
type: this.type
}).ifDialect("postgres", (query) => {
query.orderBy([{
column: "last_used_at",
order: "desc",
nulls: "last"
}]);
}).unlessDialect("postgres", (query) => {
query.orderBy([{
column: "last_used_at",
order: "asc",
nulls: "last"
}]);
}).orderBy("id", "desc").exec()).map((dbRow) => {
return this.dbRowToAccessToken(dbRow);
});
}
/**
* Verifies a publicly shared access token and returns an
* access token for it.
*
* Returns null when unable to verify the token or find it
* inside the storage
*
* @param tokenValue - The token value to verify
*
* @example
* const token = await provider.verify(new Secret('oat_abc123.def456'))
* if (token && !token.isExpired()) {
* console.log('Valid token for user:', token.tokenableId)
* }
*/
async verify(tokenValue) {
const decodedToken = AccessToken.decode(this.prefix, tokenValue.release());
if (!decodedToken) return null;
const db = await this.getDb();
const dbRow = await db.query().from(this.table).where({
id: decodedToken.identifier,
type: this.type
}).limit(1).first();
if (!dbRow) return null;
/**
* Update last time the token is used
*/
dbRow.last_used_at = /* @__PURE__ */ new Date();
await db.from(this.table).where({
id: dbRow.id,
type: dbRow.type
}).update({ last_used_at: dbRow.last_used_at });
/**
* Convert to access token instance
*/
const accessToken = this.dbRowToAccessToken(dbRow);
/**
* Ensure the token secret matches the token hash
*/
if (!accessToken.verify(decodedToken.secret) || accessToken.isExpired()) return null;
return accessToken;
}
/**
* Invalidates a token identified by its publicly shared token
*
* @param tokenValue - The token value to invalidate
*
* @example
* const wasInvalidated = await provider.invalidate(new Secret('oat_abc123.def456'))
* if (wasInvalidated) {
* console.log('Token successfully invalidated')
* }
*/
async invalidate(tokenValue) {
const decodedToken = AccessToken.decode(this.prefix, tokenValue.release());
if (!decodedToken) return false;
const deleteCount = await (await this.getDb()).query().from(this.table).where({
id: decodedToken.identifier,
type: this.type
}).del().exec();
return Boolean(deleteCount);
}
};
//#endregion
//#region modules/access_tokens_guard/user_providers/lucid.ts
/**
* Uses a lucid model to verify access tokens and find a user during
* authentication
*
* @template TokenableProperty - The property name that holds the tokens provider
* @template UserModel - The Lucid model representing the user
*
* @example
* const userProvider = new AccessTokensLucidUserProvider({
* model: () => import('#models/user'),
* tokens: 'accessTokens'
* })
*/
var AccessTokensLucidUserProvider = class {
/**
* Reference to the lazily imported model
*/
model;
/**
* Creates a new AccessTokensLucidUserProvider instance
*
* @param options - Configuration options for the user provider
*
* @example
* const provider = new AccessTokensLucidUserProvider({
* model: () => import('#models/user'),
* tokens: 'accessTokens'
* })
*/
constructor(options) {
this.options = options;
}
/**
* Imports the model from the provider, returns and caches it
* for further operations.
*
* @example
* const UserModel = await provider.getModel()
* const user = await UserModel.find(1)
*/
async getModel() {
if (this.model && !("hot" in import.meta)) return this.model;
this.model = (await this.options.model()).default;
return this.model;
}
/**
* Returns the tokens provider associated with the user model
*
* @example
* const tokensProvider = await provider.getTokensProvider()
* const token = await tokensProvider.create(user, ['read'])
*/
async getTokensProvider() {
const model = await this.getModel();
if (!model[this.options.tokens]) throw new RuntimeException(`Cannot use "${model.name}" model for verifying access tokens. Make sure to assign a token provider to the model.`);
return model[this.options.tokens];
}
/**
* Creates an adapter user for the guard
*
* @param user - The user model instance
*
* @example
* const guardUser = await provider.createUserForGuard(user)
* console.log('User ID:', guardUser.getId())
* console.log('Original user:', guardUser.getOriginal())
*/
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() {
/**
* Ensure user has a primary key
*/
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;
}
};
}
/**
* Create a token for a given user
*
* @param user - The user to create a token for
* @param abilities - Optional array of abilities the token should have
* @param options - Optional token configuration
*
* @example
* const token = await provider.createToken(user, ['read', 'write'], {
* name: 'API Token',
* expiresIn: '30d'
* })
* console.log('Created token:', token.value.release())
*/
async createToken(user, abilities, options) {
return (await this.getTokensProvider()).create(user, abilities, options);
}
/**
* Invalidates a token identified by its publicly shared token
*
* @param tokenValue - The token value to invalidate
*
* @example
* const wasInvalidated = await provider.invalidateToken(
* new Secret('oat_abc123.def456')
* )
* console.log('Token invalidated:', wasInvalidated)
*/
async invalidateToken(tokenValue) {
return (await this.getTokensProvider()).invalidate(tokenValue);
}
/**
* Finds a user by the user id
*
* @param identifier - The user identifier to search for
*
* @example
* const guardUser = await provider.findById(123)
* if (guardUser) {
* const originalUser = guardUser.getOriginal()
* console.log('Found user:', originalUser.email)
* }
*/
async findById(identifier) {
const user = await (await this.getModel()).find(identifier);
if (!user) return null;
return this.createUserForGuard(user);
}
/**
* Verifies a publicly shared access token and returns an
* access token for it.
*
* @param tokenValue - The token value to verify
*
* @example
* const token = await provider.verifyToken(
* new Secret('oat_abc123.def456')
* )
* if (token && !token.isExpired()) {
* console.log('Valid token with abilities:', token.abilities)
* }
*/
async verifyToken(tokenValue) {
return (await this.getTokensProvider()).verify(tokenValue);
}
};
//#endregion
//#region modules/access_tokens_guard/define_config.ts
/**
* Configures access tokens guard for authentication
*
* @param config - Configuration object containing the user provider
*
* @example
* const guard = tokensGuard({
* provider: tokensUserProvider({
* model: () => import('#models/user'),
* tokens: 'accessTokens'
* })
* })
*/
function tokensGuard(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 AccessTokensGuard(name, ctx, emitter, provider);
} };
}
/**
* Configures user provider that uses Lucid models to verify
* access tokens and find users during authentication.
*
* @param config - Configuration options for the Lucid user provider
*
* @example
* const userProvider = tokensUserProvider({
* model: () => import('#models/user'),
* tokens: 'accessTokens'
* })
*/
function tokensUserProvider(config) {
return new AccessTokensLucidUserProvider(config);
}
//#endregion
export { AccessToken, AccessTokensGuard, AccessTokensLucidUserProvider, DbAccessTokensProvider, tokensGuard, tokensUserProvider };