Skip to content
This repository has been archived by the owner on Aug 28, 2023. It is now read-only.

Commit

Permalink
Fix OIDC B2C problems and add some breaking changes
Browse files Browse the repository at this point in the history
(1) Rewrote the metadata loading and the configuration code. Restructured OIDCStrategy.
(2) #188 B2C mocha tests (partially done, waiting for the AAD fix of missing nonce to add test for hybrid/code flow)
(3) #165 rename 'callbackURL' and 'returnURL' to 'redirectUrl'.
(4) #189 Extensibility to allow issuer validation when going against commend end point
(5) #194 error message for 'sub' mismatch is incorrect after redeeming 'code'
(6) #218 missing email claim for B2C
  • Loading branch information
lovemaths committed Sep 27, 2016
1 parent 353999e commit d60bdb6
Show file tree
Hide file tree
Showing 12 changed files with 1,068 additions and 575 deletions.
18 changes: 16 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
<a name="3.0.0"></a>
# 3.0.0

## Breaking changes from 2.0.1
## OIDCStrategy

### OIDCStrategy
### Breaking changes
* skipUserProfile option is no longer provided. We will load 'userinfo' if AAD v1 is being used and
if there is 'code' involved in the flow (in other words, one of the following flows: 'code',
'code id_token', 'id_token code'). For all other scenarios, we do an 'id_token' fallback, since we
cannot obtain 'userinfo' (AAD v2 doesn't have an userinfo endpoint, and 'id_token' flow doesn't yield
an 'access_token' to exchange for an 'userinfo' token).

* changed the `returnURL` option name to `redirectUrl`.

* added `isB2C` option. In order to use B2C feature, user must have this option and set it to true.

* added `oid` claim in the returned profile.

* removed `email` claim, and added `emails` claim in the returned profile. `emails` claim is always an array.

### New features
* multiple nonce and state support in OIDCStrategy. Provided `nonceLifetime` option to configure the lifetime of nonce saved in session.

* enabled `issuer` validation against common endpoint. To validate issuer on common endpoint, user must
specify the allowed issuer(s) in `issuer` option, and set `validateIssuer` option to true.

# 2.0.1

## Major changes from 2.0.0
Expand Down
26 changes: 26 additions & 0 deletions lib/aadutils.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,3 +225,29 @@ exports.findAndDeleteTupleByState = (array, state) => {

return null;
};

// copy the fields from source to dest
exports.copyObjectFields = (source, dest, fields) => {
if (!source || !dest || !fields || !Array.isArray(fields))
return;

for (var i = 0; i < fields.length; i++)
dest[fields[i]] = source[fields[i]];
};

exports.getErrorMessage = (err) => {
if (typeof err === 'string')
return err;
if (err instanceof Error)
return err.message;

// if not string or Error, we try to stringify it
var str;
try {
str = JSON.stringify(err);
} catch (ex) {
return err;
}
return str;
};

974 changes: 525 additions & 449 deletions lib/oidcstrategy.js

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions lib/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,15 @@ types.isHttpsURL = {
error: 'The URL must be valid and be https://',
};

Validator.isHttpsURLIfExists = 'isHttpsURLIfExists';
types.isHttpsURLIfExists = {
validate: (value) => {
if (value)
return UrlValidator.isHttpsUri(value);
else
return true;
},
error: 'The URL must be valid and be https://',
};

exports.Validator = Validator;
184 changes: 184 additions & 0 deletions test/Chai-passport_test/b2c_oidc_implicit_flow_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/**
* Copyright (c) Microsoft Corporation
* All Rights Reserved
* MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this
* software and associated documentation files (the 'Software'), to deal in the Software
* without restriction, including without limitation the rights to use, copy, modify,
* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
* OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT
* OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

'use strict';

/* eslint no-underscore-dangle: 0 */

var chai = require('chai');
var url = require('url');
chai.use(require('chai-passport-strategy'));

var Metadata = require('../../lib/metadata').Metadata;
var OIDCStrategy = require('../../lib/index').OIDCStrategy;

