Skip to Content
Technical Articles

Bring your self-made user JWT with Keycloak OIDC.

The focus of this blog post is how to create a self-made user JWT token using Keycloak as a native OIDC provider.

And then how to use it to generate saml bearer assertion for unattended user identity propagation. This could be useful with edge devices …(for instances, where the edge device cannot be hooked to an IDP).

Good to know:

  • For convenience, I shall be using SAP BTP destination service to help generate the saml assertion but that also could be done either programmatically or by leveraging a suitable security policy shipped by SAP via API Business Hub or from APIM.

Disclaimer:

  • Please note all the code snippets below are provided “as is”.
  • All the x509 certificates, bearer access and refresh tokens and the likes have been redacted.
  • Images/data in this blog post is from SAP internal sandbox, sample data, or demo systems. Any resemblance to real data is purely coincidental.

Putting it all together

What does it mean: Bring your own user JWT with Keycloak as OpenID Connect provider ?

 

Well, it means that:

  1. we are creating a self-issued JWT and signing it with the private key of our own x509 certificate key pair as a function on SAP BTP, Kyma runtime
  2. we are adding the same x509 key pair into one the Keycloak’s realms as an active (x509) provider
  3. we can access the OIDC metadata of our keycloack realm (there is one set of metadata per realm/tenant)
  4. we can locate and access our own x509 key pair as part of the Json Web key set in the retrieved OIDC metadata.
  5. we can let destination service generate a signed saml assertion against a mock-up destination by passing the self-issued JWT in the x-user-token header of the find destination call.
Good to know:

  • You may refer to the following blog post for a detailed description of how to generate a .pfx keystore containing a x.509 certificate key pair.

 

ad1. Create your own user JWT.

JsonWebToken implementation for node.js http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html describes the self-issued JWT as follows:

JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties.

The claims in a JWT are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or integrity protected with a Message Authentication Code (MAC) and/or encrypted.

Please consider SAP BTP destination service will reject:

  • MAC protected user JWT tokens. Only digitally (x509) signed tokens are being permitted.
  • JWT tokens which are never expiring. In other words JWT must have the “iat” and “exp” attributes in it.

Let’s implement it with a very simple nodejs function

 

Good to know:

  • The function is run on SAP BTP, Kyma runtime. (Kyma is SAP-managed Kubernetes cluster).
  • As aforementioned, you may refer to the following blog post for a detailed description of how to generate a .pfx keystore containing a x.509 certificate key pair.

 

const jwt = require('jsonwebtoken');

const cert = '-----BEGIN CERTIFICATE-----\nMIIFGzCCAwMCBGBb1dwwDQYJKoZIhvcNAQELBQAwUjELMAkGA1UEBhMCVVMxDDAK\nBgNVBAoMA1NBUDEVMBMGA1UECwwMY
..........................(truncated)............................
URdaHVX6B270SUDiP6TDPApn9E+IaISzPRpk\nXT6c0QNVYg37DBU/qhSN\n-----END CERTIFICATE-----\n'; 
const key = '-----BEGIN PRIVATE KEY-----\nMIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDFz/eQv30tj5oC\nLjT1Im7OtVAVo6mB/wQbEpbOh3LSI
..........................(truncated)............................
LSxBTGdiZznqgLKnImxU1WDSA2xlKJy7J\nAwx8lLYgANSJ7qkKPgPR/t5ZHrx/plY=\n-----END PRIVATE KEY-----\n';

function generateAccessToken(username) {
  return jwt.sign( {username: username }, key, { algorithm: 'RS256', expiresIn: '1d' } );  
}
module.exports = { 
  main: async function (event, context) {

    switch (event.extensions.request.path) {
      case '/generateAccessToken' : {
        return generateAccessToken('TOTO');
      }
    }
  }
}

 

Find the function name and its running pod and then create a port forwarding tunnel for localhost execution (please consider the function generateJWT is run in total isolation on a kyma cluster)

$ kubectl get pods -n default --kubeconfig ~/.kube/kubeconfig.yaml
NAME                                      READY   STATUS      RESTARTS   AGE
generateJWT-l6kfp-6d96778ccb-dw9dh         2/2     Running     0          2d

