The DTP Sites web app development engine.
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.
 
 
 
 
 

191 lines
5.5 KiB

// oauth2.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
const passport = require('passport');
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const uuidv4 = require('uuid').v4;
const oauth2orize = require('oauth2orize');
const { SiteService/*, SiteError*/ } = require('../../lib/site-lib');
class OAuth2Service extends SiteService {
constructor (dtp) {
super(dtp, module.exports);
}
async start ( ) {
this.models = { };
/*
* OAuth2Client Model
*/
const ClientSchema = new Schema({
created: { type: Date, default: Date.now, required: true },
updated: { type: Date, default: Date.now, required: true },
secret: { type: String, required: true },
redirectURI: { type: String, required: true },
});
this.log.info('registering OAuth2Client model');
this.models.Client = mongoose.model('OAuth2Client', ClientSchema);
/*
* OAuth2AuthorizationCode model
*/
const AuthorizationCodeSchema = new Schema({
code: { type: String, required: true, index: 1 },
clientId: { type: Schema.ObjectId, required: true, index: 1 },
redirectURI: { type: String, required: true },
user: { type: Schema.ObjectId, required: true, index: 1 },
scope: { type: [String], required: true },
});
this.log.info('registering OAuth2AuthorizationCode model');
this.models.AuthorizationCode = mongoose.model('OAuth2AuthorizationCode', AuthorizationCodeSchema);
/*
* OAuth2AccessToken model
*/
const AccessTokenSchema = new Schema({
token: { type: String, required: true, unique: true, index: 1 },
user: { type: Schema.ObjectId, required: true, index: 1 },
clientId: { type: Schema.ObjectId, required: true, index: 1 },
scope: { type: [String], required: true },
});
this.log.info('registering OAuth2AccessToken model');
this.models.AccessToken = mongoose.model('OAuth2AccessToken', AccessTokenSchema);
/*
* Create OAuth2 server instance
*/
const options = { };
this.log.info('creating OAuth2 server instance', { options });
this.server = oauth2orize.createServer(options);
this.server.grant(oauth2orize.grant.code(this.processGrant.bind(this)));
this.server.exchange(oauth2orize.exchange.code(this.processExchange.bind(this)));
/*
* Register client serialization callbacks
*/
this.log.info('registering OAuth2 client serialization routines');
this.server.serializeClient(this.serializeClient.bind(this));
this.server.deserializeClient(this.deserializeClient.bind(this));
}
async serializeClient (client, done) {
return done(null, client.id);
}
async deserializeClient (clientId, done) {
try {
const client = await this.models.Client.findOne({ _id: clientId }).lean();
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(
'/dialog/authorize',
requireLogin,
this.server.authorize(this.processAuthorize.bind(this)),
this.renderAuthorizeDialog.bind(this),
);
app.post(
'/dialog/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 this.models.Clients.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 this.models.AuthorizationCode({
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 this.models.AuthorizationCode.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 this.models.AccessToken({
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);
}
}
}
module.exports = {
slug: 'oauth2',
name: 'oauth2',
create: (dtp) => { return new OAuth2Service(dtp); },
};