UNPKG

@adonisjs/auth

Version:

Official authentication provider for Adonis framework

348 lines (347 loc) 11.3 kB
"use strict"; /* * @adonisjs/auth * * (c) Harminder Virk <virk@adonisjs.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.OATGuard = void 0; const luxon_1 = require("luxon"); const crypto_1 = require("crypto"); const helpers_1 = require("@poppinss/utils/build/helpers"); const Base_1 = require("../Base"); const OpaqueToken_1 = require("../../Tokens/OpaqueToken"); const ProviderToken_1 = require("../../Tokens/ProviderToken"); const AuthenticationException_1 = require("../../Exceptions/AuthenticationException"); /** * Exposes the API to generate and authenticate HTTP request using * opaque tokens */ class OATGuard extends Base_1.BaseGuard { constructor(name, config, emitter, provider, ctx, tokenProvider) { super(name, config, provider); this.config = config; this.emitter = emitter; this.ctx = ctx; this.tokenProvider = tokenProvider; /** * Length of the raw token. The hash length will vary */ this.tokenLength = 60; /** * Token type for the persistance store */ this.tokenType = this.config.tokenProvider.type || 'opaque_token'; /** * Whether or not the authentication has been attempted * for the current request */ this.authenticationAttempted = false; /** * Find if the user has been logged out in the current request */ this.isLoggedOut = false; /** * A boolean to know if user is retrieved by authenticating * the current request or not */ this.isAuthenticated = false; } /** * Accessor to know if user is logged in */ get isLoggedIn() { return !!this.user; } /** * Accessor to know if user is a guest. It is always opposite * of [[isLoggedIn]] */ get isGuest() { return !this.isLoggedIn; } /** * Converts value to a sha256 hash */ generateHash(token) { return (0, crypto_1.createHash)('sha256').update(token).digest('hex'); } /** * Converts expiry duration to an absolute date/time value */ getExpiresAtDate(expiresIn) { if (!expiresIn) { return; } const milliseconds = typeof expiresIn === 'string' ? helpers_1.string.toMs(expiresIn) : expiresIn; return luxon_1.DateTime.local().plus({ milliseconds }); } /** * Generates a new token + hash for the persistance */ generateTokenForPersistance(expiresIn) { const token = helpers_1.string.generateRandom(this.tokenLength); return { token, hash: this.generateHash(token), expiresAt: this.getExpiresAtDate(expiresIn), }; } /** * Returns data packet for the login event. Arguments are * * - The mapping identifier * - Logged in user * - HTTP context * - API token */ getLoginEventData(user, token) { return { name: this.name, ctx: this.ctx, user, token, }; } /** * Returns data packet for the authenticate event. Arguments are * * - The mapping identifier * - Logged in user * - HTTP context * - A boolean to tell if logged in viaRemember or not */ getAuthenticateEventData(user, token) { return { name: this.name, ctx: this.ctx, user, token, }; } /** * Parses the token received in the request. The method also performs * some initial level of sanity checks. */ parsePublicToken(token) { const parts = token.split('.'); /** * Ensure the token has two parts */ if (parts.length !== 2) { throw AuthenticationException_1.AuthenticationException.invalidToken(this.name); } /** * Ensure the first part is a base64 encode id */ const tokenId = helpers_1.base64.urlDecode(parts[0], undefined, true); if (!tokenId) { throw AuthenticationException_1.AuthenticationException.invalidToken(this.name); } /** * Ensure 2nd part of the token has the expected length */ if (parts[1].length !== this.tokenLength) { throw AuthenticationException_1.AuthenticationException.invalidToken(this.name); } /** * Set parsed token */ this.parsedToken = { tokenId, value: parts[1], }; return this.parsedToken; } /** * Returns the bearer token */ getBearerToken() { /** * Ensure the "Authorization" header value exists */ const token = this.ctx.request.header('Authorization'); if (!token) { throw AuthenticationException_1.AuthenticationException.invalidToken(this.name); } /** * Ensure that token has minimum of two parts and the first * part is a constant string named `bearer` */ const [type, value] = token.split(' '); if (!type || type.toLowerCase() !== 'bearer' || !value) { throw AuthenticationException_1.AuthenticationException.invalidToken(this.name); } return value; } /** * Returns the token by reading it from the token provider */ async getProviderToken(tokenId, value) { const providerToken = await this.tokenProvider.read(tokenId, this.generateHash(value), this.tokenType); if (!providerToken) { throw AuthenticationException_1.AuthenticationException.invalidToken(this.name); } return providerToken; } /** * Returns user from the user session id */ async getUserById(id) { const authenticatable = await this.provider.findById(id); if (!authenticatable.user) { throw AuthenticationException_1.AuthenticationException.invalidToken(this.name); } return authenticatable; } /** * Verify user credentials and perform login */ async attempt(uid, password, options) { const user = await this.verifyCredentials(uid, password); return this.login(user, options); } /** * Login user using their id */ async loginViaId(id, options) { const providerUser = await this.findById(id); return this.login(providerUser.user, options); } /** * Generate token for a user. It is merely an alias for `login` */ async generate(user, options) { return this.login(user, options); } /** * Login a user */ async login(user, options) { /** * Normalize options with defaults */ const { expiresIn, name, ...meta } = Object.assign({ name: 'Opaque Access Token', }, options); /** * Since the login method is not exposed to the end user, we cannot expect * them to instantiate and pass an instance of provider user, so we * create one manually. */ const providerUser = await this.getUserForLogin(user, this.config.provider.identifierKey); /** * "getUserForLogin" raises exception when id is missing, so we can * safely assume it is defined */ const id = providerUser.getId(); const token = this.generateTokenForPersistance(expiresIn); /** * Persist token to the database. Make sure that we are always * passing the hash to the storage driver */ const providerToken = new ProviderToken_1.ProviderToken(name, token.hash, id, this.tokenType); providerToken.expiresAt = token.expiresAt; providerToken.meta = meta; const tokenId = await this.tokenProvider.write(providerToken); /** * Construct a new API Token instance */ const apiToken = new OpaqueToken_1.OpaqueToken(name, `${helpers_1.base64.urlEncode(tokenId)}.${token.token}`, providerUser.user); apiToken.tokenHash = token.hash; apiToken.expiresAt = token.expiresAt; apiToken.meta = meta || {}; /** * Emit login event. It can be used to track user logins. */ this.emitter.emit('adonis:api:login', this.getLoginEventData(providerUser.user, apiToken)); /** * Marking user as logged in */ this.markUserAsLoggedIn(providerUser.user); this.token = providerToken; return apiToken; } /** * Authenticates the current HTTP request by checking for the bearer token */ async authenticate() { /** * Return early when authentication has already attempted for * the current request */ if (this.authenticationAttempted) { return this.user; } this.authenticationAttempted = true; /** * Ensure the "Authorization" header value exists */ const token = this.getBearerToken(); const { tokenId, value } = this.parsePublicToken(token); /** * Query token and user */ const providerToken = await this.getProviderToken(tokenId, value); const providerUser = await this.getUserById(providerToken.userId); this.markUserAsLoggedIn(providerUser.user, true); this.token = providerToken; this.emitter.emit('adonis:api:authenticate', this.getAuthenticateEventData(providerUser.user, this.token)); return providerUser.user; } /** * Same as [[authenticate]] but returns a boolean over raising exceptions */ async check() { try { await this.authenticate(); } catch (error) { /** * Throw error when it is not an instance of the authentication */ if (error instanceof AuthenticationException_1.AuthenticationException === false) { throw error; } this.ctx.logger.trace(error, 'Authentication failure'); } return this.isAuthenticated; } /** * Alias for the logout method */ async revoke() { return this.logout(); } /** * Logout by removing the token from the storage */ async logout() { if (!this.authenticationAttempted) { await this.check(); } /** * Clean up token from storage */ if (this.parsedToken) { await this.tokenProvider.destroy(this.parsedToken.tokenId, this.tokenType); } this.markUserAsLoggedOut(); } /** * Serialize toJSON for JSON.stringify */ toJSON() { return { isLoggedIn: this.isLoggedIn, isGuest: this.isGuest, authenticationAttempted: this.authenticationAttempted, isAuthenticated: this.isAuthenticated, user: this.user, }; } } exports.OATGuard = OATGuard;