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
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(
|
|
'/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 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); },
|
|
};
|