Skip to content

Commit

Permalink
feat: Enable username persistence for reauthentication
Browse files Browse the repository at this point in the history
Implement username persistence on the login form to
improve user experience by eliminating the need for users
to re-enter their usernames during the reauthentication
process.

Relates to #312
  • Loading branch information
sventorben committed May 15, 2024
1 parent e544349 commit 2ea9799
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 29 deletions.
14 changes: 7 additions & 7 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@ To configure click settings/gear icon (⚙)

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

| Option | JSON property name | Type | Description |
|-------------------------------------|--------------------------|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| User attribute | `userAttribute` | `string` | 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 | `forwardUnverifiedEmail` | `boolean` | If switched on (`true`), users with unverified email addresses will be forwarded to their home IdP.<br><br> If switched off (`false`, default), users with unverified email addresses will not be forwarded to their home IdP. | |
| Bypass login page | `bypassLoginPage` | `boolean` | If switched on (`true`), 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 (`false`, default), users are only redirected after submitting/confirming their email address on the login page.<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 | `forwardToLinkedIdp` | `boolean` | If switched on (`true`), 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 (`false`, default), users will only be forwarded to IdPs with matching email domains. |
| Forward to first matched IdP | `forwardToFirstMatch` | `boolean` | If switched on (`true`, default), users will be forwarded to the first IdP that matches the email domain, even if multiply IdPs may match.<br><br>If switched off (`false`), 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 | JSON property name | Type | Description |
|-------------------------------------|--------------------------|-----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| User attribute | `userAttribute` | `string` | 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 | `forwardUnverifiedEmail` | `boolean` | If switched on (`true`), users with unverified email addresses will be forwarded to their home IdP.<br><br> If switched off (`false`, default), users with unverified email addresses will not be forwarded to their home IdP. | |
| Bypass login page | `bypassLoginPage` | `boolean` | If switched on (`true`), 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 (`false`, default), users are only redirected after submitting/confirming their email address on the login page.<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), and `max_age` 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 | `forwardToLinkedIdp` | `boolean` | If switched on (`true`), 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 (`false`, default), users will only be forwarded to IdPs with matching email domains. |
| Forward to first matched IdP | `forwardToFirstMatch` | `boolean` | If switched on (`true`, default), users will be forwarded to the first IdP that matches the email domain, even if multiply IdPs may match.<br><br>If switched off (`false`), 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
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ final class AuthenticationChallenge {
private final RememberMe rememberMe;
private final LoginHint loginHint;
private final LoginForm loginForm;
private final Reauthentication reauthentication;

AuthenticationChallenge(AuthenticationFlowContext context, RememberMe rememberMe, LoginHint loginHint, LoginForm loginForm) {
AuthenticationChallenge(AuthenticationFlowContext context, RememberMe rememberMe, LoginHint loginHint, LoginForm loginForm, Reauthentication reauthentication) {
this.context = context;
this.rememberMe = rememberMe;
this.loginHint = loginHint;
this.loginForm = loginForm;
this.reauthentication = reauthentication;
}

void forceChallenge() {
Expand All @@ -29,15 +31,27 @@ void forceChallenge() {

String rememberMeUsername = rememberMe.getUserName();

if (loginHintUsername != null || rememberMeUsername != null) {
if (loginHintUsername != null) {
formData.add(AuthenticationManager.FORM_USERNAME, loginHintUsername);
} else {
formData.add(AuthenticationManager.FORM_USERNAME, rememberMeUsername);
formData.add("rememberMe", "on");
if (reauthentication.required() && context.getUser() != null) {
String attribute = context.getAuthenticatorConfig().getConfig().getOrDefault("userAttribute", "username");
formData.add(AuthenticationManager.FORM_USERNAME, context.getUser().getFirstAttribute(attribute));
} else {
if (loginHintUsername != null || rememberMeUsername != null) {
if (loginHintUsername != null) {
formData.add(AuthenticationManager.FORM_USERNAME, loginHintUsername);
} else {
formData.add(AuthenticationManager.FORM_USERNAME, rememberMeUsername);
formData.add("rememberMe", "on");
}
}
}
Response challengeResponse = loginForm.create(formData);

Response challengeResponse;
if (reauthentication.required()) {
challengeResponse = loginForm.createWithSignInButtonOnly(formData);
} else {
challengeResponse = loginForm.create(formData);
}

context.challenge(challengeResponse);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ final class HomeIdpAuthenticationFlowContext {
private Redirector redirector;
private BaseUriLoginFormsProvider loginFormsProvider;
private LoginForm loginForm;
private Reauthentication reauthentication;

HomeIdpAuthenticationFlowContext(AuthenticationFlowContext context) {
this.context = context;
Expand All @@ -29,7 +30,7 @@ HomeIdpForwarderConfig config() {

LoginPage loginPage() {
if (loginPage == null) {
loginPage = new LoginPage(context, config());
loginPage = new LoginPage(context, config(), reauthentication());
}
return loginPage;
}
Expand Down Expand Up @@ -57,7 +58,7 @@ RememberMe rememberMe() {

AuthenticationChallenge authenticationChallenge() {
if (authenticationChallenge == null) {
authenticationChallenge = new AuthenticationChallenge(context, rememberMe(), loginHint(), loginForm());
authenticationChallenge = new AuthenticationChallenge(context, rememberMe(), loginHint(), loginForm(), reauthentication());
}
return authenticationChallenge;
}
Expand All @@ -82,4 +83,11 @@ BaseUriLoginFormsProvider loginFormsProvider() {
}
return loginFormsProvider;
}

Reauthentication reauthentication() {
if (reauthentication == null) {
reauthentication = new Reauthentication(context);
}
return reauthentication;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,10 @@ final class HomeIdpDiscoveryAuthenticator extends AbstractUsernameFormAuthentica
@Override
public void authenticate(AuthenticationFlowContext authenticationFlowContext) {
HomeIdpAuthenticationFlowContext context = new HomeIdpAuthenticationFlowContext(authenticationFlowContext);

if (context.loginPage().shouldByPass()) {
String loginHint = trimToNull(context.loginHint().getFromSession());
if (loginHint == null) {
loginHint = trimToNull(authenticationFlowContext.getAuthenticationSession().getAuthNote(ATTEMPTED_USERNAME));
}
if (loginHint != null) {
String username = setUserInContext(authenticationFlowContext, loginHint);
String usernameHint = usernameHint(authenticationFlowContext, context);
if (usernameHint != null) {
String username = setUserInContext(authenticationFlowContext, usernameHint);
final List<IdentityProviderModel> homeIdps = context.discoverer(discovererConfig).discoverForUser(authenticationFlowContext, username);
if (!homeIdps.isEmpty()) {
context.rememberMe().remember(username);
Expand All @@ -51,6 +47,14 @@ public void authenticate(AuthenticationFlowContext authenticationFlowContext) {
context.authenticationChallenge().forceChallenge();
}

private String usernameHint(AuthenticationFlowContext authenticationFlowContext, HomeIdpAuthenticationFlowContext context) {
String usernameHint = trimToNull(context.loginHint().getFromSession());
if (usernameHint == null) {
usernameHint = trimToNull(authenticationFlowContext.getAuthenticationSession().getAuthNote(ATTEMPTED_USERNAME));
}
return usernameHint;
}

private void redirectOrChallenge(HomeIdpAuthenticationFlowContext context, String username, List<IdentityProviderModel> homeIdps) {
if (homeIdps.size() == 1 || context.config().forwardToFirstMatch()) {
IdentityProviderModel homeIdp = homeIdps.get(0);
Expand All @@ -70,13 +74,21 @@ public void action(AuthenticationFlowContext authenticationFlowContext) {
return;
}

String username = setUserInContext(authenticationFlowContext, formData.getFirst(AuthenticationManager.FORM_USERNAME));
HomeIdpAuthenticationFlowContext context = new HomeIdpAuthenticationFlowContext(authenticationFlowContext);

String tryUsername;
if (context.reauthentication().required() && authenticationFlowContext.getUser() != null) {
tryUsername = authenticationFlowContext.getUser().getUsername();
} else {
tryUsername = formData.getFirst(AuthenticationManager.FORM_USERNAME);
}

String username = setUserInContext(authenticationFlowContext, tryUsername);
if (username == null) {
LOG.debugf("No username in request");
return;
}

HomeIdpAuthenticationFlowContext context = new HomeIdpAuthenticationFlowContext(authenticationFlowContext);

final List<IdentityProviderModel> homeIdps = context.discoverer(discovererConfig).discoverForUser(authenticationFlowContext, username);
if (homeIdps.isEmpty()) {
Expand Down Expand Up @@ -107,7 +119,7 @@ private String setUserInContext(AuthenticationFlowContext context, String userna
return username;
}

private String trimToNull(String username) {
private static String trimToNull(String username) {
if (username != null) {
username = username.trim();
if ("".equalsIgnoreCase(username))
Expand Down

0 comments on commit 2ea9799

Please sign in to comment.