Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support SSO with SAML #138

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ const path = require('path');
const sass_middleware = require('node-sass-middleware');
const session = require('cookie-session');

const passport = require('passport');
const saml = require('passport-saml');

// Obtain secret from config file
const config = require('./config.js');

Expand All @@ -24,6 +27,30 @@ const api = require('./routes/api/index');
const oauth2 = require('./routes/oauth2/oauth2');
const saml2 = require('./routes/saml2/saml2');

passport.serializeUser(function(user, done) {
done(null, user);
});
passport.deserializeUser(function(user, done) {
done(null, user);
});

const saml_strategy = new saml.Strategy(
{
// config options here
callbackUrl: '/auth/sso/callback', // eslint-disable-line snakecase/snakecase
entryPoint: config.external_user_sso.entry_point, // eslint-disable-line snakecase/snakecase
issuer: config.external_user_sso.issuer, // eslint-disable-line snakecase/snakecase
identifierFormat: null, // eslint-disable-line snakecase/snakecase
validateInResponseTo: false, // eslint-disable-line snakecase/snakecase
disableRequestedAuthnContext: true, // eslint-disable-line snakecase/snakecase
},
function(profile, done) {
return done(null, profile);
}
);

passport.use('samlStrategy', saml_strategy);

const app = express();

// view engine setup
Expand Down Expand Up @@ -99,6 +126,9 @@ app.use(
})
);

app.use(passport.initialize());
app.use(passport.session());

