@adonisjs/auth
Version:
Official authentication provider for Adonis framework
348 lines (347 loc) • 11.3 kB
JavaScript
"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;