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

Add multiple role support #119

Merged
merged 21 commits into from
Aug 8, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,24 @@ Replace the "arn:aws:iam:role" value with the ARN of the role in AWS you
created. Replace the "arn:aws:iam:provider" value with the ARN of the identity
provider in AWS your created.


##### Multiple Role Support
To support multiple roles, add multiple values to the `https://aws.amazon.com/SAML/Attributes/Role`
attribute. For example:

```
arn:aws:iam:role1,arn:aws:iam:provider
arn:aws:iam:role2,arn:aws:iam:provider
arn:aws:iam:role3,arn:aws:iam:provider
```

*Special note for Okta users*: Multiple roles must be passed as multiple values to a single
attribute key. By default, Okta serializes multiple values into a single value using commas.
To support multiple roles, you must contact Okta support and request that the
`SAML_SUPPORT_ARRAY_ATTRIBUTES` feature flag be enabled on your Okta account. For more details
see [this post](https://devforum.okta.com/t/multivalued-attributes/179).


### 5. Run Awsaml and give it your application's metadata.
You can find a prebuilt binary for Awsaml on [the releases page][releases]. Grab
the appropriate binary for your architecture and run the Awsaml application. It
Expand Down
73 changes: 64 additions & 9 deletions api/routes/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,73 @@ module.exports = (app, auth) => {
failureFlash: true,
failureRedirect: app.get('configureUrl'),
}), (req, res) => {
const arns = req.user['https://aws.amazon.com/SAML/Attributes/Role'].split(',');

/* eslint-disable no-param-reassign */
req.session.passport.samlResponse = req.body.SAMLResponse;
req.session.passport.roleArn = arns[0];
req.session.passport.principalArn = arns[1];
req.session.passport.accountId = arns[0].split(':')[4]; // eslint-disable-line rapid7/static-magic-numbers
/* eslint-enable no-param-reassign */
let roleAttr = req.user['https://aws.amazon.com/SAML/Attributes/Role'];
let frontend = process.env.ELECTRON_START_URL || app.get('baseUrl');

frontend = new url.URL(frontend);
frontend.searchParams.set('auth', 'true');

// Convert roleAttr to an array if it isn't already one
if (!Array.isArray(roleAttr)) {
roleAttr = [roleAttr];
}

const roles = roleAttr.map((arns, i) => {
const [roleArn, principalArn] = arns.split(',');
const roleArnSegments = roleArn.split(':');
const accountId = roleArnSegments[4];
const roleName = roleArnSegments[5].replace('role/', '');

return {
accountId,
index: i,
principalArn,
roleArn,
roleName,
};
});

const session = req.session.passport;

session.samlResponse = req.body.SAMLResponse;
session.roles = roles;

if (roles.length > 1) {
// If the session has a previous role, see if it matches
// the latest roles from the current SAML assertion. If it
// doesn't match, wipe it from the session.
if (session.roleArn && session.principalArn) {
const found = roles.find((role) =>
role.roleArn === session.roleArn && role.principalArn === session.principalArn
);

if (!found) {
session.showRole = undefined;
session.roleArn = undefined;
session.roleName = undefined;
session.principalArn = undefined;
session.accountId = undefined;
}
}

// If the session still has a previous role, proceed directly to auth.
// Otherwise ask the user to select a role.
if (session.roleArn && session.principalArn && session.roleName && session.accountId) {
frontend.searchParams.set('auth', 'true');
} else {
frontend.searchParams.set('select-role', 'true');
}
} else {
const role = roles[0];

frontend.searchParams.set('auth', 'true');

session.showRole = false;
session.roleArn = role.roleArn;
session.roleName = role.roleName;
session.principalArn = role.principalArn;
session.accountId = role.accountId;
}

res.redirect(frontend);
});

Expand Down
84 changes: 57 additions & 27 deletions api/routes/configure.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,25 @@ const HTTP_OK = 200;
const Errors = {
invalidMetadataErr: 'The SAML metadata is invalid.',
urlInvalidErr: 'The SAML metadata URL is invalid.',
uuidInvalidError: 'The profile is invalid.',
};
const ResponseObj = require('./../response');

