Add client_secret to response when token_endpoint_auth_method is not private_key_jwt (#12609)

Closes #12565
This commit is contained in:
Lex Cao 2022-06-29 16:19:18 +08:00 committed by GitHub
parent 4643fd09e3
commit c3c8b9f0c8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 71 additions and 33 deletions

View file

@ -69,4 +69,12 @@ public interface ClientAuthenticatorFactory extends ProviderFactory<ClientAuthen
*/
Set<String> getProtocolAuthenticatorMethods(String loginProtocol);
/**
* Is this authenticator supports client secret?
*
* @return if it supports secret
*/
default boolean supportsSecret() {
return false;
}
}

View file

@ -204,6 +204,11 @@ public class ClientIdAndSecretAuthenticator extends AbstractClientAuthenticator
}
}
@Override
public boolean supportsSecret() {
return true;
}
private void reportFailedAuth(ClientAuthenticationFlowContext context) {
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.UNAUTHORIZED.getStatusCode(), "unauthorized_client", "Invalid client secret");
context.failure(AuthenticationFlowError.INVALID_CLIENT_CREDENTIALS, challengeResponse);

View file

@ -16,6 +16,28 @@
*/
package org.keycloak.authentication.authenticators.client;
import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.ClientAuthenticationFlowContext;
import org.keycloak.common.util.Time;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.models.AuthenticationExecutionModel.Requirement;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.SingleUseObjectProvider;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCClientSecretConfigWrapper;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.Urls;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
@ -25,29 +47,6 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.ClientAuthenticationFlowContext;
import org.keycloak.common.util.Time;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.models.AuthenticationExecutionModel.Requirement;
import org.keycloak.models.SingleUseObjectProvider;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCClientSecretConfigWrapper;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.Urls;
/**
* Client authentication based on JWT signed by client secret instead of private key .
* See <a href="http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication">specs</a> for more details.
@ -254,6 +253,11 @@ public class JWTClientSecretAuthenticator extends AbstractClientAuthenticator {
}
}
@Override
public boolean supportsSecret() {
return true;
}
@Override
public String getId() {
return PROVIDER_ID;

View file

@ -17,15 +17,14 @@
package org.keycloak.services.clientregistration.oidc;
import com.google.common.collect.Streams;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.ClientAuthenticator;
import org.keycloak.authentication.ClientAuthenticatorFactory;
import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jwk.JWKParser;
import org.keycloak.jose.jws.Algorithm;
import org.keycloak.models.CibaConfig;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
@ -51,8 +50,6 @@ import org.keycloak.util.JWKSUtils;
import org.keycloak.util.JsonSerialization;
import org.keycloak.utils.StringUtil;
import com.google.common.collect.Streams;
import java.io.IOException;
import java.net.URI;
import java.security.PublicKey;
@ -67,8 +64,8 @@ import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.keycloak.models.OAuth2DeviceConfig.OAUTH2_DEVICE_AUTHORIZATION_GRANT_ENABLED;
import static org.keycloak.models.CibaConfig.OIDC_CIBA_GRANT_ENABLED;
import static org.keycloak.models.OAuth2DeviceConfig.OAUTH2_DEVICE_AUTHORIZATION_GRANT_ENABLED;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -245,7 +242,7 @@ public class DescriptionConverter {
configWrapper.setFrontChannelLogoutSessionRequired(false);
} else {
configWrapper.setFrontChannelLogoutSessionRequired(clientOIDC.getFrontchannelLogoutSessionRequired());
}
}
if (clientOIDC.getDefaultAcrValues() != null) {
configWrapper.setAttributeMultivalued(Constants.DEFAULT_ACR_VALUES, clientOIDC.getDefaultAcrValues());
@ -324,12 +321,12 @@ public class DescriptionConverter {
if (oidcClientAuthMethods != null && !oidcClientAuthMethods.isEmpty()) {
response.setTokenEndpointAuthMethod(oidcClientAuthMethods.iterator().next());
}
}
if (client.getClientAuthenticatorType().equals(ClientIdAndSecretAuthenticator.PROVIDER_ID)) {
response.setClientSecret(client.getSecret());
response.setClientSecretExpiresAt(
if (clientAuth.supportsSecret()) {
response.setClientSecret(client.getSecret());
response.setClientSecretExpiresAt(
OIDCClientSecretConfigWrapper.fromClientRepresentation(client).getClientSecretExpirationTime());
}
}
response.setClientName(client.getName());

View file

@ -284,6 +284,30 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
Assert.assertNull(kcClientRep.getSecret());
}
@Test
public void testClientSecretsWithAuthMethod() throws ClientRegistrationException {
OIDCClientRepresentation clientRep = createRep();
clientRep.setGrantTypes(Collections.singletonList(OAuth2Constants.CLIENT_CREDENTIALS));
clientRep.setTokenEndpointAuthMethod(OIDCLoginProtocol.CLIENT_SECRET_JWT);
OIDCClientRepresentation response = reg.oidc().create(clientRep);
Assert.assertEquals("client_secret_jwt", response.getTokenEndpointAuthMethod());
Assert.assertNotNull(response.getClientSecret());
Assert.assertNotNull(response.getClientSecretExpiresAt());
ClientRepresentation kcClientRep = getKeycloakClient(response.getClientId());
Assert.assertFalse(kcClientRep.isPublicClient());
Assert.assertNotNull(kcClientRep.getSecret());
// Updating
reg.auth(Auth.token(response));
response.setTokenEndpointAuthMethod(OIDCLoginProtocol.TLS_CLIENT_AUTH);
OIDCClientRepresentation updated = reg.oidc().update(response);
Assert.assertEquals("tls_client_auth", updated.getTokenEndpointAuthMethod());
Assert.assertNull(updated.getClientSecret());
Assert.assertNull(updated.getClientSecretExpiresAt());
}
@Test
public void createClientFrontchannelLogoutSettings() throws ClientRegistrationException {
// When frontchannelLogutSessionRequired is not set, it should be false by default per OIDC Client registration specification