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