Jack D Dunleavy & Tom Van Oppens

Introduction

API keys are often used to authenticate API clients in the context of OAuth2.0 interaction. However this approach, based on a shared secret, can present vulnerabilities. For this reason the emerging Open Banking standards are promoting the adoption of API client authentication based on digital certificates. In this post we describe how you can leverage the out of the box functionality of APIC v5 to onboard an API client based on an X.509 certificate, and then customise the behaviour of APIConnect OAuth2.0 provider to generate tokens linked to those certificates.

Key Concepts

Mutual Authentication TLS is a widely implemented security standard that requires both client and server to prove their identity (established by offering a certificate that is signed by a trusted authority) before establishing an encrypted channel of communication. M-TLS differs from the other common TLS pattern (often called ‘one-way TLS’), where only the server must prove it’s identity.

X.509 certificates are cryptographic public key certificates formated according to the X.509 standard. An X.509 certificate is essentially a metadata containing wrapper for a public key, which can be used for encryption operations (in this case when establishing a TLS connection). X.509 certificates have a corresponding private key, which is securely stored by the organisation that owns it. The client must prove ownership of this private key to establish a mutually authentication TLS connection. For this reason, security schemes using m-TLS often refer to proof-of-possession mechanisms when discussing trust or authorization.


Note: the specification for tls_client_auth is still in the draft stage at the time of writing, and as such the accuracy of this document cannot be guarenteed. If you wish to implement this feature in a live environment do first check the latest draft[1] or, if released, the complete RFC first.


Functionality

When implemented, tls_client_auth exposes a new client authentication mechanism for OAuth2.0 provider APIs. This allows third party providers (TPPs) to generate access tokens by establishing a mutual TLS connection with the authorization server. The resultant access token is coupled to the certificate, requiring the TPP to supply the same certificate for mutual TLS when accessing protected resources. This validates that the TPP owns a valid private key for both token generation and resource access.

Pre-requisites

Before configuring the API, a mechanism to accomplish the following requirements must first be delivered:

  1. Terminate the m-TLS connection with the TPP and insert the base 64 section of the TPP certificate into the header: X-Client-Certificate

Implementation

Initially implemented is an OAuth2.0 provider API. This exposes an endpoint on the DataPower Gateway from which TPPs can request access tokens. For a full example swagger see Appendix A (provider.yaml).

Creating the OAuth2.0 Provider API

  1. Create an OAuth Provider API
  2. In the OAuth 2 section, select Public client type, and the Application grant type (This guide covers setting up a client_credentials flow authenticated using m-TLS, but the same concepts would work using an authorization_code flow)
  3. In the Livecycle section, enable the Authenticate Application setting[4]
  4. In the Oauth 2 section, enter the Metadata endpoint into the Metadata URL field. (See section Metadata Endpoint). The endpoint URL should have a query parameter cert equal to the context variable $(application.certificate.Base64). For example: https://hostname.com/metadata?cert=$(application.certificate.Base64)

Creating the Metadata Endpoint