var nonce = 'OKK4cd2ftdqsj4n0Gr+GCHOMH6cJ00oO';
var policy = 'b2c_1_signin';
var id_token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IklkVG9rZW5TaWduaW5nS2V5Q29udGFpbmVyLnYyIn0.eyJleHAiOjE0NzQ2OTY0MjQsIm5iZiI6MTQ3NDY5MjgyNCwidmVyIjoiMS4wIiwiaXNzIjoiaHR0cHM6Ly9sb2dpbi5taWNyb3NvZnRvbmxpbmUuY29tLzIyYmY0MGM2LTExODYtNGVhNS1iNDliLTNkYzRlYzBmNTRlYi92Mi4wLyIsInN1YiI6Ik5vdCBzdXBwb3J0ZWQgY3VycmVudGx5LiBVc2Ugb2lkIGNsYWltLiIsImF1ZCI6ImYwYjZlNGViLTJkOGMtNDBiNi1iOWM2LWUyNmQxMDc0ODQ2ZCIsImFjciI6ImIyY18xX3NpZ25pbiIsIm5vbmNlIjoiT0tLNGNkMmZ0ZHFzajRuMEdyK0dDSE9NSDZjSjAwb08iLCJpYXQiOjE0NzQ2OTI4MjQsImF1dGhfdGltZSI6MTQ3NDY5MjgyNCwib2lkIjoiNDMyOWQ2YmMtMGY4NC00NWQ4LTg3MDktMmM4YjA5MTM1N2QxIiwiZW1haWxzIjpbInNpanVuLndvcmtAZ21haWwuY29tIl19.BzKOUVnE6s6c03CFkS1DceJNvwXwHXE4IlXxXJyjNrD6LGKoMnRqI2mFzylCpjib4QM7byjHLs6MumwjrIR4iu_m-ryU6_2NMB0ry8cVCzm7g3QQklNGlsGAeHT69yl8TBqQpUCB71NoDu830nTcLwzN490id4RiWlTiJboyCkOHGZ36hMd4L-9qR-GtWKIJQR8-bgZRjS9vysaUQigIyMEaZzqQ3HBF1gq1euXLfiL_QAaFVay1CvT3kcvFN7wUfdMP6QvpwnzKTQW3CSpLbQlcxdc1bsNWvnd9d6ASxZVHMSxljJ7ZK0YHg6mUCDEH3r4nK9Sdvy_CHeKKOuPZtQ';
// change the last character in id_token to make a id_token with wrong signature
var id_token_wrong_signature = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IklkVG9rZW5TaWduaW5nS2V5Q29udGFpbmVyLnYyIn0.eyJleHAiOjE0NzQ2OTY0MjQsIm5iZiI6MTQ3NDY5MjgyNCwidmVyIjoiMS4wIiwiaXNzIjoiaHR0cHM6Ly9sb2dpbi5taWNyb3NvZnRvbmxpbmUuY29tLzIyYmY0MGM2LTExODYtNGVhNS1iNDliLTNkYzRlYzBmNTRlYi92Mi4wLyIsInN1YiI6Ik5vdCBzdXBwb3J0ZWQgY3VycmVudGx5LiBVc2Ugb2lkIGNsYWltLiIsImF1ZCI6ImYwYjZlNGViLTJkOGMtNDBiNi1iOWM2LWUyNmQxMDc0ODQ2ZCIsImFjciI6ImIyY18xX3NpZ25pbiIsIm5vbmNlIjoiT0tLNGNkMmZ0ZHFzajRuMEdyK0dDSE9NSDZjSjAwb08iLCJpYXQiOjE0NzQ2OTI4MjQsImF1dGhfdGltZSI6MTQ3NDY5MjgyNCwib2lkIjoiNDMyOWQ2YmMtMGY4NC00NWQ4LTg3MDktMmM4YjA5MTM1N2QxIiwiZW1haWxzIjpbInNpanVuLndvcmtAZ21haWwuY29tIl19.BzKOUVnE6s6c03CFkS1DceJNvwXwHXE4IlXxXJyjNrD6LGKoMnRqI2mFzylCpjib4QM7byjHLs6MumwjrIR4iu_m-ryU6_2NMB0ry8cVCzm7g3QQklNGlsGAeHT69yl8TBqQpUCB71NoDu830nTcLwzN490id4RiWlTiJboyCkOHGZ36hMd4L-9qR-GtWKIJQR8-bgZRjS9vysaUQigIyMEaZzqQ3HBF1gq1euXLfiL_QAaFVay1CvT3kcvFN7wUfdMP6QvpwnzKTQW3CSpLbQlcxdc1bsNWvnd9d6ASxZVHMSxljJ7ZK0YHg6mUCDEH3r4nK9Sdvy_CHeKKOuPZtM';

// Mock the process of getting PEMkey
var PEMkey = "-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAs4W7xjkQZP3OwG7PfRgcYKn8eRYXHiz1iK503fS+K2FZo+Ublwwa\n2xFZWpsUU/jtoVCwIkaqZuo6xoKtlMYXXvfVHGuKBHEBVn8b8x/57BQWz1d0KdrN\nXxuMvtFe6RzMqiMqzqZrzae4UqVCkYqcR9gQx66Ehq7hPmCxJCkg7ajo7fu6E7dP\nd34KH2HSYRsaaEA/BcKTeb9H1XE/qEKjog68wUU9Ekfl3FBIRN+1Ah/BoktGFoXy\ni/jt0+L0+gKcL1BLmUlGzMusvRbjI/0+qj+mc0utGdRjY+xIN2yBj8vl4DODO+wM\nwfp+cqZbCd9TENyHaTb8iA27s+73L3ExOQIDAQAB\n-----END RSA PUBLIC KEY-----\n";

/*
* test strategy (for response_type = 'id_token') which checks the expiration of id_token
*/

