diff --git a/dashboard/src/authcallback.html b/dashboard/src/authcallback.html new file mode 100644 index 000000000..9cb0617a6 --- /dev/null +++ b/dashboard/src/authcallback.html @@ -0,0 +1,11 @@ + diff --git a/dashboard/src/js/client.js b/dashboard/src/js/client.js index dc5645b5d..7137c5f80 100644 --- a/dashboard/src/js/client.js +++ b/dashboard/src/js/client.js @@ -2713,15 +2713,22 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout Client.prototype.login = function () { this.setToken(null); - window.location.href = '/login.html?returnTo=/' + encodeURIComponent(window.location.hash); + // start oidc flow + window.location.href = '/openid/auth?client_id=dashboard&scope=openid email profile&response_type=code token&redirect_uri=' + window.location.origin + '/authcallback.html'; + // window.location.href = '/login.html?returnTo=/' + encodeURIComponent(window.location.hash); }; Client.prototype.logout = function () { - var token = this.getToken(); - this.setToken(null); + var that = this; - // invalidates the token - window.location.href = client.apiOrigin + '/api/v1/cloudron/logout?access_token=' + token; + // destroy oidc session in the spirit of true SSO + del('/api/v1/oidc/sessions', null, function (error, data, status) { + if (error) console.error('Failed to logout from oidc session'); + + that.setToken(null); + + window.location.href = '/'; + }); }; Client.prototype.getAppEventLog = function (appId, page, perPage, callback) { diff --git a/src/oidc.js b/src/oidc.js index 1054b427d..4ce749d6c 100644 --- a/src/oidc.js +++ b/src/oidc.js @@ -31,6 +31,7 @@ const assert = require('assert'), jose = require('jose'), safe = require('safetydance'), settings = require('./settings.js'), + tokens = require('./tokens.js'), url = require('url'), users = require('./users.js'), util = require('util'); @@ -74,6 +75,16 @@ async function clientsAdd(id, data) { async function clientsGet(id) { assert.strictEqual(typeof id, 'string'); + if (id === 'dashboard') { + return { + id: 'dashboard', + secret: 'notused', + response_types: ['code', 'code token'], + grant_types: ['authorization_code', 'implicit'], + loginRedirectUri: settings.dashboardOrigin() + '/authcallback.html' + }; + } + const result = await database.query(`SELECT ${OIDC_CLIENTS_FIELDS} FROM ${OIDC_CLIENTS_TABLE_NAME} WHERE id = ?`, [ id ]); if (result.length === 0) return null; @@ -166,7 +177,6 @@ async function revokeByUserId(userId) { revokeObjects('Session'); revokeObjects('Grant'); revokeObjects('AuthorizationCode'); - revokeObjects('AccessToken'); } // ----------------------------- @@ -189,7 +199,9 @@ class CloudronAdapter { debug(`Creating OpenID storage adapter for ${name}`); - if (this.name !== 'Client') { + if (this.name === 'Client' || this.name === 'AccessToken') { + return; + } else { load(name); } } @@ -209,6 +221,17 @@ class CloudronAdapter { async upsert(id, payload, expiresIn) { if (this.name === 'Client') { debug('upsert: this should not happen as it is stored in our db'); + } else if (this.name === 'AccessToken') { + const clientId = payload.clientId; + const identifier = payload.accountId; + const expires = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS; + const accessToken = id; + + const [error] = await safe(tokens.add({ clientId, identifier, expires, accessToken })); + if (error) { + console.log('Error adding access token', error); + throw error; + } } else { DATA_STORE[this.name][id] = { id, expiresIn, payload, consumed: false }; save(this.name); @@ -240,6 +263,9 @@ class CloudronAdapter { tmp.client_secret = client.secret; tmp.id_token_signed_response_alg = client.tokenSignatureAlgorithm || 'RS256'; + if (client.response_types) tmp.response_types = client.response_types; + if (client.grant_types) tmp.grant_types = client.grant_types; + if (client.appId) { const [error, app] = await safe(apps.get(client.appId)); if (error || !app) { @@ -258,6 +284,20 @@ class CloudronAdapter { if (client.logoutRedirectUri) tmp.post_logout_redirect_uris = [ client.logoutRedirectUri ]; } + return tmp; + } else if (this.name === 'AccessToken') { + debug('find: we dont support finding AccessTokens', id); + const [error, result] = await safe(tokens.getByAccessToken(id)); + if (error || !result) { + debug(`find: Unknown accessToken for id ${id}`); + return null; + } + + const tmp = { + accountId: result.identifier, + clientId: result.clientId + }; + return tmp; } else { if (!DATA_STORE[this.name][id]) return null; @@ -292,7 +332,7 @@ class CloudronAdapter { * */ async findByUid(uid) { - if (this.name === 'Client') { + if (this.name === 'Client' || this.name === 'AccessToken') { debug('findByUid: this should not happen as it is stored in our db'); } else { for (let d in DATA_STORE[this.name]) { @@ -315,7 +355,7 @@ class CloudronAdapter { * */ async consume(id) { - if (this.name === 'Client') { + if (this.name === 'Client' || this.name === 'AccessToken') { debug('consume: this should not happen as it is stored in our db'); } else { if (DATA_STORE[this.name][id]) DATA_STORE[this.name][id].consumed = true; @@ -334,7 +374,7 @@ class CloudronAdapter { * */ async destroy(id) { - if (this.name === 'Client') { + if (this.name === 'Client' || this.name === 'AccessToken') { debug('destroy: this should not happen as it is stored in our db'); } else { delete DATA_STORE[this.name][id]; @@ -353,7 +393,7 @@ class CloudronAdapter { * */ async revokeByGrantId(grantId) { - if (this.name === 'Client') { + if (this.name === 'Client' || this.name === 'AccessToken') { debug('revokeByGrantId: this should not happen as it is stored in our db'); } else { for (let d in DATA_STORE[this.name]) { @@ -685,6 +725,12 @@ async function start() { postLogoutSuccessSource }, }, + responseTypes: [ + 'code', + 'id_token', 'id_token token', + 'code id_token', 'code token', 'code id_token token', + 'none', + ], // if a client only has one redirect uri specified, the client does not have to provide it in the request allowOmittingSingleRegisteredRedirectUri: true, clients: [], diff --git a/src/routes/oidc.js b/src/routes/oidc.js index 030f902fe..6cd794b71 100644 --- a/src/routes/oidc.js +++ b/src/routes/oidc.js @@ -9,6 +9,7 @@ exports = module.exports = { del }, + dashboardLoginCallback, destroyUserSession }; @@ -17,7 +18,8 @@ const assert = require('assert'), oidc = require('../oidc.js'), HttpError = require('connect-lastmile').HttpError, HttpSuccess = require('connect-lastmile').HttpSuccess, - safe = require('safetydance'); + safe = require('safetydance'), + tokens = require('../tokens.js'); async function add(req, res, next) { assert.strictEqual(typeof req.body, 'object'); @@ -109,11 +111,26 @@ async function del(req, res, next) { next(new HttpSuccess(204)); } +const tokens = require('../tokens.js'); + +async function dashboardLoginCallback(req, res, next) { + const [error, token] = await safe(tokens.add({ clientId: tokens.ID_WEBADMIN, identifier: req.user.id, expires: Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS })); + if (error) return next(new HttpError(500, error)); + + await eventlog.add(req.user.ghost ? eventlog.ACTION_USER_LOGIN_GHOST : eventlog.ACTION_USER_LOGIN, auditSource, { userId: req.user.id, user: users.removePrivateFields(req.user) }); + + if (!req.user.ghost) safe(users.notifyLoginLocation(req.user, ip, userAgent, auditSource), { debug }); + + next(new HttpSuccess(200, token)); +} + async function destroyUserSession(req, res, next) { assert.strictEqual(typeof req.user, 'object'); const [error] = await safe(oidc.revokeByUserId(req.user.id)); if (error) return next(BoxError.toHttpError(error)); + await safe(tokens.delByAccessToken(req.token)); + next(new HttpSuccess(204)); } diff --git a/src/server.js b/src/server.js index a0da5e54f..1d58d4fb0 100644 --- a/src/server.js +++ b/src/server.js @@ -372,6 +372,9 @@ async function initializeExpressSync() { // well known router.get ('/well-known-handler/*', routes.wellknown.get); + // dashboard login callback + router.get ('/api/v1/oidc/callback', routes.oidc.dashboardLoginCallback); + // OpenID connect clients router.get ('/api/v1/oidc/clients', token, authorizeAdmin, routes.oidc.clients.list); router.post('/api/v1/oidc/clients', json, token, authorizeAdmin, routes.oidc.clients.add); diff --git a/src/tokens.js b/src/tokens.js index f8bf7ac65..6b933f582 100644 --- a/src/tokens.js +++ b/src/tokens.js @@ -100,7 +100,7 @@ async function add(token) { if (error) throw error; const id = 'tid-' + uuid.v4(); - const accessToken = hat(8 * 32); + const accessToken = token.accessToken || hat(8 * 32); await database.query('INSERT INTO tokens (id, accessToken, identifier, clientId, expires, scopeJson, name) VALUES (?, ?, ?, ?, ?, ?, ?)', [ id, accessToken, identifier, clientId, expires, JSON.stringify(scope), name ]);