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.
250 lines
7.3 KiB
250 lines
7.3 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.currentView = 'oauth2/authorize-dialog';
|
|
res.locals.oauth2 = req.oauth2;
|
|
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) {
|
|
this.log.alert('OAuth2 client not found', { clientID });
|
|
return done(null, false);
|
|
}
|
|
if (client.redirectUri !== redirectUri) {
|
|
this.log.alert('OAuth2 client redirect URI mismatch', {
|
|
redirectUri,
|
|
officialUri: client.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);
|
|
|
|
// scrub up the input data to help prevent shenanigans
|
|
clientDefinition.name = striptags(clientDefinition.name);
|
|
clientDefinition.description = striptags(clientDefinition.description);
|
|
clientDefinition.domain = striptags(clientDefinition.domain);
|
|
clientDefinition.domainKey = striptags(clientDefinition.domainKey);
|
|
|
|
clientDefinition.company = striptags(clientDefinition.company);
|
|
|
|
clientDefinition.secret = generatePassword(PASSWORD_LEN, false);
|
|
clientDefinition.coreAuth.scopes = clientDefinition.coreAuth.scopes.map((scope) => striptags(scope));
|
|
clientDefinition.coreAuth.redirectUri = striptags(clientDefinition.coreAuth.redirectUri);
|
|
|
|
/*
|
|
* Use an upsert to either update or create the OAuth2 client record for the
|
|
* calling host.
|
|
*/
|
|
|
|
const client = await OAuth2Client.findOneAndUpdate(
|
|
{
|
|
'site.domain': clientDefinition.domain,
|
|
'site.domainKey': clientDefinition.domainKey,
|
|
},
|
|
{
|
|
$setOnInsert: {
|
|
created: NOW,
|
|
'site.domain': clientDefinition.domain,
|
|
'site.domainKey': clientDefinition.domainKey,
|
|
},
|
|
$set: {
|
|
updated: NOW,
|
|
'site.name': clientDefinition.name,
|
|
'site.description': clientDefinition.description,
|
|
'site.company': clientDefinition.company,
|
|
secret: clientDefinition.secret,
|
|
scopes: clientDefinition.coreAuth.scopes,
|
|
redirectUri: clientDefinition.coreAuth.redirectUri,
|
|
},
|
|
},
|
|
{
|
|
upsert: true, // create if it doesn't exist
|
|
"new": true, // return the modified version
|
|
},
|
|
);
|
|
|
|
this.log.info('new OAuth2 client updated', {
|
|
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); },
|
|
};
|