The provider API delegates the job of inserting metadata into the access token to a remote endpoint, specified by the Metadata Endpoint value. The following API will act as the metadata endpoint for the OAuth provider, injecting the certificate thumbprint into the access token metadata. For a full example swagger see Appendix B (metadata.yaml).

  1. Create an API
  2. In the Assembly, enter the following code into a gatewayscript policy:
    const crypto = require(‘crypto’); const clientCert = apim.getvariable(‘request.parameters’).cert; const metadataForHeader = { ‘x5t#S256’: calculateX5tS256(clientCert), } apim.setvariable(‘message.headers.API-OAUTH-METADATA-FOR-ACCESSTOKEN’, JSON.stringify(metadataForHeader)); function calculateX5tS256(pemCertificate) { let derCertificate = Buffer.from(pemCertificate, ‘base64’); return shasum(256, derCertificate, ‘base64’) .replace(/\\//g, ‘_’) .replace(/\+/g, ‘-‘) .replace(/=/g, ); } function shasum(alg, data, enc) { return crypto.createHash(‘sha’ + alg) .update(data) .digest(enc) }

     

This snippet calculates the x5t#S256 (Section 3.1 of the oauth-mtls draft)[1] thumbprint of the certificate and inserts it into the designated metadata header for APIC to read. The APIC framework then inserts the metadata into the access token and responds to the TPP.

Creating A Protected Resource

The protected resource then requires an access token that is bound to the certificate used to open an m-TLS connection with the Resource server. For a full example swagger see Appendix C (resource.yaml).

  1. Create an API
  2. In the Livecycle section, enable the Authenticate Application setting[4]
  3. In the Security Definitions section, add a Security Definition for application flow using the token endpoint of the provider API
  4. In the Assembly enter the following code into a gatewayscript policy to validate the thumbprint in the token metadata:
    const crypto = require(‘crypto’); const clientCert = apim.getvariable(‘application.certificate.Base64’); if(!clientCert) apim.error(‘mtlsError’, 403, ‘Forbidden’, ‘Missing client cert’); const x5tS256 = calculateX5tS256(clientCert); const miscinfo = JSON.parse(apim.getvariable(‘oauth.miscinfo’).split(‘m:’)[1]); if (x5tS256 != miscinfo[‘x5t#S256’]){ apim.error(‘mtlsError’, 403, ‘Forbidden’, ‘Token not bound to right certificate’) } function calculateX5tS256(pemCertificate) { let derCertificate = Buffer.from(pemCertificate, ‘base64’); return shasum(256, derCertificate, ‘base64’) .replace(/\\//g, ‘_’) .replace(/\+/g, ‘-‘) .replace(/=/g, ); } function shasum(alg, data, enc) { return crypto.createHash(‘sha’ + alg) .update(data) .digest(enc) }

     

This snippet extracts the x5t#S256 thumbprint from the metadata within the access token and rejects the request with a 403 response if the metadata thumbprint does not match the suppplied certificate.

Putting the Solution Together

  1. Deploy the metadata endpoint API and determine it’s URI
  2. Insert this URI into the OAuth2.0 provider API as the metadata endpoint then publish the provider API
  3. Get the token endpoint URI for the provider API and insert this into the protected resource API’s token endpoint, then publish the resource API
  4. Register an application in the Developer Portal, providing an X.509 certificate to the registration form:
    App Registration
  5. Generate a token by calling the OAuth2.0 provider API’s token endpoint according to the following curl command: (Note: the certificate used for this call must match the certificate provided during registration, and will be bound to the resultant token.)
    curl -X POST https://$hostname/$path/oauth-mtls/oauth2/token -H “Cache-Control: no-cache” -H “Content-Type: application/x-www-form-urlencoded” -H “X-Client-Certificate: $cert -d “client_id=$clientId&grant_type=client_credentials&scope=mtls”

     

  6. Use the access token to request resource (Note: the certificate used to generate the token must match the certificate used for this request, as the token and the certificate are tightly coupled)
    curl -X GET https://$hostname/$path/oauth-mtls-resource/example -H “Cache-Control: no-cache” -H “X-Client-Certificate: $cert -H “Authorization: Bearer $accessToken

     

Conclusion

Implemented is an OAuth 2.0 provider that requires proof of ownership of a key specified during registration, and an API that is protected by that provider. The example calls and implementation instructions in the above section Putting the Solution Together allow you to implement the tls_client_auth client authentication mechanism in your API Connect environment.

Apendices

Appendix A (provider.yaml)

swagger: “2.0” info: x-ibm-name: “oauth-mtls-provider” title: “oauth-mtls-provider” version: 1.0 schemes: “https” host: “$(catalog.host)” basePath: “/oauth-mtls-provider” securityDefinitions: clientID: description: “application’s client_id” in: “query” name: “client_id” type: “apiKey” security: – clientID: [] paths: /oauth2/authorize: get: produces: “text/html” summary: “endpoint for Authorization Code and Implicit grants” description: “description” parameters: – name: “response_type” in: “query” description: “request an authorization code or or access token (implicit)” required: true type: “string” enum: “code” “token” – name: “scope” in: “query” description: “Scope being requested” type: “string” required: true – 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\n- **authorization code** for Authorization code grant\n-\ \ **access token** for Implicity grant\n- **error** in case of errors,\ \ such as the user has denied the request\n” security: – clientID: [] post: consumes: “application/x-www-form-urlencoded” produces: “text/html” summary: “submit approval to authorization code or access token” description: “Submit resource owners approval (or rejection) for the OAuth2\ \ Server to issue an\nauthorization code or access token to the application.\n” security: [] parameters: – name: “client_id” in: “formData” description: “application requesting the access code or token” required: true type: “string” – name: “scope” in: “formData” description: “requested scope of this authorization” required: true type: “string” – name: “resource-owner” in: “formData” description: “resource owners user name” required: true type: “string” – name: “redirect_uri” in: “formData” description: “URI the application is requesting this code or token to be redirected\ \ to” required: true type: “string” – name: “original-url” in: “formData” description: “URL of the original authorization request” required: true type: “string” – name: “dp-state” in: “formData” description: “state information provided in the authorization form” required: true type: “string” – name: “dp-data” in: “formData” description: “state information provided in the authorization form” required: true type: “string” responses: 200: description: “A consent form for oauth processing.” /oauth2/token: post: consumes: “application/x-www-form-urlencoded” produces: “application/json” summary: “Request Access Tokens” description: “This endpoint allows requesting an access token following one\ \ of the flows below:\n- Authorization Code (exchange code for access token)\n\ – Client Credentials (2-legged, there isnt resource owner information)\n-\ \ Resource Owner Password Credentials (2-legged, client provides resource\ \ owner name and password)\n- Refresh Token (exchange refresh token for a\ \ new access code)\n\nThe table below indicates the required parameters for\ \ each specific grant_type options.\nEmpty cells indicate a parameter is ignored\ \ for that specific grant type.\n\nClient authentication:\n- Confidential\ \ clients should authenticate using HTTP Basic Authentication. Alternatively,\ \ they may post\n their client_id and client_secret information as a formData\ \ parameter.\n- Public clients should send their client_id as formData parameter.\n\ \n| grant_type | code | client_credentials | password |\ \ refresh_token |\n|———————-|————|——————–|————-|—————|\n\ | client_id | required* | required* | required* | required*\ \ |\n| client_secret | required* | required* | required*\ \ | required* |\n| code | required | \ \ | | |\n| redirect_uri | required\ \ | | | |\n| username \ \ | | | required | |\n\ | password | | | required | \ \ |\n| scope | | optional \ \ | optional | |\n| refresh_token | |\ \ | | required |\n\nThe implicit grant\ \ requests, see /oauth2/authorize.\n” security: [] parameters: – name: “grant_type” in: “formData” description: “Type of grant” type: “string” required: true enum: “authorization_code” “password” “client_credentials” “refresh_token” – name: “client_id” in: “formData” description: “Application client ID, can be provided in formData or using\ \ HTTP Basic Authentication” required: false type: “string” – name: “client_secret” in: “formData” description: “Application secret, must be provided in formData or using HTTP\ \ Basic Authentication” required: false type: “string” – name: “code” in: “formData” description: “Authorization code provided by the /oauth2/authorize endpoint” required: false type: “string” – name: “redirect_uri” in: “formData” description: “required only if the redirect_uri parameter was included in\ \ the authorization request /oauth2/authorize; their values MUST be identical.” required: false type: “string” – name: “username” in: “formData” type: “string” description: “Resource owner username” required: false – name: “password” in: “formData” type: “string” description: “Resource owner password” required: false – name: “scope” in: “formData” type: “string” description: “Scope being requested” required: false – name: “refresh_token” in: “formData” type: “string” description: “The refresh token that the client wants to exchange for a new\ \ access token (refresh_token grant_type)” required: false responses: 200: description: “json document containing token, etc.” schema: $ref: “#/definitions/access_token_response” 400: description: “json document that may contain additional details about the\ \ failure” x-ibm-configuration: testable: true enforced: true phase: “realized” oauth2: client-type: “public” scopes: mtls: “MTLS scope” grants: “application” identity-extraction: type: “default-form” authentication: x-ibm-authentication-url: url: “https://example.com/auth/url” tls-profile: “” authorization: type: “authenticated” access-token: ttl: 3600 refresh-token: count: 2048 ttl: 2682000 revocation: url: “” tls-profile: “” metadata: metadata-url: url: “https://9.20.85.72:443/jack-org/sb/mtls-metadata-poc/path?cert=$(application.certificate.Base64)” tls-profile: “” cors: enabled: true type: “oauth” application-authentication: certificate: true definitions: access_token_response: type: “object” additionalProperties: false required: “token_type” “access_token” “expires_in” properties: token_type: enum: “bearer” access_token: type: “string” expires_in: type: “integer” scope: type: “string” refresh_token: type: “string”

 

Appendix B (metadata.yaml)

swagger: ‘2.0’ info: x-ibm-name: mtls-metadata-poc title: mtls-metadata-poc version: 1.0.0 schemes: https host: $(catalog.host) basePath: /mtls-metadata-poc consumes: application/json produces: application/json securityDefinitions: {} security: [] x-ibm-configuration: testable: true enforced: true cors: enabled: true assembly: execute: – gatewayscript: title: gatewayscript version: 1.0.0 source: | const crypto = require(‘crypto’); const b64clientCertificate = apim.getvariable(‘request.parameters’).cert; const metadataForHeader = { ‘x5t#S256’: calculateX5tS256(b64clientCertificate), } apim.setvariable(‘message.headers.API-OAUTH-METADATA-FOR-ACCESSTOKEN’, JSON.stringify(metadataForHeader)); function calculateX5tS256(pemCertificate) { let derCertificate = Buffer.from(pemCertificate, ‘base64’); return shasum(256, derCertificate, ‘base64’) .replace(/\\//g, ‘_’) .replace(/\+/g, ‘-‘) .replace(/=/g, ”); } function shasum(alg, data, enc) { return crypto.createHash(‘sha’ + alg) .update(data) .digest(enc) } phase: realized paths: /path: get: responses: ‘200’: description: 200 OK parameters: – name: cert type: string required: true in: query post: responses: ‘200’: description: 200 OK definitions: {} tags: []

 

Appendix C (resource.yaml)

swagger: ‘2.0’ info: x-ibm-name: oauth-mtls-resource title: oauth-mtls-resource version: 1.0.0 schemes: https host: $(catalog.host) basePath: /oauth-mtls-resource consumes: application/json produces: application/json securityDefinitions: oauth-1: type: oauth2 description: flow: application scopes: mtls: tokenUrl: ‘https://apimdev2019.hursley.ibm.com:555/jack-org/sb/oauth-mtls-poc/oauth2/token’ security: – oauth-1: mtls x-ibm-configuration: testable: true enforced: true cors: enabled: true assembly: execute: – gatewayscript: title: validate-client-mtls version: 1.0.0 source: | const crypto = require(‘crypto’); const clientCert = apim.getvariable(‘application.certificate.Base64’); if(!clientCert) apim.error(‘mtlsError’, 403, ‘Forbidden’, ‘Missing client cert’); const x5tS256 = calculateX5tS256(clientCert); const miscinfo = JSON.parse(apim.getvariable(‘oauth.miscinfo’).split(‘m:’)[1]); if (x5tS256 != miscinfo[‘x5t#S256’]){ apim.error(‘mtlsError’, 403, ‘Forbidden’, ‘Token not bound to right certificate’) } function calculateX5tS256(pemCertificate) { let derCertificate = Buffer.from(pemCertificate, ‘base64’); return shasum(256, derCertificate, ‘base64’) .replace(/\\//g, ‘_’) .replace(/\+/g, ‘-‘) .replace(/=/g, ”); } function shasum(alg, data, enc) { return crypto.createHash(‘sha’ + alg) .update(data) .digest(enc) } – gatewayscript: title: dummyBackend version: 1.0.0 source: apim.setvariable(‘message.body’,'{\”ok\”:\”ok\”}’)” phase: realized application-authentication: certificate: true paths: /example: get: responses: ‘200’: description: 200 OK definitions: {} tags: []

 

References

[1]: https://tools.ietf.org/html/draft-ietf-oauth-mtls
[2]: https://www.ibm.com/support/knowledgecenter/en/SSMNED_5.0.0/ com.ibm.apic.toolkit.doc/rapim_context_var.html
[3]: https://www.ibm.com/support/knowledgecenter/en/SSMNED_5.0.0/com.ibm.apic.toolkit.doc/con_metadata.html
[4]: https://www.ibm.com/support/knowledgecenter/en/SSMNED_5.0.0/com.ibm.apic.toolkit.doc/task_apionprem_creating_apis.html

 

Join The Discussion

Your email address will not be published. Required fields are marked *