From 2b54a284a15a9136687f50c916602f74ec8a30fe Mon Sep 17 00:00:00 2001 From: Benjamin Zelnick <34197604+ZelnickB@users.noreply.github.com> Date: Sat, 11 Jan 2025 17:26:40 -0500 Subject: [PATCH] Revamped verification process This commit revamps the process for verifying a Discord account. In addition, it migrates the codebase to openid-client@6.x. Rather than storing information in encrypted and authenticated cookies, the IdentiBot web interface now stores a session ID cookie, which contains a base64url-encoded, pseudorandom 256-bit value. The session ID cookie points to relevant information in the MongoDB database that would formerly have been stored in cookies. The cookie expires after 12 hours, and the associated record remains in the database for 12.5 hours after creation, at which time it should be pruned using the MongoDB TTL feature. When verification is completed, the record is removed from the database and the cookie from the browser. Furthermore, the async/promise handling during the verification flow has been improved in this commit. --- lib/oauthClients.js | 19 -- lib/oauthConfigurations.js | 17 ++ package.json | 2 +- pnpm-lock.yaml | 48 +--- routes/api/verification/confirm.endpoint.js | 242 ++++++++---------- .../oauthCallbacks/discord.endpoint.js | 70 ++--- .../oauthCallbacks/petrock.endpoint.js | 77 +++--- routes/verification/confirm.endpoint.js | 66 ++--- routes/verification/confirmed.endpoint.js | 21 +- routes/verification/discord.endpoint.js | 32 +-- .../endpointAssets/confirmed/ui.hbs | 2 +- .../endpointAssets/discord/ui.hbs | 4 +- routes/verification/touchstone.endpoint.js | 5 +- 13 files changed, 259 insertions(+), 346 deletions(-) delete mode 100644 lib/oauthClients.js create mode 100644 lib/oauthConfigurations.js diff --git a/lib/oauthClients.js b/lib/oauthClients.js deleted file mode 100644 index 6290225..0000000 --- a/lib/oauthClients.js +++ /dev/null @@ -1,19 +0,0 @@ -import * as openidClient from 'openid-client' -import * as preferencesReader from '../lib/preferencesReader.js' - -const config = await preferencesReader.config() -const secrets = await preferencesReader.secrets() - -const issuers = { - petrock: await openidClient.Issuer.discover('https://petrock.mit.edu'), - discord: await openidClient.Issuer.discover('https://discord.com') -} - -export const petrock = new issuers.petrock.Client({ - client_id: config.petrock.clientID, - client_secret: secrets.oauthClientSecrets.petrock -}) -export const discord = new issuers.discord.Client({ - client_id: config.discord.clientID, - client_secret: secrets.oauthClientSecrets.discord -}) diff --git a/lib/oauthConfigurations.js b/lib/oauthConfigurations.js new file mode 100644 index 0000000..b74da6f --- /dev/null +++ b/lib/oauthConfigurations.js @@ -0,0 +1,17 @@ +import * as openidClient from 'openid-client' +import * as preferencesReader from '../lib/preferencesReader.js' + +const config = await preferencesReader.config() +const secrets = await preferencesReader.secrets() + +export const petrock = await openidClient.discovery( + new URL('https://petrock.mit.edu'), + config.petrock.clientID, + secrets.oauthClientSecrets.petrock, + openidClient.ClientSecretBasic(secrets.oauthClientSecrets.petrock) +) +export const discord = await openidClient.discovery( + new URL('https://discord.com'), + config.discord.clientID, + secrets.oauthClientSecrets.discord +) diff --git a/package.json b/package.json index f522f72..d157a33 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "hbs": "^4.2.0", "luxon": "^3.5.0", "mongodb": "^6.12.0", - "openid-client": "^5.7.0", + "openid-client": "^6.1.7", "utf-8-validate": "^6.0.5", "yaml": "^2.6.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 31e5e34..73ad8f5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,8 +33,8 @@ importers: specifier: ^6.12.0 version: 6.12.0 openid-client: - specifier: ^5.7.0 - version: 5.7.0 + specifier: ^6.1.7 + version: 6.1.7 utf-8-validate: specifier: ^6.0.5 version: 6.0.5 @@ -785,8 +785,8 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - jose@4.15.9: - resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==} + jose@5.9.6: + resolution: {integrity: sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==} js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} @@ -825,10 +825,6 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} - luxon@3.5.0: resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==} engines: {node: '>=12'} @@ -926,9 +922,8 @@ packages: resolution: {integrity: sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==} hasBin: true - object-hash@2.2.0: - resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==} - engines: {node: '>= 6'} + oauth4webapi@3.1.4: + resolution: {integrity: sha512-eVfN3nZNbok2s/ROifO0UAc5G8nRoLSbrcKJ09OqmucgnhXEfdIQOR4gq1eJH1rN3gV7rNw62bDEgftsgFtBEg==} object-inspect@1.13.1: resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} @@ -953,16 +948,12 @@ packages: resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==} engines: {node: '>= 0.4'} - oidc-token-hash@5.0.3: - resolution: {integrity: sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==} - engines: {node: ^10.13.0 || >=12.0.0} - on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} - openid-client@5.7.0: - resolution: {integrity: sha512-4GCCGZt1i2kTHpwvaC/sCpTpQqDnBzDzuJcJMbH+y1Q5qI8U8RBvoSh28svarXszZHR5BAMXbJPX1PGPRE3VOA==} + openid-client@6.1.7: + resolution: {integrity: sha512-JfY/KvQgOutmG2P+oVNKInE7zIh+im1MQOaO7g5CtNnTWMociA563WweiEMKfR9ry9XG3K2HGvj9wEqhCQkPMg==} optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} @@ -1254,9 +1245,6 @@ packages: utf-8-validate: optional: true - yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - yaml@2.6.1: resolution: {integrity: sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==} engines: {node: '>= 14'} @@ -2141,7 +2129,7 @@ snapshots: isexe@2.0.0: {} - jose@4.15.9: {} + jose@5.9.6: {} js-yaml@4.1.0: dependencies: @@ -2176,10 +2164,6 @@ snapshots: lodash@4.17.21: {} - lru-cache@6.0.0: - dependencies: - yallist: 4.0.0 - luxon@3.5.0: {} magic-bytes.js@1.10.0: {} @@ -2235,7 +2219,7 @@ snapshots: node-gyp-build@4.8.1: {} - object-hash@2.2.0: {} + oauth4webapi@3.1.4: {} object-inspect@1.13.1: {} @@ -2267,18 +2251,14 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.0.0 - oidc-token-hash@5.0.3: {} - on-finished@2.4.1: dependencies: ee-first: 1.1.1 - openid-client@5.7.0: + openid-client@6.1.7: dependencies: - jose: 4.15.9 - lru-cache: 6.0.0 - object-hash: 2.2.0 - oidc-token-hash: 5.0.3 + jose: 5.9.6 + oauth4webapi: 3.1.4 optionator@0.9.4: dependencies: @@ -2596,8 +2576,6 @@ snapshots: bufferutil: 4.0.9 utf-8-validate: 6.0.5 - yallist@4.0.0: {} - yaml@2.6.1: {} yocto-queue@0.1.0: {} diff --git a/routes/api/verification/confirm.endpoint.js b/routes/api/verification/confirm.endpoint.js index 2c30276..70edc48 100644 --- a/routes/api/verification/confirm.endpoint.js +++ b/routes/api/verification/confirm.endpoint.js @@ -1,172 +1,138 @@ -import { TokenSet } from 'openid-client' -import { discord, petrock } from '../../../lib/oauthClients.js' import { dbClient } from '../../../lib/mongoClient.js' -import { decrypt } from '../../../lib/simpleCrypto.js' -import { botHeaders, parseNickname } from '../../../lib/utils.js' import { gateway } from '../../../lib/discordAPIClients.js' import { getConfiguredServersList, getServerConfigDocument } from '../../../lib/configurationReaders.js' +import { botHeaders, parseNickname } from '../../../lib/utils.js' +const verificationSessions = dbClient.collection('verification.sessions') const verificationUserInfoCollection = dbClient.collection('verification.userInfo') -const oauthTokenCollections = { - petrock: dbClient.collection('verification.oauthTokens.petrock'), - discord: dbClient.collection('verification.oauthTokens.discord') -} export function get (req, res) { - let petrockTokenSetJSON, discordTokenSetJSON - try { - petrockTokenSetJSON = JSON.parse( - decrypt(Buffer.from(req.cookies['oauthTokens.petrock'], 'base64url')) - .toString('utf8') - ) - discordTokenSetJSON = JSON.parse( - decrypt(Buffer.from(req.cookies['oauthTokens.discord'], 'base64url')) - .toString('utf8') - ) - } catch { - return res.sendStatus(400) - } - Promise.all([ - petrock.userinfo(new TokenSet(petrockTokenSetJSON)), - discord.requestResource('https://discord.com/api/v10/users/@me', new TokenSet(new TokenSet(discordTokenSetJSON))) - ]).then(([petrockUserInfo, discordUserInfo]) => { - discordUserInfo = JSON.parse(discordUserInfo.body.toString('utf8')) - return Promise.all([ - [petrockUserInfo, discordUserInfo], - verificationUserInfoCollection.findOne( - { - 'petrock.sub': petrockUserInfo.sub - } - ) - ]) - }).then(([[petrockUserInfo, discordUserInfo], matchingKerberosDocument]) => { - if (matchingKerberosDocument !== null && matchingKerberosDocument.discord.id !== discordUserInfo.id) { - res.render('error', { - code: '401', - description: 'Unauthorized', - explanation: 'This Kerberos identity has already been used to verify a Discord account.' + verificationSessions.findOne({ sessionID: req.cookies['verification.sessionID'] }) + .then(async (sessionInformation) => { + // STEP 1: Check if the provided Kerberos identity has already been used to verify a different Discord account. + const returnVal = { + sessionInformation, + ok: true + } + const matchingKerberosDocument = await verificationUserInfoCollection.findOne({ + 'petrock.sub': sessionInformation.petrockUser.sub }) - return false - } - return Promise.all([ - [petrockUserInfo, discordUserInfo], - oauthTokenCollections.petrock.updateOne( - { _sub: petrockUserInfo.sub }, - { - $set: { - _sub: petrockUserInfo.sub, - ...petrockTokenSetJSON - } - }, - { - upsert: true - } - ), - oauthTokenCollections.discord.updateOne( - { _sub: discordUserInfo.id }, - { - $set: { - _sub: discordUserInfo.id, - ...discordTokenSetJSON - } - }, - { - upsert: true - } - ) - ]) - }).then((val) => { - if (val === false) { - return false - } - const [petrockUserInfo, discordUserInfo] = val[0] - return Promise.all([ - [petrockUserInfo, discordUserInfo], - verificationUserInfoCollection.updateOne( + if (matchingKerberosDocument !== null && matchingKerberosDocument.discord.id !== sessionInformation.discordUser.id) { + res.status(401).render('error', { + code: '401', + description: 'Unauthorized', + explanation: 'This Kerberos identity has already been used to verify a Discord account.' + }) + returnVal.ok = false + } + return returnVal + }) + .then(async (x) => { + // STEP 2: Add linked Kerberos identity information to the database. + if (!x.ok) { + return x + } + await verificationUserInfoCollection.updateOne( { $or: [ - { 'petrock.sub': petrockUserInfo.sub }, - { 'discord.id': discordUserInfo.id } + { 'petrock.sub': x.sessionInformation.petrockUser.sub }, + { 'discord.id': x.sessionInformation.discordUser.id } ] }, { $set: { - petrock: petrockUserInfo, - discord: discordUserInfo + petrock: x.sessionInformation.petrockUser, + discord: x.sessionInformation.discordUser } }, { upsert: true } ) - ]) - }).then(async (val) => { - if (val === false) { - return false - } - const [petrockUserInfo, discordUserInfo] = val[0] - const promises = [val[0]] - // TODO: The async/promise handling here is less than ideal. - for (const serverID in await getConfiguredServersList()) { - const serverConfig = await getServerConfigDocument(serverID) - promises.push(gateway.guilds.fetch(serverID).then((server) => { - const innerPromises = [] - if (serverConfig.verification.allowedAffiliations.includes(petrockUserInfo.affiliation)) { - innerPromises.push( - server.members.fetch(discordUserInfo.id).then((member) => { + return x + }) + .then(async (x) => { + // STEP 3: Get list of registered servers and their configurations. + if (!x.ok) { + return x + } + const serverList = [] + for (const serverID of await getConfiguredServersList()) { + serverList.push(Promise.allSettled([getServerConfigDocument(serverID), gateway.guilds.fetch(serverID)])) + } + return { + ...x, + serverList: await Promise.all(serverList) + } + }) + .then(async (x) => { + // STEP 4: Add roles and update nicknames in servers where the user is a member. + if (!x.ok) { + return x + } + const tasks = [] + for (const [config, guild] of x.serverList) { + if (guild.status === 'rejected') continue + if (config.value.verification.allowedAffiliations.includes(x.sessionInformation.petrockUser.affiliation)) { + tasks.push( + guild.value.members.fetch(x.sessionInformation.discordUser.id).then((member) => { return member.roles.add( - serverConfig.verification.verifiedRole, - `User verified to control Kerberos identity ${petrockUserInfo.email}.` + config.value.verification.verifiedRole, + `User verified to control Kerberos identity ${x.sessionInformation.petrockUser.email}.` ) }) ) - if (serverConfig.verification.autochangeNickname === true) { - innerPromises.push( - server.members.fetch(discordUserInfo.id).then((member) => { + if (config.value.verification.autochangeNickname === true) { + tasks.push( + guild.value.members.fetch(x.sessionInformation.discordUser.id).then((member) => { return member.setNickname( - parseNickname(petrockUserInfo.given_name, petrockUserInfo.family_name), - `Automatically updated nickname to reflect name on record for user's verified Kerberos identity: ${petrockUserInfo.name}` + parseNickname(x.sessionInformation.petrockUser.given_name, x.sessionInformation.petrockUser.family_name), + `Automatically updated nickname to reflect name on record for user's verified Kerberos identity: ${x.sessionInformation.petrockUser.name}` ) }) ) } - return Promise.allSettled(innerPromises) } - })) - } - return Promise.allSettled(promises) - }).then((val) => { - if (val === false) { - return false - } - const [petrockUserInfo, discordUserInfo] = val[0].value - // BEGIN CLASS OF 2028 SERVER-SPECIFIC CODE - fetch(`https://tlepeopledir.mit.edu/q/${petrockUserInfo.sub}`).then((x) => x.json()).then(directoryResponse => { - if (directoryResponse.result[0] !== undefined && directoryResponse.result[0].student_year === '1') { - fetch( - `https://discord.com/api/v10/guilds/1186456227425828926/members/${discordUserInfo.id}/roles/1186460225943912539`, - { - method: 'PUT', - headers: { - ...botHeaders, - 'X-Audit-Log-Reason': 'User verified as year 1 student in MIT directory.' + } + return { + ...x, + tasks: await Promise.allSettled(tasks) + } + }) + .then(async (x) => { + // STEP 5: Clear session from database and cookies and redirect to success page. + if (!x.ok) { + return x + } + // BEGIN CLASS OF 2028 SERVER-SPECIFIC CODE + fetch(`https://tlepeopledir.mit.edu/q/${x.sessionInformation.petrockUser.sub}`).then((x) => x.json()).then(directoryResponse => { + if (directoryResponse.result[0] !== undefined && directoryResponse.result[0].student_year === '1') { + fetch( + `https://discord.com/api/v10/guilds/1186456227425828926/members/${x.sessionInformation.discordUser.id}/roles/1186460225943912539`, + { + method: 'PUT', + headers: { + ...botHeaders, + 'X-Audit-Log-Reason': 'User verified as year 1 student in MIT directory.' + } } - } - ) - } else if (directoryResponse.result[0] !== undefined) { - fetch( - `https://discord.com/api/v10/guilds/1186456227425828926/members/${discordUserInfo.id}/roles/1218369208409395301`, - { - method: 'PUT', - headers: { - ...botHeaders, - 'X-Audit-Log-Reason': `User verified as student in MIT directory, currently in year ${directoryResponse.result[0].student_year}.` + ) + } else if (directoryResponse.result[0] !== undefined) { + fetch( + `https://discord.com/api/v10/guilds/1186456227425828926/members/${x.sessionInformation.discordUser.id}/roles/1218369208409395301`, + { + method: 'PUT', + headers: { + ...botHeaders, + 'X-Audit-Log-Reason': `User verified as student in MIT directory, currently in year ${directoryResponse.result[0].student_year}.` + } } - } - ) - } + ) + } + }) + // END CLASS OF 2028 SERVER-SPECIFIC CODE + await verificationSessions.deleteOne({ sessionID: req.cookies['verification.sessionID'] }) + res.clearCookie('verification.sessionID') + return res.redirect(302, '/verification/confirmed') }) - // END CLASS OF 2028 SERVER-SPECIFIC CODE - return res.redirect(302, '/verification/confirmed') - }) } diff --git a/routes/api/verification/oauthCallbacks/discord.endpoint.js b/routes/api/verification/oauthCallbacks/discord.endpoint.js index 8452c54..853ffca 100644 --- a/routes/api/verification/oauthCallbacks/discord.endpoint.js +++ b/routes/api/verification/oauthCallbacks/discord.endpoint.js @@ -1,43 +1,45 @@ -import { errors as openidErrors } from 'openid-client' -import { discord } from '../../../../lib/oauthClients.js' -import { encrypt } from '../../../../lib/simpleCrypto.js' +import * as openidClient from 'openid-client' +import * as oauthConfigs from '../../../../lib/oauthConfigurations.js' import { configSync } from '../../../../lib/preferencesReader.js' +import { dbClient } from '../../../../lib/mongoClient.js' + +const discordOauthTokens = dbClient.collection('verification.oauthTokens.discord') +const verificationSessions = dbClient.collection('verification.sessions') export function get (req, res) { - discord.callback(`${configSync().baseURL}/api/verification/oauthCallbacks/discord`, discord.callbackParams(req.url)).then( - (tokens) => { - res.cookie('oauthTokens.discord', - encrypt(JSON.stringify(tokens)).toString('base64url'), + return openidClient.authorizationCodeGrant(oauthConfigs.discord, new URL(configSync().baseURL + req.originalUrl)) + .then(async (tokens) => { + const userInfo = await openidClient.fetchProtectedResource(oauthConfigs.discord, tokens.access_token, new URL('https://discord.com/api/v10/users/@me'), 'GET').then(response => response.json()) + return { + tokens, + userInfo + } + }) + .then(async (userInfoAndTokens) => { + await discordOauthTokens.updateOne( + { _sub: userInfoAndTokens.userInfo.id }, { - sameSite: 'lax', - path: '/', - maxAge: 21600000 + $set: { + _sub: userInfoAndTokens.userInfo.id, + ...userInfoAndTokens.tokens + } + }, + { + upsert: true } ) - return res.redirect(302, '/verification/confirm') - }, - (reason) => { - if (reason instanceof openidErrors.OPError) { - switch (reason.error) { - case 'invalid_grant': - return res.status(400).render('error', { - code: '400', - description: 'Bad Request', - explanation: 'You probably refreshed this page. Please start verification again. In tech-speak, the OAuth authorization code in the URL is invalid.' - }) - default: - return res.status(502).render('error', { - code: '502', - description: 'Bad Gateway', - explanation: 'Something bad happened when communicating with Discord, and we\'re not quite sure how to explain it to you.' - }) - } - } else { - return res.status(500).render('error', { - code: '500', - description: 'Internal Server Error', - explanation: 'Something bad happened, and we\'re not quite sure how to explain it to you.' + return userInfoAndTokens + }) + .then((userInfoAndTokens) => { + return verificationSessions.updateOne( + { sessionID: req.cookies['verification.sessionID'] }, + { + $set: { + discordUser: userInfoAndTokens.userInfo + } }) - } + }) + .then(() => { + res.redirect(302, '/verification/confirm') }) } diff --git a/routes/api/verification/oauthCallbacks/petrock.endpoint.js b/routes/api/verification/oauthCallbacks/petrock.endpoint.js index 22fe109..d4100bd 100644 --- a/routes/api/verification/oauthCallbacks/petrock.endpoint.js +++ b/routes/api/verification/oauthCallbacks/petrock.endpoint.js @@ -1,43 +1,50 @@ -import { errors as openidErrors } from 'openid-client' -import { petrock } from '../../../../lib/oauthClients.js' -import { encrypt } from '../../../../lib/simpleCrypto.js' +import * as openidClient from 'openid-client' +import * as crypto from 'crypto' +import { petrock } from '../../../../lib/oauthConfigurations.js' import { configSync } from '../../../../lib/preferencesReader.js' +import { dbClient } from '../../../../lib/mongoClient.js' -export function get (req, res) { - petrock.callback(`${configSync().baseURL}/api/verification/oauthCallbacks/petrock`, petrock.callbackParams(req.url)).then( - (tokens) => { - res.cookie('oauthTokens.petrock', - encrypt(JSON.stringify(tokens)).toString('base64url'), +const petrockOauthTokens = dbClient.collection('verification.oauthTokens.petrock') +const verificationSessions = dbClient.collection('verification.sessions') + +export async function get (req, res) { + return openidClient.authorizationCodeGrant(petrock, new URL(configSync().baseURL + req.originalUrl)) + .then(async (tokens) => { + const userInfo = await openidClient.fetchUserInfo(petrock, tokens.access_token, tokens.claims().sub) + return { + tokens, + userInfo + } + }) + .then(async (userInfoAndTokens) => { + await petrockOauthTokens.updateOne( + { _sub: userInfoAndTokens.userInfo.sub }, + { + $set: { + _sub: userInfoAndTokens.userInfo.sub, + ...userInfoAndTokens.tokens + } + }, { - sameSite: 'lax', - path: '/', - maxAge: 21600000 + upsert: true } ) + return userInfoAndTokens + }) + .then((userInfoAndTokens) => { + const sessionID = crypto.randomBytes(32).toString('base64url') + res.cookie('verification.sessionID', sessionID, { + sameSite: 'lax', + path: '/', + maxAge: 43200000 + }) + return verificationSessions.insertOne({ + _expires: new Date(Date.now() + 45000000), // Remove the session from the database 30 minutes (45000000-43200000 ms) after the cookie expires (in case of clock drift) + sessionID, + petrockUser: userInfoAndTokens.userInfo + }) + }) + .then(() => { return res.redirect(302, '/verification/discord') - }, - (reason) => { - if (reason instanceof openidErrors.OPError) { - switch (reason.error) { - case 'invalid_grant': - return res.status(400).render('error', { - code: '400', - description: 'Bad Request', - explanation: 'You probably refreshed this page. Please start verification again. In tech-speak, the OAuth authorization code in the URL is invalid.' - }) - default: - return res.status(502).render('error', { - code: '502', - description: 'Bad Gateway', - explanation: 'Something bad happened when communicating with Petrock/MIT Touchstone, and we\'re not quite sure how to explain it to you.' - }) - } - } else { - return res.status(500).render('error', { - code: '500', - description: 'Internal Server Error', - explanation: 'Something bad happened, and we\'re not quite sure how to explain it to you.' - }) - } }) } diff --git a/routes/verification/confirm.endpoint.js b/routes/verification/confirm.endpoint.js index 372cf88..5c54743 100644 --- a/routes/verification/confirm.endpoint.js +++ b/routes/verification/confirm.endpoint.js @@ -1,51 +1,29 @@ import * as path from 'path' -import { petrock, discord } from '../../lib/oauthClients.js' -import { TokenSet } from 'openid-client' -import { decrypt } from '../../lib/simpleCrypto.js' import * as utils from '../../lib/utils.js' import { configSync } from '../../lib/preferencesReader.js' +import { dbClient } from '../../lib/mongoClient.js' + +const verificationSessions = dbClient.collection('verification.sessions') export function get (req, res) { - let petrockTokenSetJSON, discordTokenSetJSON - try { - petrockTokenSetJSON = new TokenSet(JSON.parse( - decrypt(Buffer.from(req.cookies['oauthTokens.petrock'], 'base64url')) - .toString('utf8') - )) - discordTokenSetJSON = new TokenSet(JSON.parse( - decrypt(Buffer.from(req.cookies['oauthTokens.discord'], 'base64url')) - .toString('utf8') - )) - } catch { - return res.sendStatus(400) - } - Promise.all([ - petrock.userinfo( - petrockTokenSetJSON - ), - discord.requestResource( - 'https://discord.com/api/v10/users/@me', - discordTokenSetJSON - ) - ]).then(([petrockUserInfo, discordUserInfo]) => { - discordUserInfo = JSON.parse(discordUserInfo.body.toString('utf8')) - const newServerNickname = utils.parseNickname(petrockUserInfo.given_name, petrockUserInfo.family_name) - res.status(200).render(path.resolve(import.meta.dirname, 'endpointAssets', 'confirm', 'ui.hbs'), { - petrockUserInfo, - discordUserInfo, - newServerNickname, - messageVariables: - configSync().singleServerMessages - ? { - discordAccountWhereAlt: ' in this server', - discordAccountWhere: 'this server', - discordAccountWhereSome: 'this server (depending on configuration)' - } - : { - discordAccountWhereAlt: '', - discordAccountWhere: 'participating servers', - discordAccountWhereSome: 'some participating servers' - } + verificationSessions.findOne({ sessionID: req.cookies['verification.sessionID'] }) + .then((sessionInformation) => { + res.status(200).render(path.resolve(import.meta.dirname, 'endpointAssets', 'confirm', 'ui.hbs'), { + petrockUserInfo: sessionInformation.petrockUser, + discordUserInfo: sessionInformation.discordUser, + newServerNickname: utils.parseNickname(sessionInformation.petrockUser.given_name, sessionInformation.petrockUser.family_name), + messageVariables: + configSync().singleServerMessages + ? { + discordAccountWhereAlt: ' in this server', + discordAccountWhere: 'this server', + discordAccountWhereSome: 'this server (depending on configuration)' + } + : { + discordAccountWhereAlt: '', + discordAccountWhere: 'participating servers', + discordAccountWhereSome: 'some participating servers' + } + }) }) - }) } diff --git a/routes/verification/confirmed.endpoint.js b/routes/verification/confirmed.endpoint.js index 37c5620..2781fd3 100644 --- a/routes/verification/confirmed.endpoint.js +++ b/routes/verification/confirmed.endpoint.js @@ -1,23 +1,10 @@ import * as path from 'path' -import { petrock } from '../../lib/oauthClients.js' -import { TokenSet } from 'openid-client' -import { decrypt } from '../../lib/simpleCrypto.js' import { configSync } from '../../lib/preferencesReader.js' export function get (req, res) { - petrock.userinfo( - new TokenSet( - JSON.parse( - decrypt(Buffer.from(req.cookies['oauthTokens.petrock'], 'base64url')) - .toString('utf8') - ) - ) - ).then((petrockUserInfo) => { - res.status(200).render(path.resolve(import.meta.dirname, 'endpointAssets', 'confirmed', 'ui.hbs'), { - petrockUserInfo, - messageVariables: { - whereServers: configSync().singleServerMessages ? 'this server' : 'all participating Discord servers' - } - }) + res.status(200).render(path.resolve(import.meta.dirname, 'endpointAssets', 'confirmed', 'ui.hbs'), { + messageVariables: { + whereServers: configSync().singleServerMessages ? 'this server' : 'all participating Discord servers' + } }) } diff --git a/routes/verification/discord.endpoint.js b/routes/verification/discord.endpoint.js index 725cd08..b33ed48 100644 --- a/routes/verification/discord.endpoint.js +++ b/routes/verification/discord.endpoint.js @@ -1,24 +1,20 @@ import * as path from 'path' -import { TokenSet } from 'openid-client' -import { petrock, discord } from '../../lib/oauthClients.js' +import { discord } from '../../lib/oauthConfigurations.js' import { configSync } from '../../lib/preferencesReader.js' -import { decrypt } from '../../lib/simpleCrypto.js' +import { dbClient } from '../../lib/mongoClient.js' +import { buildAuthorizationUrl } from 'openid-client' + +const verificationSessions = dbClient.collection('verification.sessions') export function get (req, res) { - petrock.userinfo( - new TokenSet( - JSON.parse( - decrypt(Buffer.from(req.cookies['oauthTokens.petrock'], 'base64url')) - .toString('utf8') - ) - ) - ).then((userInfo) => { - res.status(200).render(path.resolve(import.meta.dirname, 'endpointAssets', 'discord', 'ui.hbs'), { - authorizationRedirectURL: discord.authorizationUrl({ - redirect_uri: `${configSync().baseURL}/api/verification/oauthCallbacks/discord`, - scope: 'openid identify email' - }), - petrockUserInfo: userInfo + verificationSessions.findOne({ sessionID: req.cookies['verification.sessionID'] }) + .then((sessionInformation) => { + res.status(200).render(path.resolve(import.meta.dirname, 'endpointAssets', 'discord', 'ui.hbs'), { + authorizationRedirectURL: buildAuthorizationUrl(discord, { + redirect_uri: `${configSync().baseURL}/api/verification/oauthCallbacks/discord`, + scope: 'identify email openid role_connections.write' + }), + userInfo: sessionInformation.petrockUser + }) }) - }) } diff --git a/routes/verification/endpointAssets/confirmed/ui.hbs b/routes/verification/endpointAssets/confirmed/ui.hbs index b8377e5..819051e 100644 --- a/routes/verification/endpointAssets/confirmed/ui.hbs +++ b/routes/verification/endpointAssets/confirmed/ui.hbs @@ -8,7 +8,7 @@
-

You have successfully verified your Discord account, {{petrockUserInfo.given_name}}! We're glad you're here.

+

You have successfully verified your Discord account! We're glad you're here.

Your Discord account has been verified using your Kerberos identity. You have been assigned corresponding verified roles in {{messageVariables.whereServers}}.

diff --git a/routes/verification/endpointAssets/discord/ui.hbs b/routes/verification/endpointAssets/discord/ui.hbs index 11008b3..c75c048 100644 --- a/routes/verification/endpointAssets/discord/ui.hbs +++ b/routes/verification/endpointAssets/discord/ui.hbs @@ -8,8 +8,8 @@
-

Hi, {{petrockUserInfo.given_name}}.

-

If your Kerberos identity is not {{petrockUserInfo.email}}, please start over.

+

Hi, {{userInfo.given_name}}.

+

If your Kerberos identity is not {{userInfo.email}}, please start over.

Sign in with Discord to complete verification.