parent
01a6319815
commit
31aefd1489
16 changed files with 175 additions and 61 deletions
|
@ -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);
|
||||||
|
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,4 +36,4 @@ org.keycloak.locale.LocaleSelectorSPI
|
||||||
org.keycloak.locale.LocaleUpdaterSPI
|
org.keycloak.locale.LocaleUpdaterSPI
|
||||||
org.keycloak.theme.ThemeResourceSpi
|
org.keycloak.theme.ThemeResourceSpi
|
||||||
org.keycloak.theme.ThemeSelectorSpi
|
org.keycloak.theme.ThemeSelectorSpi
|
||||||
org.keycloak.urls.HostnameSpi
|
org.keycloak.urls.HostnameSpi
|
||||||
|
|
|
@ -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() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
org.keycloak.authentication.otp.GoogleAuthenticatorProvider
|
||||||
|
org.keycloak.authentication.otp.FreeOTPProvider
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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?
|
||||||
|
|
Loading…
Reference in a new issue