@hapi/cookie
Version:
Cookie authentication plugin
234 lines (171 loc) • 7.19 kB
JavaScript
;
const Boom = require('@hapi/boom');
const Bounce = require('@hapi/bounce');
const Hoek = require('@hapi/hoek');
const Validate = require('@hapi/validate');
const internals = {};
exports.plugin = {
pkg: require('../package.json'),
requirements: {
hapi: '>=20.0.0'
},
register: (server) => {
server.auth.scheme('cookie', internals.implementation);
}
};
internals.schema = Validate.object({
appendNext: Validate.alternatives([
Validate.string(),
Validate.boolean(),
Validate.object({ raw: Validate.boolean(), name: Validate.string() })
])
.default(false),
cookie: Validate.object({
name: Validate.string().default('sid'),
encoding: Validate.valid('iron').default('iron'),
password: Validate.required(),
ignoreErrors: Validate.valid(true).default(true)
})
.unknown()
.default(),
keepAlive: Validate.boolean()
.when('cookie.ttl', { is: Validate.number().min(1), otherwise: Validate.forbidden() })
.default(false),
redirectTo: Validate.alternatives([
Validate.string(),
Validate.func()
])
.allow(false),
requestDecoratorName: Validate.string().default('cookieAuth'),
validate: Validate.func()
})
.required();
internals.CookieAuth = class {
constructor(request, settings) {
this.request = request;
this.settings = settings;
}
set(session, value) {
const { h, request, settings } = this;
if (arguments.length > 1) {
const key = session;
Hoek.assert(key && typeof key === 'string', 'Invalid session key');
session = request.auth.artifacts;
Hoek.assert(session, 'No active session to apply key to');
session[key] = value;
return h.state(settings.name, session);
}
Hoek.assert(session && typeof session === 'object', 'Invalid session');
request.auth.artifacts = session;
h.state(settings.name, session);
}
clear(key) {
const { h, request, settings } = this;
if (arguments.length) {
Hoek.assert(key && typeof key === 'string', 'Invalid session key');
const session = request.auth.artifacts;
Hoek.assert(session, 'No active session to clear key from');
delete session[key];
return h.state(settings.name, session);
}
request.auth.artifacts = null;
h.unstate(settings.name);
}
ttl(msecs) {
const { h, request, settings } = this;
const session = request.auth.artifacts;
Hoek.assert(session, 'No active session to modify ttl on');
h.state(settings.name, session, { ttl: msecs });
}
};
internals.implementation = (server, options) => {
const settings = Validate.attempt(options, internals.schema);
settings.name = settings.cookie.name;
delete settings.cookie.name;
server.state(settings.name, settings.cookie);
settings.cookie = server.states.cookies[settings.name];
if (typeof settings.appendNext === 'boolean') {
settings.appendNext = (settings.appendNext ? 'next' : '');
}
if (typeof settings.appendNext === 'object') {
settings.appendNextRaw = settings.appendNext.raw;
settings.appendNext = settings.appendNext.name || 'next';
}
const decoration = (request) => new internals.CookieAuth(request, settings);
server.decorate('request', settings.requestDecoratorName, decoration, { apply: true });
server.ext('onPreAuth', (request, h) => {
// Used for setting and unsetting state, not for replying to request
request[settings.requestDecoratorName].h = h;
return h.continue;
});
const scheme = {
authenticate: async (request, h) => {
const validate = async () => {
// Check cookie
const session = request.state[settings.name];
if (!session) {
return unauthenticated(Boom.unauthorized(null, 'cookie'));
}
if (!settings.validate) {
if (settings.keepAlive) {
h.state(settings.name, session);
}
return h.authenticated({ credentials: session, artifacts: session });
}
let credentials = session;
try {
const result = await settings.validate(request, session);
Hoek.assert(typeof result === 'object', 'Invalid return from validate function');
Hoek.assert(Object.prototype.hasOwnProperty.call(result, 'isValid'), 'validate function must have isValid property in return');
if (!result.isValid) {
throw Boom.unauthorized(null, 'cookie');
}
credentials = result.credentials ?? credentials;
if (settings.keepAlive) {
h.state(settings.name, session);
}
return h.authenticated({ credentials, artifacts: session });
}
catch (err) {
Bounce.rethrow(err, 'system');
if (settings.cookie.clearInvalid) {
h.unstate(settings.name);
}
let unauthorized = Boom.isBoom(err) && err.typeof === Boom.unauthorized ? err : null;
if (!unauthorized) {
unauthorized = Boom.unauthorized('Invalid cookie');
unauthorized.data = err;
}
return unauthenticated(unauthorized, { credentials, artifacts: session });
}
};
const unauthenticated = (err, result) => {
let redirectTo = settings.redirectTo;
if (request.route.settings.plugins.cookie?.redirectTo !== undefined) {
redirectTo = request.route.settings.plugins.cookie.redirectTo;
}
let uri = typeof redirectTo === 'function' ? redirectTo(request) : redirectTo;
if (!uri || request.auth.mode !== 'required') {
return h.unauthenticated(err);
}
if (settings.appendNext) {
if (uri.indexOf('?') !== -1) {
uri += '&';
}
else {
uri += '?';
}
if (settings.appendNextRaw) {
uri += settings.appendNext + '=' + encodeURIComponent(request.raw.req.url);
}
else {
uri += settings.appendNext + '=' + encodeURIComponent(request.url.pathname + request.url.search);
}
}
return h.response('You are being redirected...').takeover().redirect(uri);
};
return await validate();
}
};
return scheme;
};