From 18d6ccdfd3802df121d6d66786de39d0c0834a0d Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 23 Jun 2023 17:46:14 +0200 Subject: [PATCH] SSO: OIDC auth --- .gitignore | 1 + config/sso.example.js | 21 +++ customize.dist/login.js | 33 ++++ customize.dist/pages/login.js | 2 + customize.dist/pages/register.js | 3 +- lib/challenge-commands/sso.js | 105 +++++++++++++ lib/env.js | 2 + lib/http-commands.js | 7 +- lib/http-worker.js | 11 +- lib/load-config.js | 7 + lib/plugins/sso/oidc.js | 67 +++++++++ lib/sso-utils.js | 71 +++++++++ lib/storage/sso.js | 57 +++++++ package-lock.json | 250 ++++++++++++++++--------------- package.json | 2 + www/register/main.js | 27 +++- www/ssoauth/index.html | 14 ++ www/ssoauth/main.js | 3 + 18 files changed, 557 insertions(+), 126 deletions(-) create mode 100644 config/sso.example.js create mode 100644 lib/challenge-commands/sso.js create mode 100644 lib/plugins/sso/oidc.js create mode 100644 lib/sso-utils.js create mode 100644 lib/storage/sso.js create mode 100644 www/ssoauth/index.html create mode 100644 www/ssoauth/main.js diff --git a/.gitignore b/.gitignore index 3ef8d2e15..e42df1b06 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,5 @@ block/ logs/ privileged.conf config/config.js +config/sso.js *.sh diff --git a/config/sso.example.js b/config/sso.example.js new file mode 100644 index 000000000..a1b7289ab --- /dev/null +++ b/config/sso.example.js @@ -0,0 +1,21 @@ +module.exports = { + // Enable SSO login on this instance + enabled: false, + // Block registration for non-SSO users on this instance + enforced: false, + // Allow users to add an additional CryptPad password to their SSO account + cpPassword: false, + // List of SSO providers + list: [ + /* + { + name: 'google', + type: 'oidc', + url: 'https://accounts.google.com', + client_id: "{your_client_id}", + client_secret: "{your_client_secret}" + } + */ + ] +}; + diff --git a/customize.dist/login.js b/customize.dist/login.js index c8219ed0b..c0b06fa04 100644 --- a/customize.dist/login.js +++ b/customize.dist/login.js @@ -43,6 +43,39 @@ define([ setRedirectTo(); } + Exports.ssoRegister = function (provider, cb) { + var keys = Nacl.sign.keyPair(); + localStorage.CP_sso_auth = JSON.stringify({ + s: Nacl.util.encodeBase64(keys.secretKey), + p: Nacl.util.encodeBase64(keys.publicKey) + }); + ServerCommand(keys, { + command: 'SSO_AUTH', + provider: provider, + register: true + }, cb); + }; + Exports.ssoRegisterCb = function () { + var b64Keys = Util.tryParse(localStorage.CP_sso_auth); + if (!b64Keys) { + throw new Error("MISSING_SIGNATURE_KEYS"); + } + var keys = { + secretKey: Nacl.util.decodeBase64(b64Keys.s), + publicKey: Nacl.util.decodeBase64(b64Keys.p) + }; + ServerCommand(keys, { + command: 'SSO_AUTH_CB', + url: window.location.href + }, function (err, data) { + delete localStorage.CP_sso_auth; + if (data && data.state) { window.location.href = '/register/'; } + }); + }; + Exports.ssoLogin = function () { + + }; + var allocateBytes = Exports.allocateBytes = function (bytes) { var dispense = Cred.dispenser(bytes); diff --git a/customize.dist/pages/login.js b/customize.dist/pages/login.js index a4b452ffc..c6774c222 100644 --- a/customize.dist/pages/login.js +++ b/customize.dist/pages/login.js @@ -38,6 +38,8 @@ define([ undefined: h('button#register.cp-secondary', Msg.login_register) ), + h('button.login', Msg.login_login), + h('br'), h('button.login', Msg.login_login) ]) ]), diff --git a/customize.dist/pages/register.js b/customize.dist/pages/register.js index 8a0859263..44e939e72 100644 --- a/customize.dist/pages/register.js +++ b/customize.dist/pages/register.js @@ -79,7 +79,8 @@ define([ UI.createCheckbox('import-recent', Msg.register_importRecent, true) ]), termsCheck, - h('button#register', Msg.login_register) + h('button#register', Msg.login_register), + Config.sso ? h('div.cp-register-sso') : undefined ]) ]), ]) diff --git a/lib/challenge-commands/sso.js b/lib/challenge-commands/sso.js new file mode 100644 index 000000000..b00e3a89e --- /dev/null +++ b/lib/challenge-commands/sso.js @@ -0,0 +1,105 @@ +const Util = require("../common-util"); +const Commands = module.exports; +const SSOUtils = require('../sso-utils'); + +const checkConfig = (Env) => { + return Env && Env.sso && Env.sso.enabled && Array.isArray(Env.sso.list) && Env.sso.list.length; +}; +const getProviderConfig = (Env, provider) => { + if (!checkConfig) { return; } + if (!provider) { return; } + const data = Env.sso.list.find((cfg) => { return cfg.name === provider; }); + return data; +}; + +const TYPES = SSOUtils.TYPES; + +const isValidConfig = (cfg) => { + if (!cfg) { return; } + if (!cfg.type) { return; } + const type = cfg.type.toLowerCase(); + const idp = TYPES[type]; + if (!idp) { return; } + return idp.checkConfig(cfg); +}; + +const auth = Commands.SSO_AUTH = function (Env, body, cb) { + if (!checkConfig(Env)) { return void cb('INVALID_SERVER_CONFIG'); } + const { provider } = body; + const cfg = getProviderConfig(Env, provider); + if (!cfg) { return void cb('UNRECOGNIZED_IDP'); } + if (!isValidConfig(cfg)) { return void cb('INVALID_IDP_CONFIG'); } + + cb(); +}; +auth.complete = function (Env, body, cb, req, res) { + // If we're here it means a valid provider was given. We can start the authentication process + const { provider, register, publicKey } = body; + const cfg = getProviderConfig(Env, provider); + const idp = TYPES[cfg.type.toLowerCase()]; + idp.auth(Env, cfg, (err, obj) => { + if (err) { return void cb(err); } // TODO log + + const { url, token } = obj; + + SSOUtils.writeRequest(Env, { + id: token, + type: idp.type, + provider: provider, + publicKey: publicKey, + register: Boolean(register) + }, (err) => { + if (err) { return void cb("E_REQ_WRITE"); } + + let value = `ssotoken="${token}"; SameSite=Strict; HttpOnly`; + res.setHeader('Set-Cookie', value); + cb(void 0, {url: url}); + }); + }); +}; + +const authCb = Commands.SSO_AUTH_CB = function (Env, body, cb, req) { + if (!checkConfig(Env)) { return void cb('INVALID_SERVER_CONFIG'); } + const { publicKey } = body; + const cookies = req.cookies; + const ssotoken = cookies.ssotoken; + if (!ssotoken) { return void cb('NO_COOKIE'); } + SSOUtils.readRequest(Env, ssotoken, (err, value) => { + if (err) { return void cb('ENOENT'); } + const data = Util.tryParse(value); + if (!data || !data.type || !TYPES[data.type]) { return void cb('INVALID_REQUEST_DATA'); } + console.log('stored', data.publicKey); + console.log('used', publicKey); + if (publicKey !== data.publicKey) { return void cb('WRONG_SIGNATURE_KEYS'); } + cb(); + }); +}; +authCb.complete = function (Env, body, cb, req) { + // If we're here it means a valid cookie was given. We can continue the authentication + const { url } = body; + const cookies = req.cookies; + const ssotoken = cookies.ssotoken; + if (!ssotoken) { return void cb('NO_COOKIE'); } + SSOUtils.readRequest(Env, ssotoken, (err, value) => { + SSOUtils.deleteRequest(Env, ssotoken); + if (err) { return void cb('ENOENT'); } + const data = Util.tryParse(value); + const cfg = getProviderConfig(Env, data.provider); + const idp = TYPES[data.type]; + idp.authCb(Env, cfg, ssotoken, url, (err, obj) => { + if (err) { return void cb(err); } + const {id, idpData} = obj; + console.log(id, idpData); + SSOUtils.makeUser(Env, id, (err, userData) => { + if (err) { return void cb(err); } + + // TODO + // makeTempSession() + + cb(void 0, { state: true }); + }); + + }); + }); +}; + diff --git a/lib/env.js b/lib/env.js index a567b39d4..68591e8ec 100644 --- a/lib/env.js +++ b/lib/env.js @@ -223,6 +223,8 @@ module.exports.create = function (config) { evictionReport: {}, commandTimers: {}, + sso: config.sso, + // initialized as undefined bearerSecret: void 0, curvePrivate: curve.secretKey, diff --git a/lib/http-commands.js b/lib/http-commands.js index 229c27e8b..2c19de250 100644 --- a/lib/http-commands.js +++ b/lib/http-commands.js @@ -73,6 +73,9 @@ COMMANDS.TOTP_REVOKE = TOTP.TOTP_REVOKE; COMMANDS.TOTP_WRITE_BLOCK = TOTP.TOTP_WRITE_BLOCK; COMMANDS.TOTP_REMOVE_BLOCK = TOTP.TOTP_REMOVE_BLOCK; +const SSO = require("./challenge-commands/sso.js"); +COMMANDS.SSO_AUTH = SSO.SSO_AUTH; +COMMANDS.SSO_AUTH_CB = SSO.SSO_AUTH_CB; var randomToken = () => Nacl.util.encodeBase64(Nacl.randomBytes(24)).replace(/\//g, '-'); @@ -141,7 +144,7 @@ var handleCommand = function (Env, req, res) { date: date, }); }); - }); + }, req); } catch (err) { Env.Log.error("CHALLENGE_COMMAND_THROWN_ERROR", { error: Util.serializeError(err), @@ -290,7 +293,7 @@ var handleResponse = function (Env, req, res) { }); } res.status(200).json(content); - }); + }, req, res); }); }; diff --git a/lib/http-worker.js b/lib/http-worker.js index 77cb6909b..8047cdb0d 100644 --- a/lib/http-worker.js +++ b/lib/http-worker.js @@ -10,6 +10,7 @@ const AuthCommands = require("./http-commands"); const JWT = require("jsonwebtoken"); const MFA = require("./storage/mfa"); const Sessions = require("./storage/sessions"); +const cookieParser = require("cookie-parser"); const DEFAULT_QUERY_TIMEOUT = 5000; const PID = process.pid; @@ -158,6 +159,7 @@ var setHeaders = function (req, res) { const Express = require("express"); var app = Express(); +app.use(cookieParser()); (function () { if (!Env.logFeedback) { return; } @@ -187,7 +189,6 @@ const wsProxy = createProxyMiddleware({ app.use('/cryptpad_websocket', wsProxy); - app.use('/blob', function (req, res, next) { /* Head requests are used to check the size of a blob. Clients can configure a maximum size to download automatically, @@ -484,6 +485,13 @@ var makeRouteCache = function (template, cacheName) { }; }; +const ssoList = Env.sso && Env.sso.enabled && Array.isArray(Env.sso.list) && + Env.sso.list.map(function (obj) { return obj.name; }); +const ssoCfg = ssoList.length ? { + force: (Env.sso && Env.sso.enforced && 1) || 0, + password: (Env.sso && Env.sso.cpPassword && 1) || 0, + list: ssoList +} : false; var serveConfig = makeRouteCache(function () { return [ 'define(function(){', @@ -510,6 +518,7 @@ var serveConfig = makeRouteCache(function () { shouldUpdateNode: Env.shouldUpdateNode || undefined, listMyInstance: Env.listMyInstance, accounts_api: Env.accounts_api, + sso: ssoCfg }, null, '\t'), '});' ].join(';\n'); diff --git a/lib/load-config.js b/lib/load-config.js index 49dbbf58a..a847f812e 100644 --- a/lib/load-config.js +++ b/lib/load-config.js @@ -42,5 +42,12 @@ if (!isPositiveNumber(config.premiumUploadSize) || config.premiumUploadSize < co delete config.premiumUploadSize; } +config.sso = {}; +try { + config.sso = require("../config/sso"); +} catch (e) { + console.log("SSO config not found"); +} + module.exports = config; diff --git a/lib/plugins/sso/oidc.js b/lib/plugins/sso/oidc.js new file mode 100644 index 000000000..a61ce0846 --- /dev/null +++ b/lib/plugins/sso/oidc.js @@ -0,0 +1,67 @@ +const OID = require('openid-client'); + +const TYPE = 'oidc'; + +const getClient = (cfg, cb) => { + OID.Issuer.discover(cfg.url).then((issuer) => { // XXX Only once for all users? + console.log('Discovered issuer %s %O', issuer.issuer); + const client = new issuer.Client({ + client_id: cfg.client_id, + client_secret: cfg.client_secret, + redirect_uris: ['http://localhost:3000/ssoauth'], // XXX Use httpUnsafeOrigin or... + response_types: ['code'], + }); + cb(void 0, client); + }, (err) => { + cb(err); + }); +}; +module.exports = { + type: TYPE, + checkConfig: (cfg) => { + return cfg.url && cfg.client_id && cfg.client_secret; + }, + auth: (Env, cfg, cb) => { + getClient(cfg, (err, client) => { + if (err) { return void cb ('E_OIDC_CONNECT'); } + + const generators = OID.generators; + const code_verifier = generators.codeVerifier(); + // store the code_verifier in your framework's session mechanism, if it is a cookie based solution + // it should be httpOnly (not readable by javascript) and encrypted. + + console.log(code_verifier); + const code_challenge = generators.codeChallenge(code_verifier); + const url = client.authorizationUrl({ + scope: 'openid email profile',// https://www.googleapis.com/auth/contacts.readonly', // https://www.google.com/m8/feeds + resource: 'http://localhost:3000/ssoauth/', + access_type: 'offline', + code_challenge, + code_challenge_method: 'S256', + }); + + cb(void 0, { url: url, token: code_verifier }); + }); + }, + authCb: (Env, cfg, token, url, cb) => { + getClient(cfg, (err, client) => { + if (err) { return void cb ('E_OIDC_CONNECT'); } + + const params = client.callbackParams(url); + client.callback('http://localhost:3000/ssoauth', params, { code_verifier: token }) + .then((tokenSet) => { + let j = tokenSet; + let c = tokenSet.claims(); + console.log(j, c); + cb(void 0, { + id: c.sub, + idpData: { + access_token: j.access_token, + refresh_token: j.refresh_token, + id_token: j.id_token + } + }); + }); + }); + } +}; diff --git a/lib/sso-utils.js b/lib/sso-utils.js new file mode 100644 index 000000000..1bad43d5d --- /dev/null +++ b/lib/sso-utils.js @@ -0,0 +1,71 @@ +const SSO = require("./storage/sso"); +const Nacl = require("tweetnacl/nacl-fast"); + +const SSOUtils = module.exports; + +// XXX const SAML = require('node-saml'); // https://www.npmjs.com/package/node-saml +SSOUtils.TYPES = { + oidc: require('./plugins/sso/oidc') +}; + + +SSOUtils.deleteRequest = (Env, id) => { + SSO.request.delete(Env, id, (err) => { + if (!err) { return; } + console.log(`Failed to delete SSO request ${id}`); + // XXX log? + }); +}; +SSOUtils.readRequest = (Env, id, cb) => { + SSO.request.read(Env, id, cb); +}; +SSOUtils.writeRequest = (Env, data, cb) => { + if (!data || !data.id || !data.type) { return void cb ('INVALID_REQUEST'); } + const id = data.id; + const value = { + type: data.type, + register: data.register, + provider: data.provider, + publicKey: data.publicKey, + time: +new Date() + }; + + SSO.request.write(Env, id, JSON.stringify(value), cb); +}; + + +SSOUtils.makeUser = (Env, id, cb) => { + const seed = Nacl.util.encodeBase64(Nacl.util.randomBytes(24)); + SSO.User.write(Env, id, JSON.stringify({ + seed: seed, + password: false // XXX maybe we don't need that flag, we can check if the block exists later + }), (err) => { + if (err) { return void cb(err); } + cb(void 0, { seed }); + }); + +}; + +SSOUtils.makeSession = (Env, publicKey, provider, ssoData, cb) => { + const sessionId = Sessions.randomId(); + // XXX If we already have an OTP session, recover it + Sessions.write(Env, publicKey, sessionId, JSON.stringify({ + sso: { + provider: provider, + data: ssoData + } + }), function (err) { + if (err) { + Env.Log.error("SSO_SESSION_WRITE", { + error: Util.serializeError(err), + publicKey: publicKey, + sessionId: sessionId, + }); + return void cb("SESSION_WRITE_ERROR"); + } + cb(void 0, { + bearer: sessionId, + }); + }); + +}; diff --git a/lib/storage/sso.js b/lib/storage/sso.js new file mode 100644 index 000000000..65110ee30 --- /dev/null +++ b/lib/storage/sso.js @@ -0,0 +1,57 @@ +const Basic = require("./basic"); +const Path = require("node:path"); +const Util = require("../common-util"); + +const SSO = module.exports; +/* This module manages storage related to Single Sign-On (SSO) settings. + +A first part (sso-requests) contains temporary files for sso authentication with a remote service +and a second part (sso users) is a database of accounts registered via SSO. + +The path for requests is based on the "authentication request" token depending on the type of SSO. +The path for the user database is based on their persistent identifier (id) from the SSO. +*/ + +var pathFromId = function (Env, id, subPath) { + if (!id || typeof(id) !== 'string') { return; } + id = Util.escapeKeyCharacters(id); + return Path.join(Env.paths.base, subPath, id.slice(0, 2), `${id}.json`); +}; +var reqPathFromId = function (Env, id) { + return pathFromId(Env, id, 'sso_request'); +}; +var userPathFromId = function (Env, id) { + return pathFromId(Env, id, 'sso'); +}; + +const Req = SSO.request = {}; + +Req.read = function (Env, id, cb) { + var path = reqPathFromId(Env, id); + Basic.read(Env, path, cb); +}; +Req.write = function (Env, id, data, cb) { + var path = reqPathFromId(Env, id); + console.log(path); + Basic.write(Env, path, data, cb); +}; +Req.delete = function (Env, id, cb) { + var path = reqPathFromId(Env, id); + Basic.delete(Env, path, cb); +}; + + +const User = SSO.user = {}; + +User.read = function (Env, id, cb) { + var path = userPathFromId(Env, id); + Basic.read(Env, path, cb); +}; +User.write = function (Env, id, data, cb) { + var path = userPathFromId(Env, id); + Basic.write(Env, path, data, cb); +}; +User.delete = function (Env, id, cb) { + var path = userPathFromId(Env, id); + Basic.delete(Env, path, cb); +}; diff --git a/package-lock.json b/package-lock.json index d97d0d112..e6b12b5bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@mcrowe/minibloom": "^0.2.0", "chainpad-crypto": "^0.2.5", "chainpad-server": "^5.1.0", + "cookie-parser": "^1.4.6", "express": "~4.18.2", "fs-extra": "^7.0.0", "get-folder-size": "^2.0.1", @@ -20,6 +21,7 @@ "netflux-websocket": "^0.1.20", "notp": "^2.0.3", "nthen": "0.1.8", + "openid-client": "^5.4.2", "prompt-confirm": "^2.0.4", "pull-stream": "^3.6.1", "saferphore": "0.0.1", @@ -1011,6 +1013,26 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", + "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", + "dependencies": { + "cookie": "0.4.1", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -1749,25 +1771,15 @@ } }, "node_modules/glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "dependencies": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - } - }, - "node_modules/glob-parent/node_modules/is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.0" + "is-glob": "^4.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">= 6" } }, "node_modules/glob-to-regexp": { @@ -2220,6 +2232,14 @@ "node": ">=0.10.0" } }, + "node_modules/jose": { + "version": "4.14.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz", + "integrity": "sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-yaml": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", @@ -2727,6 +2747,14 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", @@ -2758,6 +2786,14 @@ "node": ">=0.10.0" } }, + "node_modules/oidc-token-hash": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", + "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==", + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -2778,6 +2814,20 @@ "wrappy": "1" } }, + "node_modules/openid-client": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.4.2.tgz", + "integrity": "sha512-lIhsdPvJ2RneBm3nGBBhQchpe3Uka//xf7WPHTIglery8gnckvW7Bd9IaQzekzXJvWthCMyi/xVEyGW0RFPytw==", + "dependencies": { + "jose": "^4.14.1", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", @@ -2808,12 +2858,6 @@ "node": ">=0.10.0" } }, - "node_modules/path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", - "dev": true - }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -3039,17 +3083,6 @@ "node": ">=0.10.0" } }, - "node_modules/prompt-choices/node_modules/set-value": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-3.0.3.tgz", - "integrity": "sha512-Xsn/XSatoVOGBbp5hs3UylFDs5Bi9i+ArpVJKdHPniZHoEgRniXTqHWrWrGQ0PbEClVT6WtfnBwR8CAHC9sveg==", - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=6.0" - } - }, "node_modules/prompt-choices/node_modules/shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -3391,39 +3424,19 @@ } }, "node_modules/set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "dev": true, + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-4.0.1.tgz", + "integrity": "sha512-ayATicCYPVnlNpFmjq2/VmVwhoCQA9+13j8qWp044fmFE3IFphosPtRM+0CJ5xoIx5Uy52fCcwg3XeH2pHbbPQ==", + "funding": [ + "https://github.com/sponsors/jonschlinkert", + "https://paypal.me/jonathanschlinkert", + "https://jonschlinkert.dev/sponsor" + ], "dependencies": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" + "is-plain-object": "^2.0.4" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/set-value/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/set-value/node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true, - "engines": { - "node": ">=0.10.0" + "node": ">=11.0" } }, "node_modules/setprototypeof": { @@ -4738,7 +4751,7 @@ "get-value": "^2.0.6", "has-value": "^1.0.0", "isobject": "^3.0.1", - "set-value": "^2.0.0", + "set-value": "4.0.1", "to-object-path": "^0.3.0", "union-value": "^1.0.0", "unset-value": "^1.0.0" @@ -4980,6 +4993,22 @@ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" }, + "cookie-parser": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", + "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", + "requires": { + "cookie": "0.4.1", + "cookie-signature": "1.0.6" + }, + "dependencies": { + "cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" + } + } + }, "cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -5387,7 +5416,7 @@ "requires": { "@mrmlnc/readdir-enhanced": "^2.2.1", "@nodelib/fs.stat": "^1.1.2", - "glob-parent": "^3.1.0", + "glob-parent": "5.1.2", "is-glob": "^4.0.0", "merge2": "^1.2.3", "micromatch": "^3.1.10" @@ -5556,24 +5585,12 @@ } }, "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } + "is-glob": "^4.0.1" } }, "glob-to-regexp": { @@ -5907,6 +5924,11 @@ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" }, + "jose": { + "version": "4.14.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz", + "integrity": "sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g==" + }, "js-yaml": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", @@ -6309,6 +6331,11 @@ } } }, + "object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==" + }, "object-inspect": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", @@ -6331,6 +6358,11 @@ "isobject": "^3.0.1" } }, + "oidc-token-hash": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", + "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==" + }, "on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -6348,6 +6380,17 @@ "wrappy": "1" } }, + "openid-client": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.4.2.tgz", + "integrity": "sha512-lIhsdPvJ2RneBm3nGBBhQchpe3Uka//xf7WPHTIglery8gnckvW7Bd9IaQzekzXJvWthCMyi/xVEyGW0RFPytw==", + "requires": { + "jose": "^4.14.1", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + } + }, "parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", @@ -6369,12 +6412,6 @@ "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", "dev": true }, - "path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", - "dev": true - }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -6528,7 +6565,7 @@ "log-utils": "^0.2.1", "pointer-symbol": "^1.0.0", "radio-symbol": "^2.0.0", - "set-value": "^3.0.0", + "set-value": "4.0.1", "strip-color": "^0.1.0", "terminal-paginator": "^2.0.2", "toggle-array": "^1.0.1" @@ -6549,14 +6586,6 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-6.0.0.tgz", "integrity": "sha512-Wu1VHeILBK8KAWJUAiSZQX94GmOE45Rg6/538fKwiloUu21KncEkYGPqob2oSZ5mUT73vLGrHQjKw3KMPwfDzg==" }, - "set-value": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-3.0.3.tgz", - "integrity": "sha512-Xsn/XSatoVOGBbp5hs3UylFDs5Bi9i+ArpVJKdHPniZHoEgRniXTqHWrWrGQ0PbEClVT6WtfnBwR8CAHC9sveg==", - "requires": { - "is-plain-object": "^2.0.4" - } - }, "shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -6836,32 +6865,11 @@ } }, "set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "dev": true, + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-4.0.1.tgz", + "integrity": "sha512-ayATicCYPVnlNpFmjq2/VmVwhoCQA9+13j8qWp044fmFE3IFphosPtRM+0CJ5xoIx5Uy52fCcwg3XeH2pHbbPQ==", "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - } + "is-plain-object": "^2.0.4" } }, "setprototypeof": { @@ -7351,7 +7359,7 @@ "arr-union": "^3.1.0", "get-value": "^2.0.6", "is-extendable": "^0.1.1", - "set-value": "^2.0.1" + "set-value": "4.0.1" }, "dependencies": { "is-extendable": { diff --git a/package.json b/package.json index 5546c1d1c..cda1e4183 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@mcrowe/minibloom": "^0.2.0", "chainpad-crypto": "^0.2.5", "chainpad-server": "^5.1.0", + "cookie-parser": "^1.4.6", "express": "~4.18.2", "fs-extra": "^7.0.0", "get-folder-size": "^2.0.1", @@ -23,6 +24,7 @@ "netflux-websocket": "^0.1.20", "notp": "^2.0.3", "nthen": "0.1.8", + "openid-client": "^5.4.2", "prompt-confirm": "^2.0.4", "pull-stream": "^3.6.1", "saferphore": "0.0.1", diff --git a/www/register/main.js b/www/register/main.js index d772c770f..3900fc855 100644 --- a/www/register/main.js +++ b/www/register/main.js @@ -1,4 +1,5 @@ define([ + '/api/config', 'jquery', '/customize/login.js', '/common/cryptpad-common.js', @@ -14,7 +15,7 @@ define([ '/customize/pages.js', 'css!/bower_components/components-font-awesome/css/font-awesome.min.css', -], function ($, Login, Cryptpad, /*Test,*/ Cred, UI, Util, Realtime, Constants, Feedback, LocalStore, h, Pages) { +], function (Config, $, Login, Cryptpad, /*Test,*/ Cred, UI, Util, Realtime, Constants, Feedback, LocalStore, h, Pages) { if (window.top !== window) { return; } var Messages = Cryptpad.Messages; $(function () { @@ -50,6 +51,30 @@ define([ var br = function () { return h('br'); }; Messages.register_nameTooLong = "Usernames must be shorter than {0} characters"; // XXX + if (Config.sso) { + // TODO + // Config.sso.force => no legacy login allowed + // Config.sso.password => cp password required or forbidden + // Config.sso.list => list of configured identity providers + var $sso = $('div.cp-register-sso'); + var list = Config.sso.list.map(function (name) { + var b = h('button.btn.btn-secondary', name); + var $b = $(b).click(function () { + console.log('sso register click:', name); + $b.prop('disabled', 'disabled'); + Login.ssoRegister(name, function (err, data) { + console.error(err, data); + if (data.url) { + window.location.href = data.url; + } + }); + // XXX Server command + }); + return b; + }); + $sso.append(list); + } + var registerClick = function () { var uname = $uname.val().trim(); // trim whitespace surrounding the username since it is otherwise included in key derivation diff --git a/www/ssoauth/index.html b/www/ssoauth/index.html new file mode 100644 index 000000000..a7bdb9b14 --- /dev/null +++ b/www/ssoauth/index.html @@ -0,0 +1,14 @@ + + + + + CryptPad: Collaboration suite, encrypted and open-source + + + + + + + + + diff --git a/www/ssoauth/main.js b/www/ssoauth/main.js new file mode 100644 index 000000000..c556e2814 --- /dev/null +++ b/www/ssoauth/main.js @@ -0,0 +1,3 @@ +define(['/customize/login.js'], function (Login) { + Login.ssoRegisterCb(); +});