SSO: prototype improvements

This commit is contained in:
yflory 2023-06-29 12:32:45 +02:00
parent b93b5eae4e
commit d6bf625733
6 changed files with 57 additions and 96 deletions

View File

@ -76,12 +76,13 @@ authCb.complete = function (Env, body, cb, req) {
const cfg = getProviderConfig(Env, data.provider);
const idp = TYPES[data.type];
const register = data.register;
const provider = data.provider;
idp.authCb(Env, cfg, ssotoken, url, (err, obj) => {
if (err) { return void cb(err); }
const {id, idpData, name} = obj;
let next = (user, isRegister) => {
SSOUtils.createJWT(Env, id, data.provider, idpData, (err, jwt) => {
SSOUtils.createJWT(Env, id, provider, idpData, (err, jwt) => {
if (err) { return void cb(err); }
cb(void 0, {
jwt: jwt,
@ -94,19 +95,22 @@ authCb.complete = function (Env, body, cb, req) {
};
if (register) {
SSOUtils.writeUser(Env, id, (err, userData) => {
SSOUtils.writeUser(Env, provider, id, (err, userData) => {
if (err && err.code === 'EEXIST') {
return void SSOUtils.readUser(Env, id, (err, userData) => {
return void SSOUtils.readUser(Env, provider, id, (err, userData) => {
if (err) { return void cb(err); }
next(userData, false);
next(userData, !userData.complete);
});
}
if (err) { return void cb(err); }
next(userData, true);
});
} else {
SSOUtils.readUser(Env, id, (err, userData) => {
SSOUtils.readUser(Env, provider, id, (err, userData) => {
if (err) { return void cb(err); }
if (!userData || !userData.complete) {
return void cb('NO_USER');
}
next(userData, false);
});
}
@ -148,8 +152,8 @@ register.complete = function (Env, body, cb) {
payload = _payload;
}));
}).nThen((w) => {
const { sub } = payload;
SSOUtils.readUser(Env, sub, w((err, user) => {
const { sub, provider } = payload;
SSOUtils.readUser(Env, provider, sub, w((err, user) => {
if (err) {
w.abort();
console.log(err, sub);
@ -170,10 +174,10 @@ register.complete = function (Env, body, cb) {
if (err) { w.abort(); }
}));
}).nThen((w) => {
if (!pw) { return; }
const { sub } = payload;
ssoUser.password = true;
SSOUtils.updateUser(Env, sub, ssoUser, w());
const { sub, provider } = payload;
ssoUser.password = Boolean(pw);
ssoUser.complete = true;
SSOUtils.updateUser(Env, provider, sub, ssoUser, w());
}).nThen(() => {
const { data, provider } = payload;
SSOUtils.makeSession(Env, publicKey, provider, data, cb);
@ -206,8 +210,8 @@ login.complete = function (Env, body, cb) {
payload = _payload;
}));
}).nThen((w) => {
const { sub } = payload;
SSOUtils.readUser(Env, sub, w((err) => {
const { sub, provider } = payload;
SSOUtils.readUser(Env, provider, sub, w((err) => {
if (err) {
w.abort();
console.log(err, sub);

View File

@ -377,7 +377,7 @@ app.use('/block/', function (req, res, next) {
if (mfa_params && !content.mfa) { return void no(); }
if (sso_params && !content.sso) { return void no(); }
if (content.mfa && content.mfa.exp && ((+new Date()) > content.mfa.exp)) {
if (content.mfa && content.mfa.exp && (+new Date()) > content.mfa.exp) {
Log.error("OTP_SESSION_EXPIRED", content.mfa);
// XXX Only delete the mfa part
Sessions.delete(Env, name, token, function (err) {
@ -391,41 +391,19 @@ app.use('/block/', function (req, res, next) {
}
if (content.sso) {
SSOUtils.checkSession(Env, content.sso, (err, state, newState) => {
if (err || !state) {
// XXX Only delete the mfa part
Sessions.delete(Env, name, token, function (err) {
if (err) {
Log.error('SSO_SESSION_DELETE_EXPIRED_ERROR', err);
return;
}
Log.info('SSO_SESSION_DELETE_EXPIRED', err);
});
return void no();
if (content.sso && content.sso.exp && (+new Date()) > content.sso.exp) {
Log.error("SSO_SESSION_EXPIRED", content.sso);
Sessions.delete(Env, name, token, function (err) {
if (err) {
Log.error('SSO_SESSION_DELETE_EXPIRED_ERROR', err);
return;
}
if (newState) {
content.sso = newState;
// XXX We should produce a new session ID here and send it back to the user
Sessions.update(Env, name, token, JSON.stringify(content), (err) => {
if (err) {
Log.error('SSO_SESSION_UPDATE_ERROR', err);
return;
}
Log.info('SSO_SESSION_UPDATED', err);
});
}
next();
Log.info('SSO_SESSION_DELETE_EXPIRED', err);
});
return;
return void no();
}
// we could also check whether the content of the file matches the token,
// but clients don't have any influence over the reference and can only
// request to create tokens that are scoped to a public key they control.
// I don' think there's any practical benefit to such a check.
// So, interpret the existence of a file in that location as the continued
// Interpret the existence of a file in that location as the continued
// validity of the session. Fall through and let the built-in webserver
// handle the 404 or serving the file.
next();

View File

@ -4,7 +4,6 @@ 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,
@ -55,30 +54,18 @@ module.exports = {
expires_at: j.expires_at,
access_token: j.access_token,
refresh_token: j.refresh_token,
id_token: j.id_token
//id_token: j.id_token // XXX no need to store id_token?
}
});
});
});
},
checkSession: (Env, cfg, data, cb) => {
const { refresh_token } = data;
/*
getData: (Env, cfg, data, cb) => {
// data = { refresh_token, access_token, expires_at }
let t = new OID.TokenSet(data);
if (!t.expired()) { return void cb(void 0, true); }
getClient(cfg, (err, client) => {
client.refresh(t.refresh_token).then((j) => {
const newData = {
refresh_token: refresh_token,
id_token: j.id_token,
access_token: j.access_token,
expires_at: j.expires_at
};
cb(void 0, true, newData);
}, () => {
// Error: can't renew token
cb(void 0, false);
});
});
// TODO get userinfo using access_token
// use refresh_token if access expired
}
*/
};

View File

@ -6,6 +6,8 @@ const Util = require("./common-util");
const SSOUtils = module.exports;
const SESSION_EXPIRATIION = 12 * 3600 * 1000; // XXX Hours? Days? Weeks? Configurable?
// XXX const SAML = require('node-saml'); // https://www.npmjs.com/package/node-saml
const TYPES = SSOUtils.TYPES = {
oidc: require('./plugins/sso/oidc')
@ -29,12 +31,6 @@ SSOUtils.isValidConfig = (cfg) => {
return idp.checkConfig(cfg);
};
SSOUtils.checkSession = (Env, sessionData, cb) => {
const cfg = getProviderConfig(Env, sessionData.provider);
const idp = TYPES[cfg.type.toLowerCase()];
idp.checkSession(Env, cfg, sessionData.data, cb);
};
SSOUtils.deleteRequest = (Env, id) => {
SSO.request.delete(Env, id, (err) => {
if (!err) { return; }
@ -60,9 +56,9 @@ SSOUtils.writeRequest = (Env, data, cb) => {
};
SSOUtils.writeUser = (Env, id, cb) => {
SSOUtils.writeUser = (Env, provider, id, cb) => {
const seed = Nacl.util.encodeBase64(Nacl.randomBytes(24));
SSO.user.write(Env, id, JSON.stringify({
SSO.user.write(Env, provider, id, JSON.stringify({
seed: seed,
password: false
}), (err) => {
@ -70,15 +66,15 @@ SSOUtils.writeUser = (Env, id, cb) => {
cb(void 0, { seed });
});
};
SSOUtils.readUser = (Env, id, cb) => {
SSO.user.read(Env, id, (err, user) => {
SSOUtils.readUser = (Env, provider, id, cb) => {
SSO.user.read(Env, provider, id, (err, user) => {
if (err) { return void cb(err); }
cb(void 0, Util.tryParse(user));
});
};
SSOUtils.updateUser = (Env, id, data, cb) => {
SSO.user.delete(Env, id, () => {
SSO.user.write(Env, id, JSON.stringify(data), (err) => {
SSOUtils.updateUser = (Env, provider, id, data, cb) => {
SSO.user.delete(Env, provider, id, () => {
SSO.user.write(Env, provider, id, JSON.stringify(data), (err) => {
if (err) { return void cb(err); }
cb();
});
@ -146,6 +142,7 @@ SSOUtils.makeSession = (Env, publicKey, provider, ssoData, cb) => {
// XXX If we already have an OTP session, recover it
Sessions.write(Env, publicKey, sessionId, JSON.stringify({
sso: {
exp: +new Date() + SESSION_EXPIRATIION,
provider: provider,
data: ssoData
}

View File

@ -42,14 +42,6 @@ Sessions.delete = function (Env, id, ref, cb) {
Basic.delete(Env, path, cb);
};
Sessions.update = function (Env, id, ref, data, cb) {
var path = pathFromId(Env, id, ref);
Basic.delete(Env, path, (err) => {
if (err) { return void cb(err); }
Basic.write(Env, path, data, cb);
});
};
Sessions.deleteUser = function (Env, id, cb) {
if (!id || typeof(id) !== 'string') { return; }
id = Util.escapeKeyCharacters(id);

View File

@ -21,13 +21,17 @@ var pathFromId = function (Env, id, subPath) {
var reqPathFromId = function (Env, id) {
return pathFromId(Env, id, 'sso_request');
};
var userPathFromId = function (Env, id) {
return pathFromId(Env, id, 'sso_user');
};
var blockPathFromId = function (Env, id) {
return pathFromId(Env, id, 'sso_block');
};
var userPathFromId = function (Env, id, provider) {
if (!id || typeof(id) !== 'string') { return; }
if (!provider || typeof(provider) !== 'string') { return; }
id = Util.escapeKeyCharacters(id);
return Path.join(Env.paths.base, 'sso_user', provider, id.slice(0, 2), `${id}.json`);
};
const Req = SSO.request = {};
Req.read = function (Env, id, cb) {
@ -36,7 +40,6 @@ Req.read = function (Env, id, 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) {
@ -47,16 +50,16 @@ Req.delete = function (Env, id, cb) {
const User = SSO.user = {};
User.read = function (Env, id, cb) {
var path = userPathFromId(Env, id);
User.read = function (Env, provider, id, cb) {
var path = userPathFromId(Env, id, provider);
Basic.read(Env, path, cb);
};
User.write = function (Env, id, data, cb) {
var path = userPathFromId(Env, id);
User.write = function (Env, provider, id, data, cb) {
var path = userPathFromId(Env, id, provider);
Basic.write(Env, path, data, cb);
};
User.delete = function (Env, id, cb) {
var path = userPathFromId(Env, id);
User.delete = function (Env, provider, id, cb) {
var path = userPathFromId(Env, id, provider);
Basic.delete(Env, path, cb);
};