Client assertion grant type with IBM API Connect

Tom Van Oppens
7 min readMay 28, 2020

--

Since the overhaul of native OAuth2 providers that came in API Connect v2018 (the one prior to v10) the OAuth2 provider became super flexible.

In this post I’ll implement support for the client_assertion grant type with a JWT as per https://tools.ietf.org/html/rfc7523. An assertion is a different way of identifying you application, there quite some flexibility of what you put in the assertion.

Some advantages.
- As the provider I don’t have to keep the secret belonging to the client
- if the token request is intercepted it’s less problematic as it can has an expiration time

I have chosen to keep it simple an only put the minimum required claims in there
- iss: issuer (our clientid)
- sub: subject (our clientid)
- aud: audience (the endpoint for whom this assertion is intended (full URL))
- exp: time (expiry time of the assertion in epoch seconds)

Below an example ( you can decode it at https://jwt.io )

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIwMDE2NDc5MzBhNTQ1NjRkMTQ5Zjg1OGMwMzIzMTEwOCIsInN1YiI6IjAwMTY0NzkzMGE1NDU2NGQxNDlmODU4YzAzMjMxMTA4IiwiYXVkIjoibXlvYXV0aHByb3ZpZGVydXJsIiwiZXhwIjoxNTE2MjM5MDIyfQ.lH-JmlzzdTtNrchMfb3fyYYcuq_LqS73fsjkMOvvIvgmm-X1bmd-EDDvTS-gOZ-H6jQXtF9Tvxwt0Q8-3FVmGO7IQg71hLCwVPngTO3e2qOZK6SlxTzbmvHmdWNmlwbjz0EmbKYHXxP5nMwREKvAig9qtgvNafle5JZVfQX9ZWkBbSrzuOJXrOeMsOdS0sx5HZggFc2S4Tqd8WgpawqA7CfZk61tJLkEEQn031dAY913aJTeXWw9pMipfBWQFDr2fWAZEuxp9yzsE5XAhaNDOgc5K0tQpolAy5ThY88KC2nTH2AQTQlvlnPw_IxeepeiYtD7K3oeoVnZ-KzAil09zw

So the request would look something like this.

curl -X POST ‘https://mygatewat/apic1/sandbox/oauth3/oauth2/token' \
-H ‘Content-Type: application/x-www-form-urlencoded’ \
— data-urlencode ‘grant_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer’ \
— data-urlencode ‘scope=scope1’ \
— data-urlencode ‘assertion=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIwMDE2NDc5MzBhNTQ1NjRkMTQ5Zjg1OGMwMzIzMTEwOCIsInN1YiI6IjAwMTY0NzkzMGE1NDU2NGQxNDlmODU4YzAzMjMxMTA4IiwiYXVkIjoibXlvYXV0aHByb3ZpZGVydXJsIiwiZXhwIjoxNTE2MjM5MDIyfQ.lH-JmlzzdTtNrchMfb3fyYYcuq_LqS73fsjkMOvvIvgmm-X1bmd-EDDvTS-gOZ-H6jQXtF9Tvxwt0Q8-3FVmGO7IQg71hLCwVPngTO3e2qOZK6SlxTzbmvHmdWNmlwbjz0EmbKYHXxP5nMwREKvAig9qtgvNafle5JZVfQX9ZWkBbSrzuOJXrOeMsOdS0sx5HZggFc2S4Tqd8WgpawqA7CfZk61tJLkEEQn031dAY913aJTeXWw9pMipfBWQFDr2fWAZEuxp9yzsE5XAhaNDOgc5K0tQpolAy5ThY88KC2nTH2AQTQlvlnPw_IxeepeiYtD7K3oeoVnZ-KzAil09zw’

What will happen in the OAuth provider is that rather than validating the client secret the provider will validate the assertion against the public key of the application (to keep it simple I have hard-coded the public JWK of the application in the verification code, normally you would take this specific to the client somewhere, but that’s outside the scope of this article)

Of-course the provider you find below isn’t fit for production use (I didn’t put in any error handling, optimizations or checked for vulnerabilities)

If you want help to achieve something like this and need some help, reach out to me or one of my colleagues at the IBM Cloud Integration Expert Labs

API Connect Native OAuth assembly with support for the assertion grant type
(below you will find the corresponding crypto material for the JWK used to validate the assertion (don’t worry , it’s throwaway crypto))

Alternatively you can use the wizard and select Resource owner — JWT in the grant types, this will generate a different assembly. The idea here is to show of the flexibility you have.

info:
description: ''
x-ibm-name: oauth2caf93f5c-bfd1-45f4-945a-dab73273ee96
version: 1.0.0
title: OAuth2
x-ibm-configuration:
gateway: datapower-api-gateway
assembly:
execute:
- switch:
version: 2.0.0
title: switch
case:
- condition: ($operationPath() = '/oauth2/token')
execute:
- gatewayscript:
version: 2.0.0
title: check if request is assertion
source: |
const apim = require("apim");
/* the context variable app is empty, in order to
access this information we call an api that
returns the metadata for the application
*/
apim.readInputAsBuffer(function (err, buffer) {
if (buffer) {
const requestString = buffer.toString("utf8");
console.error(requestString);
if (
requestString.includes(
"grant_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer"
)
) {
context.set("assertion", true);
console.error("set to true");
} else {
context.set("assertion", false);
console.error("set to false");
}
}
});
- switch:
version: 2.0.0
title: switch
case:
- condition: (assertion = false)
execute:
- oauth:
title: oauth-auto-generated-1
version: 2.0.0
description: >-
This oauth policy performs all OAuth/OpenID
Connect protocol steps that are needed for OAuth
Validation by default. The inputs and outputs of
each of the steps are driven by documented
context variables. Add or remove the Supported
OAuth Components as required.
oauth-provider-settings-ref:
default: OAuth2
supported-oauth-components:
- OAuthValidateRequest
- oauth:
title: oauth-auto-generated-3
version: 2.0.0
description: >-
This oauth policy performs all OAuth/OpenID
Connect protocol steps that are needed for token
path by default. The inputs and outputs of each
of the steps are driven by documented context
variables. Add or remove the Supported OAuth
Components as required.
oauth-provider-settings-ref:
default: OAuth2
supported-oauth-components:
- OAuthGenerateAccessToken
- OAuthCollectMetadata
- condition: (assertion = true)
execute:
- gatewayscript:
version: 2.0.0
title: >-
handle assertion and set variables to generate
token
source: >
const apim = require("apim");
const qs = require("querystring");const jwt = require("jwt");//this is the static object representing the
public key of the client
const jwkString =
' { "kty": "RSA", "kid": "signincertforassetion", "x5t": "pFYNC0f5U1K0oMOhtj-IOKf9bFA", "n": "rCh0FvgOTdgc356MZ7Zncf5udv4RE2OHRNBH-GL6RHnmBryip3RukTm3ZCn516uup_LFWeLJU7-iAGg6zyj_5WH3ICl_HIGiKt07zwkeCKIN1lonwC78Rbl1S9B4w5gnzoAdN_cVBs-IEBQP7xuUOKFx5dXl13XdvUQHqUC5q-0lieZuiQrGj475lvj9fjOTQ3FH5hmaEl30BEgjAunWW41-dqbsuzoeudRnMLYhmMbKDooDE0QBsJr0kGSXq_FOEFTr4fPRmtoaHaijgzqLouSu8QiCnvbsh6WGDTepzvsmgIHydSuvB0jlBq9pHyy4vlSWEi9aBdrTfIovsoY8Yw", "e": "AQAB" }';
const jwk = JSON.parse(jwkString);apim.readInputAsBuffer(function (err, buffer) {
if (buffer) {
const requestString = buffer.toString("utf8");
const request = qs.parse(requestString);
const decoder = new jwt.Decoder(request.assertion);
decoder.addOperation("verify", jwk);
decoder.decode((decodeError, decoded) => {
if (decodeError) {
console.error(
`An error occured while validating the request signature: ${decodeError}`
);
} else {
// see that the token is intended for us
const api = apim.getvariable("api");
//check if the audience matches
console.error("decoded.aud", decoded.aud);
console.error(
"expected aud",
`https://${api.endpoint.hostname}/${api.org.name}/${api.catalog.path}/${api.root}${api.operation.path}`
);
if (
decoded.aud ===
`https://${api.endpoint.hostname}/${api.org.name}/${api.catalog.path}/${api.root}${api.operation.path}`
) {
//check expiry
if (new Date(decoded.exp * 1000) >= new Date()) {
// set the variables to generate the token
const oauth = {
client_id: decoded.sub,
//dummy grant type that doesn't require the secret
grant_type: "authorization_code",
//we use this information to drive the clientid and other data int o the token
verified_code: {
client_id: decoded.sub,
resource_owner: decoded.sub,
scope: request.scope,
is_verified: true,
},
};
apim.setvariable("oauth.processing", oauth);
}else {
console.error('assertion expired')
}
}
}
});
}
});
- oauth:
title: oauth-auto-generated-3
version: 2.0.0
description: >-
This oauth policy performs all OAuth/OpenID
Connect protocol steps that are needed for token
path by default. The inputs and outputs of each
of the steps are driven by documented context
variables. Add or remove the Supported OAuth
Components as required.
oauth-provider-settings-ref:
default: OAuth2
supported-oauth-components:
- OAuthGenerateAccessToken
- OAuthCollectMetadata
- gatewayscript:
version: 2.0.0
title: strip refresh token
description: >-
we use a grant type that wouldn't generate a
refresh token, but because of our workarounds it
does
source: |-
const apim = require("apim");
let token = apim.getvariable("message.body");
delete token.refresh_token;
delete token.refresh_token_expires_in;
delete token.refresh_token_count;
apim.setvariable("message.body", token);

- otherwise:
- oauth:
title: oauth-auto-generated-1
version: 2.0.0
description: >-
This oauth policy performs all OAuth/OpenID Connect
protocol steps that are needed for OAuth Validation by
default. The inputs and outputs of each of the steps are
driven by documented context variables. Add or remove the
Supported OAuth Components as required.
oauth-provider-settings-ref:
default: OAuth2
supported-oauth-components:
- OAuthValidateRequest
- switch:
version: 2.0.0
title: oauth-auto-generated-switch
case:
- condition: ($operationPath() = '/oauth2/authorize')
execute:
- user-security:
title: user-security-auto-generated
version: 2.1.0
description: >-
This user security policy performs EI(basic) and
AU(auth url) check for oauth assembly. Change
the security check method as required
factor-id: default
extract-identity-method: redirect
ei-stop-on-error: true
user-auth-method: user-registry
au-stop-on-error: true
user-az-method: authenticated
az-stop-on-error: true
redirect-url: 'https://example.com'
redirect-time-limit: 300
user-registry: userandpasslettera
auth-response-headers-pattern: (?i)x-api*
auth-response-header-credential: X-API-Authenticated-Credential
- oauth:
title: oauth-auto-generated-2
version: 2.0.0
description: >-
This oauth policy performs all OAuth/OpenID
Connect protocol steps that are needed for az
code path by default. The inputs and outputs of
each of the steps are driven by documented
context variables. Add or remove the Supported
OAuth Components as required.
oauth-provider-settings-ref:
default: oauth2
supported-oauth-components:
- OAuthGenerateAZCode
- OAuthGenerateAccessToken
- OAuthVerifyAZCode
- OAuthCollectMetadata
- otherwise:
- oauth:
title: oauth-auto-generated-4
version: 2.0.0
description: >-
This oauth policy performs all OAuth/OpenID
Connect protocol steps that are needed for all
other paths by default. The inputs and outputs
of each of the steps are driven by documented
context variables. Add or remove the Supported
OAuth Components as required.
oauth-provider-settings-ref:
default: OAuth2
supported-oauth-components:
- OAuthIntrospectToken
- OAuthRevokeToken
catch: []
type: oauth
testable: true
enforced: true
phase: realized
cors:
enabled: true
properties: {}
swagger: '2.0'
host: $(catalog.host)
schemes:
- https
basePath: /oauth3
paths:
/oauth2/authorize:
get:
produces:
- text/html
summary: Endpoint for Authorization Code and Implicit grants
description: >-
This endpoint allows an access token (Implicit) or access code
(Authorization Code) request with the following parameters:
- Implicit (response_type = token, client_id, scope, redirect_uri(*), state(*))
- Authorization Code (response_type = code, client_id, scope, redirect_uri(*), state(*))
parameters:
- name: response_type
in: query
description: request an authorization code or an access token (implicit)
required: false
type: string
enum:
- code
- token
- name: client_id
in: query
description: client_id of the application which product is subscribed to
required: false
type: string
- name: scope
in: query
description: Scope being requested
type: string
required: false
- name: redirect_uri
in: query
type: string
description: URI where user is redirected to after authorization
required: false
- name: state
in: query
type: string
description: >-
This string will be echoed back to application when user is
redirected
required: false
responses:
'200':
description: An HTML form for authentication or authorization of this request.
'302':
description: >
Redirect to the clients redirect_uri containing one of the following
- **authorization code** for Authorization code grant- **access token** for Implicit grant- **error** in case of errors, such as the user has denied the
request
post:
consumes:
- application/x-www-form-urlencoded
produces:
- text/html
summary: Endpoint for Authorization Code and Implicit grants
description: Submit approval to access token.
security: []
parameters:
- name: response_type
in: formData
description: request an authorization code or an access token (implicit)
required: false
type: string
enum:
- code
- token
- name: client_id
in: formData
description: application requesting the access code or token
required: false
type: string
- name: scope
in: formData
description: requested scope of this authorization
required: false
type: string
- name: redirect_uri
in: formData
description: >-
URI the application is requesting this code or token to be
redirected to
required: false
type: string
responses:
'200':
description: 200 OK
/oauth2/token:
post:
consumes:
- application/x-www-form-urlencoded
produces:
- application/json
summary: >-
Endpoint for obtaining access token using Authorization code,
Application and Password grants
description: >-
This endpoint allows requesting an access token following one of the
flows below:
- Access Code (exchange access code for an access token)
- Client Credentials (2-legged, resource owner credentials are not obtained)
- Resource Owner Password Credentials (2-legged, client provides resource owner name and password)
- Refresh Token (exchange refresh token for a new access token)
parameters:
- name: grant_type
in: formData
required: true
type: string
enum:
- authorization_code
- password
- client_credentials
- refresh_token
responses:
'200':
description: 200 OK
definitions:
access_token_response:
type: object
additionalProperties: false
required:
- access_token
properties:
access_token:
type: string
securityDefinitions: {}
-----BEGIN CERTIFICATE-----
MIICvDCCAaSgAwIBAgIEXs6uGDANBgkqhkiG9w0BAQsFADAgMR4wHAYDVQQDDBVz
aWduaW5jZXJ0Zm9yYXNzZXRpb24wHhcNMjAwNTI3MTgxNDQ4WhcNMjEwNTI3MTgx
NDQ4WjAgMR4wHAYDVQQDDBVzaWduaW5jZXJ0Zm9yYXNzZXRpb24wggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCsKHQW+A5N2Bzfnoxntmdx/m52/hETY4dE
0Ef4YvpEeeYGvKKndG6RObdkKfnXq66n8sVZ4slTv6IAaDrPKP/lYfcgKX8cgaIq
3TvPCR4Iog3WWifALvxFuXVL0HjDmCfOgB039xUGz4gQFA/vG5Q4oXHl1eXXdd29
RAepQLmr7SWJ5m6JCsaPjvmW+P1+M5NDcUfmGZoSXfQESCMC6dZbjX52puy7Oh65
1GcwtiGYxsoOigMTRAGwmvSQZJer8U4QVOvh89Ga2hodqKODOoui5K7xCIKe9uyH
pYYNN6nO+yaAgfJ1K68HSOUGr2kfLLi+VJYSL1oF2tN8ii+yhjxjAgMBAAEwDQYJ
KoZIhvcNAQELBQADggEBADPin+FkQNYVcOW6lGX1tVKSuNiFvyi93PEZKTdOrWwH
pcW7RHHrJIocYnk2cS7aQOls6HtLsg7/P0bB35s1NbpcguA/1nk5TtxKrnuj0k0k
izgwVszNgqS1Am0rtp0wTR2asE+hCr3O7tqOrMvdC2yCUXwwR9z8pSy/5PtoLV8j
tFrjjEioaz+BR7Mna0q2w267phBsQ5Rfh4z5U5poPoa2fXf84OzUjvk3kMW1AWDu
FMnQ2uDNBxMPFy6YVK7XAKw5dKqaFzSMNE3jiRnYyH3Xgww5py8srSXWnm8tWqL6
otQL8dCYZMyeIUg0SP3OSZeEFYK645lXE4v9uK+j0+s=
-----END CERTIFICATE-----

-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEArCh0FvgOTdgc356MZ7Zncf5udv4RE2OHRNBH+GL6RHnmBryi
p3RukTm3ZCn516uup/LFWeLJU7+iAGg6zyj/5WH3ICl/HIGiKt07zwkeCKIN1lon
wC78Rbl1S9B4w5gnzoAdN/cVBs+IEBQP7xuUOKFx5dXl13XdvUQHqUC5q+0lieZu
iQrGj475lvj9fjOTQ3FH5hmaEl30BEgjAunWW41+dqbsuzoeudRnMLYhmMbKDooD
E0QBsJr0kGSXq/FOEFTr4fPRmtoaHaijgzqLouSu8QiCnvbsh6WGDTepzvsmgIHy
dSuvB0jlBq9pHyy4vlSWEi9aBdrTfIovsoY8YwIDAQABAoIBAA0w3lBCOmpZhEDY
F578uWlhGtqwNeO77QnnQZR1FQL5KhhbDMVy5OPovbYFh1iwlWXV8qwj9dQea03H
rrXfyNetDW/f7hbjkebh46rF4pC7xf6mEsSldPvsrWGdPBuUhXxLkbGPk4NdlGnS
97OU8+Lw79ax8HX1WIOfekzMIk3bciiIHCG3sFcrRHxpd7OjTLMsSmiVKFknbf10
qNiA97wBI2uU4akON3yPOdv/3b2IY3qan4uF4lvyrwAIrTynjYarknPCqUC/4any
dflCPVbZUlZ6GCvxuhGexIwTepE03fUG5ibXp8FhZh8symNif9aZT4E7JRkMmrgk
pnhM7TECgYEA0nmN+Vdn+v4dmIwUOocLXM4iYDKPd5uwkg4gID1ySNcezPXPiADD
iTYvi6Z+0NF+jrXAgg388u9vzwqxUGT19GfW6MtwsfLVTIL3pkbvwKuVkTP6vynK
pXRcVaMin1r+KR04c1bhhh3LPoRI/uZZYGvFG8Gl6WWDeWqvEVEaCDMCgYEA0WU3
AY2DLyQO4FUZNwsXKv0sKFNv/Ck/sK6FSlCGzzUc34CSM0E2MXNLNkYyOxDXQgAM
w/n2pDaZwISr8umlkNvmvu9nd0V3T3YHopSbAY/cX9hojX4ZiqH2xSilo/jmA72H
yTTiTpXFwzu4PGGdrSZGrATMGyWr0DSBB6VIixECgYA1nQ0TNLah8tUrJJOKjRfR
3hhXlMmC2D/UFJEOZViVQWbxIrRomnk0nH7j/ddT7ellBNsyxclnQKKkhL7CEdWt
Gj5eMmRUj9zRjpLy4iL0W0DQKgN3anfaSZezoMiS+yS+6FiW2My90x0QobXOaHLf
4tPkzCEtINSquwg5SwVsjQKBgQCddaExd7rIPjM5moSFkb9wQkVsZaH2WwZb1EDD
K17UfjoiD8rg5A7ejLZoL80iAX39UZBH5rYDslNYI+wxlGU+Uz5nIhwJ4qDfjgAb
z+fn+shbAp9MlyCZ0UWB6Rj1/vrooSN0uGHdel7mewgFz9oEFJ5cSJc7as6SmIjW
uQGeIQKBgBIZDxCLajJOdAdJ/kD58lAz7wrJgSxoy7jf6CT89GAfr3ehqSINEvbP
39MJ9NRktEPYPD1tB8on4KBeuUSFf5x4b1e7UTU0jJdlPonjkH0uDKhPirWK79+p
gp/8FkBiTtOmQ2Jii/WF5lucfKrQVuhaFu4o94kr3TdRlh/aX2hu
-----END RSA PRIVATE KEY-----

--

--