@hapi/bell
Version:
Third-party login plugin for hapi
206 lines (145 loc) • 5.2 kB
JavaScript
'use strict';
const Hoek = require('@hapi/hoek');
const Joi = require('joi');
const OAuth = require('./oauth');
const Providers = require('./providers');
const internals = {
simulate: false,
flexBoolean: Joi.boolean().truthy('yes', 1, '1').falsy('no', 0, '0')
};
// Utilities
exports.providers = Providers;
exports.oauth = OAuth;
// Plugin
exports.plugin = {
pkg: require('../package.json'),
requirements: {
hapi: '>=20.0.0'
},
register: function (server) {
server.auth.scheme('bell', internals.implementation);
server.expose('oauth', OAuth);
}
};
internals.provider = Joi.object({
name: Joi.string().optional().default('custom'),
protocol: ['oauth', 'oauth2'],
auth: Joi.string().required(),
token: Joi.string().required(),
headers: Joi.object(),
profile: Joi.func(),
profileMethod: Joi.valid('get', 'post').default('get')
})
.when('.protocol', {
is: 'oauth',
then: Joi.object({
temporary: Joi.string().required(),
signatureMethod: Joi.valid('HMAC-SHA1', 'RSA-SHA1').default('HMAC-SHA1')
}),
otherwise: Joi.object({
scope: Joi.alternatives(
Joi.array().items(Joi.string()),
Joi.func()
),
scopeSeparator: Joi.string(),
useParamsAuth: internals.flexBoolean.default(false),
pkce: ['S256', 'plain']
})
});
internals.schema = Joi.object({
provider: internals.provider.required(),
password: Joi.string().required(),
clientId: Joi.string().required(),
clientSecret: Joi.alternatives()
.try(Joi.string().allow(''))
.try(Joi.function())
.conditional('provider.protocol', {
not: 'oauth',
then: Joi.object()
})
.required(),
cookie: Joi.string(),
isSameSite: Joi.valid('Strict', 'Lax').allow(false).default('Strict'),
isSecure: internals.flexBoolean,
isHttpOnly: internals.flexBoolean,
ttl: Joi.number(),
domain: Joi.string().allow(null),
providerParams: Joi.alternatives(Joi.object(), Joi.func()),
allowRuntimeProviderParams: internals.flexBoolean.default(false),
scope: Joi.alternatives(
Joi.array().items(Joi.string()),
Joi.func()
)
.when('provider.protocol', { is: 'oauth2', otherwise: Joi.forbidden() }),
name: Joi.string().required(),
config: Joi.object(),
tokenParams: Joi.alternatives(Joi.object(), Joi.func()),
profileParams: Joi.object(),
skipProfile: internals.flexBoolean.optional().default(false),
forceHttps: internals.flexBoolean.optional().default(false),
location: Joi.alternatives(
Joi.func().maxArity(1),
Joi.string()
)
.default(false),
runtimeStateCallback: Joi.func().optional()
});
internals.implementation = function (server, options) {
let settings = Hoek.clone(options, { shallow: 'provider' }); // Options can be reused
// Lookup provider
if (typeof settings.provider === 'object') {
settings.name = settings.provider.name ?? 'custom';
}
else {
settings.name = settings.provider;
settings.provider = Providers[settings.provider].call(null, settings.config);
}
const results = internals.schema.validate(settings);
Hoek.assert(!results.error, results.error);
// Passed validation, use Joi converted settings
settings = results.value;
// Setup cookie for managing temporary authorization state
const cookieOptions = {
encoding: 'iron',
path: '/',
password: settings.password,
isSecure: settings.isSecure !== false, // Defaults to true
isHttpOnly: settings.isHttpOnly !== false, // Defaults to true
isSameSite: settings.isSameSite,
ttl: settings.ttl,
domain: settings.domain,
ignoreErrors: true,
clearInvalid: true
};
settings.cookie = settings.cookie ?? `bell-${settings.name}`;
server.state(settings.cookie, cookieOptions);
if (internals.simulate) {
return internals.simulated(settings);
}
return { authenticate: (settings.provider.protocol === 'oauth' ? OAuth.v1 : OAuth.v2)(settings) };
};
exports.simulate = function (credentialsFunc) {
internals.simulate = credentialsFunc;
};
internals.simulated = function (settings) {
const name = settings.name;
const protocol = settings.provider.protocol;
return {
authenticate: async function (request, h) {
const result = await internals.simulate(request);
const credentials = {
provider: name,
token: 'oauth_token',
query: request.query
};
if (protocol === 'oauth') {
credentials.secret = 'token_secret';
}
else {
credentials.refreshToken = 'refresh_token';
credentials.expiresIn = 3600;
}
return h.authenticated({ credentials: Hoek.applyToDefaults(credentials, result) });
}
};
};