Skip to content

OAuth2 client_assertion JWT expiration set to exactly 1 hour fails with clock skew #573

@edg4rgarci4

Description

@edg4rgarci4

Describe the bug?

The createClientAssertion function in okta/client.go sets the JWT exp claim to exactly 1 hour (time.Hour * time.Duration(1)):

https://github.com/okta/okta-sdk-golang/blob/master/okta/client.go#L765

Expiry: jwt.NewNumericDate(time.Now().Add(time.Hour * time.Duration(1))),

Okta's documentation states the client_assertion JWT must have an expiration of "maximum of only one hour". However, Okta enforces this as strictly less than 3600 seconds, not less than or equal.

When the client machine's clock is even slightly ahead of Okta's servers (common with NTP sync variance), the JWT expiration appears to Okta as >3600 seconds and is rejected with:
"The client_assertion token has an expiration too far into the future"

This results in OAuth2 authentication failing with "Empty access token" when using private_key authentication.

What is expected to happen?

OAuth2 authentication using private_key should succeed regardless of minor clock skew (within reasonable bounds like ±5 seconds) between the client and Okta servers.

The JWT expiration should have a safety margin (e.g., 55 minutes instead of exactly 60) to account for normal clock drift.

What is the actual behavior?

When the client's clock is ahead of Okta's servers by even 1-2 seconds, the OAuth2 token request fails with:

{
  "error": "invalid_client",
  "error_description": "The client_assertion token has an expiration too far into the future. Please see https://developer.okta.com/docs/reference/api/oidc/#token-claims-for-client-authentication-with-client-secret-or-private-key-jwt for the requirements."
}

### Reproduction Steps?

1. Configure Okta SDK with OAuth2 private_key authentication
2. Ensure your machine's clock is synced or slightly ahead of Okta's servers
3. Attempt any API call that triggers authentication

To verify clock skew against Okta:
```bash
echo "Okta: $(curl -sI https://YOUR_ORG.okta.com | grep -i '^date:' | cut -d' ' -f2-)"
echo "Local: $(date -u '+%a, %d %b %Y %H:%M:%S GMT')"

To confirm the boundary issue with a Python test:

import time, jwt, requests

OKTA_DOMAIN = "https://YOUR_ORG.okta.com"
CLIENT_ID = "your_client_id"
PRIVATE_KEY_ID = "your_key_id"
PRIVATE_KEY = open('private_key.pem').read()

def test(exp_seconds):
    now = int(time.time())
    claims = {
        'iss': CLIENT_ID, 'sub': CLIENT_ID,
        'aud': f"{OKTA_DOMAIN}/oauth2/v1/token",
        'iat': now, 'exp': now + exp_seconds
    }
    assertion = jwt.encode(claims, PRIVATE_KEY, algorithm='RS256', 
                          headers={'kid': PRIVATE_KEY_ID})
    data = {
        'grant_type': 'client_credentials',
        'scope': 'okta.users.read',
        'client_assertion_type': 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
        'client_assertion': assertion
    }
    return requests.post(f"{OKTA_DOMAIN}/oauth2/v1/token", data=data).status_code

# Test boundary
for minutes in [57, 58, 59, 60, 61]:
    status = test(minutes * 60)
    print(f"{minutes} min: {status}")  # 59 min = 200, 60 min = 401

Additional Information?

No response

Golang Version

Not directly relevant - issue is in SDK logic, not Go version specific

SDK Version

v6.0.3 (also v5.0.6 and v4.1.2 - terraform-provider-okta v6.5.5 uses multiple SDK versions)

OS version

No response

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions