Followed steps in the docs but it’s a mix of Implement OAuth for Okta with a service app | Okta Developer and Configure OAuth 2.0 Demonstrating Proof-of-Possession | Okta Developer. I get the access_token but calling Okta API /api/v1/users returns HTTP 400.
-
Create App Integration > select API Services
-
Edit Client Credentials > select Public key / Private key > Add key > copy private JWK key > Save
-
In General Settings by default Require Demonstrating Proof of Possession(DPoP) header in token requests is True
-
Okta API Scopes Tab > Grant okta.users.read
-
Admin Roles Tab > Assign Read-only Administrator > Save changes
-
Convert private JWK key to public PEM and private PEM (I use 8gwifi .org/jwkconvertfunctions.jsp)
-
Create and sign the client_credentials JWT using jwt .io
Payload:
{
"aud": "https://dev-<account#> .okta .com/oauth2/v1/token",
"iss": "<app_client_id>",
"sub": "<app_client_id>",
"exp": <epoch in future>
}
- Create and sign the DPoP JWT using jwt .io
Header:
{
"typ": "dpop+jwt",
"alg": "RS256",
"jwk": {
"kty": "RSA",
"e": "AQAB",
"use": "sig",
"kid": "<private key kid>",
"alg": "RS256",
"n": "<private key n>"
}
}
Payload:
{
"htm": "POST",
"htu": "https://dev-<account#> .okta .com/oauth2/v1/token",
"iat": <epoch now>
}
- Call POST on dev-<account#>.okta.com/oauth2/v1/token
curl --location 'dev-<account#>.okta .com/oauth2/v1/token' \
--header 'DPoP: <DPoP JWT>' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'scope=okta.users.read' \
--data-urlencode 'client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer' \
--data-urlencode 'client_assertion=<client_credentials JWT>'
- Receive HTTP 400 and use response header ‘dpop-nonce’ to create new DPoP JWT
{
"error": "use_dpop_nonce",
"error_description": "Authorization server requires nonce in DPoP proof."
}
- Create and sign the DPoP with nonce JWT using jwt .io by using the same header but adding nonce and jti in payload
{
"htm": "POST",
"htu": "dev-<account#>.okta .com/oauth2/v1/token",
"iat": <epoch now>,
"nonce": "<response header dpop-nonce from HTTP 400>",
"jti": "<any random character>"
}
- Call the same POST on dev-<account#>.okta .com/oauth2/v1/token but change DPoP header with the new DPoP with nonce JWT
Receive a HTTP 200
{
"token_type": "DPoP",
"expires_in": 3600,
"access_token": "<access_token with DPoP>",
"scope": "okta.users.read"
}
- Call GET on dev-<account#>.okta .com/oauth2/v1/users
curl --location 'dev-<account#>.okta .com/api/v1/users' \
--header 'DPoP: <DPoP JWT>' \
--header 'Authorization: DPoP <access_token with DPoP>'
- HTTP 400 Bad Request with a blank body
How do I fix this and which step is it failing?