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

Azure Active Directory Support #28

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ This module provides local authentication in Clay with a username and password a
- Slack
- Cognito
- LDAP
- Azure Active Directory OIDC

To get started editing in Clay, create a user account. The easiest way to do this is to create a `user.yml` file that looks like this:

Expand Down Expand Up @@ -81,6 +82,13 @@ export LDAP_BIND_DN=<LDAP_BIND_DN>
export LDAP_BIND_CREDENTIALS=<LDAP_BIND_CREDENTIALS>
export LDAP_SEARCH_BASE=<LDAP_SEARCH_BASE>
export LDAP_SEARCH_FILTER=<LDAP_SEARCH_FILTER>

export AD_OIDC_IDENTITY_METADATA=<AD_OIDC_IDENTITY_METADATA>
export AD_OIDC_CLIENT_ID=<AD_OIDC_CONSUMER_CLIENT>
export AD_OIDC_RESPONSE_MODE=<AD_OIDC_RESPONSE_MODE>
export AD_OIDC_REDIRECT_URL=<AD_OIDC_REDIRECT_URL>
export AD_OIDC_ALLOW_HTTP=<true or false>
export AD_OIDC_SCOPE=<AD_OIDC_SCOPE>
```

## License
Expand Down
2 changes: 2 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@
This module only requires setting environment variables in your Clay instance for whichever provider(s) you want to use. A list of providers can be found below.

- [Google](google.md)
- [Cognito](cognito.md)
- [Azure AD](active-directory-oidc.md)
24 changes: 24 additions & 0 deletions docs/active-directory-oidc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# AWS Cognito OAuth

A wrapper around the [Passport Azure Active Directory](http://www.passportjs.org/packages/passport-azure-ad/) package.

### Configuration

- `AD_OIDC_IDENTITY_METADATA` _(required)_: the metadata endpoint provided by the Microsoft Identity Portal.
- `AD_OIDC_CLIENT_ID` _(required)_: the client ID of your application in AAD (Azure Active Directory).
- `AD_OIDC_RESPONSE_MODE` _(required)_: must be 'query' or 'form_post.
- `AD_OIDC_RESPONSE_TYPE` _(required)_: must be 'code', 'code id_token', 'id_token code' or 'id_token'.
- `AD_OIDC_REDIRECT_URL` _(required)_: Must be a https url string, unless you set AD_OIDC_ALLOW_HTTP to true. This is the reply URL registered in AAD for your app.
- `AD_OIDC_CLIENT_SECRET` _(conditional)_: When responseType is not id_token, we have to provide client credential to redeem the authorization code.
- `AD_OIDC_ALLOW_HTTP` _(conditional)_: required to set to true if you want to use http url.
- `AD_OIDC_VALIDATE_ISSUER` _(conditional)_: required to set to false if you don't want to validate issuer, default value is true.
- `AD_OIDC_ISB2C` _(conditional)_: required to set to true if you are using B2C tenant.
- `AD_OIDC_ISSUER` _(conditional)_: this can be a string or an array of string.
- `AD_OIDC_SCOPE` _(conditional)_: list of scope values (comma delimited) besides openid indicating the required scope of the access token for accessing the requested resource.
- `AD_OIDC_LOGGING_LEVEL` _(conditional)_: logging level.
- `AD_OIDC_LOGGING_NO_PII` _(conditional)_: if this is set to true, no personal information such as tokens and claims will be logged.
- `AD_OIDC_NONCE_LIFETIME` _(conditional)_: the lifetime of nonce in session in seconds.
- `AD_OIDC_NONCE_MAX_AMOUNT` _(conditional)_: the max amount of nonce you want to keep in session or cookies.
- `AD_OIDC_USE_COOKIE` _(conditional)_: passport-azure-ad saves state and nonce in session by default for validation purpose.
- `AD_OIDC_COOKIE_ENCRYPTION` _(conditional)_: if useCookieInsteadOfSession is set to true, you must provide cookieEncryptionKeys.
- `AD_OIDC_CLOCKSKEW` _(conditional)_: this value is the clock skew (in seconds) allowed in token validation.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"express-flash": "0.0.2",
"express-session": "^1.15.6",
"passport": "^0.3.2",
"passport-azure-ad": ">=2.0.1",
"passport-cognito-oauth2": "^0.1.1",
"passport-google-oauth": "^1.0.0",
"passport-http-header-token": "^1.1.0",
Expand Down
2 changes: 1 addition & 1 deletion services/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const _each = require('lodash/each'),
*/
function compileLoginPage() {
const tpl = utils.compileTemplate('login.handlebars'),
icons = ['clay-logo', 'twitter', 'google', 'slack', 'ldap', 'logout', 'cognito'];
icons = ['clay-logo', 'twitter', 'google', 'slack', 'ldap', 'logout', 'cognito', 'active-directory'];

// add svgs to handlebars
_each(icons, icon => {
Expand Down
83 changes: 83 additions & 0 deletions strategies/active-directory-oidc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
'use strict';

const passport = require('passport'),
utils = require('../utils'),
OIDCStrategy = require('passport-azure-ad').OIDCStrategy,
{
verify,
getAuthUrl,
getPathOrBase,
getCallbackUrl,
generateStrategyName
} = require('../utils');

/**
* Active Directory authentication strategy
*
* @param {object} site
*/

function createActiveDirectoryOIDCStrategy(site) {
passport.use(
`adoidc-${site.slug}`,
new OIDCStrategy({
identityMetadata: process.env.AD_OIDC_IDENTITY_METADATA,
clientID: process.env.AD_OIDC_CLIENT_ID,
responseMode: process.env.AD_OIDC_RESPONSE_MODE,
responseType: process.env.AD_OIDC_RESPONSE_TYPE,
redirectUrl: process.env.AD_OIDC_REDIRECT_URL,
allowHttpForRedirectUrl: process.env.AD_OIDC_ALLOW_HTTP == 'true',
clientSecret: process.env.AD_OIDC_CLIENT_SECRET,
validateIssuer: process.env.AD_OIDC_VALIDATE_ISSUER,
isB2C: process.env.AD_OIDC_ISB2C,
issuer: process.env.AD_OIDC_ISSUER,
passReqToCallback: true,
scope: process.env.AD_OIDC_SCOPE ? process.env.AD_OIDC_SCOPE.split(',') : null,
loggingLevel: process.env.AD_OIDC_LOGGING_LEVEL,
loggingNoPII: process.env.AD_OIDC_LOGGING_NO_PII,
nonceLifetime: process.env.AD_OIDC_NONCE_LIFETIME,
nonceMaxAmount: process.env.AD_OIDC_NONCE_MAX_AMOUNT,
useCookieInsteadOfSession: process.env.AD_OIDC_USE_COOKIE,
cookieEncryptionKeys: process.env.AD_OIDC_COOKIE_ENCRYPTION,
clockSkew: process.env.AD_OIDC_CLOCKSKEW
}, verifyOIDC())
);
}

/**
* Wraps verify function for active directory
* @returns {function}
*/
function verifyOIDC(){
return function(req, iss, sub, profile, done) {
utils.verify({
username: 'preferred_username',
name: 'name',
provider: 'adoidc'
})(req, null, null, profile._json, done);
};
}

/**
* add authorization routes to the router
* @param {express.Router} router
* @param {object} site
* @param {string} provider
*/
function addAuthRoutes(router, site, provider) {
const strategy = generateStrategyName(provider, site);

router.get(`/_auth/${provider}`, passport.authenticate(strategy));

router.post(`/_auth/${provider}/callback`, passport.authenticate(strategy, {
failureRedirect: `${getAuthUrl(site)}/login`,
failureFlash: true,
successReturnToOrRedirect: getPathOrBase(site)
})); // redirect to previous page or site root
}

module.exports = createActiveDirectoryOIDCStrategy;
module.exports.addAuthRoutes = addAuthRoutes;

// For testing purposes
module.exports.verifyOIDC = verifyOIDC;
84 changes: 84 additions & 0 deletions strategies/active-directory-oidc.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
'use strict';

const _startCase = require('lodash/startCase'),
_includes = require('lodash/includes'),
passport = require('passport'),
filename = __filename.split('/').pop().split('.').shift(),
utils = require('../utils'),
db = require('../services/storage'),
lib = require(`./${filename}`);

describe(_startCase(filename), function () {
describe('verifyOIDC', function () {
const fn = lib[this.description];

it('calls verify with a slightly different function signature', function (done) {
utils.verify = jest.fn(() => (req, token, tokenSecret, profile, cb) => cb()) // eslint-disable-line
db.get = jest.fn().mockResolvedValue({ username: 'foo' });
const profile = { _json: { preferred_username: 'foo' }};
fn()({}, 'foo', 'bar', profile, function () {
expect(utils.verify).toBeCalled();
done();
});
});
});

describe('createActiveDirectoryOIDCStrategy', function () {
const siteStub = { slug: 'foo' };

it('creates active directory OIDC strategy', function () {
passport.use = jest.fn();

process.env.AD_OIDC_IDENTITY_METADATA = 'https://foo.com';
process.env.AD_OIDC_CLIENT_ID = 'abc123';
process.env.AD_OIDC_CONSUMER_CLIENT = '456';
process.env.AD_OIDC_RESPONSE_MODE = 'form_post';
process.env.AD_OIDC_RESPONSE_TYPE = 'id_token';
process.env.AD_OIDC_REDIRECT_URL = 'https://redirect.com';
lib(siteStub);

expect(passport.use).toBeCalled();
});
});

describe('createActiveDirectoryOIDCStrategy with scope', function () {
const siteStub = { slug: 'foo' };

it('creates active directory OIDC strategy', function () {
passport.use = jest.fn();

process.env.AD_OIDC_IDENTITY_METADATA = 'https://foo.com';
process.env.AD_OIDC_CLIENT_ID = 'abc123';
process.env.AD_OIDC_CONSUMER_CLIENT = '456';
process.env.AD_OIDC_RESPONSE_MODE = 'form_post';
process.env.AD_OIDC_RESPONSE_TYPE = 'id_token';
process.env.AD_OIDC_REDIRECT_URL = 'https://redirect.com';
process.env.AD_OIDC_SCOPE = 'test1,test2';
lib(siteStub);

expect(passport.use).toBeCalled();
});
});

describe('addAuthRoutes', function () {
const fn = lib[this.description],
paths = [],
router = {
get: function (path) {
// testing if the paths are added,
// we're checking the paths array after each test
paths.push(path);
},
post: function(path) {
paths.push(path);
},
use: jest.fn(),
};

it('adds active directory OIDC auth and callback routes', function () {
fn(router, {}, 'adoidc');
expect(_includes(paths, '/_auth/adoidc')).toEqual(true);
expect(_includes(paths, '/_auth/adoidc/callback')).toEqual(true);
});
});
});
1 change: 1 addition & 0 deletions strategies/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const STRATEGIES = {
apikey: require('./key'),
adoidc: require('./active-directory-oidc'),
cognito: require('./cognito'),
google: require('./google'),
ldap: require('./ldap'),
Expand Down
28 changes: 28 additions & 0 deletions views/active-directory.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.