var options = {
redirectUrl: 'http://localhost:3000/auth/openid/return',
clientID: 'f0b6e4eb-2d8c-40b6-b9c6-e26d1074846d',
identityMetadata: 'https://login.microsoftonline.com/sijun1b2c.onmicrosoft.com/v2.0/.well-known/openid-configuration',
responseType: 'id_token',
responseMode: 'form_post',
validateIssuer: true,
passReqToCallback: false,
isB2C: true,
sessionKey: 'my_key',
ignoreExpiration: true,
issuer: 'https://login.microsoftonline.com/22bf40c6-1186-4ea5-b49b-3dc4ec0f54eb/v2.0/',
};

var testStrategy = new OIDCStrategy(options, function(profile, done) {
done(null, profile.oid);
});

/*
* Begin the testing
*/
var challenge;
var user;

var setIgnoreExpirationFalse = function(options) { options.ignoreExpiration = false; };
var setWrongIssuer = function(options) { options.issuer = 'wrong_issuer'; };
var rmValidateIssuer = function(options) { options.validateIssuer = undefined; };

var testPrepare = function(id_token_to_use, nonce_to_use, policy_to_use, action) {
return function(done) {
// Mock `setOptions`
testStrategy.setOptions = function(params, oauthConfig, optionsToValidate, done) {
params.metadata.generateOidcPEM = () => { return PEMkey; };

oauthConfig.auth_endpoint = 'https://login.microsoftonline.com/sijun1b2c.onmicrosoft.com/oauth2/v2.0/authorize?p=b2c_1_signin';
oauthConfig.token_endpoint = 'https://login.microsoftonline.com/sijun1b2c.onmicrosoft.com/oauth2/v2.0/token?p=b2c_1_signin';

optionsToValidate.validateIssuer = true;
optionsToValidate.issuer = 'https://login.microsoftonline.com/22bf40c6-1186-4ea5-b49b-3dc4ec0f54eb/v2.0/';
optionsToValidate.audience = 'f0b6e4eb-2d8c-40b6-b9c6-e26d1074846d';
optionsToValidate.allowMultiAudiencesInToken = false;
optionsToValidate.ignoreExpiration = true;
optionsToValidate.algorithms = ['RS256'];
optionsToValidate.nonce = nonce_to_use;

if (action) {
for (let i = 0; i < action.length; i++)
action[i](optionsToValidate);
}
return done();
};

chai.passport
.use(testStrategy)
.fail(function(c) {
challenge = c; done();
})
.success(function(u) {
user = u; done();
})
.req(function(req) {
// reset the value of challenge and user
challenge = user = undefined;
var time = Date.now();
// add state and nonce to session
req.session = {'my_key': {'content': [{'state': 'my_state', 'nonce': nonce_to_use, 'policy': policy_to_use, 'timeStamp': time}]}};
// add id_token and state to body
req.body = {'id_token': id_token_to_use, 'state' : 'my_state'};
// empty query
req.query = {};
})
.authenticate({});
};
};

describe('B2C OIDCStrategy implicit flow test', function() {

describe('should succeed without expiration checking', function() {
before(testPrepare(id_token, nonce, policy));

it('should succeed with expected user', function() {
chai.expect(user).to.equal('4329d6bc-0f84-45d8-8709-2c8b091357d1');
});
});

describe('should fail for id_token with invalid signature', function() {
before(testPrepare(id_token_wrong_signature, nonce, policy));

it('should fail', function() {
chai.expect(challenge).to.equal('In _validateResponse: invalid signature');
});
});

describe('should fail for id_token with wrong nonce', function() {
before(testPrepare(id_token, 'wrong_nonce', policy));

it('should fail', function() {
chai.expect(challenge).to.equal('In _validateResponse: invalid nonce');
});
});

describe('should fail with id_token expiration checking', function() {
before(testPrepare(id_token, nonce, policy, [setIgnoreExpirationFalse]));

it('should fail', function() {
chai.expect(challenge).to.equal('In _validateResponse: jwt is expired');
});
});

describe('should fail with wrong issuer', function() {
// we check the issuer by default
before(testPrepare(id_token, nonce, policy, [setWrongIssuer]));

it('should fail', function() {
chai.expect(challenge).to.equal('In _validateResponse: jwt issuer is invalid. expected: wrong_issuer');
});
});

describe('should fail with wrong issuer with default value of validateIssuer', function() {
// for non-common endpoint, we force to validate issuer
before(testPrepare(id_token, nonce, policy, [rmValidateIssuer, setWrongIssuer]));

it('should fail', function() {
chai.expect(challenge).to.equal('In _validateResponse: jwt issuer is invalid. expected: wrong_issuer');
});
});

describe('should fail with wrong policy', function() {
// for non-common endpoint, we force to validate issuer
before(testPrepare(id_token, nonce, 'wrong policy'));

it('should fail', function() {
chai.expect(challenge).to.equal('In _validateResponse: acr in id_token does not match the policy used');
});
});
});

Loading

0 comments on commit d60bdb6

Please sign in to comment.