Private Key JWT Client Authentication in WSO2 Identity Server

Anuradha Karunarathna
Identity Beyond Borders
8 min readJan 30, 2022

OAuth 2.0 Authorization Framework defines two types of clients based on their ability to authenticate securely with the authorization server (RFC 6749 Section 2.1):

  1. Confidential Client — client implemented on a secure server with
    restricted access to the client credentials.
  2. Public Client — Clients executing on the device used by the
    resource owner.

Out of these two types, confidential clients MUST require client authentication when accessing the token endpoint to request an access token.

OAuth 2.0 Token API supports the following client authentication methods:

(Refer: RFC 6749 Section-2.3.1, OIDC-1.0, RFC 8705)

For methods 1, 2, and 3, the authorization server generates a pair of client ID and client secret and gives the pair to a client application in advance.

  1. client_secret_basic — Include client ID and client secret for Basic Authentication in the token request. Here, Base64 Encoded Value of <client-id>:<client-secret> is used forAuthorization header in a token request. Once the pair is sent via token request, the Authorization server checks whether the pair is valid.
Authorization: Basic czZCaGRSa3F0Mzo3RmpmcDBaQnIxS3REUmJuZlZkbUl3

2. client_secret_post — Include client ID and client secret in token request’s request body. Once the pair is sent via token request, the Authorization server checks whether the pair is valid.

POST /token HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA
&client_id=s6BhdRkqt3&client_secret=7Fjfp0ZBr1KtDRbnfVdmIw

3. client_secret_jwt — Indirectly proves that client application has a client secret.

  • The client prepares some Data in JSON format including the Required claims defined in https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication and some other optional claims.
  • Generates a signature for the data using a client secret.
  • Include the data and the signature (JWT defined in RFC 7523)in the token request.
  • Since the authorization server already has the client secret, it can verify the signature.

4. private_key_jwt Similar to client_secret_jwt, but the difference is signature is generated not based on a shared key (client secret). This uses an asymmetric key.

  • Prepare a pair of private key and public key on the client side.
  • Make the public key accessible from the authorization server (eg: by jwks client metadata)
  • The client prepares some Data in JSON format including the Required claims defined in https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication and some other optional claims.
  • Generates a signature for the data using the private key.
  • Include the data and the signature (JWT defined in RFC 7523)in the token request.
  • The authorization server verifies the JWT using the public key.

5. tls_client_auth — Mutual TLS authentication RFC 8705

Out of all let’s deep dive into the private_key_jwt mechanism and let’s see how to configure wso2is for this client authentication mechanism.

private_key_jwt

01: Setup Authorization Server.

  1. In this guide, we use wso2is-5.10.0 as the authorization server. Download the zip file here and follow the product installation guide.
  2. Download private key JWT authenticator connector from IS connector store (find the compatible version) and copy it into<IS_HOME>/repository/component/dropins the directory. In case you couldn’t find a compatible version from the connector store you can try out building source code.
  3. To register the JWT grant type, configure the <IS_HOME>/repository/conf/deployment.toml file by adding a new entry as seen below.
[[event_listener]]
id = "private_key_jwt_authenticator"
type = "org.wso2.carbon.identity.core.handler.AbstractIdentityHandler"
name = "org.wso2.carbon.identity.oauth2.token.handler.clientauth.jwt.PrivateKeyJWTClientAuthenticator"
order = "899"
[event_listener.properties]
PreventTokenReuse= false
RejectBeforeInMinutes= "100"
TokenEndpointAlias= "https://localhost:9443/oauth2/token"
# The cache configuration is needed because when too many calls are made to the database there can be a performance impact.
# To reduce this impact, the cache configuration is done so that the information is read from the cache instead of the database.
[[cache.manager]]
name="PrivateKeyJWT"
timeout="300"
capacity="5000"
isDistributed="false"

4. Restart the identity server and configure a service provider.

