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 ]);