Skip to content

Commit

Permalink
feat(fapi): FAPI (Final and ID2) is now a stable feature
Browse files Browse the repository at this point in the history
Note: Updates to draft specification versions are released as MINOR
library versions, if you utilize these specification implementations
consider using the tilde `~` operator in your package.json since breaking
changes may be introduced as part of these version updates.
Alternatively, [acknowledge](/docs/README.md#features) the version and
be notified of breaking changes as part of your CI.

BREAKING CHANGE: Draft feature `fapiRW` was replaced by a stable `fapi`
feature.
BREAKING CHANGE: The default profile for the new `fapi` feature is
Financial-grade API Security Profile 1.0 - Part 2: Advanced (Final) rather than
Financial-grade API - Part 2: Read and Write API Security Profile (ID2).
ID2 albeit being an Implementer's Draft remains a possible
`features.fapi.profile` option
  • Loading branch information
panva committed May 26, 2021
1 parent 75133bb commit 4f52a4c
Show file tree
Hide file tree
Showing 18 changed files with 464 additions and 69 deletions.
68 changes: 64 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ jobs:
response_type: code
client_registration: dynamic_client

# FAPI RW-ID2
# FAPI 1.0 R/W (ID2)
- plan: fapi-rw-id2-test-plan
configuration: ./certification/fapi/pkjwt.json
fapi_auth_request_method: by_value
Expand Down Expand Up @@ -191,6 +191,56 @@ jobs:
fapi_profile: plain_fapi
fapi_response_mode: jarm

# FAPI 1.0 Advanced (Final)
- plan: fapi1-advanced-final-test-plan
configuration: ./certification/fapi/pkjwt.json
fapi_auth_request_method: by_value
client_auth_type: private_key_jwt
fapi_profile: plain_fapi
fapi_response_mode: plain_response
- plan: fapi1-advanced-final-test-plan
configuration: ./certification/fapi/pkjwt.json
fapi_auth_request_method: pushed
client_auth_type: private_key_jwt
fapi_profile: plain_fapi
fapi_response_mode: plain_response
- plan: fapi1-advanced-final-test-plan
configuration: ./certification/fapi/pkjwt.json
fapi_auth_request_method: by_value
client_auth_type: private_key_jwt
fapi_profile: plain_fapi
fapi_response_mode: jarm
- plan: fapi1-advanced-final-test-plan
configuration: ./certification/fapi/pkjwt.json
fapi_auth_request_method: pushed
client_auth_type: private_key_jwt
fapi_profile: plain_fapi
fapi_response_mode: jarm
- plan: fapi1-advanced-final-test-plan
configuration: ./certification/fapi/mtls.json
fapi_auth_request_method: by_value
client_auth_type: mtls
fapi_profile: plain_fapi
fapi_response_mode: plain_response
- plan: fapi1-advanced-final-test-plan
configuration: ./certification/fapi/mtls.json
fapi_auth_request_method: pushed
client_auth_type: mtls
fapi_profile: plain_fapi
fapi_response_mode: plain_response
- plan: fapi1-advanced-final-test-plan
configuration: ./certification/fapi/mtls.json
fapi_auth_request_method: by_value
client_auth_type: mtls
fapi_profile: plain_fapi
fapi_response_mode: jarm
- plan: fapi1-advanced-final-test-plan
configuration: ./certification/fapi/mtls.json
fapi_auth_request_method: pushed
client_auth_type: mtls
fapi_profile: plain_fapi
fapi_response_mode: jarm

# FAPI RW-CIBA-ID1
- plan: fapi-ciba-id1-test-plan
configuration: ./certification/fapi/pkjwt.json
Expand Down Expand Up @@ -348,16 +398,26 @@ jobs:
if: ${{ steps.node_modules.outputs.cache-hit != 'true' }}
- name: Run oidc-provider (OIDC)
run: npx c8 node certification/docker &
if: ${{ startsWith(matrix.setup.plan, 'oidcc') }}
if: ${{ startsWith(matrix.setup.plan, 'oidcc-') }}
env:
PORT: 3000
ISSUER: https://172.17.0.1:3000
NODE_TLS_REJECT_UNAUTHORIZED: 0
- name: Run oidc-provider (FAPI)
- name: Run oidc-provider (FAPI 1.0 R/W - ID2)
run: npx c8 node certification/fapi &
if: ${{ startsWith(matrix.setup.plan, 'fapi-') }}
env:
ISSUER: https://172.17.0.1:3000
PORT: 3000
PROFILE: '1.0 ID2'
NODE_OPTIONS: --tls-cipher-list="ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-RSA-AES128-GCM-SHA256 ECDHE-RSA-AES256-GCM-SHA384"
NODE_TLS_REJECT_UNAUTHORIZED: 0
- name: Run oidc-provider (FAPI 1.0 Advanced - Final)
run: npx c8 node certification/fapi &
if: ${{ startsWith(matrix.setup.plan, 'fapi') }}
if: ${{ startsWith(matrix.setup.plan, 'fapi1-') }}
env:
ISSUER: https://172.17.0.1:3000
PROFILE: '1.0 Final'
PORT: 3000
NODE_OPTIONS: --tls-cipher-list="ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-RSA-AES128-GCM-SHA256 ECDHE-RSA-AES256-GCM-SHA384"
NODE_TLS_REJECT_UNAUTHORIZED: 0
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,12 @@ enabled by default, check the configuration section on how to enable them.
- [RFC8705 - OAuth 2.0 Mutual TLS Client Authentication and Certificate Bound Access Tokens (MTLS)][mtls]
- [RFC8707 - OAuth 2.0 Resource Indicators][resource-indicators]
- [JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens][jwt-at]
- [Financial-grade API Security Profile 1.0 - Part 2: Advanced (FAPI)][fapi]
- [Financial-grade API - Part 2: Read and Write API Security Profile (FAPI) - Implementer's Draft 02][fapi-id2]

The following draft specifications are implemented by oidc-provider.
- [JWT Response for OAuth Token Introspection - draft 10][jwt-introspection]
- [JWT Secured Authorization Response Mode for OAuth 2.0 (JARM) - Implementer's Draft 01][jarm]
- [Financial-grade API - Part 2: Read and Write API Security Profile (FAPI) - Implementer's Draft 02][fapi]
- [Financial-grade API: Client Initiated Backchannel Authentication Profile (FAPI-CIBA) - Implementer's Draft 01][fapi-ciba]
- [OAuth 2.0 Authorization Server Issuer Identifier in Authorization Response - draft 00][iss-auth-resp]
- [OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer (DPoP) - draft 03][dpop]
Expand Down Expand Up @@ -156,6 +157,7 @@ See the list of available emitted [event names](/docs/events.md) and their descr
[par]: https://tools.ietf.org/html/draft-ietf-oauth-par-06
[rpinitiated-logout]: https://openid.net/specs/openid-connect-rpinitiated-1_0-01.html
[iss-auth-resp]: https://tools.ietf.org/html/draft-ietf-oauth-iss-auth-resp-00
[fapi]: https://openid.net/specs/openid-financial-api-part-2-ID2.html
[fapi-id2]: https://openid.net/specs/openid-financial-api-part-2-ID2.html
[fapi]: https://openid.net/specs/openid-financial-api-part-2-1_0.html
[ciba]: https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0-03.html
[fapi-ciba]: https://openid.net/specs/openid-financial-api-ciba-ID1.html
5 changes: 4 additions & 1 deletion certification/fapi/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,10 @@ const fapi = new Provider(ISSUER, {
},
registration: { enabled: true },
registrationManagement: { enabled: true },
fapiRW: { enabled: true },
fapi: {
enabled: true,
profile: process.env.PROFILE ? process.env.PROFILE : '1.0 Final',
},
mTLS: {
enabled: true,
certificateBoundAccessTokens: true,
Expand Down
7 changes: 7 additions & 0 deletions certification/runner/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ if ('alias' in configuration) {
configuration.alias = `${configuration.alias}-${Object.values(JSON.parse(VARIANT)).join('-')}`;
}

if (PLAN_NAME === 'fapi1-advanced-final-test-plan') {
configuration.override = Object.entries(configuration.override).reduce((acc, [key, value]) => {
acc[key.replace('fapi-rw-id2', 'fapi1-advanced-final')] = value;
return acc;
}, {});
}

if (JSON.parse(VARIANT).client_registration === 'dynamic_client') {
delete configuration.alias;
}
Expand Down
43 changes: 28 additions & 15 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ location / {
- [devInteractions ❗](#featuresdevinteractions)
- [dPoP](#featuresdpop)
- [encryption](#featuresencryption)
- [fapiRW](#featuresfapirw)
- [fapi](#featuresfapi)
- [introspection](#featuresintrospection)
- [issAuthResp](#featuresissauthresp)
- [jwtIntrospection](#featuresjwtintrospection)
Expand Down Expand Up @@ -640,7 +640,7 @@ _**default value**_:

[OpenID Connect Client Initiated Backchannel Authentication Flow - Core 1.0 - draft-03](https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0-03.html)

Enables Core CIBA Flow, when combined with `features.fapiRW` enables [Financial-grade API: Client Initiated Backchannel Authentication Profile - Implementer's Draft 01](https://openid.net/specs/openid-financial-api-ciba-ID1.html) as well.
Enables Core CIBA Flow, when combined with `features.fapi` enables [Financial-grade API: Client Initiated Backchannel Authentication Profile - Implementer's Draft 01](https://openid.net/specs/openid-financial-api-ciba-ID1.html) as well.



Expand Down Expand Up @@ -1053,26 +1053,22 @@ _**default value**_:
}
```

### features.fapiRW
### features.fapi

[Financial-grade API - Part 2: Read and Write API Security Profile (FAPI) - Implementer's Draft 02](https://openid.net/specs/openid-financial-api-part-2-ID2.html)
Financial-grade API Security Profile

Enables extra behaviours defined in FAPI Part 1 & 2 that cannot be achieved by other configuration options, namely:
- Request Object `exp` claim is REQUIRED
- `userinfo_endpoint` becomes a FAPI resource, echoing back the x-fapi-interaction-id header and disabling query string as a mechanism for providing access tokens
Enables extra Authorization Server behaviours defined in FAPI that cannot be achieved by other configuration options.


_**recommendation**_: Updates to draft specification versions are released as MINOR library versions, if you utilize these specification implementations consider using the tilde `~` operator in your package.json since breaking changes may be introduced as part of these version updates. Alternatively, [acknowledge](#features) the version and be notified of breaking changes as part of your CI.


_**default value**_:
```js
{
ack: undefined,
enabled: false
enabled: false,
profile: '1.0 Final'
}
```
<a id="features-fapi-rw-other-configuration-needed-to-reach-fapi-levels"></a><details><summary>(Click to expand) other configuration needed to reach FAPI levels
<a id="features-fapi-other-configuration-needed-to-reach-fapi-conformance"></a><details><summary>(Click to expand) other configuration needed to reach FAPI conformance
</summary><br>


Expand All @@ -1085,12 +1081,29 @@ _**default value**_:
- `features.mTLS` and enable `selfSignedTlsClientAuth` and/or `tlsClientAuth`
- `features.claimsParameter`
- `features.requestObjects` and enable `request` and/or `request_uri`
- `features.requestObjects.mode` set to `strict`
- `enabledJWA`
- `enabledJWA` algorithm allow lists
- (optional) `features.pushedAuthorizationRequests`
- (optional) `features.jwtResponseModes`


</details>

<details><summary>(Click to expand) features.fapi options details</summary><br>


#### profile

The specific profile of FAPI to enable. Supported values are:
- '1.0 Final' (default) Enables behaviours from [Financial-grade API Security Profile 1.0 - Part 2: Advanced](https://openid.net/specs/openid-financial-api-part-2-1_0.html)
- '1.0 ID2' Enables behaviours from [Financial-grade API - Part 2: Read and Write API Security Profile - Implementer's Draft 02](https://openid.net/specs/openid-financial-api-part-2-ID2.html)
- Function returning one of the other supported values, or undefined if FAPI behaviours are to be ignored. The function is invoked with two arguments `(ctx, client)` and serves the purpose of allowing the used profile to be context-specific.


_**default value**_:
```js
'1.0 Final'
```

</details>

### features.introspection
Expand Down Expand Up @@ -1593,7 +1606,7 @@ _**default value**_:
defines the provider's strategy when it comes to using regular OAuth 2.0 parameters that are present. Parameters inside the Request Object are ALWAYS used, this option controls whether to combine those with the regular ones or not.
Supported values are:
- 'lax' (default) This is the behaviour expected by OIDC Core 1.0 - all parameters that are not present in the Resource Object are used when resolving the authorization request.
- 'strict' This is the behaviour expected by FAPI or JAR, all parameters outside of the Request Object are ignored.
- 'strict' This is the behaviour expected by FAPI or JAR, all parameters outside of the Request Object are ignored. For FAPI and FAPI-CIBA this value is enforced.



Expand Down
9 changes: 7 additions & 2 deletions lib/actions/authorization/check_pkce.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const checkFormat = require('../../helpers/pkce_format');
* - enforce PKCE use for native clients using hybrid or code flow
*/
module.exports = function checkPKCE(ctx, next) {
const { params } = ctx.oidc;
const { params, route } = ctx.oidc;
const { pkce } = instance(ctx.oidc.provider).configuration();

if (!params.code_challenge_method && params.code_challenge) {
Expand All @@ -31,7 +31,12 @@ module.exports = function checkPKCE(ctx, next) {

// checking for response_type presence disables the need for PKCE for device_code grant
if (typeof params.response_type === 'string' && params.response_type.includes('code')) {
if (!params.code_challenge && pkce.required(ctx, ctx.oidc.client)) {
if (
!params.code_challenge
&& (
pkce.required(ctx, ctx.oidc.client)
|| (ctx.oidc.fapiProfile === '1.0 Final' && route === 'pushed_authorization_request')
)) {
throw new InvalidRequest('Authorization Server policy requires PKCE to be used for this request');
}
}
Expand Down
2 changes: 1 addition & 1 deletion lib/actions/authorization/check_response_mode.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ module.exports = function checkResponseMode(ctx, next, forceCheck) {
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.response_type && ctx.oidc.fapiProfile !== undefined) {
if (((!params.request && !params.request_uri) || forceCheck) && !params.response_type.includes('id_token') && !JWT) {
throw new InvalidRequest('response_mode not allowed for this response_type in FAPI mode');
}
Expand Down
3 changes: 1 addition & 2 deletions lib/actions/authorization/oidc_required.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
const instance = require('../../helpers/weak_cache');
const presence = require('../../helpers/validate_presence');

/*
Expand All @@ -19,7 +18,7 @@ module.exports = function oidcRequired(ctx, next) {
required.add('nonce');
}

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

Expand Down
25 changes: 19 additions & 6 deletions lib/actions/authorization/process_request_object.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ module.exports = async function processRequestObject(PARAM_LIST, rejectDupesMidd
&& (
client.requireSignedRequestObject
|| (client.backchannelAuthenticationRequestSigningAlg && isBackchannelAuthentication)
|| (features.fapiRW.enabled && isBackchannelAuthentication)
|| (ctx.oidc.fapiProfile !== undefined && isBackchannelAuthentication)
)
) {
throw new InvalidRequest('Request Object must be used by this client');
Expand Down Expand Up @@ -110,14 +110,14 @@ module.exports = async function processRequestObject(PARAM_LIST, rejectDupesMidd
params.state = request.state;
}

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

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

if (features.fapiRW.enabled) {
if (ctx.oidc.fapiProfile !== undefined) {
if (!('exp' in payload)) {
throw new InvalidRequestObject("Request Object is missing the 'exp' claim");
}

if (ctx.oidc.fapiProfile === '1.0 Final') {
if (!('aud' in payload)) {
throw new InvalidRequestObject("Request Object is missing the 'aud' claim");
}
if (!('nbf' in payload)) {
throw new InvalidRequestObject("Request Object is missing the 'nbf' claim");
}
const diff = payload.exp - payload.nbf;
if (Math.sign(diff) !== 1 || diff > 3600) {
throw new InvalidRequestObject("Request Object 'exp' claim too far from 'nbf' claim");
}
}
}

if (isBackchannelAuthentication) {
Expand All @@ -180,7 +193,7 @@ module.exports = async function processRequestObject(PARAM_LIST, rejectDupesMidd
}
}

if (features.fapiRW.enabled) {
if (ctx.oidc.fapiProfile !== undefined) {
const diff = payload.exp - payload.nbf;
if (Math.sign(diff) !== 1 || diff > 3600) {
throw new InvalidRequestObject("Request Object 'exp' claim too far from 'nbf' claim");
Expand Down Expand Up @@ -241,7 +254,7 @@ module.exports = async function processRequestObject(PARAM_LIST, rejectDupesMidd

params.request = undefined;

const mode = isBackchannelAuthentication ? 'strict' : features.requestObjects.mode;
const mode = isBackchannelAuthentication || ctx.oidc.fapiProfile !== undefined ? 'strict' : features.requestObjects.mode;

switch (mode) {
case 'lax':
Expand Down
8 changes: 3 additions & 5 deletions lib/actions/userinfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,9 @@ module.exports = [
noCache,

function setFapiInteractionId(ctx, next) {
if (instance(ctx.oidc.provider).configuration('features.fapiRW.enabled')) {
const header = ctx.get('x-fapi-interaction-id');
if (header) {
ctx.set('x-fapi-interaction-id', header);
}
const header = ctx.get('x-fapi-interaction-id');
if (header && instance(ctx.oidc.provider).configuration('features.fapi.enabled')) {
ctx.set('x-fapi-interaction-id', header);
}

return next();
Expand Down
Loading

0 comments on commit 4f52a4c

Please sign in to comment.