5. Log in to the management console, Navigate to Main Menu->Identity ->Service Providers -> Add. Give a name for your application and register.

Then navigate to the Inbound Authentication Configuration -> OAuth/OpenID Connect Configuration and click configure. Give the callback URL of your application.

For this guide : I’ll use http://localhost:8080/pickup-dispatch/oauth2client as the callback. Once this is done, identity server generates the client key and secret. Please take a note of them.

02: Prepare private key and public key in client side.

(i) To create the public key and private key, open a terminal and execute the following keytool command to create the client keystore. Replace the <clinet_ID> part by the noted client id in the above section.

keytool -genkey -alias <client_ID> -keyalg RSA -keystore TodayApp.jks

Now you have generated TodayApp.jks successfully.

(ii) Export the public certificate from keystore. This will generate a file named client id. It is the public certificate.

keytool -export -alias <client_ID> -file <client_ID> -keystore TodayApp.jks

(iii) Next we need to extract public key and private key in .pem format.

First. convert .jks keystore to PKCS#12 format. This will generate TodayApp.p12 keystore.

keytool -importkeystore -srckeystore TodayApp.jks -destkeystore TodayApp.p12 -deststoretype PKCS12

Then, export public key from .p12 keystore. This will generate the pubcert.pem file.

openssl pkcs12 -in TodayApp.p12 -nokeys -out pubcert.pem

Then, export private key from .p12 keystore. This will generate the privatekey.pem file.

openssl pkcs12 -in TodayApp.p12 -nodes -nocerts -out privatekey.pem

03. Make the public key accessible from the authorization server.

This can be done via two options:

Use SP JWKS endpoint to access the public cert , or upload the SP public certificate to the server.

Since I’m not using an actual client for this demo, I don’t have a JWKS endpoint. So I’ll upload the SP certificate.

(i) Open pubcert.pem and copy the following section.

-----BEGIN CERTIFICATE-----
MIIDizCCAnOgAwIBAgIEC5FyUDANBgkqhkiG9w0BAQsFADB2MQswCQYDVQQGEwJT
TDEQMA4GA1UECBMHV2VzdGVybjEQMA4GA1UEBxMHQ29sb21ibzENMAsGA1UEChME
V1NPMjEUMBIGA1UECxMLRW5naW5lZXJpbmcxHjAcBgNVBAMTFUFudXJhZGhhIEth
cnVuYXJhdGhuYTAeFw0yMjAxMzAxMjA2NDlaFw0yMjA0MzAxMjA2NDlaMHYxCzAJ
BgNVBAYTAlNMMRAwDgYDVQQIEwdXZXN0ZXJuMRAwDgYDVQQHEwdDb2xvbWJvMQ0w
CwYDVQQKEwRXU08yMRQwEgYDVQQLEwtFbmdpbmVlcmluZzEeMBwGA1UEAxMVQW51
cmFkaGEgS2FydW5hcmF0aG5hMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
AQEAgBTp05Ehn1jftFPl/pHW60wzhpRpAu8ogqisXJrgqzWqnkEHtc6Essrd1hXz
UOBijRreERgqEDDhg+ivr1KhCGJZqtGIeNzFntsRkRYUs9O4JmynBiSe/sj8+auN
ErGNyFxP4706lZx71CTc4/RIrc+HgY2wJEx4JeYidhIXJ0Yg7B0HxVL5NHEYQuDs
flZUkbXLqTYsHf1d+Tug6cuTlZhexcAZGWy7ICDAsfXESll5QpiqmMoiM/IDeBpI
J0+e1tr5s1rNoIRlWkCmCNuD//CzDTaQ8mStMQJ6duNSRngPwY1O/v4jbidhRLKS
vyO9mPpkGTDVJtLi4PZ8K6YBWwIDAQABoyEwHzAdBgNVHQ4EFgQUZbqdES651kfL
Ab4MJ/BYU/DFW2swDQYJKoZIhvcNAQELBQADggEBAETLbNPOHXgJEWExVKaTS5/o
riXf9HXVCoCjDJIEmbjfxQnqvQLXQWa3v6ElAGBCubzJ63zdU859nrNDhjrW3Gs5
qPOyq3WmXjPzAGMVl+fD3SW9l8t/bEVBxTDo7gRqEml554+OMtKraKgomPtwSd2b
9fccoosZBTkHba5T4zJ9pRrNBbpm8cdX7ukIBOCA8QHm2As9UTLexN9GjmUYMC9X
OLdcPj/sBR0FxuEdWHu72Gfn0pPsUKV0Ip5151BSAsEuTSf1TojaR0hGGF8/F3jR
4WwzA0c9LX8gtBpsL8objToTNOdpRclmN6k5dfu3iNmUvoewltbpGHy8RI+JuwU=
-----END CERTIFICATE-----

