Skip to content

Commit

Permalink
feat: Support forwarding users with unverified email addresses
Browse files Browse the repository at this point in the history
Closes #261
  • Loading branch information
sventorben committed Nov 23, 2023
1 parent c4de64d commit e361cca
Show file tree
Hide file tree
Showing 8 changed files with 68 additions and 14 deletions.
13 changes: 7 additions & 6 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,13 @@ To configure click settings/gear icon (⚙)

![Authenticator configuration](images/authenticator-config.jpg)

| Option | Description |
|-------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| User attribute | The user attribute used to lookup the user's email address.<br><br>If set to `email` (default) the authenticator will use the default email property. In this case the authenticator will only forward the user if the email has been verified. For any other attribute, the authenticator will not validate if the email has been verified. <br><br> A common use case is to store a User Principal Name (UPN) in a custom attribute and forward users based on the UPN instead instead of their email address. |
| Bypass login page | If switched on, users will be forwarded to their home IdP without the need to reenter/confirm their email address on the login page iff email address is provided as an OICD [`login_hint` parameter](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest) or SAML `subject/nameID`.<br><br> If switched off, users are only redirected after submitting/confirming their email address on the login page. (default)<br> <br> *Note: This will take SAML `ForceAuthn` and OIDC [`prompt=login&#124;consent&#124;select_account`](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest) parameters into account. If one of these parameters is present, the login page will not be bypassed even if switched on.* |
| Forward to linked IdP | If switched on, federated users (with already linked IdPs) will be forwarded to a linked IdP even if no IdP has been configured for the user's email address. Federated users can also use their local username for login instead of their email address.<br><br> If switched off, users will only be forwarded to IdPs with matching email domains. (default) |
| Forward to first matched IdP | If switched on, users will be forwarded to the first IdP that matches the email domain (default), even if multiply IdPs may match.<br><br>If switched off, user will be shown all IdPs that match the email domain to choose one, iff multiple match.<br>The user will only be able to choose from IdPs that match the email domain. Please note that also IdPs that have [`Hide on Login Page`](https://www.keycloak.org/docs/latest/server_admin/#_general-idp-config) switched on will be shown.<br>If only one IdP matches, behavior is the same as if switched on. |
| Option | Description |
|-------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| User attribute | The user attribute used to lookup the user's email address.<br><br>If set to `email` (default) the authenticator will use the default email property. In this case the authenticator will only forward the user if the email has been verified or 'Forward users with unverified email' option is enabled. For any other attribute, the authenticator will not validate if the email has been verified. <br><br> A common use case is to store a User Principal Name (UPN) in a custom attribute and forward users based on the UPN instead instead of their email address. |
| Forward users with unverified email | If switched on, users with unverified email addresses will be forwarded to their home IdP.<br><br> If switched off (default), users with unverified email addresses will not be forwarded to their home IdP. | |
| Bypass login page | If switched on, users will be forwarded to their home IdP without the need to reenter/confirm their email address on the login page iff email address is provided as an OICD [`login_hint` parameter](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest) or SAML `subject/nameID`.<br><br> If switched off, users are only redirected after submitting/confirming their email address on the login page. (default)<br> <br> *Note: This will take SAML `ForceAuthn` and OIDC [`prompt=login&#124;consent&#124;select_account`](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest) parameters into account. If one of these parameters is present, the login page will not be bypassed even if switched on.* |
| Forward to linked IdP | If switched on, federated users (with already linked IdPs) will be forwarded to a linked IdP even if no IdP has been configured for the user's email address. Federated users can also use their local username for login instead of their email address.<br><br> If switched off, users will only be forwarded to IdPs with matching email domains. (default) |
| Forward to first matched IdP | If switched on, users will be forwarded to the first IdP that matches the email domain (default), even if multiply IdPs may match.<br><br>If switched off, user will be shown all IdPs that match the email domain to choose one, iff multiple match.<br>The user will only be able to choose from IdPs that match the email domain. Please note that also IdPs that have [`Hide on Login Page`](https://www.keycloak.org/docs/latest/server_admin/#_general-idp-config) switched on will be shown.<br>If only one IdP matches, behavior is the same as if switched on. |

## Email domains

Expand Down
3 changes: 3 additions & 0 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ version [here](https://access.redhat.com/articles/2342881). Above rules apply ;)
Make sure that your users email is marked as verified. You can enable the `Email verified` flag per user or switch
on `Trust Email` in the advanced settings of the identity provider.

You can also allow redirecting users with unverified email addresses by switching
on `Forward users with unverified email` option in the authenticator config.

## User is not redirected to the correct identity provider. How to analyze the problem?

You may want to increase the log level to see more fine-grained information on how the authenticator discovered the home
Expand Down
Binary file modified docs/images/authenticator-config.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ Optional<Domain> extractFrom(UserModel user) {
LOG.warnf("Could not find user attribute '%s' for user '%s'", config.userAttribute(), user.getId());
return Optional.empty();
}
if (EMAIL_ATTRIBUTE.equalsIgnoreCase(config.userAttribute()) && !user.isEmailVerified()) {
LOG.warnf("Email address of user '%s' is not verified", user.getId());
if (EMAIL_ATTRIBUTE.equalsIgnoreCase(config.userAttribute()) && !user.isEmailVerified()
&& !config.forwardUserWithUnverifiedEmail()) {
LOG.warnf("Email address of user '%s' is not verified and forwarding not enabled", user.getId());
return Optional.empty();
}
return extractFrom(userAttribute);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ final class HomeIdpDiscoveryConfig {
static final String FORWARD_TO_LINKED_IDP = "forwardToLinkedIdp";
static final String BYPASS_LOGIN_PAGE = "bypassLoginPage";
static final String USER_ATTRIBUTE = "userAttribute";
static final String FORWARD_UNVERIFIED_ATTRIBUTE = "forwardUnverifiedEmail";
static final String FORWARD_TO_FIRST_MATCH = "forwardToFirstMatch";

private final AuthenticatorConfigModel authenticatorConfigModel;
Expand All @@ -35,6 +36,12 @@ String userAttribute() {
.orElse("email");
}

boolean forwardUserWithUnverifiedEmail() {
return Optional.ofNullable(authenticatorConfigModel)
.map(it -> Boolean.parseBoolean(it.getConfig().getOrDefault(FORWARD_UNVERIFIED_ATTRIBUTE, "false")))
.orElse(false);
}

boolean forwardToFirstMatch() {
return Optional.ofNullable(authenticatorConfigModel)
.map(it -> Boolean.parseBoolean(it.getConfig().getOrDefault(FORWARD_TO_FIRST_MATCH, "true")))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import static de.sventorben.keycloak.authentication.hidpd.HomeIdpDiscoveryConfig.BYPASS_LOGIN_PAGE;
import static de.sventorben.keycloak.authentication.hidpd.HomeIdpDiscoveryConfig.FORWARD_TO_LINKED_IDP;
import static de.sventorben.keycloak.authentication.hidpd.HomeIdpDiscoveryConfig.FORWARD_TO_FIRST_MATCH;
import static de.sventorben.keycloak.authentication.hidpd.HomeIdpDiscoveryConfig.FORWARD_UNVERIFIED_ATTRIBUTE;
import static de.sventorben.keycloak.authentication.hidpd.HomeIdpDiscoveryConfig.USER_ATTRIBUTE;
import static org.keycloak.provider.ProviderConfigProperty.BOOLEAN_TYPE;
import static org.keycloak.provider.ProviderConfigProperty.STRING_TYPE;
Expand Down Expand Up @@ -46,8 +47,17 @@ final class HomeIdpDiscoveryConfigProperties {
"email",
false);

private static final ProviderConfigProperty FORWARD_UNVERIFIED_PROPERTY = new ProviderConfigProperty(
FORWARD_UNVERIFIED_ATTRIBUTE,
"Forward users with unverified email",
"If 'User attribute' is set to 'email', whether to forward existing user if user's email is not verified.",
BOOLEAN_TYPE,
false,
false);

static final List<ProviderConfigProperty> CONFIG_PROPERTIES = ProviderConfigurationBuilder.create()
.property(USER_ATTRIBUTE_PROPERTY)
.property(FORWARD_UNVERIFIED_PROPERTY)
.property(BYPASS_LOGIN_PAGE_PROPERTY)
.property(FORWARD_TO_LINKED_IDP_PROPERTY)
.property(FORWARD_TO_FIRST_MATCH_PROPERTY)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@ void setUserAttribute(String userAttribute) {
updateProperty("userAttribute", userAttribute);
}

void enableForwardingUnverifiedEmails() {
setForwardingUnverifiedEmails(true);
}

void disableForwardingUnverifiedEmails() {
setForwardingUnverifiedEmails(false);
}

private void setForwardingUnverifiedEmails(Boolean enabled) {
updateProperty("forwardUnverifiedEmail", enabled);
}

void enableForwarding() {
setForwarding(true);
}
Expand Down Expand Up @@ -67,6 +79,7 @@ void resetAuthenticatorConfig() {
disableBypassLoginPage();
setUserAttribute("email");
enableForwardToFirstMatch();
disableForwardingUnverifiedEmails();
}

private void updateProperty(String propertyName, Boolean enabled) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,30 @@ public void doNotRedirectIfUserHasNonManagedDomain() {
assertNotRedirected();
}

@Test
@DisplayName("Given user's email is not verified, do not redirect")
public void doNotRedirectIfUserEmailIsNotVerified() {
accountConsolePage().signIn();
testRealmLoginPage().signIn("test4@example.com");
assertNotRedirected();
@Nested
@DisplayName("Given user's email is not verified")
class UnverifiedEmail {

@BeforeEach
void setUp() {
accountConsolePage().signIn();
}

@Test
@DisplayName("then do not redirect")
public void doNotRedirect() {
testRealmLoginPage().signIn("test4@example.com");
assertNotRedirected();
}

@Test
@DisplayName("then redirect if enabled")
public void redirectIfEnabled() {
authenticatorConfig.enableForwardingUnverifiedEmails();
testRealmLoginPage().signIn("test4@example.com");
assertRedirectedToIdp();
}

}

@Test
Expand Down Expand Up @@ -475,6 +493,7 @@ private void assertRedirectedTo(String url) {
private static RemoteWebDriver setupDriver() {
RemoteWebDriver driver = BROWSER.getWebDriver();
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(30));
driver.manage().deleteAllCookies();
return driver;
}

Expand Down

0 comments on commit e361cca

Please sign in to comment.