Skip to content

Commit

Permalink
Azure Active Directory Support
Browse files Browse the repository at this point in the history
Adds Azure Active Directory as a provider. The `passport-azure-ad`
strategy supports OIDC and BearerStrategy. This commit only supports the
OpenID Connect protocol.
  • Loading branch information
nfarve committed Nov 22, 2019
1 parent 5f72d8a commit 7f56570
Show file tree
Hide file tree
Showing 9 changed files with 230 additions and 1 deletion.
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;
82 changes: 82 additions & 0 deletions strategies/active-directory-oidc.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
'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_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_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, {}, 'activeDirectoryOIDC');
expect(_includes(paths, '/_auth/activeDirectoryOIDC')).toEqual(true);
expect(_includes(paths, '/_auth/activeDirectoryOIDC/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.

0 comments on commit 7f56570

Please sign in to comment.