Send Client ID in token request with JWT Authentication

Closes #21444
This commit is contained in:
Justin Tay 2023-07-12 10:51:16 +08:00 committed by Marek Posolda
parent c984b6a387
commit 658c0ef19f
5 changed files with 190 additions and 1 deletions

View file

@ -407,7 +407,8 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
String jws = new JWSBuilder().type(OAuth2Constants.JWT).jsonContent(generateToken()).sign(getSignatureContext()); String jws = new JWSBuilder().type(OAuth2Constants.JWT).jsonContent(generateToken()).sign(getSignatureContext());
return tokenRequest return tokenRequest
.param(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT) .param(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT)
.param(OAuth2Constants.CLIENT_ASSERTION, jws); .param(OAuth2Constants.CLIENT_ASSERTION, jws)
.param(OAuth2Constants.CLIENT_ID, getConfig().getClientId());
} else { } else {
try (VaultStringSecret vaultStringSecret = session.vault().getStringSecret(getConfig().getClientSecret())) { try (VaultStringSecret vaultStringSecret = session.vault().getStringSecret(getConfig().getClientSecret())) {
if (getConfig().isBasicAuthentication()) { if (getConfig().isBasicAuthentication()) {

View file

@ -0,0 +1,65 @@
/*
* Copyright 2023 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.broker.oidc;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.ClientAuthenticationFlowContext;
import org.keycloak.authentication.authenticators.client.ClientAuthUtil;
import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
/**
* A {@link JWTClientAuthenticator} that requires the optional client_id parameter.
*
* @author Justin Tay
*/
public class ClientIdRequiredJWTClientAuthenticator extends JWTClientAuthenticator {
public static final String PROVIDER_ID = "testsuite-client-id-required";
@Override
public void authenticateClient(ClientAuthenticationFlowContext context) {
MultivaluedMap<String, String> params = context.getHttpRequest().getDecodedFormParameters();
String clientId = params.getFirst(OAuth2Constants.CLIENT_ID);
if (clientId == null) {
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "Missing client_id parameter");
context.challenge(challengeResponse);
return;
}
super.authenticateClient(context);
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public Set<String> getProtocolAuthenticatorMethods(String loginProtocol) {
// Do not add as it will affect the well known provider test
return Collections.emptySet();
}
}

View file

@ -15,5 +15,6 @@
# limitations under the License. # limitations under the License.
# #
org.keycloak.testsuite.broker.oidc.ClientIdRequiredJWTClientAuthenticator
org.keycloak.testsuite.forms.PassThroughClientAuthenticator org.keycloak.testsuite.forms.PassThroughClientAuthenticator
org.keycloak.testsuite.forms.DummyClientAuthenticator org.keycloak.testsuite.forms.DummyClientAuthenticator

View file

@ -94,6 +94,8 @@ public class ProvidersTest extends AbstractAuthenticationTest {
"Validates client based on a X509 Certificate"); "Validates client based on a X509 Certificate");
addProviderInfo(expected, "client-secret-jwt", "Signed Jwt with Client Secret", addProviderInfo(expected, "client-secret-jwt", "Signed Jwt with Client Secret",
"Validates client based on signed JWT issued by client and signed with the Client Secret"); "Validates client based on signed JWT issued by client and signed with the Client Secret");
addProviderInfo(expected, "testsuite-client-id-required", "Signed Jwt",
"Validates client based on signed JWT issued by client and signed with the Client private key");
compareProviders(expected, result); compareProviders(expected, result);
} }

View file

@ -0,0 +1,120 @@
/*
* Copyright 2023 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.broker;
import org.junit.Before;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.authentication.AuthenticationFlow;
import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
import org.keycloak.crypto.Algorithm;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.IdentityProviderSyncMode;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.idm.AuthenticationExecutionRepresentation;
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.KeysMetadataRepresentation.KeyMetadataRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.broker.oidc.ClientIdRequiredJWTClientAuthenticator;
import org.keycloak.testsuite.util.ExecutionBuilder;
import org.keycloak.testsuite.util.FlowBuilder;
import org.keycloak.testsuite.util.KeyUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.keycloak.testsuite.admin.authentication.AbstractAuthenticationTest.findFlowByAlias;
import static org.keycloak.testsuite.broker.BrokerTestConstants.IDP_OIDC_ALIAS;
import static org.keycloak.testsuite.broker.BrokerTestConstants.IDP_OIDC_PROVIDER_ID;
import static org.keycloak.testsuite.broker.BrokerTestTools.createIdentityProvider;
/**
* Test that the broker will send the client_id parameter.
*
* @author Justin Tay
*/
public class KcOidcBrokerPrivateKeyJwtClientIdRequiredTest extends AbstractBrokerTest {
@Override
@Before
public void beforeBrokerTest() {
super.beforeBrokerTest();
RealmResource realmResource = adminClient.realm(bc.providerRealmName());
AuthenticationFlowRepresentation clientFlow = FlowBuilder.create()
.alias("new-client-flow")
.description("Base authentication for clients")
.providerId(AuthenticationFlow.CLIENT_FLOW)
.topLevel(true)
.builtIn(false)
.build();
realmResource.flows().createFlow(clientFlow);
RealmRepresentation realm = realmResource.toRepresentation();
realm.setClientAuthenticationFlow(clientFlow.getAlias());
realmResource.update(realm);
// refresh flow to find its id
clientFlow = findFlowByAlias(clientFlow.getAlias(), realmResource.flows().getFlows());
AuthenticationExecutionRepresentation execution = ExecutionBuilder.create()
.parentFlow(clientFlow.getId())
.requirement(AuthenticationExecutionModel.Requirement.REQUIRED.toString())
.authenticator(ClientIdRequiredJWTClientAuthenticator.PROVIDER_ID)
.priority(10)
.authenticatorFlow(false)
.build();
realmResource.flows().addExecution(execution);
}
@Override
protected BrokerConfiguration getBrokerConfiguration() {
return new KcOidcBrokerConfigurationWithJWTAuthentication();
}
private class KcOidcBrokerConfigurationWithJWTAuthentication extends KcOidcBrokerConfiguration {
@Override
public List<ClientRepresentation> createProviderClients() {
List<ClientRepresentation> clientsRepList = super.createProviderClients();
log.info("Update provider clients to accept JWT authentication");
KeyMetadataRepresentation keyRep = KeyUtils.findActiveSigningKey(adminClient.realm(consumerRealmName()), Algorithm.RS256);
for (ClientRepresentation client: clientsRepList) {
client.setClientAuthenticatorType(ClientIdRequiredJWTClientAuthenticator.PROVIDER_ID);
if (client.getAttributes() == null) {
client.setAttributes(new HashMap<String, String>());
}
client.getAttributes().put(JWTClientAuthenticator.CERTIFICATE_ATTR, keyRep.getCertificate());
}
return clientsRepList;
}
@Override
public IdentityProviderRepresentation setUpIdentityProvider(IdentityProviderSyncMode syncMode) {
IdentityProviderRepresentation idp = createIdentityProvider(IDP_OIDC_ALIAS, IDP_OIDC_PROVIDER_ID);
Map<String, String> config = idp.getConfig();
applyDefaultConfiguration(config, syncMode);
config.put("clientSecret", null);
config.put("clientAuthMethod", OIDCLoginProtocol.PRIVATE_KEY_JWT);
return idp;
}
}
}