$ kubectl port-forward pod/generateJWT-l6kfp-6d96778ccb-dw9dh 8080:8080 -n default --kubeconfig ~/.kube/kubeconfig.yaml
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080

Let’s call our function locally with the /generateAccessToken endpoint as follows:

Summary
URL: http://localhost:8080/generateAccessToken
Status: 200 OK
Source: Network
Address: ::1:8080


eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IlRPVE8iLCJpYXQiOjE2MjU5MTcwMjAsImV4cCI6MTYy
..........................(truncated)............................
5W-Gjsp9oCIpSRUM0Rtyw6DefTf7pybQZPR6gxKUpvzpzfCnnCc4Ci0RWPllmTJGe2DVjTijnGvEGFbnQr4ibU4E

header:
{
  "alg": "RS256",
  "typ": "JWT"
}

payload:
{
  "username": "TOTO",
  "iat": 1625917020,
  "exp": 1626521820
}

verify signature:
RSASHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),

 

ad2. add our own x509 key pair into one the Keycloak’s realms

This must be the same x509 key pair that was used to create a self-issued JWT token.

ad3. Get OIDC provider metadata.

ad4. Retrieve jwks_uri (a pointer to jwks)

 

ad5. Find destination (SAMLAssertion)

As aforementioned, we shall have destination service generate a signed saml assertion with our self-issued user identity against a mock-up destination.

Let’s use the self-issued JWT token in the x-user-token header of find destination call.

 

Follows a mock-up destination definition and the json output of a find destination call:

{
  "owner": {
    "SubaccountId": "SubaccountId",
    "InstanceId": null
  },
  "destinationConfiguration": {
    "Name": "QUOVADIS-BRING-YOUR-OWN-JWT",
    "Type": "HTTP",
    "URL": "https://api-TOTO/oauth/token",
    "Authentication": "SAMLAssertion",
    "ProxyType": "Internet",
    "KeyStorePassword": "<KeyStorePassword>",
    "audience": "www.audience.com",
    "authnContextClassRef": "urn:oasis:names:tc:SAML:2.0:ac:classes:PreviousSession",
    "clientKey": "clientKey",
    "KeyStoreLocation": "toto.p12",
    "nameIdFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
    "x_user_token.jwks_uri": "https://keycloak.<cluster>/auth/realms/ateam-isveng/protocol/openid-connect/certs",
    "tokenServiceURL": "https://api-TOTO/oauth/token",
    "userIdSource": "username"
  },
  "certificates": [
    {
      "Name": "toto.p12",
      "Content": "MIIQeAIBAzCCEDIGCSqGSIb3DQEHAaCCECMEghAfMIIQGzCCCggGCSqGSIb3DQEHAaCCCfkEggn1MIIJ8TCCCe0GCyqGSIb3PG/HcO5xW0ai3ZkwPTAhMAkGBSsOAwIaBQAEFMyS26dKft/dWRSIVg/SQvDnQmaSBBQTST14Lse+rKA3igKg4Q7gzIB0mAICBAA=",
      "Type": "CERTIFICATE"
    }
  ],
  "authTokens": [
    {
      "type": "SAML2.0",
      "value": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDI6QXNzZXJ0aW9uIHhtbG5zOnNhbWwyPSJ1cm46C9zYW1sMjpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sMjpBc3NlcnRpb24+",
      "http_header": {
        "key": "Authorization",
        "value": "SAML2.0 PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDI6QXNzZXJ0aW9uIHhtbG5zOnNhbWwyPSJ1cm46b9zYW1sMjpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sMjpBc3NlcnRpb24+"
      }
    }
  ]
}
Good to know:

  • You can tailor the above destination definition to your needs.
  • Your destination can be any internet facing or on premise target system: S/4HANA, S/4HANA Cloud, SAP HANA XSA, CF XSUAA, SuccessFactors, Cloud4Commerce, etc…
  • You may use the same approach with other saml bearer assertion flows for instance with OAuth2SAML2BearerAssertion flow.
  • Refer to destination service documentation for specific properties that may be required for your destination.

 

Conclusion

We were able to create a self-issued user JWT token with our own username claim. And then we were able to generate a signed saml assertion…

