You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

215 lines
6.0 KiB

// oauth2.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
const mongoose = require('mongoose');
const OAuth2Client = mongoose.model('OAuth2Client');
const OAuth2AuthorizationCode = mongoose.model('OAuth2AuthorizationCode');
const OAuth2AccessToken = mongoose.model('OAuth2AccessToken');
const uuidv4 = require('uuid').v4;
const striptags = require('striptags');
const oauth2orize = require('oauth2orize');
const passport = require('passport');
const generatePassword = require('password-generator');
const { SiteService/*, SiteError*/ } = require('../../lib/site-lib');
class OAuth2Service extends SiteService {
constructor (dtp) {
super(dtp, module.exports);
}
async start ( ) {
const serverOptions = { };
this.log.info('creating OAuth2 server instance', { serverOptions });
this.server = oauth2orize.createServer(serverOptions);
this.log.info('registering OAuth2 action handlers');
this.server.grant(oauth2orize.grant.code(this.processGrant.bind(this)));
this.server.exchange(oauth2orize.exchange.code(this.processExchange.bind(this)));
this.log.info('registering OAuth2 serialization routines');
this.server.serializeClient(this.serializeClient.bind(this));
this.server.deserializeClient(this.deserializeClient.bind(this));
}
async serializeClient (client, done) {
this.log.debug('serializeClient', { client });
return done(null, client.id);
}
async deserializeClient (clientId, done) {
this.log.debug('deserializeClient', { clientId });
try {
const client = await OAuth2Client
.findOne({ _id: clientId })
.lean();
this.log.debug('OAuth2 client loaded', { clientId, client });
return done(null, client);
} catch (error) {
this.log.error('failed to deserialize OAuth2 client', { clientId, error });
return done(error);
}
}
attachRoutes (app) {
const { session: sessionService } = this.dtp.services;
const requireLogin = sessionService.authCheckMiddleware({ requireLogin: true });
app.get(
'/oauth2/authorize',
requireLogin,
this.server.authorize(this.processAuthorize.bind(this)),
this.renderAuthorizeDialog.bind(this),
);
app.post(
'/oauth2/authorize/decision',
requireLogin,
this.server.decision(),
);
app.post(
'/token',
passport.authenticate(['basic', 'oauth2-client-password'], { session: false }),
this.server.token(),
this.server.errorHandler(),
);
}
async renderAuthorizeDialog (req, res) {
res.locals.transactionID = req.oauth2.transactionID;
res.locals.client = req.oauth2.client;
res.render('oauth2/authorize-dialog');
}
async processAuthorize (clientID, redirectUri, done) {
try {
const client = await OAuth2Client.findOne({ clientID });
if (!client) {
return done(null, false);
}
if (client.redirectUri !== redirectUri) {
return done(null, false);
}
return done(null, client, client.redirectUri);
} catch (error) {
this.log.error('failed to process OAuth2 authorize', { error });
return done(error);
}
}
async processGrant (client, redirectUri, user, ares, done) {
try {
var code = uuidv4();
var ac = new OAuth2AuthorizationCode({
code,
clientId: client.id,
redirectUri,
user: user.id,
scope: ares.scope,
});
await ac.save();
return done(null, code);
} catch (error) {
this.log.error('failed to process OAuth2 grant', { error });
return done(error);
}
}
async processExchange (client, code, redirectUri, done) {
try {
const ac = await OAuth2AuthorizationCode.findOne({ code });
if (client.id !== ac.clientId) {
return done(null, false);
}
if (redirectUri !== ac.redirectUri) {
return done(null, false);
}
var token = uuidv4();
var at = new OAuth2AccessToken({
token,
user: ac.userId,
clientId: ac.clientId,
scope: ac.scope,
});
await at.save();
return done(null, token);
} catch (error) {
this.log.error('failed to process OAuth2 exchange', { error });
return done(error);
}
}
/**
* Creates a new OAuth2 client, and generates a Client ID and Secret for it.
* @param {Document} clientDefinition The definition of the client to be
* created including the name and domain of the node.
* @returns new client instance with valid _id.
*/
async createClient (clientDefinition) {
const NOW = new Date();
const PASSWORD_LEN = parseInt(process.env.DTP_CORE_AUTH_PASSWORD_LEN || '64', 10);
const client = new OAuth2Client();
client.created = NOW;
client.updated = NOW;
client.site.name = striptags(clientDefinition.name);
client.site.description = striptags(clientDefinition.description);
client.site.domain = striptags(clientDefinition.domain);
client.site.domainKey = striptags(clientDefinition.domainKey);
client.site.company = striptags(clientDefinition.company);
client.secret = generatePassword(PASSWORD_LEN, false);
client.scopes = clientDefinition.coreAuth.redirectUri.map((scope) => striptags(scope));
client.redirectUri = striptags(clientDefinition.coreAuth.redirectUri);
await client.save();
this.log.info('new OAuth2 client created', {
clientId: client._id,
site: client.site.name,
domain: client.site.domain,
});
return client.toObject();
}
async getClientById (clientId) {
const client = await OAuth2Client
.findOne({ _id: clientId })
.lean();
return client;
}
async getClientByDomain (domain) {
const client = await OAuth2Client
.findOne({ 'site.domain': domain })
.lean();
return client;
}
async getClientByDomainKey (domainKey) {
const client = await OAuth2Client
.findOne({ 'site.domainKey': domainKey })
.lean();
return client;
}
}
module.exports = {
slug: 'oauth2',
name: 'oauth2',
create: (dtp) => { return new OAuth2Service(dtp); },
};