(iii) Paste it in the input box under upload SP certificate option.

04: Prepare JSON data which conforms to the specification

refer: RFC 7523, 2.2. Using JWTs for Client Authentication, and OpenId-clinet authetication

JWT header:

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

Specify CN of the public cert usingkid (key ID)claim, if multiple keys are there to sign.

JWT payload:

iss : REQUIRED. Issuer. This MUST contain the client_id of the OAuth Client.sub : REQUIRED. Subject. This MUST contain the client_id of the OAuth Client.aud : REQUIRED. Audience. The aud (audience) Claim. Value that identifies the Authorization Server as an intended audience. The Authorization Server MUST verify that it is an intended audience for the token. The Audience SHOULD be the URL of the Authorization Server's Token Endpoint.jti : REQUIRED. JWT ID. A unique identifier for the token, which can be used to prevent reuse of the token. These tokens MUST only be used once, unless conditions for reuse were negotiated between the parties; any such negotiation is beyond the scope of this specification.exp : REQUIRED. Expiration time on or after which the ID Token MUST NOT be accepted for processing.iat : OPTIONAL. Time at which the JWT was issued.

Here is my payload.

{
"iss": "RN0I55bldQftY97uNq9iIXQA21wa",
"sub": "RN0I55bldQftY97uNq9iIXQA21wa",
"exp": 1643650350,
"iat": 1643650346,
"jti": "10003",
"aud": "https://localhost:9443/oauth2/token"
}

05: Sign the data with the private key.

(i) Go to https://jwt.io/ and copy and paste your header and body of the JWT Decoded section.

(ii) Copy the — — -BEGIN PRIVATE KEY — — — to — — -END PRIVATE KEY — — — section of privatekey.pemand paste the content of your private key in the VERIFY SIGNATURE part of Decoded section.

(iii) Then you can see the signed JWT on your left hand side.

This generated JWT is known as client_assetion . Here I just a JWT generation tool for the demonstration. You have to implement them in your client.

06: Send the token request to authorization server.

Client Credential Grant Type:

Replace <jwt_assertion> from what you generated in step 05 and <call_back_url> by service provider’s callback URL.

curl -v POST -H "Content-Type: application/x-www-form-urlencoded;charset=ISO-8859-1" -k -d "grant_type=client_credentials&scope=openid&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&client_assertion=<jwt_assertion>&redirect_uri=<call_back_url>" https://localhost:9443/oauth2/token

Authz_code Grant Type :

Replace <cliend_id> and <call_back_url> with SP data and copy this URL in browser.

https://localhost:9443/oauth2/authorize?response_type=code&client_id=<cliend_id>&redirect_uri=<call_back_url>&scope=openid

Copy the “code” param value on the callback url.

Invoke the token request by replacing <authz_code> with the returned code, <jwt_assertion> with generated JWT in step 05, and <call_back_url> with SP callback URL

