OTP Application SPI (#14800)

Closes #14800
This commit is contained in:
Stian Thorgersen 2022-10-18 14:42:35 +02:00 committed by GitHub
parent 01a6319815
commit 31aefd1489
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 175 additions and 61 deletions

View file

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

View file

@ -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<OTPApplicationProvider> {
@Override
default void init(Config.Scope config) {
}
@Override
default void postInit(KeycloakSessionFactory factory) {
}
}

View file

@ -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<? extends Provider> getProviderClass() {
return OTPApplicationProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return OTPApplicationProviderFactory.class;
}
}

View file

@ -18,6 +18,7 @@
package org.keycloak.models.utils; package org.keycloak.models.utils;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.authentication.otp.OTPApplicationProvider;
import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.AuthorizationProviderFactory; import org.keycloak.authorization.AuthorizationProviderFactory;
import org.keycloak.authorization.model.PermissionTicket; import org.keycloak.authorization.model.PermissionTicket;
@ -456,7 +457,8 @@ public class ModelToRepresentation {
rep.setOtpPolicyInitialCounter(otpPolicy.getInitialCounter()); rep.setOtpPolicyInitialCounter(otpPolicy.getInitialCounter());
rep.setOtpPolicyType(otpPolicy.getType()); rep.setOtpPolicyType(otpPolicy.getType());
rep.setOtpPolicyLookAheadWindow(otpPolicy.getLookAheadWindow()); 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()); rep.setOtpPolicyCodeReusable(otpPolicy.isCodeReusable());
WebAuthnPolicy webAuthnPolicy = realm.getWebAuthnPolicy(); WebAuthnPolicy webAuthnPolicy = realm.getWebAuthnPolicy();

View file

@ -59,6 +59,7 @@ org.keycloak.authentication.ClientAuthenticatorSpi
org.keycloak.authentication.RequiredActionSpi org.keycloak.authentication.RequiredActionSpi
org.keycloak.authentication.FormAuthenticatorSpi org.keycloak.authentication.FormAuthenticatorSpi
org.keycloak.authentication.FormActionSpi org.keycloak.authentication.FormActionSpi
org.keycloak.authentication.otp.OTPApplicationSpi
org.keycloak.authorization.policy.provider.PolicySpi org.keycloak.authorization.policy.provider.PolicySpi
org.keycloak.authorization.store.StoreFactorySpi org.keycloak.authorization.store.StoreFactorySpi
org.keycloak.authorization.AuthorizationSpi org.keycloak.authorization.AuthorizationSpi

View file

@ -26,8 +26,6 @@ import java.io.Serializable;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
@ -48,8 +46,6 @@ public class OTPPolicy implements Serializable {
private static final Map<String, String> algToKeyUriAlg = new HashMap<>(); private static final Map<String, String> algToKeyUriAlg = new HashMap<>();
private static final OtpApp[] allApplications = new OtpApp[] { new FreeOTP(), new GoogleAuthenticator() };
static { static {
algToKeyUriAlg.put(HmacOTP.HMAC_SHA1, "SHA1"); algToKeyUriAlg.put(HmacOTP.HMAC_SHA1, "SHA1");
algToKeyUriAlg.put(HmacOTP.HMAC_SHA256, "SHA256"); algToKeyUriAlg.put(HmacOTP.HMAC_SHA256, "SHA256");
@ -180,55 +176,4 @@ public class OTPPolicy implements Serializable {
} }
} }
public List<String> getSupportedApplications() {
List<String> 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;
}
}
} }

View file

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

View file

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

View file

