Possible to completely hide the email / password login form and only show external IdP's?

I’m using keycloak 26.4.2. For some realms I don’t want the email / password login form to be displayed at all. Instead, I want only the external idp buttons to show.

For example, assume I have a google oidc idp and a saml idp. When redirected to keycloak I only want to see those two options for logging in.

Both grok and chatgpt seem to think this is possible but it doesn’t work for me. Basically they both say:

  • Copy the existing “browser” flow
  • Bind it to the “Browser flow”
  • In the “Copy of browser” flow set the “Identity Provider Redirector” execution to “Alternative” but leave it’s configuration empty. As in don’t specify a “Default Identity Provider”.
  • In the “Copy of browser” flow set the “Organization” flow to “Disabled”.
  • In the “Copy of browser” flow set the “forms” flow to “Disabled”.

This doesn’t work and when login is initiated I see a keycloak screen that says:

  • We are sorry…
  • Invalid username or password

The keycloak logs show the following:

jeffvictortech-keycloak-1  | 2025-10-28 01:58:15,105 DEBUG [org.keycloak.protocol.oidc.endpoints.request.AuthzEndpointRequestParser] (executor-thread-192) The additional OIDC param 'scope' is well known. Continue with the other additional parameters.
jeffvictortech-keycloak-1  | 2025-10-28 01:58:15,105 DEBUG [org.keycloak.protocol.oidc.endpoints.request.AuthzEndpointRequestParser] (executor-thread-192) The additional OIDC param 'response_type' is well known. Continue with the other additional parameters.
jeffvictortech-keycloak-1  | 2025-10-28 01:58:15,105 DEBUG [org.keycloak.protocol.oidc.endpoints.request.AuthzEndpointRequestParser] (executor-thread-192) The additional OIDC param 'redirect_uri' is well known. Continue with the other additional parameters.
jeffvictortech-keycloak-1  | 2025-10-28 01:58:15,105 DEBUG [org.keycloak.protocol.oidc.endpoints.request.AuthzEndpointRequestParser] (executor-thread-192) The additional OIDC param 'state' is well known. Continue with the other additional parameters.
jeffvictortech-keycloak-1  | 2025-10-28 01:58:15,105 DEBUG [org.keycloak.protocol.oidc.endpoints.request.AuthzEndpointRequestParser] (executor-thread-192) The additional OIDC param 'nonce' is well known. Continue with the other additional parameters.
jeffvictortech-keycloak-1  | 2025-10-28 01:58:15,105 DEBUG [org.keycloak.protocol.oidc.endpoints.request.AuthzEndpointRequestParser] (executor-thread-192) The additional OIDC param 'client_id' is well known. Continue with the other additional parameters.
jeffvictortech-keycloak-1  | 2025-10-28 01:58:15,105 DEBUG [org.keycloak.protocol.oidc.endpoints.AuthorizationEndpointChecker] (executor-thread-192) PKCE non-supporting Client
jeffvictortech-keycloak-1  | 2025-10-28 01:58:15,105 DEBUG [org.keycloak.services.managers.AuthenticationSessionManager] (executor-thread-192) Found AUTH_SESSION_ID cookie with value NDE5YzVkNDUtNmQ1YS0wNDIyLWE3OWUtZDY5YzFlOTc0OGM5LjBtUWtEdGJ1cGJ0WFltQWtXc3JOclRlTmJZdWdzbGpFMmpoMkdzWFc1amJZb2hHX1dJWUxFT0pEdXRKeXBBNmYtcGtNbHBxaUE3UGdiTUJhLVZ6QVNR
jeffvictortech-keycloak-1  | 2025-10-28 01:58:15,105 DEBUG [org.keycloak.protocol.AuthorizationEndpointBase] (executor-thread-192) Sent request to authz endpoint. Root authentication session with ID '419c5d45-6d5a-0422-a79e-d69c1e9748c9' exists. Client is 'jeffvictortech' . Created new authentication session with tab ID: 6SidOxqkbwI
jeffvictortech-keycloak-1  | 2025-10-28 01:58:15,106 DEBUG [org.keycloak.authentication.AuthenticationProcessor] (executor-thread-192) AUTHENTICATE
jeffvictortech-keycloak-1  | 2025-10-28 01:58:15,106 DEBUG [org.keycloak.authentication.AuthenticationProcessor] (executor-thread-192) AUTHENTICATE ONLY
jeffvictortech-keycloak-1  | 2025-10-28 01:58:15,106 DEBUG [org.keycloak.authentication.DefaultAuthenticationFlow] (executor-thread-192) processFlow: Copy of browser
jeffvictortech-keycloak-1  | 2025-10-28 01:58:15,106 DEBUG [org.keycloak.authentication.DefaultAuthenticationFlow] (executor-thread-192) check execution: 'auth-cookie', requirement: 'ALTERNATIVE'
jeffvictortech-keycloak-1  | 2025-10-28 01:58:15,106 DEBUG [org.keycloak.authentication.DefaultAuthenticationFlow] (executor-thread-192) authenticator: auth-cookie
jeffvictortech-keycloak-1  | 2025-10-28 01:58:15,106 DEBUG [org.keycloak.authentication.AuthenticationSelectionResolver] (executor-thread-192) Going through the flow 'Copy of browser' for adding executions
jeffvictortech-keycloak-1  | 2025-10-28 01:58:15,106 DEBUG [org.keycloak.authentication.AuthenticationSelectionResolver] (executor-thread-192) Selections when trying execution 'auth-cookie' : [ authSelection - auth-cookie,  authSelection - identity-provider-redirector]
jeffvictortech-keycloak-1  | 2025-10-28 01:58:15,106 DEBUG [org.keycloak.authentication.DefaultAuthenticationFlow] (executor-thread-192) invoke authenticator.authenticate: auth-cookie
jeffvictortech-keycloak-1  | 2025-10-28 01:58:15,106 DEBUG [org.keycloak.services.managers.AuthenticationManager] (executor-thread-192) Could not find cookie: KEYCLOAK_IDENTITY
jeffvictortech-keycloak-1  | 2025-10-28 01:58:15,106 DEBUG [org.keycloak.authentication.DefaultAuthenticationFlow] (executor-thread-192) authenticator ATTEMPTED: auth-cookie
jeffvictortech-keycloak-1  | 2025-10-28 01:58:15,106 DEBUG [org.keycloak.authentication.DefaultAuthenticationFlow] (executor-thread-192) check execution: 'identity-provider-redirector', requirement: 'ALTERNATIVE'
jeffvictortech-keycloak-1  | 2025-10-28 01:58:15,106 DEBUG [org.keycloak.authentication.DefaultAuthenticationFlow] (executor-thread-192) authenticator: identity-provider-redirector
jeffvictortech-keycloak-1  | 2025-10-28 01:58:15,106 DEBUG [org.keycloak.authentication.AuthenticationSelectionResolver] (executor-thread-192) Going through the flow 'Copy of browser' for adding executions
jeffvictortech-keycloak-1  | 2025-10-28 01:58:15,106 DEBUG [org.keycloak.authentication.AuthenticationSelectionResolver] (executor-thread-192) Selections when trying execution 'identity-provider-redirector' : [ authSelection - identity-provider-redirector]
jeffvictortech-keycloak-1  | 2025-10-28 01:58:15,106 DEBUG [org.keycloak.authentication.DefaultAuthenticationFlow] (executor-thread-192) invoke authenticator.authenticate: identity-provider-redirector
jeffvictortech-keycloak-1  | 2025-10-28 01:58:15,106 DEBUG [org.keycloak.authentication.DefaultAuthenticationFlow] (executor-thread-192) authenticator ATTEMPTED: identity-provider-redirector
jeffvictortech-keycloak-1  | 2025-10-28 01:58:15,106 WARN  [org.keycloak.services] (executor-thread-192) KC-SERVICES0013: Failed authentication: org.keycloak.authentication.AuthenticationFlowException
jeffvictortech-keycloak-1  |    at org.keycloak.authentication.AuthenticationProcessor.authenticateOnly(AuthenticationProcessor.java:1125)
nginx-1                    | 172.22.0.1 - - [28/Oct/2025:01:58:15 +0000] "GET /realms/jeffvictortech.local/protocol/openid-connect/auth?response_type=code&client_id=jeffvictortech&scope=openid&state=ycKrdtpHpZZTALTIvfvY0zarke4f6GVQc3i003krYp8%3D&redirect_uri=http://jeffvictortech.local:18086/login/oauth2/code/jeffvictortech.local&nonce=0HN9ZWYZGuFH-qXqknuGhbIdbZYNXoSlr1GqT3aD-uw HTTP/2.0" 400 4095 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36"
jeffvictortech-keycloak-1  |    at org.keycloak.authentication.AuthenticationProcessor.authenticate(AuthenticationProcessor.java:955)
jeffvictortech-keycloak-1  |    at org.keycloak.protocol.AuthorizationEndpointBase.handleBrowserAuthenticationRequest(AuthorizationEndpointBase.java:147)
jeffvictortech-keycloak-1  |    at org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint.buildAuthorizationCodeAuthorizationResponse(AuthorizationEndpoint.java:397)
jeffvictortech-keycloak-1  |    at org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint.process(AuthorizationEndpoint.java:235)
jeffvictortech-keycloak-1  |    at org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint.buildGet(AuthorizationEndpoint.java:119)

