I have tried to switch from client_secret_basic to private_key_jwt, but I'm getting the following error when I'm sent back from the auth provider: [invalid_client] Client authentication failed. No client authentication included
It's not a Spring Boot app, but this is what I have done so far:
private ClientRegistration idPortenClientRegistration() {
return ClientRegistrations
.fromIssuerLocation("the endpoint")
.clientId("the client id")
.registrationId("idporten")
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("the redirect url")
.scope(Arrays.asList("the scopes"))
.userNameAttributeName(IdTokenClaimNames.SUB)
.clientName("idporten")
.clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT);
.build();
}
My SecurityConfig.class:
http.oauth2Client(oauth2 -> oauth2
.authorizationCodeGrant(codeGrant -> codeGrant
.accessTokenResponseClient(accessTokenResponseClient())));
[…]
private DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient() {
OAuth2AuthorizationCodeGrantRequestEntityConverter requestEntityConverter = new OAuth2AuthorizationCodeGrantRequestEntityConverter();
requestEntityConverter.addParametersConverter(
new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver));
DefaultAuthorizationCodeTokenResponseClient tokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient();
tokenResponseClient.setRequestEntityConverter(requestEntityConverter);
return tokenResponseClient;
}
private Function<ClientRegistration, JWK> jwkResolver = (clientRegistration) -> {
if (clientRegistration.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) {
JKSKeyManager keyManager = getApplicationContext().getBean("keyManager", JKSKeyManager.class);
try {
RSAPublicKey publicKey = (RSAPublicKey) keyManager.getPublicKey("idporten1");
KeyStore.PrivateKeyEntry pkEntry = (KeyStore.PrivateKeyEntry) keyManager.getKeyStore()
.getEntry("idporten1", new KeyStore.PasswordProtection(keyEntryPassword1.toCharArray()));
RSAPrivateKey privateKey = (RSAPrivateKey) pkEntry.getPrivateKey();
return new RSAKey.Builder(publicKey).privateKey(privateKey).keyID(UUID.randomUUID().toString()).build();
} catch (NoSuchAlgorithmException | UnrecoverableEntryException | KeyStoreException e) {
logger.error("Failed to configure jwkResolver: " + e.getMessage(), e);
}
}
return null;
};
As mentioned, I'm successfully redirected to the auth provider, but getting the error described above when I'm sent back to the application. I have also tried to log accessTokenResponseClient() and jwkResolver. The former method gets called just before the error occurs, but nothing gets logged from the latter.
Some documentation from the provider: https://docs.digdir.no/oidc_protocol_token.html https://oidc-ver2.difi.no/idporten-oidc-provider/.well-known/openid-configuration
I figured it out. The NimbusJwtClientAuthenticationParametersConverter
does not work at all. x5c
and kid
claims are just ignored, so I ended up with my own converter. And it is so small and simple, so I will share it here.
But first of all – there was one mistake in my previous code, so the converter was never called. Here is what you have to add in the security config:
http.oauth2Login() .tokenEndpoint().accessTokenResponseClient(accessTokenResponseClient());
My simple converter just deals with a signed JWT and nothing else:
public class SignedJwtClientAuthenticationParametersConverter<T extends AbstractOAuth2AuthorizationGrantRequest>
implements Converter<T, MultiValueMap<String, String>> {
private static final String CLIENT_ASSERTION_TYPE_VALUE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
private final Function<ClientRegistration, String> jwt;
public SignedJwtClientAuthenticationParametersConverter(
final Function<ClientRegistration, String> jwt) {
this.jwt = jwt;
}
@Override
public MultiValueMap<String, String> convert(final T authorizationGrantRequest) {
ClientRegistration clientRegistration = authorizationGrantRequest.getClientRegistration();
String jwt = this.jwt.apply(clientRegistration);
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.set(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, CLIENT_ASSERTION_TYPE_VALUE);
parameters.set(OAuth2ParameterNames.CLIENT_ASSERTION, jwt);
return parameters;
}
}
And here is the accessTokenResponseClient bean:
@Bean
public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient() {
OAuth2AuthorizationCodeGrantRequestEntityConverter requestEntityConverter = new OAuth2AuthorizationCodeGrantRequestEntityConverter();
requestEntityConverter.addParametersConverter(
new SignedJwtClientAuthenticationParametersConverter<>(jwkResolver));
DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient();
accessTokenResponseClient.setRequestEntityConverter(requestEntityConverter);
return accessTokenResponseClient;
}
private Function<ClientRegistration, String> jwkResolver = (clientRegistration) -> {
if (clientRegistration.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) {
try {
return getJwtByClientRegistration(clientRegistration);
} catch (Exception e) {
logger.error("Failed to sign JWT: " + e.getMessage(), e);
}
}
return null;
};
The method getJwtByClientRegistration()
is now shown, but here I am using the JOSE library, which I am more familiar with.