Skip to content

Commit

Permalink
feat: update FAPI RW behaviours
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed Sep 21, 2019
1 parent c663417 commit a7ed27a
Show file tree
Hide file tree
Showing 12 changed files with 146 additions and 35 deletions.
16 changes: 16 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,22 @@ jobs:
CONFIGURATION: ./certification/fapi/mtls.json
SUITE_BASE_URL: https://localhost.emobix.co.uk:8443
NODE_TLS_REJECT_UNAUTHORIZED: 0
# - name: Run private_key_jwt-jarm variant
# run: node certification/fapi/runner
# env:
# PLAN_NAME: fapi-rw-id2-test-plan
# VARIANT: private_key_jwt-jarm
# CONFIGURATION: ./certification/fapi/pkjwt.json
# SUITE_BASE_URL: https://localhost.emobix.co.uk:8443
# NODE_TLS_REJECT_UNAUTHORIZED: 0
# - name: Run mtls-jarm variant
# run: node certification/fapi/runner
# env:
# PLAN_NAME: fapi-rw-id2-test-plan
# VARIANT: mtls-jarm
# CONFIGURATION: ./certification/fapi/mtls.json
# SUITE_BASE_URL: https://localhost.emobix.co.uk:8443
# NODE_TLS_REJECT_UNAUTHORIZED: 0

