Skip to content

Commit

Permalink
test: add test coverage for ciba
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed May 24, 2021
1 parent 0309ec0 commit 75133bb
Show file tree
Hide file tree
Showing 19 changed files with 860 additions and 36 deletions.
24 changes: 12 additions & 12 deletions certification/runner/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,24 +63,19 @@ runner.createTestPlan({

SKIP = SKIP || ('SKIP' in process.env ? process.env.SKIP.split(',') : []);

let download = false;
if (fs.existsSync('.download')) {
fs.unlinkSync('.download');
}

describe(PLAN_NAME, () => {
after(() => {
if (download) {
if (fs.existsSync('.download')) {
fs.unlinkSync('.download');
return runner.downloadArtifact({ planId: PLAN_ID });
}
return undefined;
});

afterEach(function () {
const { state, err } = this.currentTest;
if (state !== 'passed') {
download = true;
process.exitCode |= 1;
console.error(err);
}
});

parallel('', () => {
for (const { testModule, variant } of MODULES) {
const test = SKIP.includes(testModule) ? it.skip : it;
Expand All @@ -91,7 +86,12 @@ runner.createTestPlan({
});
debug('Created test module, new id: %s', moduleId);
debug('%s/log-detail.html?log=%s', SUITE_BASE_URL, moduleId);
await runner.waitForState({ moduleId });
try {
await runner.waitForState({ moduleId });
} catch (err) {
fs.writeFileSync('.download', 'foo');
throw err;
}
});
}
});
Expand Down
24 changes: 22 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,7 @@ _**default value**_:

#### deliveryModes

fine-tune the supported token delivery modes. Supported values are
Fine-tune the supported token delivery modes. Supported values are
- `poll`
- `ping`

Expand Down Expand Up @@ -716,6 +716,7 @@ async function processLoginHintToken(ctx, loginHintToken) {
#### triggerAuthenticationDevice

Helper function used to trigger the authentication and authorization on end-user's Authentication Device. It is called after accepting the backchannel authentication request but before sending client back the response.
When the end-user authenticates use `provider.backchannelResult()` to finish the Consumption Device login process.



Expand All @@ -729,6 +730,25 @@ async function triggerAuthenticationDevice(ctx, request, account, client) {
throw new Error('features.ciba.triggerAuthenticationDevice not implemented');
}
```
<a id="trigger-authentication-device-provider-backchannel-result-method"></a><details><summary>(Click to expand) `provider.backchannelResult()` method</summary><br>


`backchannelResult` is a method on the Provider prototype, it returns a `Promise` with no fulfillment value.


```js
const provider = new Provider(...);
await provider.backchannelResult(...);
```
`backchannelResult(request, result[, options]);`
- `request` BackchannelAuthenticationRequest - BackchannelAuthenticationRequest instance.
- `result` Grant | OIDCProviderError - instance of a persisted Grant model or an OIDCProviderError (all exported by Provider.errors).
- `options.acr?`: string - Authentication Context Class Reference value that identifies the Authentication Context Class that the authentication performed satisfied.
- `options.amr?`: string[] - Identifiers for authentication methods used in the authentication.
- `options.authTime?`: number - Time when the End-User authentication occurred.


</details>

#### validateBindingMessage

Expand Down Expand Up @@ -2687,7 +2707,7 @@ PKCE configuration such as available methods and policy check on required use of

### pkce.methods

fine-tune the supported code challenge methods. Supported values are
Fine-tune the supported code challenge methods. Supported values are
- `S256`
- `plain`

Expand Down
10 changes: 10 additions & 0 deletions lib/actions/authorization/backchannel_request_response.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ module.exports = async function backchannelRequestResponse(ctx, next) {
scope: [...ctx.oidc.requestParamScopes].join(' '),
});

// eslint-disable-next-line default-case
switch (request.resource.length) {
case 0:
delete request.resource;
break;
case 1:
[request.resource] = request.resource;
break;
}

ctx.oidc.entity('BackchannelAuthenticationRequest', request);

const id = await request.save();
Expand Down
19 changes: 7 additions & 12 deletions lib/actions/grants/ciba.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,13 @@ const {
AuthorizationPending,
ExpiredToken,
InvalidGrant,
UnauthorizedClient,
} = errors;

const gty = 'ciba';

module.exports.handler = async function cibaHandler(ctx, next) {
presence(ctx, 'auth_req_id');

if (ctx.oidc.client.backchannelTokenDeliveryMode === 'push') {
throw new UnauthorizedClient('client is registered with a push delivery mode');
}

const {
issueRefreshToken,
conformIdTokenClaims,
Expand Down Expand Up @@ -117,11 +112,11 @@ module.exports.handler = async function cibaHandler(ctx, next) {
const at = new AccessToken({
accountId: account.accountId,
client: ctx.oidc.client,
expiresWithSession: request.expiresWithSession, // ?
expiresWithSession: request.expiresWithSession,
grantId: request.grantId,
gty,
sessionUid: request.sessionUid, // ?
sid: request.sid, // ?
sessionUid: request.sessionUid,
sid: request.sid,
});

if (ctx.oidc.client.tlsClientCertificateBoundAccessTokens) {
Expand Down Expand Up @@ -164,15 +159,15 @@ module.exports.handler = async function cibaHandler(ctx, next) {
authTime: request.authTime,
claims: request.claims,
client: ctx.oidc.client,
expiresWithSession: request.expiresWithSession, // ?
expiresWithSession: request.expiresWithSession,
grantId: request.grantId,
gty,
nonce: request.nonce,
resource: request.resource,
rotations: 0,
scope: request.scope,
sessionUid: request.sessionUid, // ?
sid: request.sid, // ?
sessionUid: request.sessionUid,
sid: request.sid,
});

if (ctx.oidc.client.tokenEndpointAuthMethod === 'none') {
Expand Down Expand Up @@ -214,7 +209,7 @@ module.exports.handler = async function cibaHandler(ctx, next) {
token.set('nonce', request.nonce);
token.set('at_hash', accessToken);
token.set('urn:openid:params:jwt:claim:rt_hash', refreshToken);
token.set('sid', request.sid); // ?
token.set('sid', request.sid);
token.set('urn:openid:params:jwt:claim:auth_req_id', ctx.oidc.params.auth_req_id);

idToken = await token.issue({ use: 'idtoken' });
Expand Down
2 changes: 1 addition & 1 deletion lib/helpers/configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ class Configuration {
this.setAlgs('idTokenEncryptionAlgValues', allowList.idTokenEncryptionAlgValues.slice());
this.setAlgs('idTokenEncryptionEncValues', allowList.idTokenEncryptionEncValues.slice(), 'encryption.enabled');

this.setAlgs('requestObjectSigningAlgValues', allowList.requestObjectSigningAlgValues.slice(), ['requestObjects.request', 'requestObjects.requestUri', 'pushedAuthorizationRequests.enabled']);
this.setAlgs('requestObjectSigningAlgValues', allowList.requestObjectSigningAlgValues.slice(), ['requestObjects.request', 'requestObjects.requestUri', 'pushedAuthorizationRequests.enabled', 'ciba.enabled']);
this.setAlgs('requestObjectEncryptionAlgValues', allowList.requestObjectEncryptionAlgValues.filter(RegExp.prototype.test.bind(/^(A|P|dir$)/)), 'encryption.enabled', ['requestObjects.request', 'requestObjects.requestUri', 'pushedAuthorizationRequests.enabled']);
this.setAlgs('requestObjectEncryptionEncValues', allowList.requestObjectEncryptionEncValues.slice(), 'encryption.enabled', ['requestObjects.request', 'requestObjects.requestUri', 'pushedAuthorizationRequests.enabled']);

Expand Down
22 changes: 20 additions & 2 deletions lib/helpers/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -933,7 +933,7 @@ function getDefaults() {
/*
* features.ciba.deliveryModes
*
* description: fine-tune the supported token delivery modes. Supported values are
* description: Fine-tune the supported token delivery modes. Supported values are
* - `poll`
* - `ping`
*
Expand All @@ -946,6 +946,24 @@ function getDefaults() {
* description: Helper function used to trigger the authentication and authorization on end-user's Authentication Device. It is called after
* accepting the backchannel authentication request but before sending client back the response.
*
* When the end-user authenticates use `provider.backchannelResult()` to finish the Consumption Device login process.
*
* example: `provider.backchannelResult()` method
*
* `backchannelResult` is a method on the Provider prototype, it returns a `Promise` with no fulfillment value.
*
* ```js
* const provider = new Provider(...);
* await provider.backchannelResult(...);
* ```
*
* `backchannelResult(request, result[, options]);`
* - `request` BackchannelAuthenticationRequest - BackchannelAuthenticationRequest instance.
* - `result` Grant | OIDCProviderError - instance of a persisted Grant model or an OIDCProviderError (all exported by Provider.errors).
* - `options.acr?`: string - Authentication Context Class Reference value that identifies the Authentication Context Class that the authentication performed satisfied.
* - `options.amr?`: string[] - Identifiers for authentication methods used in the authentication.
* - `options.authTime?`: number - Time when the End-User authentication occurred.
*
*/
triggerAuthenticationDevice,

Expand Down Expand Up @@ -2062,7 +2080,7 @@ function getDefaults() {
/*
* pkce.methods
*
* description: fine-tune the supported code challenge methods. Supported values are
* description: Fine-tune the supported code challenge methods. Supported values are
* - `S256`
* - `plain`
*/
Expand Down
1 change: 1 addition & 0 deletions lib/helpers/resolve_resource.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module.exports = async (ctx, model, config, scopes = model.scopes) => {
resource = ctx.oidc.params.resource;
break;
case !model.resource:
case Array.isArray(model.resource) && model.resource.length === 0:
break;
case model.resource && !!config.resourceIndicators.useGrantedResource(ctx, model):
case !ctx.oidc.params.resource && (!config.userinfo.enabled || !scopes.has('openid')):
Expand Down
2 changes: 1 addition & 1 deletion lib/models/backchannel_authentication_request.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const storesAuth = require('./mixins/stores_auth');
module.exports = (provider) => class BackchannelAuthenticationRequest extends apply([
consumable,
hasGrantId,
isSessionBound(provider), // ?
isSessionBound(provider),
storesAuth,
hasFormat(provider, 'BackchannelAuthenticationRequest', provider.BaseToken),
]) {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
"lodash": "^4.17.20",
"middie": "^5.2.0",
"mocha": "^8.2.0",
"mocha.parallel": "^0.15.6",
"mocha.parallel": "panva/mocha.parallel",
"moment": "^2.29.1",
"nock": "^13.0.4",
"sinon": "^10.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,23 @@ merge(config.features, {
clientCredentials: { enabled: true },
introspection: { enabled: true },
deviceFlow: { enabled: true },
ciba: {
enabled: true,
processLoginHint(ctx, loginHint) {
return loginHint;
},
validateBindingMessage() {},
validateRequestContext() {},
verifyUserCode() {},
async triggerAuthenticationDevice(ctx, request) {
const grant = new ctx.oidc.provider.Grant({
clientId: request.clientId, accountId: request.accountId,
});
grant.addOIDCScope(ctx.oidc.requestParamScopes);
await grant.save();
return ctx.oidc.provider.backchannelResult(request, grant.jti);
},
},
});

module.exports = {
Expand All @@ -26,9 +43,11 @@ module.exports = {
'authorization_code',
'refresh_token',
'urn:ietf:params:oauth:grant-type:device_code',
'urn:openid:params:grant-type:ciba',
'client_credentials',
],
response_types: ['code'],
backchannel_token_delivery_mode: 'poll',
redirect_uris: ['https://client.example.com/cb'],
tls_client_certificate_bound_access_tokens: true,
},
Expand All @@ -37,9 +56,11 @@ module.exports = {
grant_types: [
'authorization_code',
'urn:ietf:params:oauth:grant-type:device_code',
'urn:openid:params:grant-type:ciba',
'refresh_token',
],
response_types: ['code'],
backchannel_token_delivery_mode: 'poll',
redirect_uris: ['https://client.example.com/cb'],
token_endpoint_auth_method: 'none',
tls_client_certificate_bound_access_tokens: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,89 @@ describe('features.mTLS.certificateBoundAccessTokens', () => {
});
});

describe('urn:openid:params:grant-type:ciba', () => {
beforeEach(async function () {
await this.agent.post('/backchannel')
.auth('client', 'secret')
.send({
scope: 'openid offline_access',
login_hint: 'accountId',
})
.type('form')
.expect(200)
.expect(({ body: { auth_req_id: reqId } }) => {
this.reqId = reqId;
});
});

it('binds the access token to the certificate', async function () {
const spy = sinon.spy();
this.provider.once('grant.success', spy);

await this.agent.post('/token')
.auth('client', 'secret')
.send({
grant_type: 'urn:openid:params:grant-type:ciba',
auth_req_id: this.reqId,
})
.type('form')
.set('x-ssl-client-cert', crt.replace(RegExp('\\r?\\n', 'g'), ''))
.expect(200);

expect(spy).to.have.property('calledOnce', true);
const { oidc: { entities: { AccessToken, RefreshToken } } } = spy.args[0][0];
expect(AccessToken).to.have.property('x5t#S256', expectedS256);
expect(RefreshToken).not.to.have.property('x5t#S256');
});

it('verifies the request made with mutual-TLS', async function () {
const spy = sinon.spy();
this.provider.once('grant.error', spy);

await this.agent.post('/token')
.auth('client', 'secret')
.send({
grant_type: 'urn:openid:params:grant-type:ciba',
auth_req_id: this.reqId,
})
.type('form')
.expect(400)
.expect({ error: 'invalid_grant', error_description: 'grant request is invalid' });

expect(spy).to.have.property('calledOnce', true);
expect(spy.args[0][1]).to.have.property('error_detail', 'mutual TLS client certificate not provided');
});

it('binds the refresh token to the certificate for public clients', async function () {
const spy = sinon.spy();
this.provider.once('grant.success', spy);

// changes the code to client-none
this.TestAdapter.for('BackchannelAuthenticationRequest').syncUpdate(this.getTokenJti(this.reqId), {
clientId: 'client-none',
});
const { grantId } = this.TestAdapter.for('BackchannelAuthenticationRequest').syncFind(this.getTokenJti(this.reqId));
this.TestAdapter.for('Grant').syncUpdate(grantId, {
clientId: 'client-none',
});

await this.agent.post('/token')
.send({
client_id: 'client-none',
grant_type: 'urn:openid:params:grant-type:ciba',
auth_req_id: this.reqId,
})
.type('form')
.set('x-ssl-client-cert', crt.replace(RegExp('\\r?\\n', 'g'), ''))
.expect(200);

expect(spy).to.have.property('calledOnce', true);
const { oidc: { entities: { AccessToken, RefreshToken } } } = spy.args[0][0];
expect(AccessToken).to.have.property('x5t#S256', expectedS256);
expect(RefreshToken).to.have.property('x5t#S256', expectedS256);
});
});

describe('authorization flow', () => {
before(function () { return this.login({ scope: 'openid offline_access' }); });
bootstrap.skipConsent();
Expand Down
Loading

0 comments on commit 75133bb

Please sign in to comment.