// Helpers dinamicos:
app.use(function(req, res, next) {
res.set(
Expand Down
8 changes: 8 additions & 0 deletions config.js.template
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,14 @@ config.external_auth = {
}
}

// External user authentication with SAML Profile.
// SAML Profile must include field username and email.
config.external_user_sso = {
enabled: to_boolean(process.env.IDM_EX_AUTH_SSO_ENABLED, false),
entry_point: (process.env.IDM_EX_AUTH_SSO_HOST || 'https://keycloak'),
issuer: (process.env.IDM_EX_AUTH_SSO_ISSUER || 'keyrock')
}

// Email configuration
config.mail = {
transport: (process.env.IDM_EMAIL_TRANSPORT || 'smtp'),
Expand Down
1 change: 1 addition & 0 deletions controllers/web/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ module.exports = {
manage_members: require('../../controllers/web/manage_members'),
settings: require('../../controllers/web/settings'),
sessions: require('../../controllers/web/sessions'),
sso: require('../../controllers/web/sso'),
};
8 changes: 6 additions & 2 deletions controllers/web/sessions.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const Sequelize = require('sequelize');
const Op = Sequelize.Op;

const escape_paths = require('../../etc/escape_paths/paths.json').paths;
const config = require('../../config');

// MW to authorized restricted http accesses
exports.login_required = function(req, res, next) {
Expand Down Expand Up @@ -77,7 +78,11 @@ exports.new = function(req, res) {
res.locals.message = req.session.message;
delete req.session.message;
}
res.render('index', { errors, csrf_token: req.csrfToken() });
res.render('index', {
errors,
csrf_token: req.csrfToken(),
sso_enabled: config.external_user_sso.enabled,
});
};

// POST /auth/login -- Create Session
Expand Down Expand Up @@ -134,7 +139,6 @@ exports.create = function(req, res) {
if (user.admin) {
req.session.user.admin = user.admin;
}

res.redirect('/idm');
});
} else {
Expand Down
6 changes: 5 additions & 1 deletion controllers/web/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ const email_list = config.email_list_type
exports.settings = function(req, res) {
debug('--> settings');

res.render('settings/settings', { csrf_token: req.csrfToken() });
//res.render('settings/settings', { csrf_token: req.csrfToken() });
res.render('settings/settings', {
csrf_token: req.csrfToken(),
sso_enabled: config.external_user_sso.enabled,
});
};

// POST /idm/settings/password -- Change password
Expand Down
176 changes: 176 additions & 0 deletions controllers/web/sso.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
const models = require('../../models/models.js');
const config = require('../../config');
const gravatar = require('gravatar');
const util = require('util');

const debug = require('debug')('idm:web-user_controller');

const email = require('../../lib/email.js');

// Create new user by email & username in SAML profile
function create_user_from_saml(req, res, callback) {
debug('--> create user from saml');

if (!(typeof req.user.email !== 'undefined' && req.user.email)) {
debug('---> SAML Profile: email must not empty');
req.session.errors = [{ message: 'invalid' }];
return res.redirect('/auth/login');
}

if (!(typeof req.user.username !== 'undefined' && req.user.username)) {
debug('---> SAML Profile: username must not empty');
req.session.errors = [{ message: 'invalid' }];
return res.redirect('/auth/login');
}

// Build a row and validate it
const user = models.user.build({
username: req.user.username,
email: req.user.email,
password: 'test',
date_password: new Date(new Date().getTime()),
enabled: true,
});

user
.validate()
.then(function() {
debug('---> user is valid');
// Save the row in the database
user.save().then(function() {
const activation_key = Math.random()
.toString(36)
.substr(2);
const activation_expires = new Date(
new Date().getTime() + 1000 * 3600 * 24
);

models.user_registration_profile
.findOrCreate({
defaults: {
user_email: user.email,
activation_key,
activation_expires,
},
where: { user_email: user.email },
})
.then(function() {
// Send an email to the user
const link =
config.host +
'/activate?activation_key=' +
activation_key +
'&email=' +
encodeURIComponent(user.email); // eslint-disable-line snakecase/snakecase

const mail_data = {
name: user.username,
link,
};

const translation = req.app.locals.translation;

// Send an email message to the user
email.send('activate', '', user.email, mail_data, translation);
callback(req, res);
});
});
})
.catch(function(error) {
// print the error details
debug('users is invalid: ' + error);
req.session.errors = [{ message: 'invalid' }];
res.redirect('/auth/login');
});
return undefined;
}

function find_or_create_user_from_saml(req, res) {
debug('--> find_or_create_user_from_saml');
debug(
'--> SAML Prifole: ' +
util.inspect(req.user, { showHidden: false, depth: null }) // eslint-disable-line snakecase/snakecase
);

if (!(typeof req.user.email !== 'undefined' && req.user.email)) {
debug('---> SAML Profile: email must not empty');
req.session.errors = [{ message: 'invalid' }];
return res.redirect('/auth/login');
}

models.user
.find({
attributes: [
'id',
'username',
'salt',
'password',
'enabled',
'email',
'gravatar',
'image',
'admin',
'date_password',
'starters_tour_ended',
],
where: {
email: req.user.email,
},
})
.then(function(user) {
if (user) {
if (user.enabled === false) {
debug('---> user is not enabled');
req.session.errors = [{ message: 'user_not_found' }];
res.redirect('/auth/login');
}

// Create req.session.user and save id and username
// The session is defined by the existence of: req.session.user

let image = '/img/logos/small/user.png';

if (user.gravatar) {
image = gravatar.url(
user.email,
{ s: 100, r: 'g', d: 'mm' },
{ protocol: 'https' }
);
} else if (user.image === 'default') {
image = '/img/logos/original/user.png';
} else {
image = '/img/users/' + user.image;
}

// Create session
req.session.user = {
id: user.id,
username: user.username,
email: user.email,
image,
change_password: user.date_password,
starters_tour_ended: user.starters_tour_ended,
};

// If user is admin add parameter to session
if (user.admin) {
req.session.user.admin = user.admin;
}

res.redirect('/idm');
} else {
debug('---> user not found & create new user');
create_user_from_saml(req, res, find_or_create_user_from_saml);
}
})
.catch(function(error) {
debug('---> user is not found: ' + error);
req.session.errors = [{ message: 'user_not_found' }];
res.redirect('/auth/login');
});
return undefined;
}

exports.load_user_by_email = function(req, res) {
find_or_create_user_from_saml(req, res);
};
4 changes: 2 additions & 2 deletions controllers/web/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ exports.edit = function(req, res) {
.on('error', function(e) {
debug('Failed connecting to gravatar: ' + e);
res.render('users/edit', {
identity_attributes,
user: req.user,
error: [],
csrf_token: req.csrfToken(),
Expand All @@ -307,6 +308,7 @@ exports.edit = function(req, res) {
{ protocol: 'https' }
);
res.render('users/edit', {
identity_attributes,
user: req.user,
error: [],
csrf_token: req.csrfToken(),
Expand Down Expand Up @@ -643,7 +645,6 @@ exports.create = function(req, res) {
const activation_expires = new Date(
new Date().getTime() + 1000 * 3600 * 24
);

models.user_registration_profile
.findOrCreate({
defaults: {
Expand Down Expand Up @@ -698,7 +699,6 @@ exports.create = function(req, res) {
debug('Failed connecting to gravatar: ' + e);
});
}

// Send an email to the user
const link =
config.host +
Expand Down
54 changes: 54 additions & 0 deletions doc/installation_and_administration_guide/configuration.md
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ specific needs of each use case. These are the main configurations:

- External authentication.

- External Authentication with SAML.

- Authorization.

- Mail Server.
Expand Down Expand Up @@ -350,6 +352,58 @@ config.external_auth = {
The way to check password validity can be customized in with parameter
_external_auth.encryption_. SHA1 and BCrypt are currently supported.

## External Authentication with SAML

You can also configure the Identity Manager to authenticate users through an
external user in identity provider(idp).

When using this option, after the user correclty authenticates using his/her
remote credentials, a local copy of the user is created. For authenticating the
user externally Keyrock needs to read a set of user attributes from the SAML
profile. These SAML profile are:

- username: the display name of the user.

- email: the email address is the value used for authenticating the user.

For keycloak configuration(v4.8.3 Final), you create SAML client, and config

- Valid Redirect URIs to keyrock server.
- Assertion Consumer Service POST Binding URL to keyrock server.
- IDP Initiated SSO URL Name to create SAML entry point (URL). then config
mapper in keycloak for SAML profile.

An example of this configuration is:

```javascript
config.external_user_sso = {
enabled: true,
entry_point: 'https://{{keycloak-server}}/auth/realms/smartcity/protocol/saml/clients/keyrock'),
issuer: 'keyrock')
}
```

An example of keycloak configuration is:

**client configuration**

```
- Valid Redirect URIs: https://{{keyrock-server}}:3005.
- Assertion Consumer Service POST Binding URL: https://{{keyrock-server}}:3005.
- IDP Initiated SSO URL Name: keyrock.
(You will got Target IDP initiated SSO URL: https://{{keyrock-server}}/auth/realms/smartcity/protocol/saml/clients/keyrock)
```

**mapper configuration**

```
- username: the display name of the user.
(For Keycloak Mapper, Name: username, Type: User Property, Property: username)

- email: the email address is the value used for authenticating the user.
(For Keycloak Mapper, Name: email, Type: User Property, Property: email)
```

## Authorization

Configure Policy Decision Point (PDP)
Expand Down
Loading