Skip to content

Commit

Permalink
Merge bff6321 into 5f45c37
Browse files Browse the repository at this point in the history
  • Loading branch information
bturner-r7 committed Aug 8, 2018
2 parents 5f45c37 + bff6321 commit d7e9513
Show file tree
Hide file tree
Showing 21 changed files with 664 additions and 79 deletions.
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) {
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

0 comments on commit d7e9513

Please sign in to comment.