Writeup: Leaked JWT Tokens as Part of the Curity HAAPI Authorization Flow
16 April 2025TL;DR
Prior to Curity version 10.0.0, JWTs generated during the HAAPI authorization flow were signed with the same key used for access tokens. As part of the HAAPI flow, these JWTs are exposed in the browser. If an API was misconfigured to accept any Curity JWT token with a valid signature – without properly validating claims such as aud
or scope
– an attacker could use these leaked tokens to gain unauthorized access. This was mitigated in Curity 10.0.0 by signing HAAPI-specific JWTs with a separate key that is not exposed via the JWKS endpoint.
The Hypermedia Authentication API (HAAPI)
Hypermedia Authentication API (HAAPI) was introduced to improve authentication for single-page applications (SPAs) and mobile apps by eliminating browser redirects. Instead, clients interact with the authorization server through API responses, reducing user experience issues associated with redirects on a mobile phone. HAAPI replaces redirection with a hypermedia-driven authentication process, where the client interacts with the authorization server through a structured series of API responses.
HAAPI is designed primarily for mobile applications but also functions in web browsers. The following analysis is based on the Curity React HAAPI Demo, which implements HAAPI in a browser.
The demo application’s request flow can be seen below and is divided into three parts:
- initating the HAAPI flow
- initating the authorization code flow
- user authentication
The relevant flow for this potential security issue is the initiation of the HAAPI flow.
Initiating the HAAPI Flow
The HAAPI flow starts with obtaining a Client Assertion Token (CAT), which is then used to request a DPoP access token, which will be used in subsequent authorization requests. Note that the DPoP access token is not the same thing as a DPoP JWT token, which is also used in the HAAPI flow.
In the Curity React HAAPI Demo, the CAT token request is sent using the react-client
, which is a HAAPI-enabled client. For demo purposes, the react-client
also has access-tokens-as-jwt
enabled instead of opaque tokens. This is to see the similarities between the exposed tokens and the valid access tokens.
The CAT request can be seen below. The payload includes:
- challenge: A base64-encoded string containing a
state
anddata
parameter. Thestate
is a signed JWT token. - challenge_response: A response to the challenge.
- setup_message_data: Metadata related to the authorization flow.
POST /oauth/v2/oauth-token/cat?client_id=react-client HTTP/1.1
Host: localhost:8443
Content-Length: 7759
Content-Type: application/x-www-form-urlencoded
challenge=eyJzdGF0ZSI6ImV5SnJhV1FpT2lJdE1qZzBOVGc0TlRFd0lpd2llRFYwSWpvaVVHZGxXRXM1WmxSTU9USkNZVzFQZUVKaFNYRm5
SbU56YVZSM0lpd2lZV3huSWpvaVVsTXlOVFlpZlEuZXlKcGMzTWlPaUpvZEhSd2N6b3ZMMnh2WTJGc2FHOXpkRG80TkRRekwyOWhkWFJvTDNZ
eUwyOWhkWFJvTFdGdWIyNTViVzkxY3lJc0ltbGhkQ0k2TVRjME1qZzROekE0TXl3aVpYaHdJam94TnpReU9EZzNNVEl6TENKemRXSWlPaUp5W
ldGamRDMWpiR2xsYm5RaUxDSjBlWEJsSWpvaWQyVmlJaXdpYW5ScElqb2lOakpqTVdVNVlqY3hNREZoT1dNek5XUm1PVGsxTm1JMk9XWmxNV0
l4WVdNMVptRTRPVFkxTldSaE9XWTJNelV4TkdVMk16TmpNRFZoWldNMU9UWmpNU0o5LmRyU0k1bWVUZW5hY3JQZzk1d0F4dkRqVzZEVFQtS3V
BeG5nRlhnMzJHc2hHTWpTeHFiT0JkTjYyaVNzQmJ4MmdaS0daeTBwWjVjbTlUYmhqYzF2R2hZeFVuQmdnOFFiQTBSUmVYSmtBdmlidWFWRS1l
cEt1dk5RZGthRllKRGl4bHJrM0hJaFpCYndBdjZvMmh0bjJKYWppdDVnZWV4dkh6QVMxcW1waVBxU2lOcnpJSnNSNm5DdUZ5UmJpWEFBaXRjQ
VMydi02eDNITGNqM19ldF9IX18tOW9ScFpwMFZzUXZZWkxXd1BHeWFBR2NWZkpMaUN0dWRVcmRyWTBTSE1PWnZrRWxjVzAtME0tQk5qWW9jTz
ctQWJ4QWhSZm55RTB6WWpLX2lRVUFmamVrUVhXeVhxRlBPRjljWDZmUGxqVHRHbU5wYUV3ekVSLTY4dHljV3ZYUSIsImRhdGEiOiI2NEI3MkJ
FRkZEMzlCQkQ4OTMwNTgyNkNFNzg0RDNEMDkxQTBDRUFDMDhCQkUwNTU3MERGMDk2ODgxRTI2NTlERTExQTcwRkM0RDdFNzc4NjU5NEVDN0ZF
MTQzMUE1RDAzQkU2MUY0MDQwQTgxM0QxNDdDNEY0OTE3N0Y1NkNFNUU4NEM4MTlBMkVCNkU4MkQzNjc1MjI4NjFBMTQ0RUIwOTBDMjVEREE5R
UU1ODhDMzhERENFMEZFNDY3REYwMTY1RjQ0Q0FCNzlERTM0MjM4RDVCM0Y1Mzk0QURBRDdGNkQ1MzU5RTYxN0IyMENCREI3N0U3MTZDODEzQ0
VCRDk0QzI1NjAzQzc2MERGMUQ3RDZCNTAzQTc5QkVENThCNjAzN0Y1RkEyQUI1OTM1QTAzRDIyQzdCNzJFQUVBQzZCNUFDMjMzM0U5OEE5OEQ
xNEQzNkJBMTAxNEI1OUE0MjFEQkEzNEZBNUZBMzkyNjIwQzczRThFMTExNTM1RUI4NTkwOTUxNkEwMDRCMjIyREQ3MUE5QzIyRTVCQ0YyODYy
M0VDRUYzMEQ4OTM0MTI1MDBBNEIwREYyRTBGMTk4QTBFODM1MTlBRUE5NDIxNDREN0ZGNzBDODJDMjg2NzIwNzFCMzQ3RTUyRTUzODNFNTRCR
TNFOUY3RjVDOUY1NDE2N0FCOEM1MjU2ODlDRjU1MDhGODQzQzc1NkQ4OTVDNEFDMTg3OEI0RUNFNzUzMzc5OTg3RjQxMDc1OUU1NkE5QjE5Rj
EyMjlGRkZBMjYwMEU0MkQ4MUQwNTkzM0Q2NTY5QTQxNzAzMjREM0Y3M0UzMzMzMkI4QjU1MkIzMTRDRUM1QTM3RDU5OUI4Nzk3QjNFMTZGRDB
CNDdCNzUxNkNBQUZBODdGNEM4RDQwN0UxMEM4MzJEMjUxQUFGMzUwRDU5OUYzNzFBNzUzQzFFNzREQkRENzY0ODFEMjZCMEEwRUQxOEZGMTIw
OUY1QUUzRDg0MTNDODRCNTNBRjMxMDU4MTI3NTJGRDVERkExNzZBMkFBMTRGMEE2NTM1Q0Q4MUE4OThGQjc0RkZDODA0NzI5MjkxRTM0QzhCR
UMzMjM3QTc3QzlDN0VEMzYxNDM1Njg1Qzg2MEE3QjBERDRCNjBEREQ4REIxNUIxQUExNTNGNUQxODA5NTMxRkUxMEY1MTZENTRCNzA1ODk0RT
YzNzg1QzVDOERDQjVEODE3MEI1MjMwRUY3NUFBMTc5RUNCNTIzMzFEOUE1NUQ0QTM3RUJFN0U3QjExNTVDMDUxMzhBMzcyMDAxMUQ3NjI0MzU
5QzFGQTNGMEMyM0I5RjM5M0E2N0RBOTM0MkJFOTIxM0FGQjhEMjhBRkYyQjU0QzA0MTU3MjdCQTBFRDUyNUI3MERDQzRGRTY2NUNDMzVDREIy
MTdDNEUxNUQxRjlGRjE1NEM4QzhCQ0I1RjEyN0UxRDNERDhBOTQ4QUM2NTkxMkVEMkE0QkQ2QzVCQTVBOTU4OEI5QkUwRTA4M0M4NTRFODY1M
UQwQTk2MjdENzA0OTU5MUFFMTQ2NzI4NjdFNjRCODc3RkNGRTFDQTcyRTE0MTgwNjY5Q0YxQjE0NEQyMTg4QzNFRDIwRkI2RUE3NzQzMDc1QT
NDQzA4NzA4MkU1QjQyRTUyMThBMkRCMDZBMDkzRkM1Qjg0QjY1QzNFREI2OEU0MkQ3MERDODE3RjAyMEVBRUUyMTg4NURGNDMwQkU0NTVDOEI
1RjI1MDQwQzAzRDVDQ0ZEODQ1M0E4MzRERERFOUVCMTkxQzYyRDQ2Q0ZDRDM3NzU3NzA4NDNBNDhGNEI4Q0YwOTYxRDY0MTBFQkJCN0Q1MDk2
RjM4NUY3QzU2NzhDMTQwRjIzMUU0QUYxNDA0MzJBMEU2NkQ2MzM0Qzk2QUMyOTAzNTJBMDhFMUNGNjlFQUUxQjdCMUU0MTVBMjI4MjREQUE1Q
UUwMjQzNTkyMjk2Q0E2RURDREExMkI3MzQxNkU3RDMyNTJDN0ZDREI2NDIyMDdDRUQ1NDk0RjMxNEQzQjU5N0MzQTg0NUVFNzEyNjRDMTRFQU
NCNUVEQjQ5OEI3NjJGRTA1RDI3RkNENzM5M0UyOTk0OTJDNTk3QTQwQjEyMzgzNzRFRDI5QUI2RDBCOTg3NEI3NjRCNzFCNTgyRTNGN0VFMDQ
0OURENjEwMzIxNjUyMUZERkJEMjIzMEY5NUQ4NDJGRjAyNEJGODRCQ0IwQzQ2MDQwQjA1NDlCRjUwQjYwNTQxRjQ2QjVGQzY5MEE5MTZBRUY5
QzMzMjg4NUY0NjNCQjBBRkY2NkRBOEY4NjdDRkI4QzQ5NDAzNzlFQzE5MzU0MDAzMDhBOUM4QUQxNkI0NzZFODdGQzI0QUU4MUI3QzdFOUM1N
UZGQkFFMkEzMUI2REQ4NEJDMTA0QTM0QTE1N0Y2NTQ1QTQzNDE5Qjg2NzJGMEQyNTM3NzVFMUY3RjA4MjY0MUE3QUI5NUUzNjUxQkM1OTUyQk
YwN0Y5NkQzNDc2Njk1NTA1OTY4NjkwRDBCOTZCMzE1MUFCNEJFMzVDNDM5NDBCNzAwMDVERTI3RUQ3QjFBNERERkI5NDVDNzZCNTQ2MUIzMTN
BNjNBQTBFMkMzODYzMzIxQjdBMTdGNDU2MzUwQjcwRjY5Q0FFODE5NjdBNkQyM0Q3NDE1OUEwOTY5NDAxMzZGMTc0OEM4OUZFOEQ5NTAyRjI5
N0MyMTA5RUFGNDE3MDY4NEQyRURDQkM1MUM0OUE0OTc2N0YxMzgxMkQ0MUY5Njc0NTdBMEQzQTJGNUM1NTYyQjdCODkyOThFNkE2QTA0REE4N
EE4NjJFMEFEMEMzM0NCOTM0RDAzQzdDMzcwMTJCRjhFM0QwRkE2OEREQ0U2NzlGRjI5MTQxNDlEQUE0NUI3RUIzMTFBMEVEOTBFOTQ1MTNDQ0
Y4RjhCOTNCMTE2Mjg4Mzk0NkUyQTFBRUUzOUI0OTExMDREODY0OEVBNjZBNURCQTYwNEVGNUIzOUE2MzY3OEU3N0ZCNDY5REI1M0NBRTFGNzB
CM0Q4NTU0NzhFNUUyNEU4RkNDRDZCODBDQzM4NTdGQzRDMUVCMjA5NzU1RUQ4M0YzMzgzQ0JENTNGQzFGQkVDRTBBN0QwNUYxMEU1NDgxNTdG
RTc5MjU5NEZDNzBGM0U1RDA5NTNFMjcwMTZGNUU2NzY4NjU3M0QyQkMyQjMyQ0UxOTc0NEJCNDc3MTRBNDNFODIwREYyMEI4QkYzM0QyOTBDQ
zZFRkZGRDNBRDE5Qzk0NzVCNURFNUJEQThGMkMwRTU0MDNENEIyNDkyRjdGMUJFQUI0QjBBMzM5QzI2MDNDRTE5MjdDRTNCMTE2RDYyNzUyMk
I1RjE3NEE1REM4MTE4OUI3NzIwQ0RGNTk3NkYyMjY5NUJDNjY3MEMwNDhBRDhFQjI0RDA1OEQ5QUU2NTgxNDIxMkZDOUIwNkMwODRCRTU2MjY
2NTkwRTgxNUVDMDQ2QkY2NjhFQTQxOTc0QTk0ODM5REY1MUFCRDE2NDI2MkFBOUM5Q0U4Mjc5ODUwOEFBMkZDMjU1MkEyMDJGQzIwNTEwOTAz
MjQxQTI5OTgxRjU5NUZEQzI2QjAxQ0YxRjI3NjZBNzM3MzQ2QjU2MDI2N0VCQjdGMUIyMzVCMUU0NjY1Nzc5Rjc1MDA3NDMwOUMwQzQ3MzRCN
DVBNjg2MTM2MDYwN0I5ODE1NjEzRTJFMjAxRjBBMEY5Q0NBODJBMzQ2OTZGQUNDMEVCOEI2QkJBNTlGMjU3RkYzRDREMERCNjVGQTdERDA3OT
Y5QUNDNDdBNUM2MUNDQ0Y2QUU3NjFBRUY3NDgwRDdEOUJBNjk1MDcwNDBGM0U2NUQ2MkM0RjcwQTUyNzIifQ&challenge_response=93650
51b57...&setup_message_data=%7B%22startedLoadingAt%22%3A%222025-03-25T07%3A18%3A03.127Z%22%2C%22finishedLoadi
ngAt%22%3A%222025-03-25T07%3A18%3A04.095Z%22%2C%22timeout%22%3A5000%7D
The Curity Identity Server (IdP) returns a CAT token. This JWT token is signed by the IdP and includes claims such as:
issuer
(iss
): https://localhost:8443/oauth/v2/oauth-anonymoussubject
(sub
): react-clientaudience
(aud
): https://localhost:8443/oauth/v2/oauth-tokendata
, which is client-specific data such asorigin
,user agent
,local time
{
"cat": "eyJraWQiOiItMjg0NTg4NTEwIiwieDV0IjoiUGdlWEs5ZlRMOTJCYW1PeEJhSXFnRmNzaVR3IiwiYWxnIjoiUlMyNTYifQ.ey
JpYXQiOjE3NDI4ODcwODQsImV4cCI6MTc0Mjg4NzEyNCwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6ODQ0My9vYXV0aC92Mi9vYXV0aC1
hbm9ueW1vdXMiLCJzdWIiOiJyZWFjdC1jbGllbnQiLCJhdWQiOiJodHRwczovL2xvY2FsaG9zdDo4NDQzL29hdXRoL3YyL29hdXRoLXRv
a2VuIiwicHVycG9zZSI6ImNhdCIsInR5cGUiOiJ3ZWIiLCJkYXRhIjp7Im9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsInVzZ
XJBZ2VudCI6Ik1vemlsbGEvNS4wIChXaW5kb3dzIE5UIDEwLjA7IFdpbjY0OyB4NjQpIEFwcGxlV2ViS2l0LzUzNy4zNiAoS0hUTUwsIG
xpa2UgR2Vja28pIENocm9tZS8xMzQuMC4wLjAgU2FmYXJpLzUzNy4zNiIsImxvY2FsVGltZSI6IlR1ZSBNYXIgMjUgMjAyNSAwODoxODo
wNCBHTVQrMDEwMCAoQ2VudHJhbCBFdXJvcGVhbiBTdGFuZGFyZCBUaW1lKSIsInJlY2VpdmVkQ2F0c1RpbWVzdGFtcHMiOltdLCJkb3du
bG9hZFNwZWVkIjoyLCJzY3JpcHRUYWdDb3VudCI6MSwibG9jYWxTdG9yYWdlQXZhaWxhYmxlIjp0cnVlLCJjb25uZWN0aW9uVHlwZSI6I
jRnIiwiZGV2aWNlSWRlbnRpZmllciI6IiJ9LCJqa3QiOiJmbUxXUjdMLWFRQlVoRHVRekpKV2lnX2M4SEdvY1FQRWRleVQ4ei1CUlRNIn
0.eGZ8zEjPavHF9NqsSlUEw98jhj-k0V-pDIYq6qkzmd86L4vUJ5I8PYLYSbLhrc3NSJmZYh98P4NIl8rXMtvGnEpMQA9XtSGeLfkHghd
kTECnJqSbXqKhTgqYJbiVALiyWR-l2M_FwLwmGhbTH4aqT7a3ZuZIzH68kflu-T2jEHMWP6SInqjotmve1vQVJRPGnmFcaXQcHDZfLTrg
VKGbUMAxSH03-53gt2-7EOBpFDSwJneGve7HOaHcjVsfyVXAA0ag_-Djqbgt8-lYfX_2t6xmHRk1p--TE-yMyclA9A0wv0wE0gIPbA-gs
UKqY6-dgO8ajx6Ld9TFQn8kkQoscQ"
}
{
"iat": 1742887084,
"exp": 1742887124,
"iss": "https://localhost:8443/oauth/v2/oauth-anonymous",
"sub": "react-client",
"aud": "https://localhost:8443/oauth/v2/oauth-token",
"purpose": "cat",
"type": "web",
"data": {
"origin": "http://localhost:3000",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
"localTime": "Tue Mar 25 2025 08:18:04 GMT+0100 (Central European Standard Time)",
"receivedCatsTimestamps": [],
"downloadSpeed": 2,
"scriptTagCount": 1,
"localStorageAvailable": true,
"connectionType": "4g",
"deviceIdentifier": ""
},
"jkt": "fmLWR7L-aQBUhDuQzJJWig_c8HGocQPEdeyT8z-BRTM"
}
The CAT token is then used for client assertation for a token request to get a DPoP access token to use in subsequent HAAPI authorization requests.
It is important to note that no user credentials have been provided at this stage – so far, the only action taken has been initiating the HAAPI flow by loading the sign-in page.
The request is a standard token request to the /oauth-token
endpoint with the following parameters:
client_id
: react-clientscope
: urn:se:curity:scopes:haapigrant_type
: client_credentials
The authorization is done through client assertion using the CAT token from the previous request.
POST /oauth/v2/oauth-token HTTP/1.1
Host: localhost:8443
Content-Length: 1455
Dpop: eyJqd2siOns...
Content-Type: application/x-www-form-urlencoded
client_id=react-client&scope=urn%3Ase%3Acurity%3Ascopes%3Ahaapi&grant_type=client_credentials&client_assertio
n_type=urn%3Ase%3Acurity%3Aattestation%3Aclient&client_assertion=eyJraWQiOiItMjg0NTg4NTEwIiwieDV0IjoiUGdlWEs5
ZlRMOTJCYW1PeEJhSXFnRmNzaVR3IiwiYWxnIjoiUlMyNTYifQ.eyJpYXQiOjE3NDI4ODcwODQsImV4cCI6MTc0Mjg4NzEyNCwiaXNzIjoiaH
R0cHM6Ly9sb2NhbGhvc3Q6ODQ0My9vYXV0aC92Mi9vYXV0aC1hbm9ueW1vdXMiLCJzdWIiOiJyZWFjdC1jbGllbnQiLCJhdWQiOiJodHRwczo
vL2xvY2FsaG9zdDo4NDQzL29hdXRoL3YyL29hdXRoLXRva2VuIiwicHVycG9zZSI6ImNhdCIsInR5cGUiOiJ3ZWIiLCJkYXRhIjp7Im9yaWdp
biI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsInVzZXJBZ2VudCI6Ik1vemlsbGEvNS4wIChXaW5kb3dzIE5UIDEwLjA7IFdpbjY0OyB4NjQpI
EFwcGxlV2ViS2l0LzUzNy4zNiAoS0hUTUwsIGxpa2UgR2Vja28pIENocm9tZS8xMzQuMC4wLjAgU2FmYXJpLzUzNy4zNiIsImxvY2FsVGltZS
I6IlR1ZSBNYXIgMjUgMjAyNSAwODoxODowNCBHTVQrMDEwMCAoQ2VudHJhbCBFdXJvcGVhbiBTdGFuZGFyZCBUaW1lKSIsInJlY2VpdmVkQ2F
0c1RpbWVzdGFtcHMiOltdLCJkb3dubG9hZFNwZWVkIjoyLCJzY3JpcHRUYWdDb3VudCI6MSwibG9jYWxTdG9yYWdlQXZhaWxhYmxlIjp0cnVl
LCJjb25uZWN0aW9uVHlwZSI6IjRnIiwiZGV2aWNlSWRlbnRpZmllciI6IiJ9LCJqa3QiOiJmbUxXUjdMLWFRQlVoRHVRekpKV2lnX2M4SEdvY
1FQRWRleVQ4ei1CUlRNIn0.eGZ8zEjPavHF9NqsSlUEw98jhj-k0V-pDIYq6qkzmd86L4vUJ5I8PYLYSbLhrc3NSJmZYh98P4NIl8rXMtvGnE
pMQA9XtSGeLfkHghdkTECnJqSbXqKhTgqYJbiVALiyWR-l2M_FwLwmGhbTH4aqT7a3ZuZIzH68kflu-T2jEHMWP6SInqjotmve1vQVJRPGnmF
caXQcHDZfLTrgVKGbUMAxSH03-53gt2-7EOBpFDSwJneGve7HOaHcjVsfyVXAA0ag_-Djqbgt8-lYfX_2t6xmHRk1p--TE-yMyclA9A0wv0wE
0gIPbA-gsUKqY6-dgO8ajx6Ld9TFQn8kkQoscQ
In response to this request, a DPoP (Demonstrating Proof of Possession) access token is returned. This token is then used for authorization in subsequent requests during the HAAPI flow. Note that only the response body is shown below.
{
"access_token": "eyJraWQiOiItMjg0NTg4NTEwIiwieDV0IjoiUGdlWEs5ZlRMOTJCYW1PeEJhSXFnRmNzaVR3IiwiYWxnIjoiUlMy
NTYifQ.eyJkcGwiOjEsInN1YiI6InJlYWN0LWNsaWVudCIsImF0dGVzdGF0aW9uIjp7Im9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6Mz
AwMCIsInVzZXJBZ2VudCI6Ik1vemlsbGEvNS4wIChXaW5kb3dzIE5UIDEwLjA7IFdpbjY0OyB4NjQpIEFwcGxlV2ViS2l0LzUzNy4zNiA
oS0hUTUwsIGxpa2UgR2Vja28pIENocm9tZS8xMzQuMC4wLjAgU2FmYXJpLzUzNy4zNiIsImxvY2FsVGltZSI6IlR1ZSBNYXIgMjUgMjAy
NSAwODoxODowNCBHTVQrMDEwMCAoQ2VudHJhbCBFdXJvcGVhbiBTdGFuZGFyZCBUaW1lKSIsInJlY2VpdmVkQ2F0c1RpbWVzdGFtcHMiO
ltdLCJkb3dubG9hZFNwZWVkIjoyLCJzY3JpcHRUYWdDb3VudCI6MSwibG9jYWxTdG9yYWdlQXZhaWxhYmxlIjp0cnVlLCJjb25uZWN0aW
9uVHlwZSI6IjRnIiwiZGV2aWNlSWRlbnRpZmllciI6IiIsInR5cGUiOiJ3ZWIifSwicHVycG9zZSI6ImhhYXBpIiwiaXNzIjoiaHR0cHM
6Ly9sb2NhbGhvc3Q6ODQ0My9vYXV0aC92Mi9vYXV0aC1hbm9ueW1vdXMiLCJjYWwiOiJieS1wb2xpY3kiLCJhdWQiOlsiaHR0cHM6Ly9s
b2NhbGhvc3Q6ODQ0My9vYXV0aC92Mi9vYXV0aC1hbm9ueW1vdXMiLCJodHRwczovL2xvY2FsaG9zdDo4NDQzL2F1dGhuL2F1dGhlbnRpY
2F0aW9uIl0sImNhcCI6IndlYi1wb2xpY3kiLCJzY29wZSI6InVybjpzZTpjdXJpdHk6c2NvcGVzOmhhYXBpIiwiY25mIjp7ImprdCI6Im
ZtTFdSN0wtYVFCVWhEdVF6SkpXaWdfYzhIR29jUVBFZGV5VDh6LUJSVE0ifSwiZXhwIjoxNzQyODg3Njg0LCJpYXQiOjE3NDI4ODcwODQ
sImp0aSI6IjAwZmU1Mjk2LTQxZWMtNDQ0Ny1hMDhhLTM4ODhmMDViM2UyZCJ9.lRh-fpfxnANTkhskU6mPRHvabBTJLvy9OyvVYtNwuEn
kVMJqsFN9EJYeXfE0c94KZx-lV1m-yUDHMYYDpdfNT-umSphBTVr0ntHboi7YI_ter-5fLGFkh3Uy5E5lSMBbP0LPteVhL1OpWQCi4_6K
9UM9yNc4pvZVN_hMA2BgS6IUerhprJu7CeST1GE6jFe5DmJI-BtwMFfcIORxeN09nodqEA-bFpVOAV3hzMFuHKuL2PjUhTpBnMvt-RtU_
UEV-taa6VHh3jkyYfJ2Zm6PeiGV0PbJchJAKAvRBBv590y74OewsYi4GzjVl8Hmmgro31tAhixi6FlUCHwHy1iXtA",
"scope": "urn:se:curity:scopes:haapi",
"token_type": "DPoP",
"expires_in": 600
}
The HAAPI flow is now initiated.
To complete the authorization flow, the user selects an authentication method, and the standard authorization code flow follows. A request is made to the /oauth-authorize
endpoint, leading to an exchange for an ID token and an access token.
POST /oauth/v2/oauth-token HTTP/1.1
Host: localhost:8443
Content-Length: 215
Content-Type: application/x-www-form-urlencoded
client_id=react-client&grant_type=authorization_code&code=zFVYfmNg1gRmqxrTs2dTiFBwNgpS3AdV&code_verifier=f8kQ
XDd2f5wp1VpXWOQTjOgXae2xwPFRDn9Z32rEucT7q8a0sQpVpr7jCwU5FYIn&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2F
The response contains an id_token and an access_token.
{
"id_token": "eyJraWQiOiItMjg0NTg4NTEwIiwieDV0IjoiUGdlWEs5ZlRMOTJCYW1PeEJhSXFnRmNzaVR3IiwiYWxnIjoiUlMyNTYi
fQeyJleHAiOjE3NDI5MTQ2MjMsIm5iZiI6MTc0MjkxMTAyMywianRpIjoiYjllYzg4ZmMtYzFhOS00NzE2LWIxYzYtZTRmNGEzZGZlNjk
1IiwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6ODQ0My9vYXV0aC92Mi9vYXV0aC1hbm9ueW1vdXMiLCJhdWQiOiJyZWFjdC1jbGllbnQi
LCJzdWIiOiJtaXJhIiwiYXV0aF90aW1lIjoxNzQyOTExMDIyLCJpYXQiOjE3NDI5MTEwMjMsInB1cnBvc2UiOiJpZCIsImF0X2hhc2giO
iJGUm9HNW9CREZ4VTBPamFjTHpGUkRBIiwiYWNyIjoidXJuOnNlOmN1cml0eTphdXRoZW50aWNhdGlvbjp1c2VybmFtZTp1c2VybmFtZS
IsImRlbGVnYXRpb25faWQiOiJmZmM5M2YyMy01YWE5LTQyNzktODUwZS1jOTQxM2U2YjI3ZDgiLCJhenAiOiJyZWFjdC1jbGllbnQiLCJ
hbXIiOiJ1cm46c2U6Y3VyaXR5OmF1dGhlbnRpY2F0aW9uOnVzZXJuYW1lOnVzZXJuYW1lIiwic2lkIjoiN3I5M05yVkFIOElFY25BRiJ9
.t96Y45YG-VLoJ1tOQaX9xPuvq9ha3yyVx7BouYOJ2YDemSqazBa7zfGNxqiNluf04CaNlbUC2X7mV4xSaQscWqd1hzRjWOG1iXyJ2Emh
n9xCdbLvuRHqRqwQDJr2h64BFpkMmVeWkTV96vOCiOp8BiuZKPc7QtYVDvFK6i1DU2pk4QuBgIefJBSLJ4gQNRrpbSMl4Auwbqo3gX9mm
waMU-V9OGlgHbYZab31Zof65tQ-sYEnx2FV5d_a2XgNPZXT3P_l7d9ytzPyLEKYHJhwXx3hwh45yh_zcZbGfkH13xOMft6zDEWd3QCEXw
Y7zQN4ii7EW9Sxn07LuziiQoWsvA",
"token_type": "bearer",
"access_token": "eyJraWQiOiItMjg0NTg4NTEwIiwieDV0IjoiUGdlWEs5ZlRMOTJCYW1PeEJhSXFnRmNzaVR3IiwiYWxnIjoiUlMy
NTYifQ.eyJqdGkiOiJhZjc1MGZkYy1mZWJjLTQ2MDAtOWM0MC0xOWJkMTBmNGVhZmMiLCJkZWxlZ2F0aW9uSWQiOiJmZmM5M2YyMy01YW
E5LTQyNzktODUwZS1jOTQxM2U2YjI3ZDgiLCJleHAiOjE3NDI5MTEzMjMsIm5iZiI6MTc0MjkxMTAyMywic2NvcGUiOiJvcGVuaWQiLCJ
pc3MiOiJodHRwczovL2xvY2FsaG9zdDo4NDQzL29hdXRoL3YyL29hdXRoLWFub255bW91cyIsInN1YiI6Im1pcmEiLCJhdWQiOiJyZWFj
dC1jbGllbnQiLCJpYXQiOjE3NDI5MTEwMjMsInB1cnBvc2UiOiJhY2Nlc3NfdG9rZW4ifQ.fdWmGTRtm9_Hgqk66CvW3ZB-OmGIVeAnTP
6cUvK39X2eXZGse8-jb9hwH1yy09qif0_QaYTu8PzWFfi_3xtr5JaT4BrdD_M1mXeotaLvaa7bRsR3DjuV0-JkAtfGNB4D4RiMuD_Ivn9
_ccIJtLAIHGkycb2aXWKrWhWLXKR6we72nXYRLD9qOyh3WI5WQnoe11IhrQccXLxTwx46OQG_wsyjxgt25U4N3gAPFqQCkc6msnB_WpmI
-GZPelC_wtja3D1WAwBKRoANv0vwjlj3kCdLeJfjLBlMy8JZl8onr4o78BMxE6u_LNP0Nehp5nna7hyjF7CHia3p3CC6uyr7BA",
"scope": "openid",
"expires_in": 300
}
The Issue: Leaked HAAPI JWT Tokens Signed by the Same Key as Curity JWT Access Tokens
HAAPI JWTs are exposed in the browser before user authentication and signed by the same issuer (iss
) as Curity JWT access tokens. Before Curity version 10.0.0, the JWT tokens used in the HAAPI flow were also signed using the same key as access tokens.
If an API that consumes access tokens was misconfigured (e.g., accepting tokens based only on the signing key without proper audience validation), it was possible for attackers to use leaked HAAPI JWT tokens for unauthorized API access. Although HAAPI JWT tokens contain different claims and audiences than access tokens, they were exposed in the browser, introducing an unnecessary attack vector.
This potential security issue was confirmed by running the Curity Identity Server (curity.azurecr.io/curity/idsvr:9.7.1
) locally with the React HAAPI Demo.
If the valid access token is inspected, it is possible to see that the issuer and signing key is the same as the access token of type DPoP.
Valid Access Token Header
{
"kid": "-284588510",
"x5t": "PgeXK9fTL92BamOxBaIqgFcsiTw",
"alg": "RS256"
}
DPoP Access Token Header (From HAAPI Flow)
{
"kid": "-284588510",
"x5t": "PgeXK9fTL92BamOxBaIqgFcsiTw",
"alg": "RS256"
}
Both tokens were signed with the same key, PgeXK9fTL92BamOxBaIqgFcsiTw
, making it possible for a misconfigured API to mistakenly accept HAAPI JWTs as valid access tokens. The JWT tokens used for the HAAPI flow are exposed to unauthenticated attackers, providing them with signed tokens from the same issuer as the valid access tokens. While it is the API’s responsibility to validate the token, leaking signed JWT tokens in the browser is not good practice.
The DPoP access token can thus be used in misconfigured APIs, where the API does not validate the audience correctly. This was confirmed by building a simple API which accepts JWT tokens from issuer https://localhost:8443/oauth/v2/oauth-anonymous
, but does not validate the audience.
Misconfigured Token Validation
APIs must validate tokens used for access. Best practices, as outlined by Curity, recommend verifying the issuer, audience, and scopes (if applicable). If the typ
header claim is set, it should be checked for at+jwt
. Curity also offers an alternative purpose
claim to indicate token intent, as Curity tokens do not include a typ
claim.
Validating the audience, purpose, or scopes helps prevent misuse. Note that the HAAPI tokens provide other claims (cnf
and sub
) that can be used to distinguish it from an expected access token. It should also be noted that using reference tokens instead of JWT as access tokens will also mitigate this issue. For example, Curity reccomends using the Phantom Token flow.
In .NET, proper token validation should include audience, issuer, lifetime, and signature checks, as seen below:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, jwtOptions =>
{
jwtOptions.Authority = builder.Configuration["Api:Authority"];
jwtOptions.Audience = builder.Configuration["Api:Audience"];
});
However, during penetration tests we often see token validation misconfigurations, such as disabling ValidateAudience
. Broken authentication ranks #2 in the OWASP Top 10 API Security Risks – 2023, highlighting its prevalence. These misconfigurations could be exploited more easily if the HAAPI flow is used and signed tokens are exposed in the browser.
A vulnerable configuration can be seen below. We have encountered similar configurations during several penetration tests of customer implementations.
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, jwtOptions =>
{
jwtOptions.Authority = builder.Configuration["Api:Authority"];
jwtOptions.Audience = builder.Configuration["Api:Audience"];
jwtOptions.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = false,
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
ValidIssuer = builder.Configuration["Api:Authority"]
};
});
Exposing signed tokens in the browser opens up an unneccessary attack vector for one of the most common security risks.
The reporting process
The potential security issue was communicated to Curity on 2024-10-28 through the Curity support channel. The initial response was swift and after a meeting with Curity on 2024-10-28, where the issue of leaking signed tokens was explained to the Curity team, the response was that they will be mitigating the problem. A mitigation was released on 2025-02-10 with the new Curity version 10.0.0.
Curity addressed this issue in version 10.0.0, as noted in the release:
OAuth and OpenID Connect: Sign HAAPI JWT tokens using the internal symmetric key defined for each zone
JWT tokens used in HAAPI are now secured using keys derived from the zone secrets. [IS-9557]
In Curity version 10.0.0, HAAPI-specific JWT tokens are now signed using a different key than the valid access tokens. The signing key is not available at the JWKS endpoint. This change stops misconfigured APIs to accidentally accepting HAAPI-specific tokens, since looking up the signature key will fail.
This can be seen by the different values in the headers of the JWT tokens.
Token Type | kid (Key ID) | x5t | alg (Algorithm) |
---|---|---|---|
Access Token (RS256) | "1704594210" |
"7QrRNsvdUPhPf7S-d2tv539kFvM" |
"RS256" |
HAAPI JWT (HS256) | "dzs-VgYEefSNhJjmZzWa" |
- |
"HS256" |
Timeline
- 2024-10-25: Potential security issue identified
- 2024-10-28: Meeting with Curity, discussing the need to sign HAAPI related JWT tokens with a different key than the valid access tokens.
- 2024-10-30: Report acknowledged by Curity.
- 2025-02-10: Curity 10.0.0 released, which mitigates the identified issue
- 2025-04-11: Writeup draft shared with Curity
- 2025-04-14: Feedback from Curity
- 2025-04-16: Writeup published
More in this series:
- Writeup: AWS API Gateway header smuggling and cache confusion
- Writeup: Keycloak open redirect (CVE-2023-6927)
- Writeup: Exploiting TruffleHog v3 - Bending a Security Tool to Steal Secrets
- Writeup: Stored XSS in Apache Syncope (CVE-2024-45031)
- Writeup: Account Takeover in Authentik due to Insecure Redirect URIs (CVE-2024-52289)
- Writeup: Leaked JWT Tokens as Part of the Curity HAAPI Authorization Flow
- Writeup: Subreport Remote Code Execution in Stimulsoft Reports