curl -v POST -H "Content-Type: application/x-www-form-urlencoded;charset=UTF-8" -k -d "grant_type=authorization_code&code=<authz_code>&scope=openid&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&client_assertion=<jwt_assertion>&redirect_uri=<call_back_url>" https://localhost:9443/oauth2/token

07: Authorization server extracts the client assertion from the token request, and verify by public key.

Once the client authentication is successful, token response will be returned.

Token Response of Client Credential Grant Type.

{"access_token":"ccbaa39f-7d4f-3efa-aa9a-ecfac5a8be3c","token_type":"Bearer","expires_in":3542}

Bydefault, for client credential grant id token is not returned in the token response. If you want to return id token in this response add the following config in to the deployement.toml and restart the server.

[oauth.grant_type.client_credentials]
allow_id_token = true

Token Response of Authz_code Grant Type.

{"access_token":"4c6bc82e-76ea-3e75-aa7e-7c3b8a4f15da","refresh_token":"77f1d999-d2f3-369d-89cd-16593de821e9","scope":"openid","id_token":"eyJ4NXQiOiJNell4TW1Ga09HWXdNV0kwWldObU5EY3hOR1l3WW1NNFpUQTNNV0kyTkRBelpHUXpOR00wWkdSbE5qSmtPREZrWkRSaU9URmtNV0ZoTXpVMlpHVmxOZyIsImtpZCI6Ik16WXhNbUZrT0dZd01XSTBaV05tTkRjeE5HWXdZbU00WlRBM01XSTJOREF6WkdRek5HTTBaR1JsTmpKa09ERmtaRFJpT1RGa01XRmhNelUyWkdWbE5nX1JTMjU2IiwiYWxnIjoiUlMyNTYifQ.eyJhdF9oYXNoIjoiTHFHT28yWGdtY2ZuVHlqR3FGd3JLUSIsImF1ZCI6IlJOMEk1NWJsZFFmdFk5N3VOcTlpSVhRQTIxd2EiLCJjX2hhc2giOiJ2RFFYRlVBd19sMXhkMUhGbGM4T2tBIiwic3ViIjoiYWRtaW4iLCJuYmYiOjE2NDM1NTcyNjIsImF6cCI6IlJOMEk1NWJsZFFmdFk5N3VOcTlpSVhRQTIxd2EiLCJhbXIiOlsiQmFzaWNBdXRoZW50aWNhdG9yIl0sImlzcyI6Imh0dHBzOlwvXC9sb2NhbGhvc3Q6OTQ0M1wvb2F1dGgyXC90b2tlbiIsImV4cCI6MTY0MzU2MDg2MiwiaWF0IjoxNjQzNTU3MjYyLCJzaWQiOiI4YWFjZjlhYy03MTI2LTQxZGUtYjRlOS1iOWVjMTYyYWZhYzIifQ.IkBeA9HRBMVxLNat-QwPp0mUQyxCNUorh63ML4oKNoEe_CWCOFYK5jmzj4NnnmKMm0OffELwTsQNyORyWY-aEmIhvy12OBRiP2ovtU9Pww7pJ7ogFb3WIkV-5N0h41yQ5mIbpS9AFgIr4F4wwaiWh6W8KfOOurB4Q0hr9zlSRovsydd40o0z5dU4zjocVPIKr4dalzlgir3dDoBgzs8wy0yAMLQC1ePDjHZvIPkeR7uHAK1badAnkshLIp-qjvXP96ar2jHVnOjcLsXwnssqLFsBSFSj-eVNmH2B5EobblATg_RweitRlaRTMRc7O5n6y5rf9x2lluYGIjIvx-HmkQ","token_type":"Bearer","expires_in":3600}

Hurrey!! We successfully tried out Private Key JWT Client Authentication. 🥳

Reference:

--

--

Anuradha Karunarathna
Identity Beyond Borders

Technical Lead @ WSO2 | Computer Science and Engineering graduate@ University of Moratuwa, SriLanka