deploy:
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions certification/fapi/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ class API {
const { status, result } = await this.getModuleInfo({ moduleId });
debug('module id %s status is %s', moduleId, status);
if (FINISHED.has(status)) {
if (!status || !result) continue; // eslint-disable-line no-continue
if (!RESULTS.has(result)) {
throw new Error(`module id ${moduleId} is ${status} but ${result}`);
}
Expand Down
34 changes: 25 additions & 9 deletions certification/fapi/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,13 @@ const fapi = new Provider(ISSUER, {
clients: [
{
client_id: 'pkjwt-one',
response_types: ['code', 'code id_token'],
grant_types: ['implicit', 'authorization_code', 'refresh_token'],
redirect_uris: [
`${SUITE_BASE_URL}/test/a/pkjwt/callback`,
`${SUITE_BASE_URL}/test/a/pkjwt/callback?dummy1=lorem&dummy2=ipsum`,
`${SUITE_BASE_URL}/test/a/oidc-provider-pkjwt/callback`,
'https://staging.certification.openid.net/test/a/oidc-provider-pkjwt/callback',
`${SUITE_BASE_URL}/test/a/oidc-provider-pkjwt/callback?dummy1=lorem&dummy2=ipsum`,
'https://staging.certification.openid.net/test/a/oidc-provider-pkjwt/callback?dummy1=lorem&dummy2=ipsum',
],
token_endpoint_auth_method: 'private_key_jwt',
jwks: {
Expand All @@ -75,9 +79,13 @@ const fapi = new Provider(ISSUER, {
},
{
client_id: 'pkjwt-two',
response_types: ['code', 'code id_token'],
grant_types: ['implicit', 'authorization_code', 'refresh_token'],
redirect_uris: [
`${SUITE_BASE_URL}/test/a/pkjwt/callback`,
`${SUITE_BASE_URL}/test/a/pkjwt/callback?dummy1=lorem&dummy2=ipsum`,
`${SUITE_BASE_URL}/test/a/oidc-provider-pkjwt/callback`,
'https://staging.certification.openid.net/test/a/oidc-provider-pkjwt/callback',
`${SUITE_BASE_URL}/test/a/oidc-provider-pkjwt/callback?dummy1=lorem&dummy2=ipsum`,
'https://staging.certification.openid.net/test/a/oidc-provider-pkjwt/callback?dummy1=lorem&dummy2=ipsum',
],
token_endpoint_auth_method: 'private_key_jwt',
jwks: {
Expand All @@ -86,9 +94,13 @@ const fapi = new Provider(ISSUER, {
},
{
client_id: 'mtls-one',
response_types: ['code', 'code id_token'],
grant_types: ['implicit', 'authorization_code', 'refresh_token'],
redirect_uris: [
`${SUITE_BASE_URL}/test/a/mtls/callback`,
`${SUITE_BASE_URL}/test/a/mtls/callback?dummy1=lorem&dummy2=ipsum`,
`${SUITE_BASE_URL}/test/a/oidc-provider-mtls/callback`,
'https://staging.certification.openid.net/test/a/oidc-provider-mtls/callback',
`${SUITE_BASE_URL}/test/a/oidc-provider-mtls/callback?dummy1=lorem&dummy2=ipsum`,
'https://staging.certification.openid.net/test/a/oidc-provider-mtls/callback?dummy1=lorem&dummy2=ipsum',
],
token_endpoint_auth_method: 'self_signed_tls_client_auth',
jwks: {
Expand All @@ -97,9 +109,13 @@ const fapi = new Provider(ISSUER, {
},
{
client_id: 'mtls-two',
response_types: ['code', 'code id_token'],
grant_types: ['implicit', 'authorization_code', 'refresh_token'],
redirect_uris: [
`${SUITE_BASE_URL}/test/a/mtls/callback`,
`${SUITE_BASE_URL}/test/a/mtls/callback?dummy1=lorem&dummy2=ipsum`,
`${SUITE_BASE_URL}/test/a/oidc-provider-mtls/callback`,
'https://staging.certification.openid.net/test/a/oidc-provider-mtls/callback',
`${SUITE_BASE_URL}/test/a/oidc-provider-mtls/callback?dummy1=lorem&dummy2=ipsum`,
'https://staging.certification.openid.net/test/a/oidc-provider-mtls/callback?dummy1=lorem&dummy2=ipsum',
],
token_endpoint_auth_method: 'self_signed_tls_client_auth',
jwks: {
Expand Down Expand Up @@ -152,7 +168,7 @@ const fapi = new Provider(ISSUER, {
},
revocation: { enabled: true },
},
responseTypes: ['code id_token'],
responseTypes: ['code id_token', 'code'],
tokenEndpointAuthMethods,
whitelistedJWA: {
authorizationSigningAlgValues: ALGS,
Expand Down
6 changes: 3 additions & 3 deletions certification/fapi/mtls.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"alias": "mtls",
"alias": "oidc-provider-mtls",
"server": {
"discoveryUrl": "https://fapi.panva.cz/.well-known/openid-configuration"
},
Expand Down Expand Up @@ -36,7 +36,7 @@
},
"client2": {
"client_id": "mtls-two",
"scope": "openid",
"scope": "openid offline_access",
"jwks": {
"keys": [
{
Expand Down Expand Up @@ -111,7 +111,7 @@
},
{
"task": "Verify Complete",
"match": "https://*/test/a/mtls/callback*",
"match": "https://*/test/a/oidc-provider-mtls/callback*",
"commands": [
[
"wait",
Expand Down
6 changes: 3 additions & 3 deletions certification/fapi/pkjwt.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"alias": "pkjwt",
"alias": "oidc-provider-pkjwt",
"server": {
"discoveryUrl": "https://fapi.panva.cz/.well-known/openid-configuration"
},
Expand Down Expand Up @@ -31,7 +31,7 @@
},
"client2": {
"client_id": "pkjwt-two",
"scope": "openid",
"scope": "openid offline_access",
"jwks": {
"keys": [
{
Expand Down Expand Up @@ -101,7 +101,7 @@
},
{
"task": "Verify Complete",
"match": "https://*/test/a/pkjwt/callback*",
"match": "https://*/test/a/oidc-provider-pkjwt/callback*",
"commands": [
[
"wait",
Expand Down
12 changes: 10 additions & 2 deletions lib/actions/authorization/check_response_mode.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const { isFrontChannel } = require('../../helpers/resolve_response_mode');
*
* @throws: invalid_request
*/
module.exports = function checkResponseMode(ctx, next) {
module.exports = function checkResponseMode(ctx, next, forceCheck) {
const { params, client } = ctx.oidc;

const frontChannel = isFrontChannel(params.response_type);
Expand All @@ -23,8 +23,10 @@ module.exports = function checkResponseMode(ctx, next) {
throw new UnsupportedResponseMode();
}

const JWT = /jwt/.test(mode);

if (
mode !== undefined && mode.includes('jwt')
mode !== undefined && JWT
&& (
/^HS/.test(client.authorizationSignedResponseAlg)
|| /^(A|P|dir$)/.test(client.authorizationEncryptedResponseAlg)
Expand All @@ -45,5 +47,11 @@ module.exports = function checkResponseMode(ctx, next) {
throw new InvalidRequest('response_mode not allowed for this response_type unless encrypted');
}

if (params.response_type && instance(ctx.oidc.provider).configuration('features.fapiRW.enabled')) {
if ((!params.request || forceCheck) && !params.response_type.includes('id_token') && !JWT) {
throw new InvalidRequest('response_mode not allowed for this response_type in FAPI mode');
}
}

return next();
};
19 changes: 16 additions & 3 deletions lib/actions/authorization/oidc_required.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const instance = require('../../helpers/weak_cache');
const presence = require('../../helpers/validate_presence');

/*
Expand All @@ -10,13 +11,25 @@ const presence = require('../../helpers/validate_presence');
module.exports = function oidcRequired(ctx, next) {
const { params } = ctx.oidc;

const required = new Set(['redirect_uri']);

// Check for nonce if implicit or hybrid flow responding with id_token issued by the authorization
// endpoint
if (typeof params.response_type === 'string' && params.response_type.includes('id_token')) {
presence(ctx, 'redirect_uri', 'nonce');
} else {
presence(ctx, 'redirect_uri');
required.add('nonce');
}

// TODO: add the following once https://bitbucket.org/openid/fapi/issues/270/jarm-fapi-rw-openid-client-session-binding
// is resolved and the FAPI suite updated
// else if (instance(ctx.oidc.provider).configuration('features.fapiRW.enabled')) {
// required.add('state');
// }

if (instance(ctx.oidc.provider).configuration('features.fapiRW.enabled')) {
required.add(ctx.oidc.requestParamScopes.has('openid') ? 'nonce' : 'state');
}

presence(ctx, ...required);

return next();
};
15 changes: 11 additions & 4 deletions lib/actions/authorization/process_request_object.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,16 @@ module.exports = async function processRequestObject(PARAM_LIST, rejectDupesMidd
params.state = request.state;
}

if (request.response_mode !== undefined) {
params.response_mode = request.response_mode;
checkResponseMode(ctx, () => {});
const fapiRW = conf('features.fapiRW.enabled');

if (request.response_mode !== undefined || fapiRW) {
if (request.response_mode !== undefined) {
params.response_mode = request.response_mode;
}
if (request.response_type !== undefined) {
params.response_type = request.response_type;
}
checkResponseMode(ctx, () => {}, fapiRW);
}

if (request.request !== undefined || request.request_uri !== undefined) {
Expand Down Expand Up @@ -142,7 +149,7 @@ module.exports = async function processRequestObject(PARAM_LIST, rejectDupesMidd
ignoreAzp: true,
};

if (conf('features.fapiRW.enabled')) {
if (fapiRW) {
if (!('exp' in payload)) {
throw new InvalidRequestObject('Request Object is missing the "exp" claim');
}
Expand Down
2 changes: 1 addition & 1 deletion lib/helpers/features.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const DRAFTS = new Map(Object.entries({
name: 'Financial-grade API - Part 2: Read and Write API Security Profile',
type: 'OIDF FAPI Working Group draft',
url: 'https://openid.net/specs/openid-financial-api-part-2-ID2.html',
version: 'id02-rev.2',
version: 'id02-rev.3',
},
mTLS: {
name: 'OAuth 2.0 Mutual TLS Client Authentication and Certificate-Bound Access Tokens - draft 17',
Expand Down
2 changes: 1 addition & 1 deletion lib/models/id_token.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ module.exports = function getIdToken(provider) {
alg = client.authorizationSignedResponseAlg;
signOptions = {
audience: client.clientId,
expiresIn: 60,
expiresIn: 120,
issuer: provider.issuer,
noIat: true,
};
Expand Down
5 changes: 3 additions & 2 deletions test/fapirw/fapirw.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const config = clone(require('../default.config'));

config.features = {
fapiRW: { enabled: true },
jwtResponseModes: { enabled: true },
requestObjects: {
request: true,
mergingStrategy: { name: 'strict' },
Expand All @@ -17,8 +18,8 @@ module.exports = {
config,
clients: [{
client_id: 'client',
response_types: ['id_token'],
grant_types: ['implicit'],
response_types: ['code id_token', 'code'],
grant_types: ['implicit', 'authorization_code'],
redirect_uris: ['https://client.example.com/cb'],
token_endpoint_auth_method: 'none',
}],
Expand Down
63 changes: 56 additions & 7 deletions test/fapirw/fapirw.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,57 @@ describe('Financial-grade API - Part 2: Read and Write API Security Profile beha
});
});

describe('FAPI Mode Authorization Request', () => {
beforeEach(function () { return this.login(); });
afterEach(function () { return this.logout(); });

it('requires jwt response mode to be used when id token is not issued by authorization endpoint', function () {
const auth = new this.AuthorizationRequest({
scope: 'openid',
client_id: 'client',
response_type: 'code',
nonce: 'foo', // TODO: see oidc_required.js
});

return this.wrap({
agent: this.agent,
route: '/auth',
verb: 'get',
auth,
})
.expect(302)
.expect(auth.validateClientLocation)
.expect(auth.validateError('invalid_request'))
.expect(auth.validateErrorDescription('response_mode not allowed for this response_type in FAPI mode'));
});

it('requires jwt response mode to be used when id token is not issued by authorization endpoint (JAR)', function () {
const request = `${base64url.encode(JSON.stringify({ alg: 'none' }))}.${base64url.encode(JSON.stringify({
scope: 'openid',
client_id: 'client',
response_type: 'code',
nonce: 'foo', // TODO: see oidc_required.js
exp: epochTime() + 60,
}))}.`;

const auth = new this.AuthorizationRequest({
request,
state: undefined,
});

return this.wrap({
agent: this.agent,
route: '/auth',
verb: 'get',
auth,
})
.expect(302)
.expect(auth.validateClientLocation)
.expect(auth.validateError('invalid_request'))
.expect(auth.validateErrorDescription('response_mode not allowed for this response_type in FAPI mode'));
});
});

describe('Request Object', () => {
beforeEach(function () { return this.login(); });
afterEach(function () { return this.logout(); });
Expand All @@ -39,7 +90,7 @@ describe('Financial-grade API - Part 2: Read and Write API Security Profile beha
const request = `${base64url.encode(JSON.stringify({ alg: 'none' }))}.${base64url.encode(JSON.stringify({
client_id: 'client',
scope: 'openid',
response_type: 'id_token',
response_type: 'code id_token',
nonce: 'foo',
redirect_uri: 'https://client.example.com/cb',
state: 'foo',
Expand All @@ -50,7 +101,7 @@ describe('Financial-grade API - Part 2: Read and Write API Security Profile beha
request,
scope: 'openid',
client_id: 'client',
response_type: 'id_token',
response_type: 'code id_token',
nonce: 'foo',
state: 'foo',
});
Expand All @@ -63,24 +114,23 @@ describe('Financial-grade API - Part 2: Read and Write API Security Profile beha
})
.expect(302)
.expect(auth.validateFragment)
.expect(auth.validatePresence(['id_token', 'state']))
.expect(auth.validateState)
.expect(auth.validatePresence(['id_token', 'code', 'state']))
.expect(auth.validateClientLocation);
});

it('requires exp to be provided in the Request Object', function () {
const request = `${base64url.encode(JSON.stringify({ alg: 'none' }))}.${base64url.encode(JSON.stringify({
client_id: 'client',
scope: 'openid',
response_type: 'id_token',
response_type: 'code id_token',
nonce: 'foo',
}))}.`;

const auth = new this.AuthorizationRequest({
request,
scope: 'openid',
client_id: 'client',
response_type: 'id_token',
response_type: 'code id_token',
nonce: 'foo',
});

Expand All @@ -93,7 +143,6 @@ describe('Financial-grade API - Part 2: Read and Write API Security Profile beha
.expect(302)
.expect(auth.validateFragment)
.expect(auth.validatePresence(['error', 'error_description', 'state']))
.expect(auth.validateState)
.expect(auth.validateClientLocation)
.expect(auth.validateError('invalid_request_object'))
.expect(auth.validateErrorDescription('Request Object is missing the "exp" claim'));
Expand Down

0 comments on commit a7ed27a

Please sign in to comment.