Is it possible to do hide the email / password form through some sort of configuration alone? Or is this only possible by customizing the login theme and hiding the form there?

What I did was just creating a login theme without user/password auth and assigning that in the realm.

I ended up creating a custom spi for this.

In my app I allow a tenant to dynamically change the ways that users can log in. I support email / password, google, facebook, linkedin, and saml and use the keycloak admin api to provision / deprovision these idp’s.

I could have gone with a custom theme only option but this would mean that I would have to maintain 2 different themes as the custom theme I already have adds a background image, the actual name of my app, etc. So I would have needed one custom theme that includes email / password on the login form and one that doesn’t. Then I could have used the admin api to swap the theme depending on the enabled idp’s.

I only want to maintain one theme so I created a custom spi that extends FreeMarkerLoginFormsProvider. Then I use the admin api to add a custom boolean attribute to the realm called emailPasswordEnabled. I then wrapped the existing RealmBean so that I could expose this attribute in the freemarker template and include / exclude the email / password part of the form based on this attribute.

To add the attribute to the realm using the admin api is quite simple:

	public void enableEmailPasswordLogin(String realm, boolean enabled) {
		
		RealmRepresentation realmRepresentation = keycloak.realm(realm).toRepresentation();
		
		Map<String, String> existingRealmAttributes = realmRepresentation.getAttributes();
		
		existingRealmAttributes.put("emailPasswordEnabled", enabled + "");
		
		keycloak.realm(realm).update(realmRepresentation);
	}

