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.
203 lines
5.8 KiB
203 lines
5.8 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 {User} user The authenticated user issuing the request to create an
|
|
* "app" for use when calling DTP APIs.
|
|
* @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 (user, 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.redirectURI = clientDefinition.redirectURI;
|
|
|
|
await client.save();
|
|
|
|
this.log.info('new OAuth2 client created', {
|
|
clientId: client._id,
|
|
owner: user._id,
|
|
node: client.node.name,
|
|
domain: client.node.domain,
|
|
});
|
|
|
|
return client.toObject();
|
|
}
|
|
|
|
async getClientById (clientId) {
|
|
const client = await OAuth2Client
|
|
.findOne({ _id: clientId })
|
|
.lean();
|
|
return client;
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
slug: 'oauth2',
|
|
name: 'oauth2',
|
|
create: (dtp) => { return new OAuth2Service(dtp); },
|
|
};
|