diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/otp/OTPApplicationProvider.java b/server-spi-private/src/main/java/org/keycloak/authentication/otp/OTPApplicationProvider.java new file mode 100644 index 0000000000..950c2cc7d1 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/authentication/otp/OTPApplicationProvider.java @@ -0,0 +1,12 @@ +package org.keycloak.authentication.otp; + +import org.keycloak.models.OTPPolicy; +import org.keycloak.provider.Provider; + +public interface OTPApplicationProvider extends Provider { + + String getName(); + + boolean supports(OTPPolicy policy); + +} diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/otp/OTPApplicationProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/authentication/otp/OTPApplicationProviderFactory.java new file mode 100644 index 0000000000..203ffacceb --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/authentication/otp/OTPApplicationProviderFactory.java @@ -0,0 +1,17 @@ +package org.keycloak.authentication.otp; + +import org.keycloak.Config; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.provider.ProviderFactory; + +public interface OTPApplicationProviderFactory extends ProviderFactory { + + @Override + default void init(Config.Scope config) { + } + + @Override + default void postInit(KeycloakSessionFactory factory) { + } + +} diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/otp/OTPApplicationSpi.java b/server-spi-private/src/main/java/org/keycloak/authentication/otp/OTPApplicationSpi.java new file mode 100644 index 0000000000..8e481d9b4b --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/authentication/otp/OTPApplicationSpi.java @@ -0,0 +1,29 @@ +package org.keycloak.authentication.otp; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +public class OTPApplicationSpi implements Spi { + + @Override + public boolean isInternal() { + return true; + } + + @Override + public String getName() { + return "otp-application"; + } + + @Override + public Class getProviderClass() { + return OTPApplicationProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return OTPApplicationProviderFactory.class; + } + +} diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index b6b7058086..a189c415de 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -18,6 +18,7 @@ package org.keycloak.models.utils; import org.jboss.logging.Logger; +import org.keycloak.authentication.otp.OTPApplicationProvider; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.AuthorizationProviderFactory; import org.keycloak.authorization.model.PermissionTicket; @@ -456,7 +457,8 @@ public class ModelToRepresentation { rep.setOtpPolicyInitialCounter(otpPolicy.getInitialCounter()); rep.setOtpPolicyType(otpPolicy.getType()); rep.setOtpPolicyLookAheadWindow(otpPolicy.getLookAheadWindow()); - rep.setOtpSupportedApplications(otpPolicy.getSupportedApplications()); + + rep.setOtpSupportedApplications(session.getAllProviders(OTPApplicationProvider.class).stream().map(OTPApplicationProvider::getName).collect(Collectors.toList())); rep.setOtpPolicyCodeReusable(otpPolicy.isCodeReusable()); WebAuthnPolicy webAuthnPolicy = realm.getWebAuthnPolicy(); diff --git a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi index 6ddd89006b..e460f50021 100755 --- a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -59,6 +59,7 @@ org.keycloak.authentication.ClientAuthenticatorSpi org.keycloak.authentication.RequiredActionSpi org.keycloak.authentication.FormAuthenticatorSpi org.keycloak.authentication.FormActionSpi +org.keycloak.authentication.otp.OTPApplicationSpi org.keycloak.authorization.policy.provider.PolicySpi org.keycloak.authorization.store.StoreFactorySpi org.keycloak.authorization.AuthorizationSpi diff --git a/server-spi/src/main/java/org/keycloak/models/OTPPolicy.java b/server-spi/src/main/java/org/keycloak/models/OTPPolicy.java index f70f88e960..982fbe2b6b 100755 --- a/server-spi/src/main/java/org/keycloak/models/OTPPolicy.java +++ b/server-spi/src/main/java/org/keycloak/models/OTPPolicy.java @@ -26,8 +26,6 @@ import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; import java.util.Map; /** @@ -48,8 +46,6 @@ public class OTPPolicy implements Serializable { private static final Map algToKeyUriAlg = new HashMap<>(); - private static final OtpApp[] allApplications = new OtpApp[] { new FreeOTP(), new GoogleAuthenticator() }; - static { algToKeyUriAlg.put(HmacOTP.HMAC_SHA1, "SHA1"); algToKeyUriAlg.put(HmacOTP.HMAC_SHA256, "SHA256"); @@ -180,55 +176,4 @@ public class OTPPolicy implements Serializable { } } - public List getSupportedApplications() { - List applications = new LinkedList<>(); - for (OtpApp a : allApplications) { - if (a.supports(this)) { - applications.add(a.getName()); - } - } - return applications; - } - - public interface OtpApp { - - String getName(); - - boolean supports(OTPPolicy policy); - } - - public static class GoogleAuthenticator implements OtpApp { - - @Override - public String getName() { - return "Google Authenticator"; - } - - @Override - public boolean supports(OTPPolicy policy) { - if (policy.digits != 6) { - return false; - } - - if (!policy.getAlgorithm().equals("HmacSHA1")) { - return false; - } - - return policy.getType().equals("totp") && policy.getPeriod() == 30; - } - } - - public static class FreeOTP implements OtpApp { - - @Override - public String getName() { - return "FreeOTP"; - } - - @Override - public boolean supports(OTPPolicy policy) { - return true; - } - } - } diff --git a/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi index ed23b179f3..36e4e70529 100755 --- a/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -36,4 +36,4 @@ org.keycloak.locale.LocaleSelectorSPI org.keycloak.locale.LocaleUpdaterSPI org.keycloak.theme.ThemeResourceSpi org.keycloak.theme.ThemeSelectorSpi -org.keycloak.urls.HostnameSpi \ No newline at end of file +org.keycloak.urls.HostnameSpi diff --git a/services/src/main/java/org/keycloak/authentication/otp/FreeOTPProvider.java b/services/src/main/java/org/keycloak/authentication/otp/FreeOTPProvider.java new file mode 100644 index 0000000000..a48a39fc1c --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/otp/FreeOTPProvider.java @@ -0,0 +1,32 @@ +package org.keycloak.authentication.otp; + +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.OTPPolicy; + +public class FreeOTPProvider implements OTPApplicationProviderFactory, OTPApplicationProvider { + + @Override + public OTPApplicationProvider create(KeycloakSession session) { + return this; + } + + @Override + public String getId() { + return "freeotp"; + } + + @Override + public String getName() { + return "totpAppFreeOTPName"; + } + + @Override + public boolean supports(OTPPolicy policy) { + return true; + } + + @Override + public void close() { + } + +} diff --git a/services/src/main/java/org/keycloak/authentication/otp/GoogleAuthenticatorProvider.java b/services/src/main/java/org/keycloak/authentication/otp/GoogleAuthenticatorProvider.java new file mode 100644 index 0000000000..302b733d00 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/otp/GoogleAuthenticatorProvider.java @@ -0,0 +1,40 @@ +package org.keycloak.authentication.otp; + +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.OTPPolicy; + +public class GoogleAuthenticatorProvider implements OTPApplicationProviderFactory, OTPApplicationProvider { + + @Override + public OTPApplicationProvider create(KeycloakSession session) { + return this; + } + + @Override + public String getId() { + return "google"; + } + + @Override + public String getName() { + return "totpAppGoogleName"; + } + + @Override + public boolean supports(OTPPolicy policy) { + if (policy.getDigits() != 6) { + return false; + } + + if (!policy.getAlgorithm().equals("HmacSHA1")) { + return false; + } + + return policy.getType().equals("totp") && policy.getPeriod() == 30; + } + + @Override + public void close() { + } + +} diff --git a/services/src/main/java/org/keycloak/forms/account/freemarker/model/TotpBean.java b/services/src/main/java/org/keycloak/forms/account/freemarker/model/TotpBean.java index 430c1bd601..096c73110f 100644 --- a/services/src/main/java/org/keycloak/forms/account/freemarker/model/TotpBean.java +++ b/services/src/main/java/org/keycloak/forms/account/freemarker/model/TotpBean.java @@ -17,6 +17,7 @@ package org.keycloak.forms.account.freemarker.model; +import org.keycloak.authentication.otp.OTPApplicationProvider; import org.keycloak.credential.CredentialModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.OTPPolicy; @@ -46,10 +47,13 @@ public class TotpBean { private final String totpSecretEncoded; private final String totpSecretQrCode; private final boolean enabled; + private KeycloakSession session; private final UriBuilder uriBuilder; private final List otpCredentials; + private final List supportedApplications; public TotpBean(KeycloakSession session, RealmModel realm, UserModel user, UriBuilder uriBuilder) { + this.session = session; this.uriBuilder = uriBuilder; this.enabled = user.credentialManager().isConfiguredFor(OTPCredentialModel.TYPE); if (enabled) { @@ -70,6 +74,12 @@ public class TotpBean { this.totpSecret = HmacOTP.generateSecret(20); this.totpSecretEncoded = TotpUtils.encode(totpSecret); this.totpSecretQrCode = TotpUtils.qrCode(totpSecret, realm, user); + + OTPPolicy otpPolicy = realm.getOTPPolicy(); + this.supportedApplications = session.getAllProviders(OTPApplicationProvider.class).stream() + .filter(p -> p.supports(otpPolicy)) + .map(OTPApplicationProvider::getName) + .collect(Collectors.toList()); } public boolean isEnabled() { @@ -100,6 +110,10 @@ public class TotpBean { return realm.getOTPPolicy(); } + public List getSupportedApplications() { + return supportedApplications; + } + public List getOtpCredentials() { return otpCredentials; } diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/model/TotpBean.java b/services/src/main/java/org/keycloak/forms/login/freemarker/model/TotpBean.java index 7875268542..77bac99f82 100755 --- a/services/src/main/java/org/keycloak/forms/login/freemarker/model/TotpBean.java +++ b/services/src/main/java/org/keycloak/forms/login/freemarker/model/TotpBean.java @@ -16,6 +16,7 @@ */ package org.keycloak.forms.login.freemarker.model; +import org.keycloak.authentication.otp.OTPApplicationProvider; import org.keycloak.credential.CredentialModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.OTPPolicy; @@ -37,6 +38,7 @@ import java.util.stream.Collectors; */ public class TotpBean { + private KeycloakSession session; private final RealmModel realm; private final String totpSecret; private final String totpSecretEncoded; @@ -44,8 +46,10 @@ public class TotpBean { private final boolean enabled; private UriBuilder uriBuilder; private final List otpCredentials; + private final List supportedApplications; public TotpBean(KeycloakSession session, RealmModel realm, UserModel user, UriBuilder uriBuilder) { + this.session = session; this.realm = realm; this.uriBuilder = uriBuilder; this.enabled = user.credentialManager().isConfiguredFor(OTPCredentialModel.TYPE); @@ -58,6 +62,12 @@ public class TotpBean { this.totpSecret = HmacOTP.generateSecret(20); this.totpSecretEncoded = TotpUtils.encode(totpSecret); this.totpSecretQrCode = TotpUtils.qrCode(totpSecret, realm, user); + + OTPPolicy otpPolicy = realm.getOTPPolicy(); + this.supportedApplications = session.getAllProviders(OTPApplicationProvider.class).stream() + .filter(p -> p.supports(otpPolicy)) + .map(OTPApplicationProvider::getName) + .collect(Collectors.toList()); } public boolean isEnabled() { @@ -89,6 +99,10 @@ public class TotpBean { return realm.getOTPPolicy(); } + public List getSupportedApplications() { + return supportedApplications; + } + public List getOtpCredentials() { return otpCredentials; } diff --git a/services/src/main/resources/META-INF/services/org.keycloak.authentication.otp.OTPApplicationProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.authentication.otp.OTPApplicationProviderFactory new file mode 100644 index 0000000000..7a4a57b90c --- /dev/null +++ b/services/src/main/resources/META-INF/services/org.keycloak.authentication.otp.OTPApplicationProviderFactory @@ -0,0 +1,2 @@ +org.keycloak.authentication.otp.GoogleAuthenticatorProvider +org.keycloak.authentication.otp.FreeOTPProvider \ No newline at end of file diff --git a/themes/src/main/resources/theme/base/account/messages/messages_en.properties b/themes/src/main/resources/theme/base/account/messages/messages_en.properties index 5da255fb93..5fbcc8d3a6 100755 --- a/themes/src/main/resources/theme/base/account/messages/messages_en.properties +++ b/themes/src/main/resources/theme/base/account/messages/messages_en.properties @@ -163,6 +163,9 @@ totpInterval=Interval totpCounter=Counter totpDeviceName=Device Name +totpAppFreeOTPName=FreeOTP +totpAppGoogleName=Google Authenticator + irreversibleAction=This action is irreversible deletingImplies=Deleting your account implies: errasingData=Erasing all your data diff --git a/themes/src/main/resources/theme/base/account/totp.ftl b/themes/src/main/resources/theme/base/account/totp.ftl index 987fe2473b..d7a02e6765 100755 --- a/themes/src/main/resources/theme/base/account/totp.ftl +++ b/themes/src/main/resources/theme/base/account/totp.ftl @@ -56,8 +56,8 @@

${msg("totpStep1")}

    - <#list totp.policy.supportedApplications as app> -
  • ${app}
  • + <#list totp.supportedApplications as app> +
  • ${msg(app)}
diff --git a/themes/src/main/resources/theme/base/login/login-config-totp.ftl b/themes/src/main/resources/theme/base/login/login-config-totp.ftl index c82948d038..80145856d8 100755 --- a/themes/src/main/resources/theme/base/login/login-config-totp.ftl +++ b/themes/src/main/resources/theme/base/login/login-config-totp.ftl @@ -9,8 +9,8 @@

${msg("loginTotpStep1")}

    - <#list totp.policy.supportedApplications as app> -
  • ${app}
  • + <#list totp.supportedApplications as app> +
  • ${msg(app)}
diff --git a/themes/src/main/resources/theme/base/login/messages/messages_en.properties b/themes/src/main/resources/theme/base/login/messages/messages_en.properties index a003cc501b..7a1d70a2a6 100755 --- a/themes/src/main/resources/theme/base/login/messages/messages_en.properties +++ b/themes/src/main/resources/theme/base/login/messages/messages_en.properties @@ -135,6 +135,9 @@ loginTotpDeviceName=Device Name loginTotp.totp=Time-based loginTotp.hotp=Counter-based +totpAppFreeOTPName=FreeOTP +totpAppGoogleName=Google Authenticator + loginChooseAuthenticator=Select login method oauthGrantRequest=Do you grant these access privileges?