This works well and now if emailPasswordEnabled = false the login form will only display the idp’s that are configured and not the email / password section.

I’ll publish the spi code for this on github some time in the next week in case anyone else in interested in this type of solution and then will post a link to the repo here.

You didn’t mention in your first post, that you are looking for a dynamic solution. So it’s basically two different things.

Additionally, there’s no need for a custom SPI. The RealmBean already as a getter method to access the realm attributes. So, if you set your custom attribute to the realm through the admin API, you can access it in the freemarker template with ${realm.attributes.emailPasswordEnabled}.
Things can be easy.

Well that’s good news. I’ll give it a try as you suggested @dasniko without a custom spi.
I will say that what led me down the custom spi path was that originally I thought I would just hijack the isPassword() method in RealmBean since the email / password form in login.ftl is wrapped in an <#if realm.password> tag. Prior to creating the spi I wasn’t too familiar with how I could set a custom attribute and use it in the freemarker template. So I asked grok how I could do that and it suggested a custom spi so I tried it and it worked.

@dasniko do you know where in the code CredentialRepresentation.PASSWORD is added to realm.getRequiredCredentialsStream()? This is what drives the isPassword() method in RealmBean:

public boolean isPassword() {
    return realm.getRequiredCredentialsStream()
            .anyMatch(r -> Objects.equals(r.getType(), CredentialRepresentation.PASSWORD));
}

I couldn’t find anything in the code which would indicate that this is configurable. It seemed like CredentialRepresentation.PASSWORD is always there. Is there some configuration in the admin UI where this can be toggled?

I’ve tested the approach that @dasniko suggested and it works but realm.attributes.emailPasswordEnabled isn’t the correct syntax to use in the template. The correct syntax is:

realm.getAttribute(“emailPasswordEnabled”) == “true”

This is because when setting the value of the attribute via the admin API it gets stored as a string.

RealmRepresentation realmRepresentation = keycloak.realm(realm).toRepresentation();

Map<String, String> existingRealmAttributes = realmRepresentation.getAttributes();

existingRealmAttributes.put(“emailPasswordEnabled”, enabled + “”);

keycloak.realm(realm).update(realmRepresentation);

I can confirm that this works as expected.