Files
openid-client/test/passport/passport_strategy.test.js
Filip Skokan f0d95390e9 feat: experimental Bun support
This attempts to work around:

- missing Node.js APIs in Bun
- Bun's bugs in url.parse(..., true)
- Bun loading `jose`'s bun target instead of the require one

It is not possible to run openid-client's test suite due to other Bun
Node.js compatibility bugs which is why this is "experimental"

Refs #622
Refs #623
2023-10-03 16:58:50 +02:00

686 lines
20 KiB
JavaScript

const http = require('http');
const sinon = require('sinon');
const MockRequest = require('readable-mock-req');
const { expect } = require('chai');
const { Issuer, Strategy } = require('../../lib');
describe('OpenIDConnectStrategy', () => {
before(function () {
this.origIncomingMessage = http.IncomingMessage;
http.IncomingMessage = MockRequest;
});
after(function () {
http.IncomingMessage = this.origIncomingMessage;
});
beforeEach(function () {
this.issuer = new Issuer({
issuer: 'https://op.example.com',
authorization_endpoint: 'https://op.example.com/auth',
jwks_uri: 'https://op.example.com/jwks',
token_endpoint: 'https://op.example.com/token',
userinfo_endpoint: 'https://op.example.com/userinfo',
code_challenge_methods_supported: ['plain', 'S256'],
});
this.client = new this.issuer.Client({
client_id: 'foo',
client_secret: 'barbaz',
respose_types: ['code'],
redirect_uris: ['http://rp.example.com/cb'],
});
});
it('checks that client is a Client instance', () => {
expect(() => Strategy({ client: 'foo' })).to.throw(
'client must be an instance of openid-client Client',
);
});
it('checks that verify callback is a function', function () {
expect(() => Strategy({ client: this.client })).to.throw('verify callback must be a function');
});
it('checks that issuer has an issuer identifier', () => {
const issuer = new Issuer({});
const client = new issuer.Client({ client_id: 'identifier' });
expect(() => Strategy({ client }, () => {})).to.throw(
'client must have an issuer with an identifier',
);
});
it('checks for session presence', function (next) {
const strategy = new Strategy({ client: this.client }, () => {});
const req = new MockRequest('GET', '/login/oidc');
strategy.error = (error) => {
try {
expect(error).to.be.an.instanceof(Error);
expect(error.message).to.match(/session/);
next();
} catch (err) {
next(err);
}
};
strategy.authenticate(req);
});
describe('authenticate', function () {
it('forwards options.extras to callback as extras param', async function () {
const extras = {
clientAssertionPayload: {
aud: 'https://oidc.corp.com/default-oidc-provider',
},
};
const params = {
redirect_uri: 'http://domain.inc/oauth2/callback',
};
const strategy = new Strategy({ client: this.client, params, extras }, () => {});
const req = new MockRequest('GET', '/login/oidc');
req.session = { 'oidc:op.example.com': sinon.match.object };
/* Fake callback params */
const callbackParams = { code: 'some-code' };
sinon.stub(this.client, 'callbackParams').callsFake(() => callbackParams);
this.client.callback = sinon.spy();
strategy.authenticate(req, {});
sinon.assert.calledOnce(this.client.callback);
sinon.assert.calledWith(
this.client.callback,
params.redirect_uri,
callbackParams,
sinon.match.object,
extras,
);
});
});
describe('initiate', function () {
it('starts authentication requests for GETs', function () {
const params = { foo: 'bar' };
const strategy = new Strategy({ client: this.client, params }, () => {});
const req = new MockRequest('GET', '/login/oidc');
req.session = {};
strategy.redirect = sinon.spy();
strategy.authenticate(req);
expect(params).to.eql({ foo: 'bar' });
expect(strategy.redirect.calledOnce).to.be.true;
const target = strategy.redirect.firstCall.args[0];
expect(target).to.include('redirect_uri=');
expect(target).to.include('scope=');
expect(req.session).to.have.property('oidc:op.example.com');
expect(req.session['oidc:op.example.com']).to.have.keys(
'state',
'response_type',
'code_verifier',
);
});
it('starts authentication requests for TPIL GETs', function () {
const params = { iss: 'https://op.example.com' };
const strategy = new Strategy({ client: this.client, params }, () => {});
const req = new MockRequest('GET', '/login/oidc');
req.session = {};
strategy.redirect = sinon.spy();
strategy.authenticate(req);
expect(strategy.redirect.calledOnce).to.be.true;
const target = strategy.redirect.firstCall.args[0];
expect(target).to.include('redirect_uri=');
expect(target).to.include('scope=');
expect(req.session).to.have.property('oidc:op.example.com');
expect(req.session['oidc:op.example.com']).to.have.keys(
'state',
'response_type',
'code_verifier',
);
});
it('starts authentication requests for POSTs', function () {
const strategy = new Strategy({ client: this.client }, () => {});
const req = new MockRequest('POST', '/login/oidc');
req.session = {};
req.body = {};
strategy.redirect = sinon.spy();
strategy.authenticate(req);
expect(strategy.redirect.calledOnce).to.be.true;
const target = strategy.redirect.firstCall.args[0];
expect(target).to.include('redirect_uri=');
expect(target).to.include('scope=');
expect(req.session).to.have.property('oidc:op.example.com');
expect(req.session['oidc:op.example.com']).to.have.keys(
'state',
'response_type',
'code_verifier',
);
});
it('can have redirect_uri and scope specified', function () {
const strategy = new Strategy(
{
client: this.client,
params: {
redirect_uri: 'https://example.com/cb',
scope: 'openid profile',
},
},
() => {},
);
const req = new MockRequest('GET', '/login/oidc');
req.session = {};
strategy.redirect = sinon.spy();
strategy.authenticate(req);
expect(strategy.redirect.calledOnce).to.be.true;
const target = strategy.redirect.firstCall.args[0];
expect(target).to.include(`redirect_uri=${encodeURIComponent('https://example.com/cb')}`);
expect(target).to.include('scope=openid%20profile');
});
it('can have authorization parameters specified at runtime', function () {
const strategy = new Strategy(
{
client: this.client,
params: {
redirect_uri: 'https://example.com/cb',
scope: 'openid profile',
},
},
() => {},
);
const req = new MockRequest('GET', '/login/oidc');
req.session = {};
strategy.redirect = sinon.spy();
strategy.authenticate(req, { resource: 'urn:example:foo' });
expect(strategy.redirect.calledOnce).to.be.true;
const target = strategy.redirect.firstCall.args[0];
expect(target).to.include(`resource=${encodeURIComponent('urn:example:foo')}`);
});
it('automatically includes nonce for where it applies (and ignores one from params)', function () {
const strategy = new Strategy(
{
client: this.client,
params: {
response_type: 'code id_token token',
response_mode: 'form_post',
nonce: 'foo',
},
},
() => {},
);
const req = new MockRequest('GET', '/login/oidc');
req.session = {};
strategy.redirect = sinon.spy();
strategy.authenticate(req);
expect(strategy.redirect.calledOnce).to.be.true;
const target = strategy.redirect.firstCall.args[0];
expect(target).to.include('redirect_uri=');
expect(target).to.include('scope=');
expect(target).to.include('nonce=');
expect(target).not.to.include('nonce=foo');
expect(target).to.include('response_mode=form_post');
expect(req.session).to.have.property('oidc:op.example.com');
expect(req.session['oidc:op.example.com']).to.have.keys(
'state',
'nonce',
'response_type',
'code_verifier',
);
});
it('ignores static state coming from params', function () {
const strategy = new Strategy(
{
client: this.client,
params: {
state: 'foo',
},
},
() => {},
);
const req = new MockRequest('GET', '/login/oidc');
req.session = {};
strategy.redirect = sinon.spy();
strategy.authenticate(req);
expect(strategy.redirect.calledOnce).to.be.true;
const target = strategy.redirect.firstCall.args[0];
expect(target).to.include('redirect_uri=');
expect(target).to.include('scope=');
expect(target).to.include('state=');
expect(target).not.to.include('state=foo');
expect(req.session).to.have.property('oidc:op.example.com');
expect(req.session['oidc:op.example.com']).to.have.keys(
'state',
'response_type',
'code_verifier',
);
});
describe('use pkce', () => {
it('will throw when explictly provided value is not supported', function () {
expect(() => {
new Strategy(
{
client: this.client,
usePKCE: 'foobar',
},
() => {},
);
}).to.throw('foobar is not valid/implemented PKCE code_challenge_method');
});
it('can be set to use PKCE (S256)', function () {
const strategy = new Strategy(
{
client: this.client,
usePKCE: 'S256',
},
() => {},
);
const req = new MockRequest('GET', '/login/oidc');
req.session = {};
strategy.redirect = sinon.spy();
strategy.authenticate(req);
expect(strategy.redirect.calledOnce).to.be.true;
const target = strategy.redirect.firstCall.args[0];
expect(target).to.include('code_challenge_method=S256');
expect(target).to.include('code_challenge=');
expect(req.session).to.have.property('oidc:op.example.com');
expect(req.session['oidc:op.example.com']).to.have.property('code_verifier');
});
it('can be set to use PKCE (plain)', function () {
const strategy = new Strategy(
{
client: this.client,
usePKCE: 'plain',
},
() => {},
);
const req = new MockRequest('GET', '/login/oidc');
req.session = {};
strategy.redirect = sinon.spy();
strategy.authenticate(req);
expect(strategy.redirect.calledOnce).to.be.true;
const target = strategy.redirect.firstCall.args[0];
expect(target).not.to.include('code_challenge_method');
expect(target).to.include('code_challenge=');
expect(req.session).to.have.property('oidc:op.example.com');
expect(req.session['oidc:op.example.com']).to.have.property('code_verifier');
});
});
it('can have session key specifed', function () {
const strategy = new Strategy(
{
client: this.client,
sessionKey: 'oidc:op.example.com:foo',
},
() => {},
);
const req = new MockRequest('GET', '/login/oidc');
req.session = {};
strategy.redirect = sinon.spy();
strategy.authenticate(req);
expect(req.session).to.have.property('oidc:op.example.com:foo');
expect(req.session['oidc:op.example.com:foo']).to.have.keys(
'state',
'response_type',
'code_verifier',
);
});
});
describe('callback', function () {
it('triggers the verify function and then the success one', function (next) {
const ts = { foo: 'bar' };
sinon.stub(this.client, 'callback').callsFake(async () => ts);
const strategy = new Strategy({ client: this.client }, (tokenset, done) => {
expect(tokenset).to.equal(ts);
done(null, tokenset);
});
strategy.success = () => {
next();
};
const req = new MockRequest('GET', '/login/oidc/callback?code=foobar&state=state');
req.session = {
'oidc:op.example.com': {
nonce: 'nonce',
state: 'state',
response_type: 'code',
},
};
strategy.authenticate(req);
});
it('triggers the error function when server_error is encountered', function (next) {
const strategy = new Strategy({ client: this.client }, () => {});
const req = new MockRequest('GET', '/login/oidc/callback?error=server_error&state=state');
req.session = {
'oidc:op.example.com': {
nonce: 'nonce',
state: 'state',
response_type: 'code',
},
};
strategy.error = (error) => {
try {
expect(error.error).to.equal('server_error');
next();
} catch (err) {
next(err);
}
};
strategy.authenticate(req);
});
it('lets the dev know when most common problems with session occur', function (next) {
const strategy = new Strategy({ client: this.client }, () => {});
const req = new MockRequest('GET', '/login/oidc/callback?code=code&state=foo');
req.session = {};
strategy.error = (error) => {
try {
expect(error.message).to.eql(
'did not find expected authorization request details in session, req.session["oidc:op.example.com"] is undefined',
);
next();
} catch (err) {
next(err);
}
};
strategy.authenticate(req);
});
it('triggers the error function when non oidc error is encountered', function (next) {
const strategy = new Strategy({ client: this.client }, () => {});
sinon.stub(this.client, 'callback').callsFake(async () => {
throw new Error('callback error');
});
const req = new MockRequest('GET', '/login/oidc/callback?code=code&state=state');
req.session = {
'oidc:op.example.com': {
nonce: 'nonce',
state: 'state',
response_type: 'code',
},
};
strategy.error = (error) => {
try {
expect(error.message).to.equal('callback error');
next();
} catch (err) {
next(err);
}
};
strategy.authenticate(req);
});
it('triggers the fail function when oidc error is encountered', function (next) {
const strategy = new Strategy({ client: this.client }, () => {});
const req = new MockRequest('GET', '/login/oidc/callback?error=login_required&state=state');
req.session = {
'oidc:op.example.com': {
nonce: 'nonce',
state: 'state',
response_type: 'code',
},
};
strategy.fail = (error) => {
try {
expect(error.error).to.equal('login_required');
next();
} catch (err) {
next(err);
}
};
strategy.authenticate(req);
});
it('triggers the error function for errors during verify', function (next) {
const strategy = new Strategy({ client: this.client }, (tokenset, done) => {
done(new Error('user find error'));
});
const ts = { foo: 'bar' };
sinon.stub(this.client, 'callback').callsFake(async () => ts);
const req = new MockRequest('GET', '/login/oidc/callback?code=foo&state=state');
req.session = {
'oidc:op.example.com': {
nonce: 'nonce',
state: 'state',
response_type: 'code',
},
};
strategy.error = (error) => {
try {
expect(error.message).to.equal('user find error');
next();
} catch (err) {
next(err);
}
};
strategy.authenticate(req);
});
it('triggers the fail function when verify yields no account', function (next) {
const strategy = new Strategy({ client: this.client }, (tokenset, done) => {
done();
});
const ts = { foo: 'bar' };
sinon.stub(this.client, 'callback').callsFake(async () => ts);
const req = new MockRequest('GET', '/login/oidc/callback?code=foo&state=state');
req.session = {
'oidc:op.example.com': {
nonce: 'nonce',
response_type: 'code',
state: 'state',
},
};
strategy.fail = () => {
next();
};
strategy.authenticate(req);
});
it('does userinfo request too if part of verify arity and resulting tokenset', function (next) {
const strategy = new Strategy({ client: this.client }, (tokenset, userinfo, done) => {
try {
expect(tokenset).to.be.ok;
expect(userinfo).to.be.ok;
done(null, { sub: 'foobar' });
} catch (err) {
next(err);
}
});
const ts = { access_token: 'foo' };
const ui = { sub: 'bar' };
sinon.stub(this.client, 'callback').callsFake(async () => ts);
sinon.stub(this.client, 'userinfo').callsFake(async () => ui);
const req = new MockRequest('GET', '/login/oidc/callback?code=foo&state=state');
req.session = {
'oidc:op.example.com': {
nonce: 'nonce',
response_type: 'code',
state: 'state',
},
};
strategy.success = () => {
next();
};
strategy.authenticate(req);
});
it('throws when userinfo is requested but no access_token was returned', function (next) {
const strategy = new Strategy({ client: this.client }, (tokenset, userinfo, done) => {});
const ts = { id_token: 'foo' };
sinon.stub(this.client, 'callback').callsFake(async () => ts);
const req = new MockRequest('GET', '/login/oidc/callback?code=foo&state=state');
req.session = {
'oidc:op.example.com': {
nonce: 'nonce',
response_type: 'code',
state: 'state',
},
};
strategy.fail = (err) => {
try {
expect(err.name).to.equal('RPError');
expect(err.message).to.equal(
'expected access_token to be returned when asking for userinfo in verify callback',
);
expect(err).to.have.property('tokenset');
next();
} catch (e) {
next(e);
}
};
strategy.authenticate(req);
});
it('receives a request as the first parameter if passReqToCallback is set', function (next) {
const strategy = new Strategy(
{
client: this.client,
passReqToCallback: true,
},
(req, tokenset, done) => {
try {
expect(req).to.be.an.instanceof(MockRequest);
expect(tokenset).to.be.ok;
done(null, { sub: 'foobar' });
} catch (err) {
next(err);
}
},
);
const ts = { id_token: 'foo' };
sinon.stub(this.client, 'callback').callsFake(async () => ts);
const req = new MockRequest('GET', '/login/oidc/callback?code=foo&state=state');
req.session = {
'oidc:op.example.com': {
nonce: 'nonce',
response_type: 'code',
state: 'state',
},
};
strategy.success = () => {
next();
};
strategy.authenticate(req);
});
it('receives a request and userinfo with passReqToCallback: true and userinfo', function (next) {
const strategy = new Strategy(
{
client: this.client,
passReqToCallback: true,
},
(req, tokenset, userinfo, done) => {
try {
expect(req).to.be.an.instanceof(MockRequest);
expect(tokenset).to.be.ok;
expect(userinfo).to.be.ok;
done(null, { sub: 'foobar' });
} catch (err) {
next(err);
}
},
);
const ts = { access_token: 'foo' };
const ui = { sub: 'bar' };
sinon.stub(this.client, 'callback').callsFake(async () => ts);
sinon.stub(this.client, 'userinfo').callsFake(async () => ui);
const req = new MockRequest('GET', '/login/oidc/callback?code=foo&state=state');
req.session = {
'oidc:op.example.com': {
nonce: 'nonce',
response_type: 'code',
state: 'state',
},
};
strategy.success = () => {
next();
};
strategy.authenticate(req);
});
});
});