// 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({ requireAuth: true, loginUri: '/welcome/login' }); 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( '/oauth2/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.callbackUrl !== redirectUri) { this.log.alert('OAuth2 client redirect URI mismatch', { redirectUri, officialUri: client.callbackUrl, }); return done(null, false); } return done(null, client, client.callbackUrl); } 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, scopes: client.scopes, }); 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.callbackUrl = striptags(clientDefinition.coreAuth.callbackUrl); /* * 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, callbackUrl: clientDefinition.coreAuth.callbackUrl, }, }, { 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); }, };