From a3f2ebb1935c544cab0bd101fbba92913c63d7ee Mon Sep 17 00:00:00 2001 From: mposolda Date: Mon, 24 Apr 2023 09:32:56 +0200 Subject: [PATCH] Ability to override default/built-in providers with same providerId. Using ProviderFactory.order() for choosing priority providers Closes #19867 --- .../server_development/topics/providers.adoc | 31 ++++++++ .../keycloak/provider/ProviderManager.java | 35 +++++++-- .../DefaultKeycloakSessionFactory.java | 2 +- .../CustomFreemarkerAccountProvider1.java | 33 ++++++++ .../CustomFreemarkerAccountProvider2.java | 33 ++++++++ ...stomFreemarkerAccountProviderFactory1.java | 40 ++++++++++ ...stomFreemarkerAccountProviderFactory2.java | 40 ++++++++++ .../CustomLoginFormsProvider.java | 33 ++++++++ .../CustomLoginFormsProviderFactory.java | 38 ++++++++++ .../providersoverride/CustomValidateOTP.java | 34 +++++++++ .../CustomValidatePassword1.java | 34 +++++++++ .../CustomValidatePassword2.java | 35 +++++++++ .../CustomValidatePassword3.java | 34 +++++++++ .../CustomValidateUsername.java | 30 ++++++++ .../rest/TestingResourceProvider.java | 15 +++- ...ycloak.authentication.AuthenticatorFactory | 7 +- ...cloak.forms.account.AccountProviderFactory | 21 +++++ ...loak.forms.login.LoginFormsProviderFactory | 20 +++++ .../client/resources/TestingResource.java | 10 +++ .../providers/ProvidersOverrideTest.java | 76 +++++++++++++++++++ .../tests/base/testsuites/base-suite | 1 + 21 files changed, 591 insertions(+), 11 deletions(-) create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomFreemarkerAccountProvider1.java create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomFreemarkerAccountProvider2.java create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomFreemarkerAccountProviderFactory1.java create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomFreemarkerAccountProviderFactory2.java create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomLoginFormsProvider.java create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomLoginFormsProviderFactory.java create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomValidateOTP.java create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomValidatePassword1.java create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomValidatePassword2.java create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomValidatePassword3.java create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomValidateUsername.java create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.forms.account.AccountProviderFactory create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.forms.login.LoginFormsProviderFactory create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/providers/ProvidersOverrideTest.java diff --git a/docs/documentation/server_development/topics/providers.adoc b/docs/documentation/server_development/topics/providers.adoc index 948c1cdc20f..94461ec1c12 100644 --- a/docs/documentation/server_development/topics/providers.adoc +++ b/docs/documentation/server_development/topics/providers.adoc @@ -47,6 +47,9 @@ public class MyThemeSelectorProviderFactory implements ThemeSelectorProviderFact } ---- +It is recommended that your provider factory implementation returns unique id by method `getId()`. However +there can be some exceptions to this rule as mentioned below in the <<_override_builtin_providers,Overriding providers>> section. + NOTE: {project_name} creates a single instance of provider factories which makes it possible to store state for multiple requests. Provider instances are created by calling create on the factory for each request so these should be light-weight object. @@ -119,6 +122,34 @@ public class MyThemeSelectorProvider implements ThemeSelectorProvider { } ---- +[[_override_builtin_providers]] +==== Override built-in providers + +As mentioned above, it is recommended that your `ProviderFactory` implementations use unique ID. However at the same time, it can be useful to override one of the {project_name} built-in providers. +The recommended way for this is still ProviderFactory implementation with unique ID and then for instance set the default provider as +specified in the link:https://www.keycloak.org/server/configuration-provider[Configuring Providers] guide. On the other hand, this may not be always possible. + +For instance when you need some customizations to default OpenID Connect protocol behaviour and you want to override +default {project_name} implementation of `OIDCLoginProtocolFactory` you need to preserve same providerId. As for example admin console, OIDC protocol well-known endpoint and various other things rely on +the ID of the protocol factory being `openid-connect`. + +For this case, it is highly recommended to implement method `order()` of your custom implementation and make sure that it has higher order than the built-in implementation. + +[source,java] +---- +public class CustomOIDCLoginProtocolFactory extends OIDCLoginProtocolFactory { + + // Some customizations here + + @Override + public int order() { + return 1; + } +} +---- + +In case of multiple implementations with same provider ID, only the one with highest order will be used by {project_name} runtime. + [[_providers_admin_console]] ==== Show info from your SPI implementation in the Admin Console diff --git a/services/src/main/java/org/keycloak/provider/ProviderManager.java b/services/src/main/java/org/keycloak/provider/ProviderManager.java index 2a24597fed0..149846aff15 100644 --- a/services/src/main/java/org/keycloak/provider/ProviderManager.java +++ b/services/src/main/java/org/keycloak/provider/ProviderManager.java @@ -18,15 +18,14 @@ package org.keycloak.provider; import org.jboss.logging.Logger; import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.services.DefaultKeycloakSessionFactory; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.ServiceLoader; -import java.util.Set; /** * @author Stian Thorgersen @@ -89,24 +88,46 @@ public class ProviderManager { public synchronized List load(Spi spi) { if (!cache.containsKey(spi.getProviderClass())) { - Set loaded = new HashSet<>(); + Map loaded = new HashMap<>(); for (ProviderLoader loader : loaders) { List f = loader.load(spi); if (f != null) { for (ProviderFactory pf: f) { - String uniqueId = spi.getName() + "-" + pf.getId() + "-" + pf.getClass().getName(); - if (!loaded.contains(uniqueId)) { - cache.add(spi.getProviderClass(), pf); - loaded.add(uniqueId); + String uniqueId = spi.getName() + "-" + pf.getId(); + if (!loaded.containsKey(uniqueId)) { + loaded.put(uniqueId, pf); + } else { + ProviderFactory currentFactory = loaded.get(uniqueId); + ProviderFactory factoryToUse = compareFactories(currentFactory, pf); + loaded.put(uniqueId, factoryToUse); + + logger.debugf("Found multiple provider factories of same provider ID implementing same SPI. SPI is '%s', providerFactory ID '%s'. Factories are '%s' and '%s'. Using provider factory '%s'.", + spi.getName(), pf.getId(), currentFactory.getClass().getName(), pf.getClass().getName(), factoryToUse.getClass().getName()); } } } } + + for (ProviderFactory providerFactory : loaded.values()) { + cache.add(spi.getProviderClass(), providerFactory); + } } List rtn = cache.get(spi.getProviderClass()); return rtn == null ? Collections.EMPTY_LIST : rtn; } + // Compare provider factories of same providerId. Just one of them needs to be chosen to be used in Keycloak + public ProviderFactory compareFactories(ProviderFactory p1, ProviderFactory p2) { + if (p1.order() != p2.order()) return (p1.order() > p2.order()) ? p1 : p2; + + // Internal factory is supposed to be overriden by custom factory + if (DefaultKeycloakSessionFactory.isInternal(p1) ^ DefaultKeycloakSessionFactory.isInternal(p2)) { + return DefaultKeycloakSessionFactory.isInternal(p1) ? p2 : p1; + } + + return p1; + } + /** * returns a copy of internal factories. * diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java index d30f91108f3..c79c78534cb 100755 --- a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java +++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java @@ -405,7 +405,7 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr } } - protected boolean isInternal(ProviderFactory factory) { + public static boolean isInternal(ProviderFactory factory) { String packageName = factory.getClass().getPackage().getName(); return packageName.startsWith("org.keycloak") && !packageName.startsWith("org.keycloak.examples"); } diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomFreemarkerAccountProvider1.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomFreemarkerAccountProvider1.java new file mode 100644 index 00000000000..f11c38f7a87 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomFreemarkerAccountProvider1.java @@ -0,0 +1,33 @@ +/* + * 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.examples.providersoverride; + +import org.keycloak.forms.account.freemarker.FreeMarkerAccountProvider; +import org.keycloak.models.KeycloakSession; + +/** + * @author Marek Posolda + */ +public class CustomFreemarkerAccountProvider1 extends FreeMarkerAccountProvider { + + public CustomFreemarkerAccountProvider1(KeycloakSession session) { + super(session); + } +} diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomFreemarkerAccountProvider2.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomFreemarkerAccountProvider2.java new file mode 100644 index 00000000000..e68326835d5 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomFreemarkerAccountProvider2.java @@ -0,0 +1,33 @@ +/* + * 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.examples.providersoverride; + +import org.keycloak.forms.account.freemarker.FreeMarkerAccountProvider; +import org.keycloak.models.KeycloakSession; + +/** + * @author Marek Posolda + */ +public class CustomFreemarkerAccountProvider2 extends FreeMarkerAccountProvider { + + public CustomFreemarkerAccountProvider2(KeycloakSession session) { + super(session); + } +} diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomFreemarkerAccountProviderFactory1.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomFreemarkerAccountProviderFactory1.java new file mode 100644 index 00000000000..b2c578889a6 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomFreemarkerAccountProviderFactory1.java @@ -0,0 +1,40 @@ +/* + * 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.examples.providersoverride; + +import org.keycloak.forms.account.AccountProvider; +import org.keycloak.forms.account.freemarker.FreeMarkerAccountProviderFactory; +import org.keycloak.models.KeycloakSession; + +/** + * Won't be used due lower order than CustomFreemarkerAccountProviderFactory2 + */ +public class CustomFreemarkerAccountProviderFactory1 extends FreeMarkerAccountProviderFactory { + + @Override + public int order() { + return 1; + } + + @Override + public AccountProvider create(KeycloakSession session) { + return new CustomFreemarkerAccountProvider1(session); + } +} diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomFreemarkerAccountProviderFactory2.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomFreemarkerAccountProviderFactory2.java new file mode 100644 index 00000000000..3b64a18c291 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomFreemarkerAccountProviderFactory2.java @@ -0,0 +1,40 @@ +/* + * 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.examples.providersoverride; + +import org.keycloak.forms.account.AccountProvider; +import org.keycloak.forms.account.freemarker.FreeMarkerAccountProviderFactory; +import org.keycloak.models.KeycloakSession; + +/** + * Test for order (This one should be called in favour of FreemarkerAccountProviderFactory and CustomFreemarkerAccountProviderFactory1 as it has highest order) + */ +public class CustomFreemarkerAccountProviderFactory2 extends FreeMarkerAccountProviderFactory { + + @Override + public int order() { + return 2; + } + + @Override + public AccountProvider create(KeycloakSession session) { + return new CustomFreemarkerAccountProvider2(session); + } +} diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomLoginFormsProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomLoginFormsProvider.java new file mode 100644 index 00000000000..770c597ccf1 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomLoginFormsProvider.java @@ -0,0 +1,33 @@ +/* + * 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.examples.providersoverride; + +import org.keycloak.forms.login.freemarker.FreeMarkerLoginFormsProvider; +import org.keycloak.models.KeycloakSession; + +/** + * @author Marek Posolda + */ +public class CustomLoginFormsProvider extends FreeMarkerLoginFormsProvider { + + public CustomLoginFormsProvider(KeycloakSession session) { + super(session); + } +} diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomLoginFormsProviderFactory.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomLoginFormsProviderFactory.java new file mode 100644 index 00000000000..12180a20c4f --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomLoginFormsProviderFactory.java @@ -0,0 +1,38 @@ +/* + * 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.examples.providersoverride; + +import org.keycloak.forms.login.LoginFormsProvider; +import org.keycloak.forms.login.freemarker.FreeMarkerLoginFormsProviderFactory; +import org.keycloak.models.KeycloakSession; + +/** + * This has same providerID like built-in ValidateUsername provider. But it should be called in favour of ValidateUsername even + * if it doesn't have "order" set. Ass it is custom provider and it worked this way in previous versions + */ +public class CustomLoginFormsProviderFactory extends FreeMarkerLoginFormsProviderFactory { + + @Override + public LoginFormsProvider create(KeycloakSession session) { + return new CustomLoginFormsProvider(session); + } + + +} diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomValidateOTP.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomValidateOTP.java new file mode 100644 index 00000000000..38c5830a28b --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomValidateOTP.java @@ -0,0 +1,34 @@ +/* + * 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.examples.providersoverride; + +import org.keycloak.authentication.authenticators.directgrant.ValidateOTP; + +/** + * Overrides built-in, but should not be called due the different order + * + */ +public class CustomValidateOTP extends ValidateOTP { + + @Override + public int order() { + return -1; + } +} diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomValidatePassword1.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomValidatePassword1.java new file mode 100644 index 00000000000..200f782d232 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomValidatePassword1.java @@ -0,0 +1,34 @@ +/* + * 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.examples.providersoverride; + +import org.keycloak.authentication.authenticators.directgrant.ValidatePassword; + +/** + * Test for order (This one is not called due CustomValidatePassword2 has bigger order) + * + */ +public class CustomValidatePassword1 extends ValidatePassword { + + @Override + public int order() { + return 1; + } +} diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomValidatePassword2.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomValidatePassword2.java new file mode 100644 index 00000000000..7027a7ef6fe --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomValidatePassword2.java @@ -0,0 +1,35 @@ +/* + * 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.examples.providersoverride; + +import org.keycloak.authentication.authenticators.directgrant.ValidatePassword; + +/** + * Test for order (This one should be called in favour of ValidatePassword, CustomValidatePassword1, CustomValidatePassword3 as it has highest order) + * + * @author Marek Posolda + */ +public class CustomValidatePassword2 extends ValidatePassword { + + @Override + public int order() { + return 2; + } +} diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomValidatePassword3.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomValidatePassword3.java new file mode 100644 index 00000000000..759b26790ab --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomValidatePassword3.java @@ -0,0 +1,34 @@ +/* + * 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.examples.providersoverride; + +import org.keycloak.authentication.authenticators.directgrant.ValidatePassword; + +/** + * Test for order (This one is not called due CustomValidatePassword2 has bigger order) + * + */ +public class CustomValidatePassword3 extends ValidatePassword { + + @Override + public int order() { + return -1; + } +} diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomValidateUsername.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomValidateUsername.java new file mode 100644 index 00000000000..be702602730 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/examples/providersoverride/CustomValidateUsername.java @@ -0,0 +1,30 @@ +/* + * 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.examples.providersoverride; + +import org.keycloak.authentication.authenticators.directgrant.ValidateUsername; + +/** + * This has same providerID like built-in ValidateUsername provider. But it should be called in favour of ValidateUsername even + * if it doesn't have "order" set. Ass it is custom provider and it worked this way in previous versions + * + */ +public class CustomValidateUsername extends ValidateUsername { +} diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java index 0970f236d9b..2af998235b8 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java @@ -46,10 +46,8 @@ import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserProvider; import org.keycloak.models.UserSessionModel; -import org.keycloak.models.UserSessionProvider; import org.keycloak.models.UserSessionSpi; import org.keycloak.models.map.common.AbstractMapProviderFactory; -import org.keycloak.models.map.storage.MapStorageProvider; import org.keycloak.models.map.storage.hotRod.connections.DefaultHotRodConnectionProviderFactory; import org.keycloak.models.map.storage.hotRod.connections.HotRodConnectionProvider; import org.keycloak.models.map.userSession.MapUserSessionProviderFactory; @@ -1062,6 +1060,19 @@ public class TestingResourceProvider implements RealmResourceProvider { return ErrorPage.error(session, session.getContext().getAuthenticationSession(), Response.Status.BAD_REQUEST, message == null ? "" : message); } + @GET + @Path("/get-provider-implementation-class") + @Produces(MediaType.APPLICATION_JSON) + public String getProviderClassName(@QueryParam("providerClass") String providerClass, @QueryParam("providerId") String providerId) { + try { + Class providerClazz = (Class) Class.forName(providerClass); + Provider provider = (providerId == null) ? session.getProvider(providerClazz) : session.getProvider(providerClazz, providerId); + return provider.getClass().getName(); + } catch (ClassNotFoundException cnfe) { + throw new RuntimeException("Cannot find provider class: " + providerClass, cnfe); + } + } + private RealmModel getRealmByName(String realmName) { RealmProvider realmProvider = session.getProvider(RealmProvider.class); RealmModel realm = realmProvider.getRealmByName(realmName); diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory index ccdd6f66a20..379af6eefea 100755 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory @@ -23,4 +23,9 @@ org.keycloak.testsuite.authentication.ExpectedParamAuthenticatorFactory org.keycloak.testsuite.authentication.PushButtonAuthenticatorFactory org.keycloak.testsuite.forms.UsernameOnlyAuthenticator org.keycloak.testsuite.authentication.SetUserAttributeAuthenticatorFactory -org.keycloak.testsuite.authentication.CustomAuthenticationFlowCallbackFactory \ No newline at end of file +org.keycloak.testsuite.authentication.CustomAuthenticationFlowCallbackFactory +org.keycloak.examples.providersoverride.CustomValidateUsername +org.keycloak.examples.providersoverride.CustomValidatePassword1 +org.keycloak.examples.providersoverride.CustomValidatePassword2 +org.keycloak.examples.providersoverride.CustomValidatePassword3 +org.keycloak.examples.providersoverride.CustomValidateOTP \ No newline at end of file diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.forms.account.AccountProviderFactory b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.forms.account.AccountProviderFactory new file mode 100644 index 00000000000..41429aea854 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.forms.account.AccountProviderFactory @@ -0,0 +1,21 @@ +# +# 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. +# +# + +org.keycloak.examples.providersoverride.CustomFreemarkerAccountProviderFactory1 +org.keycloak.examples.providersoverride.CustomFreemarkerAccountProviderFactory2 \ No newline at end of file diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.forms.login.LoginFormsProviderFactory b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.forms.login.LoginFormsProviderFactory new file mode 100644 index 00000000000..e6fa2eaa917 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.forms.login.LoginFormsProviderFactory @@ -0,0 +1,20 @@ +# +# 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. +# +# + +org.keycloak.examples.providersoverride.CustomLoginFormsProviderFactory \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java index 08ff3c5f669..3788db5b937 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java @@ -398,4 +398,14 @@ public interface TestingResource { @Produces(MediaType.APPLICATION_JSON) @Path("/display-error-message") Response displayErrorMessage(@QueryParam("message") String message); + + /** + * @param providerClass Full name of class such as for example "org.keycloak.authentication.Authenticator" + * @param providerId providerId referenced in particular provider factory. Can be null (in this case we're returning default provider for particular providerClass) + * @return fullname of provider implementation class + */ + @GET + @Path("/get-provider-implementation-class") + @Produces(MediaType.APPLICATION_JSON) + String getProviderClassName(@QueryParam("providerClass") String providerClass, @QueryParam("providerId") String providerId); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/providers/ProvidersOverrideTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/providers/ProvidersOverrideTest.java new file mode 100644 index 00000000000..e575efaf6fe --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/providers/ProvidersOverrideTest.java @@ -0,0 +1,76 @@ +/* + * 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.providers; + +import java.util.List; + +import org.junit.Test; +import org.keycloak.authentication.Authenticator; +import org.keycloak.authentication.authenticators.directgrant.ValidateOTP; +import org.keycloak.authentication.authenticators.directgrant.ValidatePassword; +import org.keycloak.authentication.authenticators.directgrant.ValidateUsername; +import org.keycloak.examples.providersoverride.CustomValidatePassword2; +import org.keycloak.examples.providersoverride.CustomValidateUsername; +import org.keycloak.forms.account.AccountProvider; +import org.keycloak.forms.login.LoginFormsProvider; +import org.keycloak.provider.Provider; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.testsuite.AbstractKeycloakTest; +import org.keycloak.testsuite.Assert; +import org.keycloak.examples.providersoverride.CustomFreemarkerAccountProvider2; +import org.keycloak.examples.providersoverride.CustomLoginFormsProvider; + +/** + * Test for having multiple providerFactory of smae SPI with same providerId + * + * @author Marek Posolda + */ +public class ProvidersOverrideTest extends AbstractKeycloakTest { + + @Override + public void addTestRealms(List testRealms) { + } + + @Test + public void testBuiltinAuthenticatorsOverride() { + // The custom provider would be preferred over the internal ValidateUsername. Both has same order, so custom provider would be chosen (backwards compatibility with previous versions) + testProviderImplementationClass(Authenticator.class, ValidateUsername.PROVIDER_ID, CustomValidateUsername.class); + + // The provider with highest order is chosen + testProviderImplementationClass(Authenticator.class, ValidatePassword.PROVIDER_ID, CustomValidatePassword2.class); + + // The builtin ValidateOTP class is chosen as it has higher order than the CustomValidateOTP + testProviderImplementationClass(Authenticator.class, ValidateOTP.PROVIDER_ID, ValidateOTP.class); + } + + @Test + public void testDefaultProvidersOverride() { + // The custom provider would be preferred over the internal FreemarkerLoginFormsProvider. Both has same order, so custom provider would be chosen (backwards compatibility with previous versions) + testProviderImplementationClass(LoginFormsProvider.class, null, CustomLoginFormsProvider.class); + + // The provider with highest order is chosen + testProviderImplementationClass(AccountProvider.class, null, CustomFreemarkerAccountProvider2.class); + } + + private void testProviderImplementationClass(Class providerClass, String providerId, Class expectedProviderImplClass) { + String providerImplClass = getTestingClient().testing().getProviderClassName(providerClass.getName(), providerId); + Assert.assertEquals(expectedProviderImplClass.getName(), providerImplClass); + } +} diff --git a/testsuite/integration-arquillian/tests/base/testsuites/base-suite b/testsuite/integration-arquillian/tests/base/testsuites/base-suite index 87019cadb22..60e8dfdfa88 100644 --- a/testsuite/integration-arquillian/tests/base/testsuites/base-suite +++ b/testsuite/integration-arquillian/tests/base/testsuites/base-suite @@ -30,6 +30,7 @@ oauth,6 oidc,6 openshift,6 policy,6 +providers,4 runonserver,6 saml,6 script,6