Refresh OIDC token doesn't work with PrivateKeyJWT configuration

9 views
Skip to first unread message

Alexandre Pique

unread,
Dec 27, 2025, 6:06:42 AM12/27/25
to Pac4j development mailing list
Hi,

I tried to configure my application with a PRIVATE_KEY_JWT authentication.
The initial connection works, but when the access token expires, it tries to refresh the token and I always have the following exception :

2025-12-23 14:55:31.831;ERROR;"https-jsse-nio-0.0.0.0-8090-exec-1";org.pac4j.core.profile.ProfileManager;"[]";Servlet:AmadeaWeb;055C14B7C5CCF1559E5BDF14EED357A2;138;"org.pac4j.core.profile.ProfileManager Unable to renew the user profile for key: OidcClientorg.pac4j.oidc.exceptions.OidcTokenException: Bad token response, error=invalid_client, description=null, status=400 at org.pac4j.oidc.credentials.authenticator.OidcAuthenticator.executeTokenRequest(OidcAuthenticator.java:121) ~[pac4j-oidc-6.3.1.jar:6.3.1] at org.pac4j.oidc.credentials.authenticator.OidcAuthenticator.refresh(OidcAuthenticator.java:84) ~[pac4j-oidc-6.3.1.jar:6.3.1] at org.pac4j.oidc.client.OidcClient.renewUserProfile(OidcClient.java:83) ~[pac4j-oidc-6.3.1.jar:6.3.1] at org.pac4j.core.profile.ProfileManager.removeOrRenewExpiredProfiles(ProfileManager.java:133) ~[pac4j-core-6.3.1.jar:6.3.1] at org.pac4j.core.profile.ProfileManager.retrieveAll(ProfileManager.java:108) ~[pac4j-core-6.3.1.jar:6.3.1]

I dig a little more, and I think the problem is the PrivateKeyJWTobject which is signed on initial connection and not resigned on refresh. I looked at the token in the HTTP Request and the JWT was expired.

I did an ugly workaround to confirm this. I used my own OidcAuthenticatorand override the createTokenRequest to reinit the OidcOpMetadataResolver and force it to creates a new PrivateKeyJWT.

protected TokenRequest createTokenRequest(final AuthorizationGrant grant) {

OidcOpMetadataResolver metadataResolver = configuration.getOpMetadataResolver();

URI tokenEndpointUri = metadataResolver.load().getTokenEndpointURI();

ClientAuthentication clientAuthentication = metadataResolver.getClientAuthentication();

if (clientAuthentication != null) {

if (clientAuthentication instanceof PrivateKeyJWT pvk) {

System.out.println(pvk.getClientAssertion());

metadataResolver.init(true);

metadataResolver.load();

clientAuthentication = metadataResolver.getClientAuthentication();

System.out.println(((PrivateKeyJWT)clientAuthentication).getClientAssertion());

}

return new TokenRequest(

tokenEndpointUri, clientAuthentication, grant, Scope.parse(configuration.getScope()));

} else {

return new TokenRequest(

tokenEndpointUri, new ClientID(configuration.getClientId()), grant, Scope.parse(configuration.getScope()));

}

}


With that ugly and temporary workaround, the refresh process ends successfully.
I think the PrivateKeyJWT, shouldn't be kept like other authentication method and could be regenerated on each call. (maybe with a cache if the JWT is not expired, but the expiration seems to be very short)

What do you think about it ?

Best regards,
Alexandre

Jérôme LELEU

unread,
Jan 5, 2026, 2:43:22 AM (7 days ago) Jan 5
to Alexandre Pique, Pac4j development mailing list
Hi,

It makes sense, though I'm a bit surprised by the "PrivateKeyJWTobject which is signed on initial connection and not resigned on refresh".

Does the same problem happen at login (to retrieve the access token in exchange for the code)?
I mean, the PrivateKeyJWT is reused to get access tokens multiple times at login for different users. How can it work?

Thanks.
Best regards,
Jérôme


--
You received this message because you are subscribed to the Google Groups "Pac4j development mailing list" group.
To unsubscribe from this group and stop receiving emails from it, send an email to pac4j-dev+...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/pac4j-dev/32b832aa-1f6b-4eee-8bcd-f77d91a487b0n%40googlegroups.com.

Alexandre Pique

unread,
Jan 5, 2026, 8:04:56 AM (7 days ago) Jan 5
to Pac4j development mailing list
Hello and happy new year !

I just did the test with a different user and a different browser and I've got the same exception (org.pac4j.oidc.exceptions.OidcTokenException: Bad token response, error=invalid_client, description=null, status=400).
The assertion used is stored in com.nimbusds.oauth2.sdk.auth.JWTAuthentication as a final SignedJWT clientAssertion, so it can't be renewed when it expired.
When the assertion expires, nobody can log in.
It did a workaround just before my holidays which works and smarter than the first one.
By overriding the OidcOpMetadataResolver, it is possible to override the getClientAuthentication() method and renew the PrivateKeyJWT when expired, something like that :

/**

* Check if the PrivateKeyJWK is expired

* @param i_pvk the key to test

* @return true if expired

*/

public static boolean isJWTExpired(@Nonnull PrivateKeyJWT i_pvk) {

try {

// Gets expiration time in claims (claims can't be null they are built in constructor or it generates an IllegalArgumentExecption

Date expiryTime = i_pvk.getJWTAuthenticationClaimsSet().getExpirationTime();


// Check if the JWT is expired

if (expiryTime == null) {

// No expiration date, not expired

return false;

}

// Check if expiration time is greater than now + some milli-seconds

return expiryTime.before(Date.from(Instant.now().plusMillis(EXPIRATION_TOLERANCE)));

} catch (RuntimeException e) {

logger.errorMessage(e, I_ISLogConstants.kLogError, "An unexpected error occured while checking PrivateKeyJWT expiration occurred");

// In case of error, consider expired

return true;

}

}


/**

* Overrides getClientAuthentication to permits regenerating expired PrivateKeyJWT

* @return In most cases returns supe.getClientAuthentication(), except for expired PrivateKeyJWT

*/

@Override

public ClientAuthentication getClientAuthentication() {

// Gets result of super method

ClientAuthentication auth = super.getClientAuthentication();

// When workaround is enabled, recreate expired PrivateKeyJWT tokens

if ((auth instanceof PrivateKeyJWT pvk) && isJWTExpired(pvk)) {

// Private key signature is expired, recreate it

var privateKeyJwtConfig = configuration.getPrivateKeyJWTClientAuthnMethodConfig();

if (privateKeyJwtConfig != null) {

var jwsAlgo = privateKeyJwtConfig.getJwsAlgorithm();

var privateKey = privateKeyJwtConfig.getPrivateKey();

var keyID = privateKeyJwtConfig.getKeyID();

try {

PrivateKeyJWT newPvk = new PrivateKeyJWT(pvk.getClientID(), this.loaded.getTokenEndpointURI(), jwsAlgo, privateKey, keyID, null);

clientAuthentication = newPvk;

return newPvk;

} catch (final JOSEException e) {

logger.errorMessage(e, I_ISLogConstants.kLogError, "Cannot recreate a new PrivateKeyJWT, use previous token instead");

}

}

}

return auth;

}


I just came back from holidays this morning, so  I won’t be able to submit a pull request for a few days.

Best regards,
Alexandre

Jérôme LELEU

unread,
Jan 5, 2026, 9:11:04 AM (7 days ago) Jan 5
to Alexandre Pique, Pac4j development mailing list
Hi,

Good! Please submit a PR when you have time.
Thanks.
Best regards,
Jérôme


Reply all
Reply to author
Forward
0 new messages