How to rotate public/private key pair using client credentials grant flow?

We want to use the client credential grant flow (https://developer.okta.com/docs/guides/implement-oauth-for-okta-serviceapp/overview/) so we can access the Okta APIs using OAuth scoped access tokens.
However, we were unable to find information on how to rotate the public/private keys. Any guidance is appreciated.

Hello,
You could update the public key in the application using the API Update Client Application. The private key should remain private from Okta, only the public key is registered.

Sample update:

curl --location --request PUT 'https://{domain}.okta.com/oauth2/v1/clients/0oa8q0...d6' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header 'Authorization: SSWS 00RSM...tZ' \
--data-raw '{
    "client_id": "0o...d6",
    "client_name": "Maintenance OKTA API",
    "client_uri": null,
    "logo_uri": null,
    "redirect_uris": [],
    "response_types": [
        "token"
    ],
    "grant_types": [
        "client_credentials"
    ],
    "jwks": {
        "keys": [
            {
                "kty": "RSA",
                "alg": "RS256",
                "kid": "myKeyUpdate",
                "use": "sig",
                "e": "AQAB",
                "n": "jTGRlrlODVi7sDW..."
            }
        ]
    },
    "token_endpoint_auth_method": "private_key_jwt",
    "application_type": "service"
}'

Hello, thank you for replying.

Does this endpoint support having multiple keys at once?

When I tried adding another key, neither key seemed to work, and I got a “Okta HTTP 401 undefined” error.

How are you testing? If you have multiple keys registered, when the JWT assertion is created the key id of the corresponding public key needs to be specified. If only one key is registered, then when the JWT is generated the key id does not need to be specified.

I don’t believe the backend will test every key until the right one is found, or the list is exhausted.

If you are testing with one of our SDKs could you specify which one. I am not sure if all of them provide a way to specify the key id.

I am using the @okta/okta-sdk-nodejs sdk for testing. (https://developer.okta.com/okta-sdk-nodejs/jsdocs/index.html)

I am specifying the public key along with the kid in the creation of the client.

Specifically I am making this call to add another key pair.

curl --location --request PUT 'https://{domain}.okta.com/oauth2/v1/clients/{clientID}' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header 'Authorization: SSWS {API_TOKEN}' \
--data-raw '{
    "client_id": {clientID},
    "client_name": "test",
    "response_types": [ "token" ],
    "grant_types": [ "client_credentials"],
    "jwks": {
        "keys": [
            {
                "kty": "RSA",
                "alg": "RS256",
                "kid": "AAB",
                "use": "sig",
                "e": "AQAB",
                "n": "xxxxxxx"
            },
            {
                "kty": "RSA",
                "alg": "RS256",
                "kid": "AAB2",
                "use": "sig",
                "e": "AQAB",
                "n": "xxxxxxx"
            }
        ]
    },
    "token_endpoint_auth_method": "private_key_jwt",
    "application_type": "service"
}'

And the code I am using to generate the client is

const client = new okta.Client({
    orgUrl: 'https://{domain}.okta.com/',
    authorizationMode: 'PrivateKey',
    clientId: {clientId},
    scopes: ['okta.users.read','okta.groups.read'],
    privateKey: {
        "p": "xx...xxxx",
        "kty": "RSA",
        "q": "xx...xxxx",
        "d": "xx...xxxx",
        "e": "AQAB",
        "use": "sig",
        "kid": "AAB",
        "qi": "xx...xxxx",
        "dp": "xx...xxxx",
        "alg": "RS256",
        "dq": "xx...xxxx",
        "n": "xx...xxxx"
    }
  });

The client is generated successfully, and can call the Okta APIs when only the one key is specified is in the PUT jwk command. However, when a second key is added to the list I can not generate a client and call the Okta APIs with either key pair.

Is this the correct way to be using the kid?

Are you creating a new client or updating an existing one? If the latter, make sure the authorization you are using for your request does NOT use the same client as the one you are trying to update. At this time, clients are not able to manage themselves and you will need to use a separate client or an API key to update an existing client.

Have you tried making this same API call manually, instead of using this SDK?

I am trying to update an existing client. Can you explain what you mean why using “an API key to update an existing client”?

The reason I am trying to update the existing client is so we can rotate the key pair. I wanted to add another key, switch our application over, and then delete the previous key. In order to do this, will I need to use a new client?

I am able to register two keys with my app, however in my testing with one of our SDKs, I receive the same error you do.

If I use https://www.jsonwebtoken.dev/. to generate JWT, but first I remove the ‘kid’ line from the keyset, the header of the JWT will not contain the ‘kid’ and I will get a 401

The client_assertion JWT kid is invalid.

If I then add the ‘kid’ line back, generate the JWT and use that assertion, I am able to retrieve the access token via postman.

It appears the SDK may not be adding the key id to the header, or I am not setting it correctly. I will check with the node-sdk if this is possible.

Thanks

I was able to replicate this with the node sdk. If I modify part of the code and manually set the ‘kid’ header

setHeader('kid', 'MyKeyId');

then it works for me.

Will let you know if we officially support client credential apps with multiple keys registered.

Thank you!
Where did you set the header in the code to get this to work? I am not sure where the header is set within the code.

Hello,
I have entered a ticket for an official fix.

In the mean time see if the following works for you.

Edit ‘jwt.js’, in the makeJwt() function, add .setHeader('kid', jwk.kid);

This assumes the private key passed in contains a ‘kid’, which your included sample does. Below is what the nJwt.create() should look like after the change.

let jwt = nJwt.create(claims, pem, alg)
.setIssuedAt(now)
.setExpiration(plus5Minutes)
.setIssuer(client.clientId)
.setSubject(client.clientId)
.setHeader(‘kid’, jwk.kid);

Hope this works out for you.

This works, thank you!

Is the ticket for the fix externally viewable? So we can keep an eye on it?

https://github.com/okta/okta-sdk-nodejs/issues/223

1 Like

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.