Ability to override default/built-in providers with same providerId. Using ProviderFactory.order() for choosing priority providers

Closes #19867
This commit is contained in:
mposolda 2023-04-24 09:32:56 +02:00 committed by Marek Posolda
parent 78958ae434
commit a3f2ebb193
21 changed files with 591 additions and 11 deletions

View file

@ -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

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -89,24 +88,46 @@ public class ProviderManager {
public synchronized List<ProviderFactory> load(Spi spi) {
if (!cache.containsKey(spi.getProviderClass())) {
Set<String> loaded = new HashSet<>();
Map<String, ProviderFactory> loaded = new HashMap<>();
for (ProviderLoader loader : loaders) {
List<ProviderFactory> 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<ProviderFactory> 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.
*

View file

@ -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");
}

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class CustomFreemarkerAccountProvider1 extends FreeMarkerAccountProvider {
public CustomFreemarkerAccountProvider1(KeycloakSession session) {
super(session);
}
}

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class CustomFreemarkerAccountProvider2 extends FreeMarkerAccountProvider {
public CustomFreemarkerAccountProvider2(KeycloakSession session) {
super(session);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class CustomLoginFormsProvider extends FreeMarkerLoginFormsProvider {
public CustomLoginFormsProvider(KeycloakSession session) {
super(session);
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class CustomValidatePassword2 extends ValidatePassword {
@Override
public int order() {
return 2;
}
}

View file

@ -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;
}
}

View file

@ -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 {
}

View file

@ -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<? extends Provider> providerClazz = (Class<? extends Provider>) 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);

View file

@ -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
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

View file

@ -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

View file

@ -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

View file

@ -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);
}

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ProvidersOverrideTest extends AbstractKeycloakTest {
@Override
public void addTestRealms(List<RealmRepresentation> 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<? extends Provider> providerClass, String providerId, Class<? extends Provider> expectedProviderImplClass) {
String providerImplClass = getTestingClient().testing().getProviderClassName(providerClass.getName(), providerId);
Assert.assertEquals(expectedProviderImplClass.getName(), providerImplClass);
}
}

View file

@ -30,6 +30,7 @@ oauth,6
oidc,6
openshift,6
policy,6
providers,4
runonserver,6
saml,6
script,6