@ -17,6 +17,7 @@
package org.keycloak.forms.account.freemarker.model; package org.keycloak.forms.account.freemarker.model;
import org.keycloak.authentication.otp.OTPApplicationProvider;
import org.keycloak.credential.CredentialModel; import org.keycloak.credential.CredentialModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OTPPolicy; import org.keycloak.models.OTPPolicy;
@ -46,10 +47,13 @@ public class TotpBean {
private final String totpSecretEncoded; private final String totpSecretEncoded;
private final String totpSecretQrCode; private final String totpSecretQrCode;
private final boolean enabled; private final boolean enabled;
private KeycloakSession session;
private final UriBuilder uriBuilder; private final UriBuilder uriBuilder;
private final List<CredentialModel> otpCredentials; private final List<CredentialModel> otpCredentials;
private final List<String> supportedApplications;
public TotpBean(KeycloakSession session, RealmModel realm, UserModel user, UriBuilder uriBuilder) { public TotpBean(KeycloakSession session, RealmModel realm, UserModel user, UriBuilder uriBuilder) {
this.session = session;
this.uriBuilder = uriBuilder; this.uriBuilder = uriBuilder;
this.enabled = user.credentialManager().isConfiguredFor(OTPCredentialModel.TYPE); this.enabled = user.credentialManager().isConfiguredFor(OTPCredentialModel.TYPE);
if (enabled) { if (enabled) {
@ -70,6 +74,12 @@ public class TotpBean {
this.totpSecret = HmacOTP.generateSecret(20); this.totpSecret = HmacOTP.generateSecret(20);
this.totpSecretEncoded = TotpUtils.encode(totpSecret); this.totpSecretEncoded = TotpUtils.encode(totpSecret);
this.totpSecretQrCode = TotpUtils.qrCode(totpSecret, realm, user); 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() { public boolean isEnabled() {
@ -100,6 +110,10 @@ public class TotpBean {
return realm.getOTPPolicy(); return realm.getOTPPolicy();
} }
public List<String> getSupportedApplications() {
return supportedApplications;
}
public List<CredentialModel> getOtpCredentials() { public List<CredentialModel> getOtpCredentials() {
return otpCredentials; return otpCredentials;
} }

View file

@ -16,6 +16,7 @@
*/ */
package org.keycloak.forms.login.freemarker.model; package org.keycloak.forms.login.freemarker.model;
import org.keycloak.authentication.otp.OTPApplicationProvider;
import org.keycloak.credential.CredentialModel; import org.keycloak.credential.CredentialModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OTPPolicy; import org.keycloak.models.OTPPolicy;
@ -37,6 +38,7 @@ import java.util.stream.Collectors;
*/ */
public class TotpBean { public class TotpBean {
private KeycloakSession session;
private final RealmModel realm; private final RealmModel realm;
private final String totpSecret; private final String totpSecret;
private final String totpSecretEncoded; private final String totpSecretEncoded;
@ -44,8 +46,10 @@ public class TotpBean {
private final boolean enabled; private final boolean enabled;
private UriBuilder uriBuilder; private UriBuilder uriBuilder;
private final List<CredentialModel> otpCredentials; private final List<CredentialModel> otpCredentials;
private final List<String> supportedApplications;
public TotpBean(KeycloakSession session, RealmModel realm, UserModel user, UriBuilder uriBuilder) { public TotpBean(KeycloakSession session, RealmModel realm, UserModel user, UriBuilder uriBuilder) {
this.session = session;
this.realm = realm; this.realm = realm;
this.uriBuilder = uriBuilder; this.uriBuilder = uriBuilder;
this.enabled = user.credentialManager().isConfiguredFor(OTPCredentialModel.TYPE); this.enabled = user.credentialManager().isConfiguredFor(OTPCredentialModel.TYPE);
@ -58,6 +62,12 @@ public class TotpBean {
this.totpSecret = HmacOTP.generateSecret(20); this.totpSecret = HmacOTP.generateSecret(20);
this.totpSecretEncoded = TotpUtils.encode(totpSecret); this.totpSecretEncoded = TotpUtils.encode(totpSecret);
this.totpSecretQrCode = TotpUtils.qrCode(totpSecret, realm, user); 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() { public boolean isEnabled() {
@ -89,6 +99,10 @@ public class TotpBean {
return realm.getOTPPolicy(); return realm.getOTPPolicy();
} }
public List<String> getSupportedApplications() {
return supportedApplications;
}
public List<CredentialModel> getOtpCredentials() { public List<CredentialModel> getOtpCredentials() {
return otpCredentials; return otpCredentials;
} }

View file

@ -0,0 +1,2 @@
org.keycloak.authentication.otp.GoogleAuthenticatorProvider
org.keycloak.authentication.otp.FreeOTPProvider

View file

@ -163,6 +163,9 @@ totpInterval=Interval
totpCounter=Counter totpCounter=Counter
totpDeviceName=Device Name totpDeviceName=Device Name
totpAppFreeOTPName=FreeOTP
totpAppGoogleName=Google Authenticator
irreversibleAction=This action is irreversible irreversibleAction=This action is irreversible
deletingImplies=Deleting your account implies: deletingImplies=Deleting your account implies:
errasingData=Erasing all your data errasingData=Erasing all your data

View file

@ -56,8 +56,8 @@
<p>${msg("totpStep1")}</p> <p>${msg("totpStep1")}</p>
<ul> <ul>
<#list totp.policy.supportedApplications as app> <#list totp.supportedApplications as app>
<li>${app}</li> <li>${msg(app)}</li>
</#list> </#list>
</ul> </ul>
</li> </li>

View file

@ -9,8 +9,8 @@
<p>${msg("loginTotpStep1")}</p> <p>${msg("loginTotpStep1")}</p>
<ul id="kc-totp-supported-apps"> <ul id="kc-totp-supported-apps">
<#list totp.policy.supportedApplications as app> <#list totp.supportedApplications as app>
<li>${app}</li> <li>${msg(app)}</li>
</#list> </#list>
</ul> </ul>
</li> </li>

View file

@ -135,6 +135,9 @@ loginTotpDeviceName=Device Name
loginTotp.totp=Time-based loginTotp.totp=Time-based
loginTotp.hotp=Counter-based loginTotp.hotp=Counter-based
totpAppFreeOTPName=FreeOTP
totpAppGoogleName=Google Authenticator
loginChooseAuthenticator=Select login method loginChooseAuthenticator=Select login method
oauthGrantRequest=Do you grant these access privileges? oauthGrantRequest=Do you grant these access privileges?