mirror of
https://github.com/keycloak/keycloak.git
synced 2025-12-30 03:19:54 -06:00
Add federated subject configuration option to federated-jwt authenticator (#42610)
Closes #42608 Signed-off-by: stianst <stianst@gmail.com>
This commit is contained in:
@@ -16,7 +16,7 @@ export const SpiffeSettings = () => {
|
||||
/>
|
||||
|
||||
<TextControl
|
||||
name="config.trustDomain"
|
||||
name="config.issuer"
|
||||
label={t("spiffeTrustDomain")}
|
||||
rules={{
|
||||
required: t("required"),
|
||||
|
||||
@@ -50,7 +50,7 @@ export async function createSPIFFEProvider(
|
||||
bundleEndpoint: string,
|
||||
) {
|
||||
await clickProviderCard(page, providerName);
|
||||
await page.getByTestId("config.trustDomain").fill(trustDomain);
|
||||
await page.getByTestId("config.issuer").fill(trustDomain);
|
||||
await page.getByTestId("config.bundleEndpoint").fill(bundleEndpoint);
|
||||
await clickAddButton(page);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,13 @@ test.afterAll(() => adminClient.deleteIdentityProvider("spiffe"));
|
||||
|
||||
test.describe.serial("SPIFFE identity provider test", () => {
|
||||
test("should create a SPIFFE provider", async ({ page }) => {
|
||||
await createSPIFFEProvider(page, "spiffe", "mytrust", "https://mytrust");
|
||||
await createSPIFFEProvider(
|
||||
page,
|
||||
"spiffe",
|
||||
"spiffe://mytrust2",
|
||||
"https://mytrust",
|
||||
);
|
||||
|
||||
await assertNotificationMessage(
|
||||
page,
|
||||
"Identity provider successfully created",
|
||||
@@ -24,7 +30,7 @@ test.describe.serial("SPIFFE identity provider test", () => {
|
||||
await goToIdentityProviders(page);
|
||||
await clickTableRowItem(page, "Spiffe");
|
||||
|
||||
await page.getByTestId("config.trustDomain").fill("mytrust2");
|
||||
await page.getByTestId("config.issuer").fill("spiffe://mytrust2");
|
||||
await page.getByTestId("config.bundleEndpoint").fill("https://mytrust2");
|
||||
|
||||
await clickSaveButton(page);
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
package org.keycloak.models.cache.infinispan;
|
||||
|
||||
import org.infinispan.Cache;
|
||||
import org.infinispan.CacheStream;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
|
||||
@@ -34,7 +33,6 @@ import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
@@ -129,13 +127,6 @@ public class RealmCacheManager extends CacheManager {
|
||||
((RealmCacheInvalidationEvent) event).addInvalidations(this, invalidations);
|
||||
}
|
||||
|
||||
public <T> CacheStream<T> searchWithPredicate(Predicate<T> predicate, Class<T> tClass) {
|
||||
return cache.values().stream()
|
||||
.filter(tClass::isInstance)
|
||||
.map(tClass::cast)
|
||||
.filter(predicate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a cached realm and ensure that this happens only once with the current Keycloak instance.
|
||||
* Use this to avoid concurrent preparations of a realm in parallel threads. This helps to break the load on
|
||||
|
||||
@@ -41,7 +41,6 @@ import org.keycloak.models.GroupModel.Type;
|
||||
import org.keycloak.models.GroupProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakTransaction;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RealmProvider;
|
||||
import org.keycloak.models.RoleModel;
|
||||
@@ -61,7 +60,6 @@ import org.keycloak.models.cache.infinispan.entities.ClientScopeListQuery;
|
||||
import org.keycloak.models.cache.infinispan.entities.GroupListQuery;
|
||||
import org.keycloak.models.cache.infinispan.entities.GroupNameQuery;
|
||||
import org.keycloak.models.cache.infinispan.entities.RealmListQuery;
|
||||
import org.keycloak.models.cache.infinispan.entities.Revisioned;
|
||||
import org.keycloak.models.cache.infinispan.entities.RoleListQuery;
|
||||
import org.keycloak.models.cache.infinispan.entities.RoleByNameQuery;
|
||||
import org.keycloak.models.cache.infinispan.events.ClientAddedEvent;
|
||||
@@ -1312,16 +1310,6 @@ public class RealmCacheSession implements CacheRealmProvider {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientModel getClientByAttribute(RealmModel realm, String name, String value) {
|
||||
List<CachedClient> clients = cache.searchWithPredicate(c -> value.equals(c.getAttributes().get(name)), CachedClient.class).limit(2).toList();
|
||||
return switch (clients.size()) {
|
||||
case 0 -> getClientDelegate().getClientByAttribute(realm, name, value);
|
||||
case 1 -> getClientById(realm, clients.get(0).getId());
|
||||
default -> throw new ModelException("Multiple clients found with the same attribute name and value");
|
||||
};
|
||||
}
|
||||
|
||||
private ClientModel prepareCachedClientByClientId(RealmModel realm, String clientId, String cacheKey) {
|
||||
ClientListQuery query = cache.get(cacheKey, ClientListQuery.class);
|
||||
String id;
|
||||
|
||||
@@ -46,6 +46,7 @@ public class JpaClientProviderFactory implements ClientProviderFactory {
|
||||
private static final List<String> REQUIRED_SEARCHABLE_ATTRIBUTES = Arrays.asList(
|
||||
"saml_idp_initiated_sso_url_name",
|
||||
SamlConfigAttributes.SAML_ARTIFACT_BINDING_IDENTIFIER,
|
||||
"jwt.credential.issuer",
|
||||
"jwt.credential.sub"
|
||||
);
|
||||
|
||||
|
||||
@@ -979,16 +979,6 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, ClientSc
|
||||
return session.clients().getClientById(realm, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientModel getClientByAttribute(RealmModel realm, String name, String value) {
|
||||
List<ClientModel> clients = searchClientsByAttributes(realm, Map.of(name, value), 0, 2).toList();
|
||||
return switch (clients.size()) {
|
||||
case 0 -> null;
|
||||
case 1 -> clients.get(0);
|
||||
default -> throw new ModelException("Multiple clients found with the same attribute name and value");
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<ClientModel> searchClientsByClientIdStream(RealmModel realm, String clientId, Integer firstResult, Integer maxResults) {
|
||||
CriteriaBuilder builder = em.getCriteriaBuilder();
|
||||
|
||||
@@ -157,19 +157,6 @@ public class ClientStorageManager implements ClientProvider {
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientModel getClientByAttribute(RealmModel realm, String name, String value) {
|
||||
ClientModel client = localStorage().getClientByAttribute(realm, name, value);
|
||||
if (client != null) {
|
||||
return client;
|
||||
}
|
||||
return getEnabledStorageProviders(session, realm, ClientLookupProvider.class)
|
||||
.map(provider -> provider.getClientByAttribute(realm, name, value))
|
||||
.filter(Objects::nonNull)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<ClientModel> searchClientsByClientIdStream(RealmModel realm, String clientId, Integer firstResult, Integer maxResults) {
|
||||
return query((p, f, m) -> p.searchClientsByClientIdStream(realm, clientId, f, m), realm, firstResult, maxResults);
|
||||
|
||||
16
server-spi-private/src/main/java/org/keycloak/cache/AlternativeLookupProvider.java
vendored
Normal file
16
server-spi-private/src/main/java/org/keycloak/cache/AlternativeLookupProvider.java
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
package org.keycloak.cache;
|
||||
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.provider.Provider;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public interface AlternativeLookupProvider extends Provider {
|
||||
|
||||
IdentityProviderModel lookupIdentityProviderFromIssuer(KeycloakSession session, String issuerUrl);
|
||||
|
||||
ClientModel lookupClientFromClientAttributes(KeycloakSession session, Map<String, String> attributes);
|
||||
|
||||
}
|
||||
6
server-spi-private/src/main/java/org/keycloak/cache/AlternativeLookupProviderFactory.java
vendored
Normal file
6
server-spi-private/src/main/java/org/keycloak/cache/AlternativeLookupProviderFactory.java
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
package org.keycloak.cache;
|
||||
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
|
||||
public interface AlternativeLookupProviderFactory extends ProviderFactory<AlternativeLookupProvider> {
|
||||
}
|
||||
28
server-spi-private/src/main/java/org/keycloak/cache/AlternativeLookupSPI.java
vendored
Normal file
28
server-spi-private/src/main/java/org/keycloak/cache/AlternativeLookupSPI.java
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
package org.keycloak.cache;
|
||||
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.provider.Spi;
|
||||
|
||||
public class AlternativeLookupSPI implements Spi {
|
||||
|
||||
@Override
|
||||
public boolean isInternal() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "alternativeLookup";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Provider> getProviderClass() {
|
||||
return AlternativeLookupProvider.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends ProviderFactory> getProviderFactoryClass() {
|
||||
return AlternativeLookupProviderFactory.class;
|
||||
}
|
||||
}
|
||||
@@ -109,3 +109,4 @@ org.keycloak.models.workflow.WorkflowStepSpi
|
||||
org.keycloak.models.workflow.WorkflowSpi
|
||||
org.keycloak.models.workflow.WorkflowConditionSpi
|
||||
org.keycloak.protocol.oidc.rar.AuthorizationDetailsProcessorSpi
|
||||
org.keycloak.cache.AlternativeLookupSPI
|
||||
|
||||
@@ -34,6 +34,7 @@ public class IdentityProviderModel implements Serializable {
|
||||
|
||||
public static final String ALIAS = "alias";
|
||||
public static final String ALIAS_NOT_IN = "aliasNotIn";
|
||||
public static final String ISSUER = "issuer";
|
||||
public static final String ALLOWED_CLOCK_SKEW = "allowedClockSkew";
|
||||
public static final String AUTHENTICATE_BY_DEFAULT = "authenticateByDefault";
|
||||
public static final String CASE_SENSITIVE_ORIGINAL_USERNAME = "caseSensitiveOriginalUsername";
|
||||
|
||||
@@ -48,17 +48,6 @@ public interface ClientLookupProvider {
|
||||
*/
|
||||
ClientModel getClientByClientId(RealmModel realm, String clientId);
|
||||
|
||||
/**
|
||||
* Exact search for a client by an attribute. Throws an exception if
|
||||
* multi clients are found.
|
||||
*
|
||||
* @param realm Realm to limit the search for clients.
|
||||
* @param name The name of the client attribute
|
||||
* @param value The value of the client attribute
|
||||
* @return
|
||||
*/
|
||||
ClientModel getClientByAttribute(RealmModel realm, String name, String value);
|
||||
|
||||
/**
|
||||
* Case-insensitive search for clients that contain the given string in their public client identifier.
|
||||
* @param realm Realm to limit the search for clients.
|
||||
|
||||
@@ -112,8 +112,15 @@ public abstract class AbstractJWTClientValidator {
|
||||
return failure("client_id parameter does not match sub claim");
|
||||
}
|
||||
|
||||
String expectedTokenIssuer = getExpectedTokenIssuer();
|
||||
if (expectedTokenIssuer != null && !expectedTokenIssuer.equals(token.getIssuer())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
context.getEvent().client(clientId);
|
||||
client = realm.getClientByClientId(clientId);
|
||||
|
||||
client = clientAssertionState.getClient();
|
||||
|
||||
if (client == null) {
|
||||
return failure(AuthenticationFlowError.CLIENT_NOT_FOUND);
|
||||
} else {
|
||||
@@ -129,11 +136,6 @@ public abstract class AbstractJWTClientValidator {
|
||||
return false;
|
||||
}
|
||||
|
||||
String expectedTokenIssuer = getExpectedTokenIssuer();
|
||||
if (expectedTokenIssuer != null && !expectedTokenIssuer.equals(token.getIssuer())) {
|
||||
return failure("Invalid token issuer", Response.Status.UNAUTHORIZED.getStatusCode());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,24 +7,31 @@ import org.keycloak.authentication.ClientAuthenticationFlowContextSupplier;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.jose.jws.JWSInputException;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
|
||||
public class ClientAssertionState {
|
||||
|
||||
private static final Supplier SUPPLIER = new Supplier();
|
||||
|
||||
private ClientModel client;
|
||||
private final String clientAssertionType;
|
||||
private final String clientAssertion;
|
||||
private final JWSInput jws;
|
||||
private final JsonWebToken token;
|
||||
|
||||
public ClientAssertionState(String clientAssertionType, String clientAssertion, JWSInput jws, JsonWebToken token) {
|
||||
public ClientAssertionState(ClientModel client, String clientAssertionType, String clientAssertion, JWSInput jws, JsonWebToken token) {
|
||||
this.client = client;
|
||||
this.clientAssertionType = clientAssertionType;
|
||||
this.clientAssertion = clientAssertion;
|
||||
this.jws = jws;
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
public void setClient(ClientModel client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
public String getClientAssertionType() {
|
||||
return clientAssertionType;
|
||||
}
|
||||
@@ -41,6 +48,10 @@ public class ClientAssertionState {
|
||||
return token;
|
||||
}
|
||||
|
||||
public ClientModel getClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
public static ClientAuthenticationFlowContextSupplier<ClientAssertionState> supplier() {
|
||||
return SUPPLIER;
|
||||
}
|
||||
@@ -56,6 +67,9 @@ public class ClientAssertionState {
|
||||
|
||||
JWSInput jws = null;
|
||||
JsonWebToken token = null;
|
||||
|
||||
ClientModel client = null;
|
||||
|
||||
if (clientAssertion != null) {
|
||||
jws = new JWSInput(clientAssertion);
|
||||
token = jws.readJsonContent(JsonWebToken.class);
|
||||
@@ -64,9 +78,13 @@ public class ClientAssertionState {
|
||||
event.detail(Details.CLIENT_ASSERTION_ID, token.getId());
|
||||
event.detail(Details.CLIENT_ASSERTION_ISSUER, token.getIssuer());
|
||||
event.detail(Details.CLIENT_ASSERTION_SUB, token.getSubject());
|
||||
|
||||
if (token.getSubject() != null) {
|
||||
client = context.getRealm().getClientByClientId(token.getSubject());
|
||||
}
|
||||
}
|
||||
|
||||
return new ClientAssertionState(clientAssertionType, clientAssertion, jws, token);
|
||||
return new ClientAssertionState(client, clientAssertionType, clientAssertion, jws, token);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,14 +9,18 @@ import org.keycloak.authentication.ConfigurableAuthenticatorFactory;
|
||||
import org.keycloak.broker.provider.ClientAssertionIdentityProvider;
|
||||
import org.keycloak.broker.provider.IdentityProvider;
|
||||
import org.keycloak.broker.spiffe.SpiffeConstants;
|
||||
import org.keycloak.cache.AlternativeLookupProvider;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
import org.keycloak.services.resources.IdentityBrokerService;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -29,9 +33,11 @@ public class FederatedJWTClientAuthenticator extends AbstractClientAuthenticator
|
||||
public static final String PROVIDER_ID = "federated-jwt";
|
||||
|
||||
public static final String JWT_CREDENTIAL_ISSUER_KEY = "jwt.credential.issuer";
|
||||
public static final String JWT_CREDENTIAL_SUBJECT_KEY = "jwt.credential.sub";
|
||||
|
||||
private static final List<ProviderConfigProperty> CONFIG = List.of(
|
||||
new ProviderConfigProperty(JWT_CREDENTIAL_ISSUER_KEY, "Identity provider", "Issuer of the client assertion", ProviderConfigProperty.STRING_TYPE, null)
|
||||
private static final List<ProviderConfigProperty> CLIENT_CONFIG = List.of(
|
||||
new ProviderConfigProperty(JWT_CREDENTIAL_ISSUER_KEY, "Identity provider", "Issuer of the client assertion", ProviderConfigProperty.STRING_TYPE, null),
|
||||
new ProviderConfigProperty(JWT_CREDENTIAL_SUBJECT_KEY, "Federated subject", "External clientId (subject)", ProviderConfigProperty.STRING_TYPE, null)
|
||||
);
|
||||
|
||||
private static final Set<String> SUPPORTED_ASSERTION_TYPES = Set.of(OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT, SpiffeConstants.CLIENT_ASSERTION_TYPE);
|
||||
@@ -42,24 +48,36 @@ public class FederatedJWTClientAuthenticator extends AbstractClientAuthenticator
|
||||
}
|
||||
|
||||
@Override
|
||||
// TODO Should share code with JWTClientAuthenticator/JWTClientValidator rather than duplicating, but that requires quite a bit of refactoring
|
||||
public void authenticateClient(ClientAuthenticationFlowContext context) {
|
||||
try {
|
||||
ClientAssertionState clientAssertionState = context.getState(ClientAssertionState.class, ClientAssertionState.supplier());
|
||||
JsonWebToken token = clientAssertionState.getToken();
|
||||
|
||||
if (!SUPPORTED_ASSERTION_TYPES.contains(clientAssertionState.getClientAssertionType())) {
|
||||
return;
|
||||
}
|
||||
|
||||
ClientModel client = lookupClient(context, token.getSubject());
|
||||
final String issuer = clientAssertionState.getToken().getIssuer() != null ?
|
||||
clientAssertionState.getToken().getIssuer() :
|
||||
toIssuer(clientAssertionState.getToken().getSubject());
|
||||
if (issuer == null) return;
|
||||
|
||||
AlternativeLookupProvider lookupProvider = context.getSession().getProvider(AlternativeLookupProvider.class);
|
||||
|
||||
IdentityProviderModel identityProviderModel = lookupProvider.lookupIdentityProviderFromIssuer(context.getSession(), issuer);
|
||||
ClientAssertionIdentityProvider identityProvider = getClientAssertionIdentityProvider(context.getSession(), identityProviderModel);
|
||||
if (identityProvider == null) return;
|
||||
|
||||
String federatedClientId = clientAssertionState.getToken().getSubject();
|
||||
|
||||
ClientModel client = lookupProvider.lookupClientFromClientAttributes(
|
||||
context.getSession(),
|
||||
Map.of(FederatedJWTClientAuthenticator.JWT_CREDENTIAL_ISSUER_KEY, identityProviderModel.getAlias(), FederatedJWTClientAuthenticator.JWT_CREDENTIAL_SUBJECT_KEY, federatedClientId));
|
||||
if (client == null) return;
|
||||
|
||||
if (!PROVIDER_ID.equals(client.getClientAuthenticatorType())) {
|
||||
return;
|
||||
}
|
||||
clientAssertionState.setClient(client);
|
||||
|
||||
if (!PROVIDER_ID.equals(client.getClientAuthenticatorType())) return;
|
||||
|
||||
ClientAssertionIdentityProvider identityProvider = lookupIdentityProvider(context, client);
|
||||
if (identityProvider.verifyClientAssertion(context)) {
|
||||
context.success();
|
||||
} else {
|
||||
@@ -71,22 +89,11 @@ public class FederatedJWTClientAuthenticator extends AbstractClientAuthenticator
|
||||
}
|
||||
}
|
||||
|
||||
private ClientModel lookupClient(ClientAuthenticationFlowContext context, String subject) {
|
||||
ClientModel client = context.getRealm().getClientByClientId(subject);
|
||||
if (client == null) {
|
||||
context.failure(AuthenticationFlowError.CLIENT_NOT_FOUND);
|
||||
private ClientAssertionIdentityProvider getClientAssertionIdentityProvider(KeycloakSession session, IdentityProviderModel identityProviderModel) {
|
||||
if (identityProviderModel == null) {
|
||||
return null;
|
||||
}
|
||||
if (!client.isEnabled()) {
|
||||
context.failure(AuthenticationFlowError.CLIENT_DISABLED);
|
||||
return null;
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
private ClientAssertionIdentityProvider lookupIdentityProvider(ClientAuthenticationFlowContext context, ClientModel client) {
|
||||
String idpAlias = client.getAttribute(FederatedJWTClientAuthenticator.JWT_CREDENTIAL_ISSUER_KEY);
|
||||
IdentityProvider<?> identityProvider = IdentityBrokerService.getIdentityProvider(context.getSession(), idpAlias);
|
||||
IdentityProvider<?> identityProvider = IdentityBrokerService.getIdentityProvider(session, identityProviderModel);
|
||||
if (identityProvider instanceof ClientAssertionIdentityProvider clientAssertionProvider) {
|
||||
return clientAssertionProvider;
|
||||
} else {
|
||||
@@ -121,7 +128,7 @@ public class FederatedJWTClientAuthenticator extends AbstractClientAuthenticator
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigPropertiesPerClient() {
|
||||
return CONFIG;
|
||||
return CLIENT_CONFIG;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -139,4 +146,15 @@ public class FederatedJWTClientAuthenticator extends AbstractClientAuthenticator
|
||||
return Profile.isFeatureEnabled(Profile.Feature.CLIENT_AUTH_FEDERATED);
|
||||
}
|
||||
|
||||
protected static String toIssuer(String subject) {
|
||||
try {
|
||||
URI uri = new URI(subject);
|
||||
String scheme = uri.getScheme();
|
||||
String authority = uri.getRawAuthority();
|
||||
return scheme != null && authority != null ? scheme + "://" + authority : null;
|
||||
} catch (URISyntaxException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ public class JWTClientValidator extends AbstractJWTClientValidator {
|
||||
|
||||
@Override
|
||||
protected String getExpectedTokenIssuer() {
|
||||
return client.getClientId();
|
||||
return clientAssertionState.getToken().getSubject();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -32,7 +32,6 @@ public class OIDCIdentityProviderConfig extends OAuth2IdentityProviderConfig {
|
||||
public static final String USE_JWKS_URL = "useJwksUrl";
|
||||
public static final String VALIDATE_SIGNATURE = "validateSignature";
|
||||
public static final String IS_ACCESS_TOKEN_JWT = "isAccessTokenJWT";
|
||||
public static final String ISSUER = "issuer";
|
||||
public static final String SUPPORTS_CLIENT_ASSERTIONS = "supportsClientAssertions";
|
||||
public static final String SUPPORTS_CLIENT_ASSERTION_REUSE = "supportsClientAssertionReuse";
|
||||
|
||||
|
||||
@@ -67,13 +67,7 @@ public class SpiffeIdentityProvider implements IdentityProvider<SpiffeIdentityPr
|
||||
String trustedDomain = config.getTrustDomain();
|
||||
|
||||
JsonWebToken token = validator.getState().getToken();
|
||||
|
||||
URI uri = URI.create(token.getSubject());
|
||||
if (!uri.getScheme().equals("spiffe")) {
|
||||
throw new RuntimeException("Not a SPIFFE ID");
|
||||
}
|
||||
|
||||
if (!uri.getRawAuthority().equals(trustedDomain)) {
|
||||
if (!token.getSubject().startsWith(trustedDomain + "/")) {
|
||||
throw new RuntimeException("Invalid trust-domain");
|
||||
}
|
||||
|
||||
|
||||
@@ -10,10 +10,9 @@ import static org.keycloak.common.util.UriUtils.checkUrl;
|
||||
|
||||
public class SpiffeIdentityProviderConfig extends IdentityProviderModel {
|
||||
|
||||
public static final String TRUST_DOMAIN_KEY = "trustDomain";
|
||||
public static final String BUNDLE_ENDPOINT_KEY = "bundleEndpoint";
|
||||
|
||||
private static final Pattern TRUST_DOMAIN_PATTERN = Pattern.compile("[a-z0-9.\\-_]*");
|
||||
private static final Pattern TRUST_DOMAIN_PATTERN = Pattern.compile("spiffe://[a-z0-9.\\-_]*");
|
||||
|
||||
public SpiffeIdentityProviderConfig() {
|
||||
getConfig().put(IdentityProviderModel.SHOW_IN_ACCOUNT_CONSOLE, IdentityProviderShowInAccountConsole.NEVER.name());
|
||||
@@ -42,7 +41,7 @@ public class SpiffeIdentityProviderConfig extends IdentityProviderModel {
|
||||
}
|
||||
|
||||
public String getTrustDomain() {
|
||||
return getConfig().get(TRUST_DOMAIN_KEY);
|
||||
return getConfig().get(ISSUER);
|
||||
}
|
||||
|
||||
public String getBundleEndpoint() {
|
||||
|
||||
40
services/src/main/java/org/keycloak/cache/ComputedKey.java
vendored
Normal file
40
services/src/main/java/org/keycloak/cache/ComputedKey.java
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
package org.keycloak.cache;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Map;
|
||||
|
||||
class ComputedKey {
|
||||
|
||||
private ComputedKey() {
|
||||
}
|
||||
|
||||
public static String computeKey(String realm, String type, String alternativeKey) {
|
||||
MessageDigest md = getMessageDigest();
|
||||
md.update(realm.getBytes(StandardCharsets.UTF_8));
|
||||
md.update(type.getBytes(StandardCharsets.UTF_8));
|
||||
md.update(alternativeKey.getBytes(StandardCharsets.UTF_8));
|
||||
return new String(md.digest(), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public static String computeKey(String realm, String type, Map<String, String> attributes) {
|
||||
MessageDigest md = getMessageDigest();
|
||||
md.update(realm.getBytes(StandardCharsets.UTF_8));
|
||||
md.update(type.getBytes(StandardCharsets.UTF_8));
|
||||
attributes.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(e -> {
|
||||
md.update(e.getKey().getBytes(StandardCharsets.UTF_8));
|
||||
md.update(e.getValue().getBytes(StandardCharsets.UTF_8));
|
||||
});
|
||||
return new String(md.digest(), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static MessageDigest getMessageDigest() {
|
||||
try {
|
||||
return MessageDigest.getInstance("MD5");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
78
services/src/main/java/org/keycloak/cache/DefaultAlternativeLookupProvider.java
vendored
Normal file
78
services/src/main/java/org/keycloak/cache/DefaultAlternativeLookupProvider.java
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
package org.keycloak.cache;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class DefaultAlternativeLookupProvider implements AlternativeLookupProvider {
|
||||
|
||||
private final Cache<String, String> lookupCache;
|
||||
|
||||
public DefaultAlternativeLookupProvider(Cache<String, String> lookupCache) {
|
||||
this.lookupCache = lookupCache;
|
||||
}
|
||||
|
||||
public IdentityProviderModel lookupIdentityProviderFromIssuer(KeycloakSession session, String issuerUrl) {
|
||||
String alternativeKey = ComputedKey.computeKey(session.getContext().getRealm().getId(), "idp", issuerUrl);
|
||||
|
||||
String cachedIdpAlias = lookupCache.getIfPresent(alternativeKey);
|
||||
if (cachedIdpAlias != null) {
|
||||
IdentityProviderModel idp = session.identityProviders().getByAlias(cachedIdpAlias);
|
||||
if (idp != null && issuerUrl.equals(idp.getConfig().get(IdentityProviderModel.ISSUER))) {
|
||||
return idp;
|
||||
} else {
|
||||
lookupCache.invalidate(alternativeKey);
|
||||
}
|
||||
}
|
||||
|
||||
IdentityProviderModel idp = session.identityProviders().getAllStream()
|
||||
.filter(i -> issuerUrl.equals(i.getConfig().get(IdentityProviderModel.ISSUER)))
|
||||
.findFirst().orElse(null);
|
||||
if (idp != null && idp.getAlias() != null) {
|
||||
lookupCache.put(alternativeKey, idp.getAlias());
|
||||
}
|
||||
return idp;
|
||||
}
|
||||
|
||||
public ClientModel lookupClientFromClientAttributes(KeycloakSession session, Map<String, String> attributes) {
|
||||
String alternativeKey = ComputedKey.computeKey(session.getContext().getRealm().getId(), "client", attributes);
|
||||
|
||||
String cachedClientId = lookupCache.getIfPresent(alternativeKey);
|
||||
if (cachedClientId != null) {
|
||||
ClientModel client = session.clients().getClientByClientId(session.getContext().getRealm(), cachedClientId);
|
||||
boolean match = client != null;
|
||||
if (match) {
|
||||
for (Map.Entry<String, String> e : attributes.entrySet()) {
|
||||
if (!e.getValue().equals(client.getAttribute(e.getKey()))) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (match) {
|
||||
return client;
|
||||
} else {
|
||||
lookupCache.invalidate(alternativeKey);
|
||||
}
|
||||
}
|
||||
|
||||
ClientModel client = null;
|
||||
List<ClientModel> clients = session.clients().searchClientsByAttributes(session.getContext().getRealm(), attributes, 0, 2).toList();
|
||||
if (clients.size() == 1) {
|
||||
client = clients.get(0);
|
||||
lookupCache.put(alternativeKey, client.getClientId());
|
||||
} else if (clients.size() > 1) {
|
||||
throw new RuntimeException("Multiple clients matches attributes");
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
}
|
||||
43
services/src/main/java/org/keycloak/cache/DefaultAlternativeLookupProviderFactory.java
vendored
Normal file
43
services/src/main/java/org/keycloak/cache/DefaultAlternativeLookupProviderFactory.java
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
package org.keycloak.cache;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class DefaultAlternativeLookupProviderFactory implements AlternativeLookupProviderFactory {
|
||||
|
||||
private Cache<String, String> lookupCache;
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "default";
|
||||
}
|
||||
|
||||
@Override
|
||||
public AlternativeLookupProvider create(KeycloakSession session) {
|
||||
return new DefaultAlternativeLookupProvider(lookupCache);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
Integer maximumSize = config.getInt("maximumSize", 1000);
|
||||
Integer expireAfter = config.getInt("expireAfter", 60);
|
||||
|
||||
this.lookupCache = Caffeine.newBuilder().maximumSize(maximumSize).expireAfterAccess(expireAfter, TimeUnit.MINUTES).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
lookupCache.cleanUp();
|
||||
lookupCache = null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1344,18 +1344,19 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
||||
|
||||
public static IdentityProvider<?> getIdentityProvider(KeycloakSession session, String alias) {
|
||||
IdentityProviderModel identityProviderModel = session.identityProviders().getByAlias(alias);
|
||||
IdentityProvider<?> identityProvider = getIdentityProvider(session, identityProviderModel);
|
||||
if (identityProvider == null) {
|
||||
throw new IdentityBrokerException("Identity Provider [" + alias + "] not found.");
|
||||
}
|
||||
return identityProvider;
|
||||
}
|
||||
|
||||
public static IdentityProvider<?> getIdentityProvider(KeycloakSession session, IdentityProviderModel identityProviderModel) {
|
||||
if (identityProviderModel != null) {
|
||||
IdentityProviderFactory<?> providerFactory = getIdentityProviderFactory(session, identityProviderModel);
|
||||
|
||||
if (providerFactory == null) {
|
||||
throw new IdentityBrokerException("Could not find factory for identity provider [" + alias + "].");
|
||||
}
|
||||
|
||||
return providerFactory.create(session, identityProviderModel);
|
||||
return providerFactory != null ? providerFactory.create(session, identityProviderModel) : null;
|
||||
}
|
||||
|
||||
throw new IdentityBrokerException("Identity Provider [" + alias + "] not found.");
|
||||
return null;
|
||||
}
|
||||
|
||||
private static IdentityProviderFactory<?> getIdentityProviderFactory(KeycloakSession session, IdentityProviderModel model) {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
org.keycloak.cache.DefaultAlternativeLookupProviderFactory
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.keycloak.authentication.authenticators.client;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
|
||||
public class FederatedJWTClientAuthenticatorTest {
|
||||
|
||||
@Test
|
||||
public void testToIssuer() {
|
||||
Assertions.assertEquals("https://something", FederatedJWTClientAuthenticator.toIssuer("https://something/client"));
|
||||
Assertions.assertEquals("spiffe://trust-domain", FederatedJWTClientAuthenticator.toIssuer("spiffe://trust-domain/the"));
|
||||
Assertions.assertEquals("spiffe://trust-domain", FederatedJWTClientAuthenticator.toIssuer("spiffe://trust-domain/the/client"));
|
||||
Assertions.assertNull(FederatedJWTClientAuthenticator.toIssuer("client"));
|
||||
Assertions.assertNull(FederatedJWTClientAuthenticator.toIssuer("the/client"));
|
||||
Assertions.assertNull(FederatedJWTClientAuthenticator.toIssuer("spiffe:/the/client"));
|
||||
}
|
||||
|
||||
}
|
||||
32
services/src/test/java/org/keycloak/cache/ComputedKeyTest.java
vendored
Normal file
32
services/src/test/java/org/keycloak/cache/ComputedKeyTest.java
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
package org.keycloak.cache;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class ComputedKeyTest {
|
||||
|
||||
@Test
|
||||
public void testComputedKeyWithStrings() {
|
||||
String k1 = ComputedKey.computeKey("realm", "type", "key1");
|
||||
Assertions.assertEquals(k1, ComputedKey.computeKey("realm", "type", "key1"));
|
||||
Assertions.assertNotEquals(k1, ComputedKey.computeKey("realm2", "type", "key"));
|
||||
Assertions.assertNotEquals(k1, ComputedKey.computeKey("realm", "type2", "key"));
|
||||
Assertions.assertNotEquals(k1, ComputedKey.computeKey("realm", "type", "key2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testComputedKeyWithAttributes() {
|
||||
String k1 = ComputedKey.computeKey("realm", "type", Map.of("one", "one", "two", "two"));
|
||||
Assertions.assertEquals(k1, ComputedKey.computeKey("realm", "type", Map.of("one", "one", "two", "two")));
|
||||
Assertions.assertEquals(k1, ComputedKey.computeKey("realm", "type", Map.of("two", "two", "one", "one")));
|
||||
Assertions.assertNotEquals(k1, ComputedKey.computeKey("realm2", "type", Map.of("one", "one", "two", "two")));
|
||||
Assertions.assertNotEquals(k1, ComputedKey.computeKey("realm", "type2", Map.of("one", "one", "two", "two")));
|
||||
Assertions.assertNotEquals(k1, ComputedKey.computeKey("realm", "type", Map.of("one2", "one", "two", "two")));
|
||||
Assertions.assertNotEquals(k1, ComputedKey.computeKey("realm", "type", Map.of("one", "one2", "two", "two")));
|
||||
Assertions.assertNotEquals(k1, ComputedKey.computeKey("realm", "type", Map.of("one", "one", "two2", "two")));
|
||||
Assertions.assertNotEquals(k1, ComputedKey.computeKey("realm", "type", Map.of("one", "one", "two", "two2")));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package org.keycloak.tests.admin.client;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.admin.client.Keycloak;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.cache.infinispan.ClientAdapter;
|
||||
import org.keycloak.testframework.annotations.InjectAdminClient;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testframework.realm.ManagedRealm;
|
||||
import org.keycloak.testframework.realm.RealmConfig;
|
||||
import org.keycloak.testframework.realm.RealmConfigBuilder;
|
||||
import org.keycloak.testframework.remote.providers.runonserver.FetchOnServer;
|
||||
import org.keycloak.testframework.remote.providers.runonserver.FetchOnServerWrapper;
|
||||
import org.keycloak.testframework.remote.runonserver.InjectRunOnServer;
|
||||
import org.keycloak.testframework.remote.runonserver.RunOnServerClient;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
@KeycloakIntegrationTest
|
||||
public class ClientByAttributesTest {
|
||||
|
||||
@InjectRealm(config = ClientByAttributesRealm.class)
|
||||
ManagedRealm realm;
|
||||
|
||||
@InjectRunOnServer
|
||||
RunOnServerClient runOnServer;
|
||||
|
||||
@InjectAdminClient
|
||||
Keycloak adminClient;
|
||||
|
||||
@Test
|
||||
public void lookupByAttribute() {
|
||||
runOnServer.run(s -> {
|
||||
ClientModel c = s.clients().getClientByAttribute(s.getContext().getRealm(), "jwt.credential.sub", "value1");
|
||||
Assertions.assertEquals("client1", c.getClientId());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lookupByAttributeMultipleMatches() {
|
||||
runOnServer.run(s -> {
|
||||
try {
|
||||
s.clients().getClientByAttribute(s.getContext().getRealm(), "jwt.credential.sub", "value2");
|
||||
Assertions.fail("Expected exception");
|
||||
} catch (Exception e) {
|
||||
Assertions.assertEquals("Multiple clients found with the same attribute name and value", e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lookupByAttributeTestCached() {
|
||||
CachedTimeStamp cachedTimeStamp = new CachedTimeStamp("value1");
|
||||
Long cachedTimeStamp1 = runOnServer.fetch(cachedTimeStamp);
|
||||
Assertions.assertEquals(cachedTimeStamp1, runOnServer.fetch(cachedTimeStamp));
|
||||
|
||||
realm.admin().clearRealmCache();
|
||||
|
||||
Assertions.assertNotEquals(cachedTimeStamp1, runOnServer.fetch(cachedTimeStamp));
|
||||
}
|
||||
|
||||
public static class ClientByAttributesRealm implements RealmConfig {
|
||||
@Override
|
||||
public RealmConfigBuilder configure(RealmConfigBuilder realm) {
|
||||
realm.addClient("client1").attribute("jwt.credential.sub", "value1");
|
||||
realm.addClient("client2").attribute("jwt.credential.sub", "value2");
|
||||
realm.addClient("client3").attribute("jwt.credential.sub", "value2");
|
||||
return realm;
|
||||
}
|
||||
}
|
||||
|
||||
private record CachedTimeStamp(String jwtCredentialSub) implements FetchOnServerWrapper<Long>, Serializable {
|
||||
|
||||
@Override
|
||||
public FetchOnServer getRunOnServer() {
|
||||
return s -> {
|
||||
ClientModel client = s.clients().getClientByAttribute(s.getContext().getRealm(), "jwt.credential.sub", jwtCredentialSub);
|
||||
return ((ClientAdapter) client).getCacheTimestamp();
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<Long> getResultClass() {
|
||||
return Long.class;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -69,7 +69,8 @@ public class FederatedClientAuthFromKeycloakTest {
|
||||
return client.clientId("myclient")
|
||||
.serviceAccountsEnabled(true)
|
||||
.authenticatorType(FederatedJWTClientAuthenticator.PROVIDER_ID)
|
||||
.attribute(FederatedJWTClientAuthenticator.JWT_CREDENTIAL_ISSUER_KEY, IDP_ALIAS);
|
||||
.attribute(FederatedJWTClientAuthenticator.JWT_CREDENTIAL_ISSUER_KEY, IDP_ALIAS)
|
||||
.attribute(FederatedJWTClientAuthenticator.JWT_CREDENTIAL_SUBJECT_KEY, "myclient");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package org.keycloak.tests.client.authentication.external;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.authentication.authenticators.client.FederatedJWTClientAuthenticator;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testframework.oauth.OAuthClient;
|
||||
import org.keycloak.testframework.oauth.OAuthIdentityProvider;
|
||||
import org.keycloak.testframework.oauth.annotations.InjectOAuthClient;
|
||||
import org.keycloak.testframework.oauth.annotations.InjectOAuthIdentityProvider;
|
||||
import org.keycloak.testframework.realm.ClientConfigBuilder;
|
||||
import org.keycloak.testframework.realm.ManagedRealm;
|
||||
import org.keycloak.testframework.realm.RealmConfig;
|
||||
import org.keycloak.testframework.realm.RealmConfigBuilder;
|
||||
import org.keycloak.testsuite.util.IdentityProviderBuilder;
|
||||
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@KeycloakIntegrationTest(config = ClientAuthIdpServerConfig.class)
|
||||
public class FederatedClientAuthMappingTest {
|
||||
|
||||
private static final String IDP_ALIAS = "external-idp";
|
||||
|
||||
@InjectRealm(config = ExernalClientAuthRealmConfig.class)
|
||||
protected ManagedRealm realm;
|
||||
|
||||
@InjectOAuthClient
|
||||
OAuthClient oAuthClient;
|
||||
|
||||
@InjectOAuthIdentityProvider
|
||||
OAuthIdentityProvider identityProvider;
|
||||
|
||||
@Test
|
||||
public void testSimple() {
|
||||
Assertions.assertTrue(doClientGrant(createDefaultToken("external-simple-1"), "internal-simple-1"));
|
||||
Assertions.assertTrue(doClientGrant(createDefaultToken("external-simple-2"), "internal-simple-2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUrn() {
|
||||
Assertions.assertTrue(doClientGrant(createDefaultToken("spiffe://client/urn"), "internal-urn"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUri() {
|
||||
Assertions.assertTrue(doClientGrant(createDefaultToken("bf4c696e-89dc-4e40-a833-90fa5f8786e0"), "internal-uuid"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDuplicatedExternal() {
|
||||
Assertions.assertFalse(doClientGrant(createDefaultToken("external-duplicated"), null));
|
||||
}
|
||||
|
||||
private boolean doClientGrant(JsonWebToken token, String expectedInternalClientId) {
|
||||
String jws = identityProvider.encodeToken(token);
|
||||
AccessTokenResponse response = oAuthClient.clientCredentialsGrantRequest().clientJwt(jws).send();
|
||||
if (response.isSuccess()) {
|
||||
AccessToken accessToken = oAuthClient.parseToken(response.getAccessToken(), AccessToken.class);
|
||||
Assertions.assertEquals(expectedInternalClientId, accessToken.getIssuedFor());
|
||||
}
|
||||
return response.isSuccess();
|
||||
}
|
||||
|
||||
private JsonWebToken createDefaultToken(String externalClientId) {
|
||||
JsonWebToken token = new JsonWebToken();
|
||||
token.id(UUID.randomUUID().toString());
|
||||
token.issuer("http://127.0.0.1:8500");
|
||||
token.audience(oAuthClient.getEndpoints().getIssuer());
|
||||
token.iat((long) Time.currentTime());
|
||||
token.exp((long) (Time.currentTime() + 300));
|
||||
token.subject(externalClientId);
|
||||
return token;
|
||||
}
|
||||
|
||||
public static class ExernalClientAuthRealmConfig implements RealmConfig {
|
||||
|
||||
@Override
|
||||
public RealmConfigBuilder configure(RealmConfigBuilder realm) {
|
||||
realm.identityProvider(
|
||||
IdentityProviderBuilder.create()
|
||||
.providerId(OIDCIdentityProviderFactory.PROVIDER_ID)
|
||||
.alias(IDP_ALIAS)
|
||||
.setAttribute("issuer", "http://127.0.0.1:8500")
|
||||
.setAttribute(OIDCIdentityProviderConfig.USE_JWKS_URL, "true")
|
||||
.setAttribute(OIDCIdentityProviderConfig.JWKS_URL, "http://127.0.0.1:8500/idp/jwks")
|
||||
.setAttribute(OIDCIdentityProviderConfig.VALIDATE_SIGNATURE, "true")
|
||||
.setAttribute(OIDCIdentityProviderConfig.SUPPORTS_CLIENT_ASSERTIONS, "true")
|
||||
.build());
|
||||
|
||||
createClient(realm.addClient("internal-simple-1"), "external-simple-1");
|
||||
createClient(realm.addClient("internal-simple-2"), "external-simple-2");
|
||||
createClient(realm.addClient("internal-urn"), "spiffe://client/urn");
|
||||
createClient(realm.addClient("internal-uuid"), "bf4c696e-89dc-4e40-a833-90fa5f8786e0");
|
||||
createClient(realm.addClient("internal-duplicated-1"), "external-duplicated");
|
||||
createClient(realm.addClient("internal-duplicated-2"), "external-duplicated");
|
||||
|
||||
return realm;
|
||||
}
|
||||
|
||||
private static void createClient(ClientConfigBuilder client, String externalId) {
|
||||
client.serviceAccountsEnabled(true)
|
||||
.authenticatorType(FederatedJWTClientAuthenticator.PROVIDER_ID)
|
||||
.attribute(FederatedJWTClientAuthenticator.JWT_CREDENTIAL_ISSUER_KEY, IDP_ALIAS)
|
||||
.attribute(FederatedJWTClientAuthenticator.JWT_CREDENTIAL_SUBJECT_KEY, externalId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import org.keycloak.authentication.authenticators.client.FederatedJWTClientAuthe
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||
import org.keycloak.testframework.annotations.InjectClient;
|
||||
@@ -32,7 +33,8 @@ public class FederatedClientAuthTest {
|
||||
|
||||
private static final String IDP_ALIAS = "external-idp";
|
||||
|
||||
private static final String CLIENT_ID = "myclient";
|
||||
private static final String INTERNAL_CLIENT_ID = "internal-myclient";
|
||||
private static final String EXTERNAL_CLIENT_ID = "external-myclient";
|
||||
|
||||
@InjectRealm(config = ExernalClientAuthRealmConfig.class)
|
||||
protected ManagedRealm realm;
|
||||
@@ -142,12 +144,13 @@ public class FederatedClientAuthTest {
|
||||
rep.getConfig().put(OIDCIdentityProviderConfig.SUPPORTS_CLIENT_ASSERTION_REUSE, "true");
|
||||
idp.update(rep);
|
||||
|
||||
JsonWebToken token = createDefaultToken();
|
||||
Assertions.assertTrue(doClientGrant(token));
|
||||
Assertions.assertTrue(doClientGrant(token));
|
||||
|
||||
rep.getConfig().remove(OIDCIdentityProviderConfig.SUPPORTS_CLIENT_ASSERTION_REUSE);
|
||||
idp.update(rep);
|
||||
try {
|
||||
JsonWebToken token = createDefaultToken();
|
||||
Assertions.assertTrue(doClientGrant(token));
|
||||
} finally {
|
||||
rep.getConfig().remove(OIDCIdentityProviderConfig.SUPPORTS_CLIENT_ASSERTION_REUSE);
|
||||
idp.update(rep);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -170,6 +173,10 @@ public class FederatedClientAuthTest {
|
||||
|
||||
private boolean doClientGrant(String jws) {
|
||||
AccessTokenResponse response = oAuthClient.clientCredentialsGrantRequest().clientJwt(jws).send();
|
||||
if (response.isSuccess()) {
|
||||
AccessToken accessToken = oAuthClient.parseToken(response.getAccessToken(), AccessToken.class);
|
||||
Assertions.assertEquals(INTERNAL_CLIENT_ID, accessToken.getIssuedFor());
|
||||
}
|
||||
return response.isSuccess();
|
||||
}
|
||||
|
||||
@@ -180,7 +187,7 @@ public class FederatedClientAuthTest {
|
||||
token.audience(oAuthClient.getEndpoints().getIssuer());
|
||||
token.iat((long) Time.currentTime());
|
||||
token.exp((long) (Time.currentTime() + 300));
|
||||
token.subject(CLIENT_ID);
|
||||
token.subject(EXTERNAL_CLIENT_ID);
|
||||
return token;
|
||||
}
|
||||
|
||||
@@ -205,10 +212,11 @@ public class FederatedClientAuthTest {
|
||||
|
||||
@Override
|
||||
public ClientConfigBuilder configure(ClientConfigBuilder client) {
|
||||
return client.clientId(CLIENT_ID)
|
||||
return client.clientId(INTERNAL_CLIENT_ID)
|
||||
.serviceAccountsEnabled(true)
|
||||
.authenticatorType(FederatedJWTClientAuthenticator.PROVIDER_ID)
|
||||
.attribute(FederatedJWTClientAuthenticator.JWT_CREDENTIAL_ISSUER_KEY, IDP_ALIAS);
|
||||
.attribute(FederatedJWTClientAuthenticator.JWT_CREDENTIAL_ISSUER_KEY, IDP_ALIAS)
|
||||
.attribute(FederatedJWTClientAuthenticator.JWT_CREDENTIAL_SUBJECT_KEY, EXTERNAL_CLIENT_ID);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.keycloak.broker.spiffe.SpiffeIdentityProviderConfig;
|
||||
import org.keycloak.broker.spiffe.SpiffeIdentityProviderFactory;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||
import org.keycloak.testframework.annotations.InjectClient;
|
||||
@@ -70,14 +71,14 @@ public class SpiffeClientAuthTest {
|
||||
@Test
|
||||
public void testInvalidConfig() {
|
||||
testInvalidConfig("with-port:8080", "https://localhost");
|
||||
testInvalidConfig("spiffe://with-spiffe-scheme", "https://localhost");
|
||||
testInvalidConfig("with-spiffe-scheme", "https://localhost");
|
||||
testInvalidConfig("valid", "invalid-url");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidTrustDomain() {
|
||||
IdentityProviderUpdater.updateWithRollback(realm, IDP_ALIAS, rep -> {
|
||||
rep.getConfig().put(SpiffeIdentityProviderConfig.TRUST_DOMAIN_KEY, "different-domain");
|
||||
rep.getConfig().put(IdentityProviderModel.ISSUER, "spiffe://different-domain");
|
||||
});
|
||||
|
||||
Assertions.assertFalse(doClientGrant(createDefaultToken()));
|
||||
@@ -155,7 +156,7 @@ public class SpiffeClientAuthTest {
|
||||
private void testInvalidConfig(String trustDomain, String bundleEndpoint) {
|
||||
IdentityProviderRepresentation idp = IdentityProviderBuilder.create().providerId(SpiffeIdentityProviderFactory.PROVIDER_ID)
|
||||
.alias("another")
|
||||
.setAttribute(SpiffeIdentityProviderConfig.TRUST_DOMAIN_KEY, trustDomain)
|
||||
.setAttribute(IdentityProviderModel.ISSUER, trustDomain)
|
||||
.setAttribute(SpiffeIdentityProviderConfig.BUNDLE_ENDPOINT_KEY, bundleEndpoint).build();
|
||||
|
||||
try (Response r = realm.admin().identityProviders().create(idp)) {
|
||||
@@ -187,7 +188,7 @@ public class SpiffeClientAuthTest {
|
||||
IdentityProviderBuilder.create()
|
||||
.providerId(SpiffeIdentityProviderFactory.PROVIDER_ID)
|
||||
.alias(IDP_ALIAS)
|
||||
.setAttribute(SpiffeIdentityProviderConfig.TRUST_DOMAIN_KEY, "mytrust-domain")
|
||||
.setAttribute(IdentityProviderModel.ISSUER, "spiffe://mytrust-domain")
|
||||
.setAttribute(SpiffeIdentityProviderConfig.BUNDLE_ENDPOINT_KEY, "http://127.0.0.1:8500/idp/jwks")
|
||||
.build());
|
||||
}
|
||||
@@ -197,10 +198,11 @@ public class SpiffeClientAuthTest {
|
||||
|
||||
@Override
|
||||
public ClientConfigBuilder configure(ClientConfigBuilder client) {
|
||||
return client.clientId(CLIENT_ID)
|
||||
return client.clientId("myclient")
|
||||
.serviceAccountsEnabled(true)
|
||||
.authenticatorType(FederatedJWTClientAuthenticator.PROVIDER_ID)
|
||||
.attribute(FederatedJWTClientAuthenticator.JWT_CREDENTIAL_ISSUER_KEY, IDP_ALIAS);
|
||||
.attribute(FederatedJWTClientAuthenticator.JWT_CREDENTIAL_ISSUER_KEY, IDP_ALIAS)
|
||||
.attribute(FederatedJWTClientAuthenticator.JWT_CREDENTIAL_SUBJECT_KEY, CLIENT_ID);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -74,11 +74,6 @@ public class HardcodedClientStorageProvider implements ClientStorageProvider, Cl
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientModel getClientByAttribute(RealmModel realm, String name, String value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
|
||||
@@ -748,7 +748,7 @@ public class ClientAuthSignedJWTTest extends AbstractClientAuthSignedJWTTest {
|
||||
@Test
|
||||
public void testMissingIssuerClaim() throws Exception {
|
||||
AccessTokenResponse response = testMissingClaim("issuer");
|
||||
assertError(response,401, "client1", OAuthErrorException.INVALID_CLIENT, Errors.INVALID_CLIENT_CREDENTIALS);
|
||||
assertError(response,401, null, "invalid_client", Errors.CLIENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user