Of course if we were to use this signed saml assertion as a user identity bearer, both the username claimed would have to exist in the target system and the target system would need to have either saml bearer assertion provider or oauth provider set up with the x509 certificate that was used to sign the assertion.

__________

 

Appendix

Good to know:

  • Keycloak is running on kyma runtime as well.

 

Get OIDC provider metadata.

https://keycloak.<cluster>/auth/realms/ateam-isveng/.well-known/openid-configuration
{
  "issuer": "https://keycloak.<cluster>/auth/realms/ateam-isveng",
  "authorization_endpoint": "https://keycloak.<cluster>/auth/realms/ateam-isveng/protocol/openid-connect/auth",
  "token_endpoint": "https://keycloak.<cluster>/auth/realms/ateam-isveng/protocol/openid-connect/token",
  "introspection_endpoint": "https://keycloak.<cluster>/auth/realms/ateam-isveng/protocol/openid-connect/token/introspect",
  "userinfo_endpoint": "https://keycloak.<cluster>/auth/realms/ateam-isveng/protocol/openid-connect/userinfo",
  "end_session_endpoint": "https://keycloak.<cluster>/auth/realms/ateam-isveng/protocol/openid-connect/logout",
  "jwks_uri": "https://keycloak.<cluster>/auth/realms/ateam-isveng/protocol/openid-connect/certs",
  "check_session_iframe": "https://keycloak.<cluster>/auth/realms/ateam-isveng/protocol/openid-connect/login-status-iframe.html",
  "grant_types_supported": [
    "authorization_code",
    "implicit",
    "refresh_token",
    "password",
    "client_credentials",
    "urn:ietf:params:oauth:grant-type:device_code",
    "urn:openid:params:grant-type:ciba"
  ],
  "response_types_supported": [
    "code",
    "none",
    "id_token",
    "token",
    "id_token token",
    "code id_token",
    "code token",
    "code id_token token"
  ],
................................
}

 

Retrieve jwks_uri (a pointer to jwks) from the above metadata.

https://keycloak.<cluster>/auth/realms/ateam-isveng/protocol/openid-connect/certs

{
  "keys": [
    {
    },
    {
      "kid": "QtV2kV6VfnvJpRHCHFVXjDPFKydXWVHjuz4XIE0xxxx",
      "kty": "RSA",
      "alg": "RS256",
      "use": "sig",
      "n": "xc_3kL99LY-aAi409SJuzrVQFaOpgf8EGxKWzody0iPIf39NH8CzCf7kN52B4sKrBJU7ygNIdAebZu_cP2dRQcf7646q2yr0BBVUP8x92FRLHQT5J-Au4jL7psDBAq4G4TUxTgAFYqrAyQCUSlfW1VoRxzCMYJVtV0HUpC0yyV-RsaXQ6T6RNJTmk1FpFpgI2wO_gtaRSdVxA1cMpzvfsfiDhK-gEDDhoYQCYWzsLxxxx",
      "e": "AQAB",
      "x5c": [
        "MIIFGzCCAwMCBGBb1dwwDQYJKoZIhvcNAQELBQAwUjELMAkGA1UEBhMCVVMxDDAKBgNVBAoMA1NBUDEVMBMGA1UECwwMYXRxxxx"
      ],
      "x5t": "DQMOdr9ITZxhd6NYI7l8y6Lxxxx",
      "x5t#S256": "NINub7bHeCqT1mEJiQcUdbJtJgSmTeQhw48DGAJxxxx"
    },
    {
    }
  ]
}

 

JSON Web Key Set Properties.

alg The specific cryptographic algorithm used with the key.
kty The family of cryptographic algorithms used with the key.
use How the key was meant to be used; sig represents the signature.
x5c The x.509 certificate chain. The first entry in the array is the certificate to use for token verification; the other certificates can be used to verify this first certificate.
n The modulus for the RSA public key.
e The exponent for the RSA public key.
kid The unique identifier for the key.
x5t The thumbprint of the x.509 cert (SHA-1 thumbprint).

 

Additional resources.

 

JSON Web Key Set Properties

Understanding ID Token

Self-issued JWT

Usage of external JWT in CDS Services

Mapping of SAML attributes with XSUAA JWT in Cloud Foundry

 

Be the first to leave a comment
You must be Logged on to comment or reply to a post.