module.exports = (app, auth) => {
router.get('/', (req, res) => {
const storedMetadataUrls = Storage.get('metadataUrls') || [];

// Migrate metadataUrls to include a profileUuid. This makes
// profile deletes/edits a little safer since they will no longer be
// based on the iteration index.
let migrated = false;

storedMetadataUrls.forEach((metadata) => {
const storedMetadataUrls = (Storage.get('metadataUrls') || []).map((metadata) => {
if (metadata.profileUuid === undefined) {
migrated = true;
metadata.profileUuid = uuidv4();
}

return metadata;
});

if (migrated) {
Storage.set('metadataUrls', storedMetadataUrls);
}
Expand Down Expand Up @@ -59,7 +60,11 @@ module.exports = (app, auth) => {
});

router.post('/', (req, res) => {
const profileUuid = req.body.profileUuid;
const profileName = req.body.profileName;
const metadataUrl = req.body.metadataUrl;
let storedMetadataUrls = Storage.get('metadataUrls') || [];
let profile;

if (!metadataUrl) {
Storage.set('metadataUrlValid', false);
Expand All @@ -71,23 +76,44 @@ module.exports = (app, auth) => {
}));
}

const origin = req.body.origin;
const metaDataResponseObj = Object.assign({}, ResponseObj, {defaultMetadataUrl: metadataUrl});
// If a profileUuid is passed, validate it and update storage
// with the submitted profile name.
if (profileUuid) {
profile = storedMetadataUrls.find((metadata) => metadata.profileUuid === profileUuid);

let storedMetadataUrls = Storage.get('metadataUrls') || [];
const profileName = req.body.profileName === '' ? metadataUrl : req.body.profileName;
const profile = storedMetadataUrls.find((profile) => profile.url === metadataUrl);
if (!profile) {
return res.status(404).json(Object.assign({}, ResponseObj, {
error: Errors.uuidInvalidErr,
uuidUrlValid: false,
}));
}

if (profile.url !== metadataUrl) {
return res.status(422).json(Object.assign({}, ResponseObj, {
error: Errors.urlInvalidErr,
metadataUrlValid: false,
}));
}

storedMetadataUrls = storedMetadataUrls.map((storedMetadataUrl) => {
if (profileName && storedMetadataUrl.url === metadataUrl && storedMetadataUrl.name !== profileName) {
storedMetadataUrl.name = profileName;
if (profileName) {
storedMetadataUrls = storedMetadataUrls.map((metadata) => {
if (metadata.profileUuid === profileUuid && metadata.name !== profileName) {
metadata.name = profileName;
}

return metadata;
});
Storage.set('metadataUrls', storedMetadataUrls);
}
} else {
profile = storedMetadataUrls.find((metadata) => metadata.url === metadataUrl);
}

return storedMetadataUrl;
});
Storage.set('metadataUrls', storedMetadataUrls);
app.set('metadataUrl', metadataUrl);

const origin = req.body.origin;
const metaDataResponseObj = Object.assign({}, ResponseObj, {defaultMetadataUrl: metadataUrl});

const xmlReq = https.get(metadataUrl, (xmlRes) => {
let xml = '';

Expand Down Expand Up @@ -141,18 +167,22 @@ module.exports = (app, auth) => {

if (cert && issuer && entryPoint) {
Storage.set('previousMetadataUrl', metadataUrl);
const metadataUrls = Storage.get('metadataUrls') || [];

Storage.set(
'metadataUrls',
profile ? metadataUrls : metadataUrls.concat([
{
name: profileName || metadataUrl,
profileUuid: uuidv4(),
url: metadataUrl,
},
])
);

// Add a profile for this URL if one does not already exist
if (!profile) {
const metadataUrls = Storage.get('metadataUrls') || [];

Storage.set(
'metadataUrls',
metadataUrls.concat([
{
name: profileName || metadataUrl,
profileUuid: uuidv4(),
url: metadataUrl,
},
])
);
}

app.set('entryPointUrl', config.auth.entryPoint);
auth.configure(config.auth);
Expand Down
23 changes: 18 additions & 5 deletions api/routes/refresh.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ module.exports = (app) => {

const refreshResponseObj = Object.assign({}, ResponseObj, {
accountId: session.accountId,
roleName: session.roleName,
showRole: session.showRole,
});

sts.assumeRoleWithSAML({
Expand All @@ -42,17 +44,28 @@ module.exports = (app) => {

const profileName = `awsaml-${session.accountId}`;
const metadataUrl = app.get('metadataUrl');
// If the stored metadataUrl label value is the same as the URL default to the profile name!
const metadataUrls = Storage.get('metadataUrls', []).map((storedMetadataUrl) => {
if (storedMetadataUrl.url === metadataUrl && storedMetadataUrl.name === metadataUrl) {
storedMetadataUrl.name = profileName;

// Update the stored profile with account number(s) and profile names
const metadataUrls = (Storage.get('metadataUrls') || []).map((metadata) => {
if (metadata.url === metadataUrl) {
// If the stored metadataUrl label value is the same as the URL
// default to the profile name!
if (metadata.name === metadataUrl) {
metadata.name = profileName;
}
metadata.roles = session.roles.map((role) => role.roleArn);
}

return storedMetadataUrl;
return metadata;
});

Storage.set('metadataUrls', metadataUrls);

// Fetch the metadata profile name for this URL
const profile = metadataUrls.find((metadata) => metadata.url === metadataUrl);

credentialResponseObj.profileName = profile.name;

credentials.save(data.Credentials, profileName, (credSaveErr) => {
if (credSaveErr) {
res.json(Object.assign({}, credentialResponseObj, {
Expand Down
49 changes: 49 additions & 0 deletions api/routes/select-role.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const express = require('express');

const router = express.Router();

module.exports = () => {
router.get('/', (req, res) => {
const session = req.session.passport;

if (!session) {
return res.status(401).json({
error: 'Invalid session',
});
}

res.json({
roles: session.roles,
});
});

router.post('/', (req, res) => {
const session = req.session.passport;

if (!session) {
return res.status(401).json({
error: 'Invalid session',
});
}

if (req.body.index === undefined) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be if (!req.body.index) {

Copy link
Contributor Author

@bturner-r7 bturner-r7 Aug 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

req.body.index can be 0, so we need to test for undefined explicitly.

return res.status(422).json({
error: 'Missing role',
});
}

const role = session.roles[req.body.index];

session.showRole = true;
session.roleArn = role.roleArn;
session.roleName = role.roleName;
session.principalArn = role.principalArn;
session.accountId = role.accountId;

res.json({
status: 'selected',
});
});

return router;
};
3 changes: 3 additions & 0 deletions api/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ const app = require('./server-config')(auth, config, sessionSecret);
}, {
name: '/profile',
route: require('./routes/profile')(),
}, {
name: '/select-role',
route: require('./routes/select-role')(),
}, {
name: '/',
route: require('./routes/static')(),
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
},
"scripts": {
"electron": "electron electron/electron.js",
"electron-dev": "NODE_ENV=development; ELECTRON_START_URL=http://localhost:3000 electron electron/electron.js",
"electron-dev": "NODE_ENV=development ELECTRON_START_URL=http://localhost:3000 electron electron/electron.js",
"react-start": "BROWSER=none; NODE_ENV=development react-scripts start",
"react-build": "react-scripts build",
"test": "mocha",
Expand Down
Loading