hotp
This commit is contained in:
parent
eff0054b4a
commit
07efba364e
53 changed files with 1113 additions and 231 deletions
|
@ -10,5 +10,34 @@
|
||||||
<delete tableName="USER_SESSION"/>
|
<delete tableName="USER_SESSION"/>
|
||||||
|
|
||||||
<dropColumn tableName="AUTHENTICATION_EXECUTION" columnName="USER_SETUP_ALLOWED"/>
|
<dropColumn tableName="AUTHENTICATION_EXECUTION" columnName="USER_SETUP_ALLOWED"/>
|
||||||
|
<addColumn tableName="CREDENTIAL">
|
||||||
|
<column name="COUNTER" type="INT" defaultValueNumeric="0">
|
||||||
|
<constraints nullable="true"/>
|
||||||
|
</column>
|
||||||
|
<column name="DIGITS" type="INT" defaultValueNumeric="6">
|
||||||
|
<constraints nullable="true"/>
|
||||||
|
</column>
|
||||||
|
<column name="ALGORITHM" type="VARCHAR(36)" defaultValue="HmacSHA1">
|
||||||
|
<constraints nullable="true"/>
|
||||||
|
</column>
|
||||||
|
</addColumn>
|
||||||
|
<addColumn tableName="REALM">
|
||||||
|
<column name="OTP_POLICY_COUNTER" type="INT" defaultValueNumeric="0">
|
||||||
|
<constraints nullable="true"/>
|
||||||
|
</column>
|
||||||
|
<column name="OTP_POLICY_WINDOW" type="INT" defaultValueNumeric="1">
|
||||||
|
<constraints nullable="true"/>
|
||||||
|
</column>
|
||||||
|
<column name="OTP_POLICY_DIGITS" type="INT" defaultValueNumeric="6">
|
||||||
|
<constraints nullable="true"/>
|
||||||
|
</column>
|
||||||
|
<column name="OTP_POLICY_ALG" type="VARCHAR(36)" defaultValue="HmacSHA1">
|
||||||
|
<constraints nullable="true"/>
|
||||||
|
</column>
|
||||||
|
<column name="OTP_POLICY_TYPE" type="VARCHAR(36)" defaultValue="totp">
|
||||||
|
<constraints nullable="true"/>
|
||||||
|
</column>
|
||||||
|
</addColumn>
|
||||||
|
|
||||||
</changeSet>
|
</changeSet>
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|
|
@ -9,6 +9,7 @@ public class CredentialRepresentation {
|
||||||
public static final String PASSWORD = "password";
|
public static final String PASSWORD = "password";
|
||||||
public static final String PASSWORD_TOKEN = "password-token";
|
public static final String PASSWORD_TOKEN = "password-token";
|
||||||
public static final String TOTP = "totp";
|
public static final String TOTP = "totp";
|
||||||
|
public static final String HOTP = "hotp";
|
||||||
public static final String CLIENT_CERT = "cert";
|
public static final String CLIENT_CERT = "cert";
|
||||||
public static final String KERBEROS = "kerberos";
|
public static final String KERBEROS = "kerberos";
|
||||||
|
|
||||||
|
@ -22,6 +23,10 @@ public class CredentialRepresentation {
|
||||||
protected String hashedSaltedValue;
|
protected String hashedSaltedValue;
|
||||||
protected String salt;
|
protected String salt;
|
||||||
protected Integer hashIterations;
|
protected Integer hashIterations;
|
||||||
|
protected Integer counter;
|
||||||
|
private String algorithm;
|
||||||
|
private Integer digits;
|
||||||
|
|
||||||
// only used when updating a credential. Might set required action
|
// only used when updating a credential. Might set required action
|
||||||
protected boolean temporary;
|
protected boolean temporary;
|
||||||
|
|
||||||
|
@ -80,4 +85,28 @@ public class CredentialRepresentation {
|
||||||
public void setTemporary(boolean temporary) {
|
public void setTemporary(boolean temporary) {
|
||||||
this.temporary = temporary;
|
this.temporary = temporary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Integer getCounter() {
|
||||||
|
return counter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCounter(Integer counter) {
|
||||||
|
this.counter = counter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAlgorithm() {
|
||||||
|
return algorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAlgorithm(String algorithm) {
|
||||||
|
this.algorithm = algorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getDigits() {
|
||||||
|
return digits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDigits(Integer digits) {
|
||||||
|
this.digits = digits;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,6 +49,12 @@ public class RealmRepresentation {
|
||||||
@Deprecated
|
@Deprecated
|
||||||
protected Set<String> requiredCredentials;
|
protected Set<String> requiredCredentials;
|
||||||
protected String passwordPolicy;
|
protected String passwordPolicy;
|
||||||
|
protected String otpPolicyType;
|
||||||
|
protected String otpPolicyAlgorithm;
|
||||||
|
protected Integer otpPolicyInitialCounter;
|
||||||
|
protected Integer otpPolicyDigits;
|
||||||
|
protected Integer otpPolicyLookAheadWindow;
|
||||||
|
|
||||||
protected List<UserRepresentation> users;
|
protected List<UserRepresentation> users;
|
||||||
protected List<ScopeMappingRepresentation> scopeMappings;
|
protected List<ScopeMappingRepresentation> scopeMappings;
|
||||||
protected Map<String, List<ScopeMappingRepresentation>> clientScopeMappings;
|
protected Map<String, List<ScopeMappingRepresentation>> clientScopeMappings;
|
||||||
|
@ -653,4 +659,44 @@ public class RealmRepresentation {
|
||||||
public void setRequiredActions(List<RequiredActionProviderRepresentation> requiredActions) {
|
public void setRequiredActions(List<RequiredActionProviderRepresentation> requiredActions) {
|
||||||
this.requiredActions = requiredActions;
|
this.requiredActions = requiredActions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getOtpPolicyType() {
|
||||||
|
return otpPolicyType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOtpPolicyType(String otpPolicyType) {
|
||||||
|
this.otpPolicyType = otpPolicyType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOtpPolicyAlgorithm() {
|
||||||
|
return otpPolicyAlgorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOtpPolicyAlgorithm(String otpPolicyAlgorithm) {
|
||||||
|
this.otpPolicyAlgorithm = otpPolicyAlgorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getOtpPolicyInitialCounter() {
|
||||||
|
return otpPolicyInitialCounter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOtpPolicyInitialCounter(Integer otpPolicyInitialCounter) {
|
||||||
|
this.otpPolicyInitialCounter = otpPolicyInitialCounter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getOtpPolicyDigits() {
|
||||||
|
return otpPolicyDigits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOtpPolicyDigits(Integer otpPolicyDigits) {
|
||||||
|
this.otpPolicyDigits = otpPolicyDigits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getOtpPolicyLookAheadWindow() {
|
||||||
|
return otpPolicyLookAheadWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOtpPolicyLookAheadWindow(Integer otpPolicyLookAheadWindow) {
|
||||||
|
this.otpPolicyLookAheadWindow = otpPolicyLookAheadWindow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -150,15 +150,6 @@ public class UserRepresentation {
|
||||||
this.credentials = credentials;
|
this.credentials = credentials;
|
||||||
}
|
}
|
||||||
|
|
||||||
public UserRepresentation credential(String type, String value) {
|
|
||||||
if (this.credentials == null) credentials = new ArrayList<CredentialRepresentation>();
|
|
||||||
CredentialRepresentation cred = new CredentialRepresentation();
|
|
||||||
cred.setType(type);
|
|
||||||
cred.setValue(value);
|
|
||||||
credentials.add(cred);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> getRequiredActions() {
|
public List<String> getRequiredActions() {
|
||||||
return requiredActions;
|
return requiredActions;
|
||||||
}
|
}
|
||||||
|
|
|
@ -313,6 +313,9 @@ public class ExportUtils {
|
||||||
credRep.setHashedSaltedValue(userCred.getValue());
|
credRep.setHashedSaltedValue(userCred.getValue());
|
||||||
if (userCred.getSalt() != null) credRep.setSalt(Base64.encodeBytes(userCred.getSalt()));
|
if (userCred.getSalt() != null) credRep.setSalt(Base64.encodeBytes(userCred.getSalt()));
|
||||||
credRep.setHashIterations(userCred.getHashIterations());
|
credRep.setHashIterations(userCred.getHashIterations());
|
||||||
|
credRep.setCounter(userCred.getCounter());
|
||||||
|
credRep.setAlgorithm(userCred.getAlgorithm());
|
||||||
|
credRep.setDigits(userCred.getDigits());
|
||||||
return credRep;
|
return credRep;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -174,7 +174,7 @@ public class FreeMarkerAccountProvider implements AccountProvider {
|
||||||
|
|
||||||
switch (page) {
|
switch (page) {
|
||||||
case TOTP:
|
case TOTP:
|
||||||
attributes.put("totp", new TotpBean(realm, user, baseUri));
|
attributes.put("totp", new TotpBean(session, realm, user, baseUri));
|
||||||
break;
|
break;
|
||||||
case FEDERATED_IDENTITY:
|
case FEDERATED_IDENTITY:
|
||||||
attributes.put("federatedIdentity", new AccountFederatedIdentityBean(session, realm, user, uriInfo.getBaseUri(), stateChecker));
|
attributes.put("federatedIdentity", new AccountFederatedIdentityBean(session, realm, user, uriInfo.getBaseUri(), stateChecker));
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.account.freemarker.model;
|
package org.keycloak.account.freemarker.model;
|
||||||
|
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.utils.Base32;
|
import org.keycloak.models.utils.Base32;
|
||||||
|
@ -41,14 +42,16 @@ public class TotpBean {
|
||||||
private final boolean enabled;
|
private final boolean enabled;
|
||||||
private final String contextUrl;
|
private final String contextUrl;
|
||||||
private final String realmName;
|
private final String realmName;
|
||||||
|
private final String keyUri;
|
||||||
|
|
||||||
public TotpBean(RealmModel realm, UserModel user, URI baseUri) {
|
public TotpBean(KeycloakSession session, RealmModel realm, UserModel user, URI baseUri) {
|
||||||
this.realmName = realm.getName();
|
this.realmName = realm.getName();
|
||||||
this.enabled = user.isTotp();
|
this.enabled = session.users().configuredForCredentialType(realm.getOTPPolicy().getType(), realm, user);
|
||||||
this.contextUrl = baseUri.getPath();
|
this.contextUrl = baseUri.getPath();
|
||||||
|
|
||||||
this.totpSecret = randomString(20);
|
this.totpSecret = randomString(20);
|
||||||
this.totpSecretEncoded = Base32.encode(totpSecret.getBytes());
|
this.totpSecretEncoded = Base32.encode(totpSecret.getBytes());
|
||||||
|
this.keyUri = realm.getOTPPolicy().getKeyURI(realm, this.totpSecret);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String randomString(int length) {
|
private static String randomString(int length) {
|
||||||
|
@ -89,7 +92,7 @@ public class TotpBean {
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getTotpSecretQrCodeUrl() throws UnsupportedEncodingException {
|
public String getTotpSecretQrCodeUrl() throws UnsupportedEncodingException {
|
||||||
String contents = URLEncoder.encode("otpauth://totp/" + realmName + "?secret=" + totpSecretEncoded, "utf-8");
|
String contents = URLEncoder.encode(keyUri, "utf-8");
|
||||||
return contextUrl + "qrcode" + "?size=246x246&contents=" + contents;
|
return contextUrl + "qrcode" + "?size=246x246&contents=" + contents;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1162,6 +1162,18 @@ module.config([ '$routeProvider', function($routeProvider) {
|
||||||
},
|
},
|
||||||
controller : 'RealmPasswordPolicyCtrl'
|
controller : 'RealmPasswordPolicyCtrl'
|
||||||
})
|
})
|
||||||
|
.when('/realms/:realm/authentication/otp-policy', {
|
||||||
|
templateUrl : resourceUrl + '/partials/otp-policy.html',
|
||||||
|
resolve : {
|
||||||
|
realm : function(RealmLoader) {
|
||||||
|
return RealmLoader();
|
||||||
|
},
|
||||||
|
serverInfo : function(ServerInfo) {
|
||||||
|
return ServerInfo.delay;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
controller : 'RealmOtpPolicyCtrl'
|
||||||
|
})
|
||||||
.when('/realms/:realm/authentication/config/:provider/:config', {
|
.when('/realms/:realm/authentication/config/:provider/:config', {
|
||||||
templateUrl : resourceUrl + '/partials/authenticator-config.html',
|
templateUrl : resourceUrl + '/partials/authenticator-config.html',
|
||||||
resolve : {
|
resolve : {
|
||||||
|
|
|
@ -372,6 +372,10 @@ module.controller('RealmLoginSettingsCtrl', function($scope, Current, Realm, rea
|
||||||
genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications, "/realms/" + realm.realm + "/login-settings");
|
genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications, "/realms/" + realm.realm + "/login-settings");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
module.controller('RealmOtpPolicyCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications) {
|
||||||
|
genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications, "/realms/" + realm.realm + "/authentication/otp-policy");
|
||||||
|
});
|
||||||
|
|
||||||
module.controller('RealmThemeCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications) {
|
module.controller('RealmThemeCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications) {
|
||||||
genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications, "/realms/" + realm.realm + "/theme-settings");
|
genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications, "/realms/" + realm.realm + "/theme-settings");
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
|
||||||
|
<h1>Authentication</h1>
|
||||||
|
|
||||||
|
<kc-tabs-authentication></kc-tabs-authentication>
|
||||||
|
|
||||||
|
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="type" class="col-md-2 control-label">OTP Type</label>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<div>
|
||||||
|
<select id="type" ng-model="realm.otpPolicyType" class="form-control">
|
||||||
|
<option value="totp">Time Based</option>
|
||||||
|
<option value="hotp">Counter Based</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<kc-tooltip>totp is Time-Based One Time Password. 'hotp' is a counter base one time password in which the server keeps a counter to hash against.</kc-tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="alg" class="col-md-2 control-label">OTP Hash Algorithm</label>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<div>
|
||||||
|
<select id="alg" ng-model="realm.otpPolicyAlgorithm" class="form-control">
|
||||||
|
<option value="HmacSHA1">SHA1</option>
|
||||||
|
<option value="HmacSHA256">SHA256</option>
|
||||||
|
<option value="HmacSHA512">SHA512</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<kc-tooltip>What hashing algorithm should be used to generate the OTP.</kc-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-2 control-label" for="digits">Number of Digits</label>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<div>
|
||||||
|
<select id="digits" ng-model="realm.otpPolicyDigits" class="form-control">
|
||||||
|
<option value="6">6</option>
|
||||||
|
<option value="8">8</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<kc-tooltip>How many digits should the OTP have?</kc-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-2 control-label" for="lookAhead">Look ahead window</label>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<input class="form-control" type="text" id="lookAhead" name="lookAhead" data-ng-model="realm.otpPolicyLookAheadWindow" autofocus>
|
||||||
|
</div>
|
||||||
|
<kc-tooltip>How far ahead should the server look just in case the token generator and server are out of time sync or counter sync?</kc-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" data-ng-show="realm.otpPolicyType == 'hotp'">
|
||||||
|
<label class="col-md-2 control-label" for="counter">Initial Counter</label>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<input class="form-control" type="text" id="counter" name="counter" data-ng-model="realm.otpPolicyInitialCounter" autofocus>
|
||||||
|
</div>
|
||||||
|
<kc-tooltip>What should the initial counter value be?</kc-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" data-ng-show="access.manageRealm">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<button kc-save data-ng-disabled="!changed">Save</button>
|
||||||
|
<button kc-reset data-ng-disabled="!changed">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<kc-menu></kc-menu>
|
|
@ -2,4 +2,5 @@
|
||||||
<li ng-class="{active: path[3] == 'flows'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/authentication/flows">Flows</a></li>
|
<li ng-class="{active: path[3] == 'flows'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/authentication/flows">Flows</a></li>
|
||||||
<li ng-class="{active: path[3] == 'required-actions'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/authentication/required-actions">Required Actions</a></li>
|
<li ng-class="{active: path[3] == 'required-actions'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/authentication/required-actions">Required Actions</a></li>
|
||||||
<li ng-class="{active: path[3] == 'password-policy'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/authentication/password-policy">Password Policy</a></li>
|
<li ng-class="{active: path[3] == 'password-policy'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/authentication/password-policy">Password Policy</a></li>
|
||||||
|
<li ng-class="{active: path[3] == 'otp-policy'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/authentication/otp-policy">OTP Policy</a></li>
|
||||||
</ul>
|
</ul>
|
|
@ -24,11 +24,11 @@ package org.keycloak.login.freemarker.model;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.utils.Base32;
|
import org.keycloak.models.utils.Base32;
|
||||||
|
import org.keycloak.models.utils.HmacOTP;
|
||||||
|
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
@ -40,25 +40,16 @@ public class TotpBean {
|
||||||
private final boolean enabled;
|
private final boolean enabled;
|
||||||
private final String contextUrl;
|
private final String contextUrl;
|
||||||
private final String realmName;
|
private final String realmName;
|
||||||
|
private final String keyUri;
|
||||||
|
|
||||||
public TotpBean(RealmModel realm, UserModel user, URI baseUri) {
|
public TotpBean(RealmModel realm, UserModel user, URI baseUri) {
|
||||||
this.realmName = realm.getName();
|
this.realmName = realm.getName();
|
||||||
this.enabled = user.isTotp();
|
this.enabled = user.isOtpEnabled();
|
||||||
this.contextUrl = baseUri.getPath();
|
this.contextUrl = baseUri.getPath();
|
||||||
|
|
||||||
this.totpSecret = randomString(20);
|
this.totpSecret = HmacOTP.generateSecret(20);
|
||||||
this.totpSecretEncoded = Base32.encode(totpSecret.getBytes());
|
this.totpSecretEncoded = Base32.encode(totpSecret.getBytes());
|
||||||
}
|
this.keyUri = realm.getOTPPolicy().getKeyURI(realm, this.totpSecret);
|
||||||
|
|
||||||
private static String randomString(int length) {
|
|
||||||
String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVW1234567890";
|
|
||||||
Random r = new Random();
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
for (int i = 0; i < length; i++) {
|
|
||||||
char c = chars.charAt(r.nextInt(chars.length()));
|
|
||||||
sb.append(c);
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
|
@ -81,7 +72,7 @@ public class TotpBean {
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getTotpSecretQrCodeUrl() throws UnsupportedEncodingException {
|
public String getTotpSecretQrCodeUrl() throws UnsupportedEncodingException {
|
||||||
String contents = URLEncoder.encode("otpauth://totp/" + realmName + "?secret=" + totpSecretEncoded, "utf-8");
|
String contents = URLEncoder.encode(keyUri, "utf-8");
|
||||||
return contextUrl + "qrcode" + "?size=246x246&contents=" + contents;
|
return contextUrl + "qrcode" + "?size=246x246&contents=" + contents;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ public interface MigrationModel {
|
||||||
/**
|
/**
|
||||||
* Must have the form of major.minor.micro as the version is parsed and numbers are compared
|
* Must have the form of major.minor.micro as the version is parsed and numbers are compared
|
||||||
*/
|
*/
|
||||||
public static final String LATEST_VERSION = "1.4.0";
|
public static final String LATEST_VERSION = "1.5.0";
|
||||||
|
|
||||||
String getStoredVersion();
|
String getStoredVersion();
|
||||||
void setStoredVersion(String version);
|
void setStoredVersion(String version);
|
||||||
|
|
|
@ -3,6 +3,7 @@ package org.keycloak.migration;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.migration.migrators.MigrateTo1_3_0;
|
import org.keycloak.migration.migrators.MigrateTo1_3_0;
|
||||||
import org.keycloak.migration.migrators.MigrateTo1_4_0;
|
import org.keycloak.migration.migrators.MigrateTo1_4_0;
|
||||||
|
import org.keycloak.migration.migrators.MigrateTo1_5_0;
|
||||||
import org.keycloak.migration.migrators.MigrationTo1_2_0_CR1;
|
import org.keycloak.migration.migrators.MigrationTo1_2_0_CR1;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
|
||||||
|
@ -40,6 +41,12 @@ public class MigrationModelManager {
|
||||||
}
|
}
|
||||||
new MigrateTo1_4_0().migrate(session);
|
new MigrateTo1_4_0().migrate(session);
|
||||||
}
|
}
|
||||||
|
if (stored == null || stored.lessThan(MigrateTo1_5_0.VERSION)) {
|
||||||
|
if (stored != null) {
|
||||||
|
logger.debug("Migrating older model to 1.5.0 updates");
|
||||||
|
}
|
||||||
|
new MigrateTo1_4_0().migrate(session);
|
||||||
|
}
|
||||||
|
|
||||||
model.setStoredVersion(MigrationModel.LATEST_VERSION);
|
model.setStoredVersion(MigrationModel.LATEST_VERSION);
|
||||||
}
|
}
|
||||||
|
|
31
model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_5_0.java
Executable file
31
model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_5_0.java
Executable file
|
@ -0,0 +1,31 @@
|
||||||
|
package org.keycloak.migration.migrators;
|
||||||
|
|
||||||
|
import org.keycloak.migration.ModelVersion;
|
||||||
|
import org.keycloak.models.ImpersonationConstants;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.OTPPolicy;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserFederationMapperModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.models.utils.DefaultAuthenticationFlows;
|
||||||
|
import org.keycloak.models.utils.DefaultRequiredActions;
|
||||||
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
public class MigrateTo1_5_0 {
|
||||||
|
public static final ModelVersion VERSION = new ModelVersion("1.5.0");
|
||||||
|
|
||||||
|
public void migrate(KeycloakSession session) {
|
||||||
|
List<RealmModel> realms = session.realms().getRealms();
|
||||||
|
for (RealmModel realm : realms) {
|
||||||
|
realm.setOTPPolicy(OTPPolicy.DEFAULT_POLICY);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
92
model/api/src/main/java/org/keycloak/models/OTPPolicy.java
Executable file
92
model/api/src/main/java/org/keycloak/models/OTPPolicy.java
Executable file
|
@ -0,0 +1,92 @@
|
||||||
|
package org.keycloak.models;
|
||||||
|
|
||||||
|
import org.keycloak.models.utils.Base32;
|
||||||
|
import org.keycloak.models.utils.HmacOTP;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
public class OTPPolicy {
|
||||||
|
|
||||||
|
|
||||||
|
protected String type;
|
||||||
|
protected String algorithm;
|
||||||
|
protected int initialCounter;
|
||||||
|
protected int digits;
|
||||||
|
protected int lookAheadWindow;
|
||||||
|
|
||||||
|
private static final Map<String, String> algToKeyUriAlg = new HashMap<>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
algToKeyUriAlg.put(HmacOTP.HMAC_SHA1, "SHA1");
|
||||||
|
algToKeyUriAlg.put(HmacOTP.HMAC_SHA256, "SHA256");
|
||||||
|
algToKeyUriAlg.put(HmacOTP.HMAC_SHA512, "SHA512");
|
||||||
|
}
|
||||||
|
|
||||||
|
public OTPPolicy() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public OTPPolicy(String type, String algorithm, int initialCounter, int digits, int lookAheadWindow) {
|
||||||
|
this.type = type;
|
||||||
|
this.algorithm = algorithm;
|
||||||
|
this.initialCounter = initialCounter;
|
||||||
|
this.digits = digits;
|
||||||
|
this.lookAheadWindow = lookAheadWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static OTPPolicy DEFAULT_POLICY = new OTPPolicy(UserCredentialModel.TOTP, HmacOTP.HMAC_SHA1, 0, 6, 1);
|
||||||
|
|
||||||
|
public String getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setType(String type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAlgorithm() {
|
||||||
|
return algorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAlgorithm(String algorithm) {
|
||||||
|
this.algorithm = algorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getInitialCounter() {
|
||||||
|
return initialCounter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInitialCounter(int initialCounter) {
|
||||||
|
this.initialCounter = initialCounter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDigits() {
|
||||||
|
return digits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDigits(int digits) {
|
||||||
|
this.digits = digits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getLookAheadWindow() {
|
||||||
|
return lookAheadWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLookAheadWindow(int lookAheadWindow) {
|
||||||
|
this.lookAheadWindow = lookAheadWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getKeyURI(RealmModel realm, String secret) {
|
||||||
|
|
||||||
|
String uri = "otpauth://" + type + "/" + realm.getName() + "?secret=" + Base32.encode(secret.getBytes()) + "&digits=" + digits + "&algorithm=" + algToKeyUriAlg.get(algorithm);
|
||||||
|
if (type.equals(UserCredentialModel.HOTP)) {
|
||||||
|
uri += "&counter=" + initialCounter;
|
||||||
|
}
|
||||||
|
return uri;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -148,6 +148,9 @@ public interface RealmModel extends RoleContainerModel {
|
||||||
|
|
||||||
void setPasswordPolicy(PasswordPolicy policy);
|
void setPasswordPolicy(PasswordPolicy policy);
|
||||||
|
|
||||||
|
OTPPolicy getOTPPolicy();
|
||||||
|
void setOTPPolicy(OTPPolicy policy);
|
||||||
|
|
||||||
RoleModel getRoleById(String id);
|
RoleModel getRoleById(String id);
|
||||||
|
|
||||||
List<String> getDefaultRoles();
|
List<String> getDefaultRoles();
|
||||||
|
|
|
@ -14,6 +14,7 @@ public class UserCredentialModel {
|
||||||
// Secret is same as password but it is not hashed
|
// Secret is same as password but it is not hashed
|
||||||
public static final String SECRET = "secret";
|
public static final String SECRET = "secret";
|
||||||
public static final String TOTP = "totp";
|
public static final String TOTP = "totp";
|
||||||
|
public static final String HOTP = "hotp";
|
||||||
public static final String CLIENT_CERT = "cert";
|
public static final String CLIENT_CERT = "cert";
|
||||||
public static final String KERBEROS = "kerberos";
|
public static final String KERBEROS = "kerberos";
|
||||||
|
|
||||||
|
@ -44,6 +45,12 @@ public class UserCredentialModel {
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static UserCredentialModel otp(String type, String key) {
|
||||||
|
if (type.equals(HOTP)) return hotp(key);
|
||||||
|
if (type.equals(TOTP)) return totp(key);
|
||||||
|
throw new RuntimeException("Unknown OTP type");
|
||||||
|
}
|
||||||
|
|
||||||
public static UserCredentialModel totp(String key) {
|
public static UserCredentialModel totp(String key) {
|
||||||
UserCredentialModel model = new UserCredentialModel();
|
UserCredentialModel model = new UserCredentialModel();
|
||||||
model.setType(TOTP);
|
model.setType(TOTP);
|
||||||
|
@ -51,6 +58,13 @@ public class UserCredentialModel {
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static UserCredentialModel hotp(String key) {
|
||||||
|
UserCredentialModel model = new UserCredentialModel();
|
||||||
|
model.setType(HOTP);
|
||||||
|
model.setValue(key);
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
public static UserCredentialModel kerberos(String token) {
|
public static UserCredentialModel kerberos(String token) {
|
||||||
UserCredentialModel model = new UserCredentialModel();
|
UserCredentialModel model = new UserCredentialModel();
|
||||||
model.setType(KERBEROS);
|
model.setType(KERBEROS);
|
||||||
|
@ -65,6 +79,10 @@ public class UserCredentialModel {
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isOtp(String type) {
|
||||||
|
return TOTP.equals(type) || HOTP.equals(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public String getType() {
|
public String getType() {
|
||||||
return type;
|
return type;
|
||||||
|
|
|
@ -16,6 +16,12 @@ public class UserCredentialValueModel implements Serializable {
|
||||||
private int hashIterations;
|
private int hashIterations;
|
||||||
private Long createdDate;
|
private Long createdDate;
|
||||||
|
|
||||||
|
// otp stuff
|
||||||
|
private int counter;
|
||||||
|
private String algorithm;
|
||||||
|
private int digits;
|
||||||
|
|
||||||
|
|
||||||
public String getType() {
|
public String getType() {
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
@ -64,4 +70,27 @@ public class UserCredentialValueModel implements Serializable {
|
||||||
this.createdDate = createdDate;
|
this.createdDate = createdDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getCounter() {
|
||||||
|
return counter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCounter(int counter) {
|
||||||
|
this.counter = counter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAlgorithm() {
|
||||||
|
return algorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAlgorithm(String algorithm) {
|
||||||
|
this.algorithm = algorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDigits() {
|
||||||
|
return digits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDigits(int digits) {
|
||||||
|
this.digits = digits;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -411,9 +411,23 @@ public class UserFederationManager implements UserProvider {
|
||||||
Set<String> supportedCredentialTypes = link.getSupportedCredentialTypes(user);
|
Set<String> supportedCredentialTypes = link.getSupportedCredentialTypes(user);
|
||||||
if (supportedCredentialTypes.contains(type)) return true;
|
if (supportedCredentialTypes.contains(type)) return true;
|
||||||
}
|
}
|
||||||
|
if (UserCredentialModel.isOtp(type)) {
|
||||||
|
if (!user.isOtpEnabled()) return false;
|
||||||
|
}
|
||||||
|
|
||||||
List<UserCredentialValueModel> creds = user.getCredentialsDirectly();
|
List<UserCredentialValueModel> creds = user.getCredentialsDirectly();
|
||||||
for (UserCredentialValueModel cred : creds) {
|
for (UserCredentialValueModel cred : creds) {
|
||||||
if (cred.getType().equals(type)) return true;
|
if (cred.getType().equals(type)) {
|
||||||
|
if (UserCredentialModel.isOtp(type)) {
|
||||||
|
OTPPolicy otpPolicy = realm.getOTPPolicy();
|
||||||
|
if (!cred.getAlgorithm().equals(otpPolicy.getAlgorithm())
|
||||||
|
|| cred.getDigits() != otpPolicy.getDigits()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ public interface UserModel {
|
||||||
|
|
||||||
boolean isEnabled();
|
boolean isEnabled();
|
||||||
|
|
||||||
boolean isTotp();
|
boolean isOtpEnabled();
|
||||||
|
|
||||||
void setEnabled(boolean enabled);
|
void setEnabled(boolean enabled);
|
||||||
|
|
||||||
|
@ -86,7 +86,7 @@ public interface UserModel {
|
||||||
|
|
||||||
void setEmailVerified(boolean verified);
|
void setEmailVerified(boolean verified);
|
||||||
|
|
||||||
void setTotp(boolean totp);
|
void setOtpEnabled(boolean totp);
|
||||||
|
|
||||||
void updateCredential(UserCredentialModel cred);
|
void updateCredential(UserCredentialModel cred);
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,9 @@ public class CredentialEntity {
|
||||||
private int hashIterations;
|
private int hashIterations;
|
||||||
private Long createdDate;
|
private Long createdDate;
|
||||||
private UserEntity user;
|
private UserEntity user;
|
||||||
|
private int counter;
|
||||||
|
private String algorithm;
|
||||||
|
private int digits;
|
||||||
|
|
||||||
|
|
||||||
public String getId() {
|
public String getId() {
|
||||||
|
@ -79,4 +82,27 @@ public class CredentialEntity {
|
||||||
this.user = user;
|
this.user = user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getCounter() {
|
||||||
|
return counter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCounter(int counter) {
|
||||||
|
this.counter = counter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAlgorithm() {
|
||||||
|
return algorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAlgorithm(String algorithm) {
|
||||||
|
this.algorithm = algorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDigits() {
|
||||||
|
return digits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDigits(int digits) {
|
||||||
|
this.digits = digits;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,14 @@ public class RealmEntity extends AbstractIdentifiableEntity {
|
||||||
private boolean verifyEmail;
|
private boolean verifyEmail;
|
||||||
private boolean resetPasswordAllowed;
|
private boolean resetPasswordAllowed;
|
||||||
private String passwordPolicy;
|
private String passwordPolicy;
|
||||||
|
|
||||||
|
protected String otpPolicyType;
|
||||||
|
protected String otpPolicyAlgorithm;
|
||||||
|
protected int otpPolicyInitialCounter;
|
||||||
|
protected int otpPolicyDigits;
|
||||||
|
protected int otpPolicyLookAheadWindow;
|
||||||
|
|
||||||
|
|
||||||
private boolean editUsernameAllowed;
|
private boolean editUsernameAllowed;
|
||||||
//--- brute force settings
|
//--- brute force settings
|
||||||
private boolean bruteForceProtected;
|
private boolean bruteForceProtected;
|
||||||
|
@ -509,6 +517,46 @@ public class RealmEntity extends AbstractIdentifiableEntity {
|
||||||
public void setRequiredActionProviders(List<RequiredActionProviderEntity> requiredActionProviders) {
|
public void setRequiredActionProviders(List<RequiredActionProviderEntity> requiredActionProviders) {
|
||||||
this.requiredActionProviders = requiredActionProviders;
|
this.requiredActionProviders = requiredActionProviders;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getOtpPolicyType() {
|
||||||
|
return otpPolicyType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOtpPolicyType(String otpPolicyType) {
|
||||||
|
this.otpPolicyType = otpPolicyType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOtpPolicyAlgorithm() {
|
||||||
|
return otpPolicyAlgorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOtpPolicyAlgorithm(String otpPolicyAlgorithm) {
|
||||||
|
this.otpPolicyAlgorithm = otpPolicyAlgorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getOtpPolicyInitialCounter() {
|
||||||
|
return otpPolicyInitialCounter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOtpPolicyInitialCounter(int otpPolicyInitialCounter) {
|
||||||
|
this.otpPolicyInitialCounter = otpPolicyInitialCounter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getOtpPolicyDigits() {
|
||||||
|
return otpPolicyDigits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOtpPolicyDigits(int otpPolicyDigits) {
|
||||||
|
this.otpPolicyDigits = otpPolicyDigits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getOtpPolicyLookAheadWindow() {
|
||||||
|
return otpPolicyLookAheadWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOtpPolicyLookAheadWindow(int otpPolicyLookAheadWindow) {
|
||||||
|
this.otpPolicyLookAheadWindow = otpPolicyLookAheadWindow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ package org.keycloak.models.utils;
|
||||||
|
|
||||||
import org.keycloak.jose.jws.JWSInput;
|
import org.keycloak.jose.jws.JWSInput;
|
||||||
import org.keycloak.jose.jws.crypto.RSAProvider;
|
import org.keycloak.jose.jws.crypto.RSAProvider;
|
||||||
|
import org.keycloak.models.OTPPolicy;
|
||||||
import org.keycloak.models.PasswordPolicy;
|
import org.keycloak.models.PasswordPolicy;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserCredentialModel;
|
import org.keycloak.models.UserCredentialModel;
|
||||||
|
@ -84,11 +85,43 @@ public class CredentialValidation {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean validHOTP(RealmModel realm, UserModel user, String otp) {
|
||||||
|
UserCredentialValueModel passwordCred = null;
|
||||||
|
OTPPolicy policy = realm.getOTPPolicy();
|
||||||
|
HmacOTP validator = new HmacOTP(policy.getDigits(), policy.getAlgorithm(), policy.getLookAheadWindow());
|
||||||
|
for (UserCredentialValueModel cred : user.getCredentialsDirectly()) {
|
||||||
|
if (cred.getType().equals(UserCredentialModel.HOTP)) {
|
||||||
|
int counter = validator.validateHOTP(otp, cred.getValue(), cred.getCounter());
|
||||||
|
if (counter < 0) return false;
|
||||||
|
cred.setCounter(counter);
|
||||||
|
user.updateCredentialDirectly(cred);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean validOTP(RealmModel realm, String token, String secret) {
|
||||||
|
OTPPolicy policy = realm.getOTPPolicy();
|
||||||
|
if (policy.getType().equals(UserCredentialModel.TOTP)) {
|
||||||
|
TimeBasedOTP validator = new TimeBasedOTP(policy.getAlgorithm(), policy.getDigits(), 30, policy.getLookAheadWindow());
|
||||||
|
return validator.validateTOTP(token, secret.getBytes());
|
||||||
|
} else {
|
||||||
|
HmacOTP validator = new HmacOTP(policy.getDigits(), policy.getAlgorithm(), policy.getLookAheadWindow());
|
||||||
|
int c = validator.validateHOTP(token, secret, policy.getInitialCounter());
|
||||||
|
return c > -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean validTOTP(RealmModel realm, UserModel user, String otp) {
|
public static boolean validTOTP(RealmModel realm, UserModel user, String otp) {
|
||||||
UserCredentialValueModel passwordCred = null;
|
UserCredentialValueModel passwordCred = null;
|
||||||
|
OTPPolicy policy = realm.getOTPPolicy();
|
||||||
|
TimeBasedOTP validator = new TimeBasedOTP(policy.getAlgorithm(), policy.getDigits(), 30, policy.getLookAheadWindow());
|
||||||
for (UserCredentialValueModel cred : user.getCredentialsDirectly()) {
|
for (UserCredentialValueModel cred : user.getCredentialsDirectly()) {
|
||||||
if (cred.getType().equals(UserCredentialModel.TOTP)) {
|
if (cred.getType().equals(UserCredentialModel.TOTP)) {
|
||||||
if (new TimeBasedOTP().validate(otp, cred.getValue().getBytes())) {
|
if (validator.validateTOTP(otp, cred.getValue().getBytes())) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -149,6 +182,10 @@ public class CredentialValidation {
|
||||||
if (!validTOTP(realm, user, credential.getValue())) {
|
if (!validTOTP(realm, user, credential.getValue())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
} else if (credential.getType().equals(UserCredentialModel.HOTP)) {
|
||||||
|
if (!validHOTP(realm, user, credential.getValue())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
} else if (credential.getType().equals(UserCredentialModel.SECRET)) {
|
} else if (credential.getType().equals(UserCredentialModel.SECRET)) {
|
||||||
if (!validSecret(realm, user, credential.getValue())) {
|
if (!validSecret(realm, user, credential.getValue())) {
|
||||||
return false;
|
return false;
|
||||||
|
|
164
model/api/src/main/java/org/keycloak/models/utils/HmacOTP.java
Executable file
164
model/api/src/main/java/org/keycloak/models/utils/HmacOTP.java
Executable file
|
@ -0,0 +1,164 @@
|
||||||
|
package org.keycloak.models.utils;
|
||||||
|
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
public class HmacOTP {
|
||||||
|
public static final String HMAC_SHA1 = "HmacSHA1";
|
||||||
|
public static final String HMAC_SHA256 = "HmacSHA256";
|
||||||
|
public static final String HMAC_SHA512 = "HmacSHA512";
|
||||||
|
public static final String DEFAULT_ALGORITHM = HMAC_SHA1;
|
||||||
|
public static final int DEFAULT_NUMBER_DIGITS = 6;
|
||||||
|
// 0 1 2 3 4 5 6 7 8
|
||||||
|
private static final int[] DIGITS_POWER = {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000};
|
||||||
|
protected final String algorithm;
|
||||||
|
protected final int numberDigits;
|
||||||
|
protected final int lookAheadWindow;
|
||||||
|
|
||||||
|
public HmacOTP(int numberDigits, String algorithm, int delayWindow) {
|
||||||
|
this.numberDigits = numberDigits;
|
||||||
|
this.algorithm = algorithm;
|
||||||
|
this.lookAheadWindow = delayWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String generateSecret(int length) {
|
||||||
|
String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVW1234567890";
|
||||||
|
Random r = new Random();
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
char c = chars.charAt(r.nextInt(chars.length()));
|
||||||
|
sb.append(c);
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String generateHOTP(String key, int counter) {
|
||||||
|
String steps = Integer.toHexString(counter).toUpperCase();
|
||||||
|
|
||||||
|
// Just get a 16 digit string
|
||||||
|
while (steps.length() < 16)
|
||||||
|
steps = "0" + steps;
|
||||||
|
|
||||||
|
return generateOTP(key, steps, numberDigits, algorithm);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param token
|
||||||
|
* @param key
|
||||||
|
* @param counter
|
||||||
|
* @return -1 if not a match. A positive number means successful validation. This positive number is also the new value of the counter
|
||||||
|
*/
|
||||||
|
public int validateHOTP(String token, String key, int counter) {
|
||||||
|
|
||||||
|
int newCounter = counter;
|
||||||
|
for (newCounter = counter; newCounter < counter + lookAheadWindow; newCounter++) {
|
||||||
|
String candidate = generateHOTP(key, counter);
|
||||||
|
if (candidate.equals(token)) {
|
||||||
|
return newCounter + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method generates an OTP value for the given set of parameters.
|
||||||
|
*
|
||||||
|
* @param key the shared secret, HEX encoded
|
||||||
|
* @param counter a value that reflects a time
|
||||||
|
* @param returnDigits number of digits to return
|
||||||
|
* @param crypto the crypto function to use
|
||||||
|
* @return A numeric String in base 10 that includes return digits
|
||||||
|
* @throws java.security.GeneralSecurityException
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public String generateOTP(String key, String counter, int returnDigits, String crypto) {
|
||||||
|
String result = null;
|
||||||
|
byte[] hash;
|
||||||
|
|
||||||
|
// Using the counter
|
||||||
|
// First 8 bytes are for the movingFactor
|
||||||
|
// Complaint with base RFC 4226 (HOTP)
|
||||||
|
while (counter.length() < 16)
|
||||||
|
counter = "0" + counter;
|
||||||
|
|
||||||
|
// Get the HEX in a Byte[]
|
||||||
|
byte[] msg = hexStr2Bytes(counter);
|
||||||
|
|
||||||
|
// Adding one byte to get the right conversion
|
||||||
|
// byte[] k = hexStr2Bytes(key);
|
||||||
|
byte[] k = key.getBytes();
|
||||||
|
|
||||||
|
hash = hmac_sha1(crypto, k, msg);
|
||||||
|
|
||||||
|
// put selected bytes into result int
|
||||||
|
int offset = hash[hash.length - 1] & 0xf;
|
||||||
|
|
||||||
|
int binary = ((hash[offset] & 0x7f) << 24) | ((hash[offset + 1] & 0xff) << 16) | ((hash[offset + 2] & 0xff) << 8)
|
||||||
|
| (hash[offset + 3] & 0xff);
|
||||||
|
|
||||||
|
int otp = binary % DIGITS_POWER[returnDigits];
|
||||||
|
|
||||||
|
result = Integer.toString(otp);
|
||||||
|
|
||||||
|
while (result.length() < returnDigits) {
|
||||||
|
result = "0" + result;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method uses the JCE to provide the crypto algorithm. HMAC computes a Hashed Message Authentication Code with the
|
||||||
|
* crypto hash algorithm as a parameter.
|
||||||
|
*
|
||||||
|
* @param crypto the crypto algorithm (HmacSHA1, HmacSHA256, HmacSHA512)
|
||||||
|
* @param keyBytes the bytes to use for the HMAC key
|
||||||
|
* @param text the message or text to be authenticated.
|
||||||
|
* @throws java.security.NoSuchAlgorithmException
|
||||||
|
*
|
||||||
|
* @throws java.security.InvalidKeyException
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private byte[] hmac_sha1(String crypto, byte[] keyBytes, byte[] text) {
|
||||||
|
byte[] value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Mac hmac = Mac.getInstance(crypto);
|
||||||
|
SecretKeySpec macKey = new SecretKeySpec(keyBytes, "RAW");
|
||||||
|
|
||||||
|
hmac.init(macKey);
|
||||||
|
|
||||||
|
value = hmac.doFinal(text);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method converts HEX string to Byte[]
|
||||||
|
*
|
||||||
|
* @param hex the HEX string
|
||||||
|
* @return A byte array
|
||||||
|
*/
|
||||||
|
private byte[] hexStr2Bytes(String hex) {
|
||||||
|
// Adding one byte to get the right conversion
|
||||||
|
// values starting with "0" can be converted
|
||||||
|
byte[] bArray = new BigInteger("10" + hex, 16).toByteArray();
|
||||||
|
|
||||||
|
// Copy all the REAL bytes, not the "first"
|
||||||
|
byte[] ret = new byte[bArray.length - 1];
|
||||||
|
for (int i = 0; i < ret.length; i++)
|
||||||
|
ret[i] = bArray[i + 1];
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ import org.keycloak.models.FederatedIdentityModel;
|
||||||
import org.keycloak.models.IdentityProviderMapperModel;
|
import org.keycloak.models.IdentityProviderMapperModel;
|
||||||
import org.keycloak.models.IdentityProviderModel;
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
import org.keycloak.models.ModelException;
|
import org.keycloak.models.ModelException;
|
||||||
|
import org.keycloak.models.OTPPolicy;
|
||||||
import org.keycloak.models.ProtocolMapperModel;
|
import org.keycloak.models.ProtocolMapperModel;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.RequiredActionProviderModel;
|
import org.keycloak.models.RequiredActionProviderModel;
|
||||||
|
@ -64,7 +65,7 @@ public class ModelToRepresentation {
|
||||||
rep.setEmail(user.getEmail());
|
rep.setEmail(user.getEmail());
|
||||||
rep.setEnabled(user.isEnabled());
|
rep.setEnabled(user.isEnabled());
|
||||||
rep.setEmailVerified(user.isEmailVerified());
|
rep.setEmailVerified(user.isEmailVerified());
|
||||||
rep.setTotp(user.isTotp());
|
rep.setTotp(user.isOtpEnabled());
|
||||||
rep.setFederationLink(user.getFederationLink());
|
rep.setFederationLink(user.getFederationLink());
|
||||||
|
|
||||||
List<String> reqActions = new ArrayList<String>();
|
List<String> reqActions = new ArrayList<String>();
|
||||||
|
@ -152,6 +153,12 @@ public class ModelToRepresentation {
|
||||||
if (realm.getPasswordPolicy() != null) {
|
if (realm.getPasswordPolicy() != null) {
|
||||||
rep.setPasswordPolicy(realm.getPasswordPolicy().toString());
|
rep.setPasswordPolicy(realm.getPasswordPolicy().toString());
|
||||||
}
|
}
|
||||||
|
OTPPolicy otpPolicy = realm.getOTPPolicy();
|
||||||
|
rep.setOtpPolicyAlgorithm(otpPolicy.getAlgorithm());
|
||||||
|
rep.setOtpPolicyDigits(otpPolicy.getDigits());
|
||||||
|
rep.setOtpPolicyInitialCounter(otpPolicy.getInitialCounter());
|
||||||
|
rep.setOtpPolicyType(otpPolicy.getType());
|
||||||
|
rep.setOtpPolicyLookAheadWindow(otpPolicy.getLookAheadWindow());
|
||||||
|
|
||||||
List<String> defaultRoles = realm.getDefaultRoles();
|
List<String> defaultRoles = realm.getDefaultRoles();
|
||||||
if (!defaultRoles.isEmpty()) {
|
if (!defaultRoles.isEmpty()) {
|
||||||
|
|
|
@ -15,6 +15,7 @@ import org.keycloak.models.IdentityProviderMapperModel;
|
||||||
import org.keycloak.models.IdentityProviderModel;
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.ModelException;
|
import org.keycloak.models.ModelException;
|
||||||
|
import org.keycloak.models.OTPPolicy;
|
||||||
import org.keycloak.models.PasswordPolicy;
|
import org.keycloak.models.PasswordPolicy;
|
||||||
import org.keycloak.models.ProtocolMapperModel;
|
import org.keycloak.models.ProtocolMapperModel;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
|
@ -63,7 +64,16 @@ import java.util.TreeSet;
|
||||||
public class RepresentationToModel {
|
public class RepresentationToModel {
|
||||||
|
|
||||||
private static Logger logger = Logger.getLogger(RepresentationToModel.class);
|
private static Logger logger = Logger.getLogger(RepresentationToModel.class);
|
||||||
|
public static OTPPolicy toPolicy(RealmRepresentation rep) {
|
||||||
|
OTPPolicy policy = new OTPPolicy();
|
||||||
|
policy.setType(rep.getOtpPolicyType());
|
||||||
|
policy.setLookAheadWindow(rep.getOtpPolicyLookAheadWindow());
|
||||||
|
policy.setInitialCounter(rep.getOtpPolicyInitialCounter());
|
||||||
|
policy.setAlgorithm(rep.getOtpPolicyAlgorithm());
|
||||||
|
policy.setDigits(rep.getOtpPolicyDigits());
|
||||||
|
return policy;
|
||||||
|
|
||||||
|
}
|
||||||
public static void importRealm(KeycloakSession session, RealmRepresentation rep, RealmModel newRealm) {
|
public static void importRealm(KeycloakSession session, RealmRepresentation rep, RealmModel newRealm) {
|
||||||
convertDeprecatedSocialProviders(rep);
|
convertDeprecatedSocialProviders(rep);
|
||||||
convertDeprecatedApplications(session, rep);
|
convertDeprecatedApplications(session, rep);
|
||||||
|
@ -144,6 +154,8 @@ public class RepresentationToModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rep.getPasswordPolicy() != null) newRealm.setPasswordPolicy(new PasswordPolicy(rep.getPasswordPolicy()));
|
if (rep.getPasswordPolicy() != null) newRealm.setPasswordPolicy(new PasswordPolicy(rep.getPasswordPolicy()));
|
||||||
|
if (rep.getOtpPolicyType() != null) newRealm.setOTPPolicy(toPolicy(rep));
|
||||||
|
else newRealm.setOTPPolicy(OTPPolicy.DEFAULT_POLICY);
|
||||||
|
|
||||||
importIdentityProviders(rep, newRealm);
|
importIdentityProviders(rep, newRealm);
|
||||||
importIdentityProviderMappers(rep, newRealm);
|
importIdentityProviderMappers(rep, newRealm);
|
||||||
|
@ -497,6 +509,7 @@ public class RepresentationToModel {
|
||||||
|
|
||||||
|
|
||||||
if (rep.getPasswordPolicy() != null) realm.setPasswordPolicy(new PasswordPolicy(rep.getPasswordPolicy()));
|
if (rep.getPasswordPolicy() != null) realm.setPasswordPolicy(new PasswordPolicy(rep.getPasswordPolicy()));
|
||||||
|
if (rep.getOtpPolicyType() != null) realm.setOTPPolicy(toPolicy(rep));
|
||||||
|
|
||||||
if (rep.getDefaultRoles() != null) {
|
if (rep.getDefaultRoles() != null) {
|
||||||
realm.updateDefaultRoles(rep.getDefaultRoles().toArray(new String[rep.getDefaultRoles().size()]));
|
realm.updateDefaultRoles(rep.getDefaultRoles().toArray(new String[rep.getDefaultRoles().size()]));
|
||||||
|
@ -847,7 +860,7 @@ public class RepresentationToModel {
|
||||||
user.setFirstName(userRep.getFirstName());
|
user.setFirstName(userRep.getFirstName());
|
||||||
user.setLastName(userRep.getLastName());
|
user.setLastName(userRep.getLastName());
|
||||||
user.setFederationLink(userRep.getFederationLink());
|
user.setFederationLink(userRep.getFederationLink());
|
||||||
user.setTotp(userRep.isTotp());
|
user.setOtpEnabled(userRep.isTotp());
|
||||||
if (userRep.getAttributes() != null) {
|
if (userRep.getAttributes() != null) {
|
||||||
for (Map.Entry<String, Object> entry : userRep.getAttributes().entrySet()) {
|
for (Map.Entry<String, Object> entry : userRep.getAttributes().entrySet()) {
|
||||||
Object value = entry.getValue();
|
Object value = entry.getValue();
|
||||||
|
@ -922,13 +935,22 @@ public class RepresentationToModel {
|
||||||
UserCredentialValueModel hashedCred = new UserCredentialValueModel();
|
UserCredentialValueModel hashedCred = new UserCredentialValueModel();
|
||||||
hashedCred.setType(cred.getType());
|
hashedCred.setType(cred.getType());
|
||||||
hashedCred.setDevice(cred.getDevice());
|
hashedCred.setDevice(cred.getDevice());
|
||||||
hashedCred.setHashIterations(cred.getHashIterations());
|
if (cred.getHashIterations() != null) hashedCred.setHashIterations(cred.getHashIterations());
|
||||||
try {
|
try {
|
||||||
if (cred.getSalt() != null) hashedCred.setSalt(Base64.decode(cred.getSalt()));
|
if (cred.getSalt() != null) hashedCred.setSalt(Base64.decode(cred.getSalt()));
|
||||||
} catch (IOException ioe) {
|
} catch (IOException ioe) {
|
||||||
throw new RuntimeException(ioe);
|
throw new RuntimeException(ioe);
|
||||||
}
|
}
|
||||||
hashedCred.setValue(cred.getHashedSaltedValue());
|
hashedCred.setValue(cred.getHashedSaltedValue());
|
||||||
|
if (cred.getCounter() != null) hashedCred.setCounter(cred.getCounter());
|
||||||
|
if (cred.getDigits() != null) hashedCred.setDigits(cred.getDigits());
|
||||||
|
if (cred.getAlgorithm() != null) hashedCred.setAlgorithm(cred.getAlgorithm());
|
||||||
|
if (cred.getDigits() == null && UserCredentialModel.isOtp(cred.getType())) {
|
||||||
|
hashedCred.setDigits(6);
|
||||||
|
}
|
||||||
|
if (cred.getAlgorithm() == null && UserCredentialModel.isOtp(cred.getType())) {
|
||||||
|
hashedCred.setAlgorithm(HmacOTP.HMAC_SHA1);
|
||||||
|
}
|
||||||
user.updateCredentialDirectly(hashedCred);
|
user.updateCredentialDirectly(hashedCred);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
package org.keycloak.models.utils;
|
package org.keycloak.models.utils;
|
||||||
|
|
||||||
import javax.crypto.Mac;
|
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
|
||||||
import java.math.BigInteger;
|
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.GregorianCalendar;
|
import java.util.GregorianCalendar;
|
||||||
import java.util.TimeZone;
|
import java.util.TimeZone;
|
||||||
|
@ -13,24 +10,12 @@ import java.util.TimeZone;
|
||||||
* @author anil saldhana
|
* @author anil saldhana
|
||||||
* @since Sep 20, 2010
|
* @since Sep 20, 2010
|
||||||
*/
|
*/
|
||||||
public class TimeBasedOTP {
|
public class TimeBasedOTP extends HmacOTP {
|
||||||
|
|
||||||
public static final String HMAC_SHA1 = "HmacSHA1";
|
|
||||||
public static final String HMAC_SHA256 = "HmacSHA256";
|
|
||||||
public static final String HMAC_SHA512 = "HmacSHA512";
|
|
||||||
|
|
||||||
public static final String DEFAULT_ALGORITHM = HMAC_SHA1;
|
|
||||||
public static final int DEFAULT_NUMBER_DIGITS = 6;
|
|
||||||
public static final int DEFAULT_INTERVAL_SECONDS = 30;
|
public static final int DEFAULT_INTERVAL_SECONDS = 30;
|
||||||
public static final int DEFAULT_DELAY_WINDOW = 1;
|
public static final int DEFAULT_DELAY_WINDOW = 1;
|
||||||
|
|
||||||
// 0 1 2 3 4 5 6 7 8
|
|
||||||
private static final int[] DIGITS_POWER = {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000};
|
|
||||||
|
|
||||||
private Clock clock;
|
private Clock clock;
|
||||||
private final String algorithm;
|
|
||||||
private final int numberDigits;
|
|
||||||
private final int delayWindow;
|
|
||||||
|
|
||||||
public TimeBasedOTP() {
|
public TimeBasedOTP() {
|
||||||
this(DEFAULT_ALGORITHM, DEFAULT_NUMBER_DIGITS, DEFAULT_INTERVAL_SECONDS, DEFAULT_DELAY_WINDOW);
|
this(DEFAULT_ALGORITHM, DEFAULT_NUMBER_DIGITS, DEFAULT_INTERVAL_SECONDS, DEFAULT_DELAY_WINDOW);
|
||||||
|
@ -40,13 +25,11 @@ public class TimeBasedOTP {
|
||||||
* @param algorithm the encryption algorithm
|
* @param algorithm the encryption algorithm
|
||||||
* @param numberDigits the number of digits for tokens
|
* @param numberDigits the number of digits for tokens
|
||||||
* @param timeIntervalInSeconds the number of seconds a token is valid
|
* @param timeIntervalInSeconds the number of seconds a token is valid
|
||||||
* @param delayWindow the number of previous intervals that should be used to validate tokens.
|
* @param lookAheadWindow the number of previous intervals that should be used to validate tokens.
|
||||||
*/
|
*/
|
||||||
public TimeBasedOTP(String algorithm, int numberDigits, int timeIntervalInSeconds, int delayWindow) {
|
public TimeBasedOTP(String algorithm, int numberDigits, int timeIntervalInSeconds, int lookAheadWindow) {
|
||||||
this.algorithm = algorithm;
|
super(numberDigits, algorithm, lookAheadWindow);
|
||||||
this.numberDigits = numberDigits;
|
|
||||||
this.clock = new Clock(timeIntervalInSeconds);
|
this.clock = new Clock(timeIntervalInSeconds);
|
||||||
this.delayWindow = delayWindow;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -54,7 +37,7 @@ public class TimeBasedOTP {
|
||||||
*
|
*
|
||||||
* @param secretKey the secret key to derive the token from.
|
* @param secretKey the secret key to derive the token from.
|
||||||
*/
|
*/
|
||||||
public String generate(String secretKey) {
|
public String generateTOTP(String secretKey) {
|
||||||
long T = this.clock.getCurrentInterval();
|
long T = this.clock.getCurrentInterval();
|
||||||
|
|
||||||
String steps = Long.toHexString(T).toUpperCase();
|
String steps = Long.toHexString(T).toUpperCase();
|
||||||
|
@ -63,53 +46,7 @@ public class TimeBasedOTP {
|
||||||
while (steps.length() < 16)
|
while (steps.length() < 16)
|
||||||
steps = "0" + steps;
|
steps = "0" + steps;
|
||||||
|
|
||||||
return generateTOTP(secretKey, steps, this.numberDigits, this.algorithm);
|
return generateOTP(secretKey, steps, this.numberDigits, this.algorithm);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method generates an TOTP value for the given set of parameters.
|
|
||||||
*
|
|
||||||
* @param key the shared secret, HEX encoded
|
|
||||||
* @param time a value that reflects a time
|
|
||||||
* @param returnDigits number of digits to return
|
|
||||||
* @param crypto the crypto function to use
|
|
||||||
* @return A numeric String in base 10 that includes {@link truncationDigits} digits
|
|
||||||
* @throws java.security.GeneralSecurityException
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public String generateTOTP(String key, String time, int returnDigits, String crypto) {
|
|
||||||
String result = null;
|
|
||||||
byte[] hash;
|
|
||||||
|
|
||||||
// Using the counter
|
|
||||||
// First 8 bytes are for the movingFactor
|
|
||||||
// Complaint with base RFC 4226 (HOTP)
|
|
||||||
while (time.length() < 16)
|
|
||||||
time = "0" + time;
|
|
||||||
|
|
||||||
// Get the HEX in a Byte[]
|
|
||||||
byte[] msg = hexStr2Bytes(time);
|
|
||||||
|
|
||||||
// Adding one byte to get the right conversion
|
|
||||||
// byte[] k = hexStr2Bytes(key);
|
|
||||||
byte[] k = key.getBytes();
|
|
||||||
|
|
||||||
hash = hmac_sha1(crypto, k, msg);
|
|
||||||
|
|
||||||
// put selected bytes into result int
|
|
||||||
int offset = hash[hash.length - 1] & 0xf;
|
|
||||||
|
|
||||||
int binary = ((hash[offset] & 0x7f) << 24) | ((hash[offset + 1] & 0xff) << 16) | ((hash[offset + 2] & 0xff) << 8)
|
|
||||||
| (hash[offset + 3] & 0xff);
|
|
||||||
|
|
||||||
int otp = binary % DIGITS_POWER[returnDigits];
|
|
||||||
|
|
||||||
result = Integer.toString(otp);
|
|
||||||
|
|
||||||
while (result.length() < returnDigits) {
|
|
||||||
result = "0" + result;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -119,17 +56,17 @@ public class TimeBasedOTP {
|
||||||
* @param secret Shared secret
|
* @param secret Shared secret
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public boolean validate(String token, byte[] secret) {
|
public boolean validateTOTP(String token, byte[] secret) {
|
||||||
long currentInterval = this.clock.getCurrentInterval();
|
long currentInterval = this.clock.getCurrentInterval();
|
||||||
|
|
||||||
for (int i = this.delayWindow; i >= 0; --i) {
|
for (int i = this.lookAheadWindow; i >= 0; --i) {
|
||||||
String steps = Long.toHexString(currentInterval - i).toUpperCase();
|
String steps = Long.toHexString(currentInterval - i).toUpperCase();
|
||||||
|
|
||||||
// Just get a 16 digit string
|
// Just get a 16 digit string
|
||||||
while (steps.length() < 16)
|
while (steps.length() < 16)
|
||||||
steps = "0" + steps;
|
steps = "0" + steps;
|
||||||
|
|
||||||
String candidate = generateTOTP(new String(secret), steps, this.numberDigits, this.algorithm);
|
String candidate = generateOTP(new String(secret), steps, this.numberDigits, this.algorithm);
|
||||||
|
|
||||||
if (candidate.equals(token)) {
|
if (candidate.equals(token)) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -143,53 +80,6 @@ public class TimeBasedOTP {
|
||||||
this.clock.setCalendar(calendar);
|
this.clock.setCalendar(calendar);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This method uses the JCE to provide the crypto algorithm. HMAC computes a Hashed Message Authentication Code with the
|
|
||||||
* crypto hash algorithm as a parameter.
|
|
||||||
*
|
|
||||||
* @param crypto the crypto algorithm (HmacSHA1, HmacSHA256, HmacSHA512)
|
|
||||||
* @param keyBytes the bytes to use for the HMAC key
|
|
||||||
* @param text the message or text to be authenticated.
|
|
||||||
* @throws java.security.NoSuchAlgorithmException
|
|
||||||
*
|
|
||||||
* @throws java.security.InvalidKeyException
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
private byte[] hmac_sha1(String crypto, byte[] keyBytes, byte[] text) {
|
|
||||||
byte[] value;
|
|
||||||
|
|
||||||
try {
|
|
||||||
Mac hmac = Mac.getInstance(crypto);
|
|
||||||
SecretKeySpec macKey = new SecretKeySpec(keyBytes, "RAW");
|
|
||||||
|
|
||||||
hmac.init(macKey);
|
|
||||||
|
|
||||||
value = hmac.doFinal(text);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method converts HEX string to Byte[]
|
|
||||||
*
|
|
||||||
* @param hex the HEX string
|
|
||||||
* @return A byte array
|
|
||||||
*/
|
|
||||||
private byte[] hexStr2Bytes(String hex) {
|
|
||||||
// Adding one byte to get the right conversion
|
|
||||||
// values starting with "0" can be converted
|
|
||||||
byte[] bArray = new BigInteger("10" + hex, 16).toByteArray();
|
|
||||||
|
|
||||||
// Copy all the REAL bytes, not the "first"
|
|
||||||
byte[] ret = new byte[bArray.length - 1];
|
|
||||||
for (int i = 0; i < ret.length; i++)
|
|
||||||
ret[i] = bArray[i + 1];
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
private class Clock {
|
private class Clock {
|
||||||
|
|
||||||
private final int interval;
|
private final int interval;
|
||||||
|
|
|
@ -43,8 +43,8 @@ public class UserModelDelegate implements UserModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isTotp() {
|
public boolean isOtpEnabled() {
|
||||||
return delegate.isTotp();
|
return delegate.isOtpEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -148,8 +148,8 @@ public class UserModelDelegate implements UserModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setTotp(boolean totp) {
|
public void setOtpEnabled(boolean totp) {
|
||||||
delegate.setTotp(totp);
|
delegate.setOtpEnabled(totp);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
32
model/api/src/test/java/org/keycloak/models/HmacTest.java
Executable file
32
model/api/src/test/java/org/keycloak/models/HmacTest.java
Executable file
|
@ -0,0 +1,32 @@
|
||||||
|
package org.keycloak.models;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.models.utils.Base32;
|
||||||
|
import org.keycloak.models.utils.HmacOTP;
|
||||||
|
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.Scanner;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
public class HmacTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testHmac() throws Exception {
|
||||||
|
HmacOTP hmacOTP = new HmacOTP(6, HmacOTP.HMAC_SHA1, 10);
|
||||||
|
String secret = "JNSVMMTEKZCUGSKJIVGHMNSQOZBDA5JT";
|
||||||
|
String decoded = new String(Base32.decode(secret));
|
||||||
|
System.out.println(hmacOTP.generateHOTP(decoded, 0));
|
||||||
|
System.out.println(hmacOTP.validateHOTP("550233", decoded, 0));
|
||||||
|
Assert.assertEquals(1, hmacOTP.validateHOTP("550233", decoded, 0));
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,6 +26,7 @@ import org.keycloak.models.IdentityProviderMapperModel;
|
||||||
import org.keycloak.models.IdentityProviderModel;
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.ModelDuplicateException;
|
import org.keycloak.models.ModelDuplicateException;
|
||||||
|
import org.keycloak.models.OTPPolicy;
|
||||||
import org.keycloak.models.PasswordPolicy;
|
import org.keycloak.models.PasswordPolicy;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.RequiredActionProviderModel;
|
import org.keycloak.models.RequiredActionProviderModel;
|
||||||
|
@ -81,6 +82,7 @@ public class RealmAdapter implements RealmModel {
|
||||||
protected volatile transient Key codeSecretKey;
|
protected volatile transient Key codeSecretKey;
|
||||||
|
|
||||||
private volatile transient PasswordPolicy passwordPolicy;
|
private volatile transient PasswordPolicy passwordPolicy;
|
||||||
|
private volatile transient OTPPolicy otpPolicy;
|
||||||
private volatile transient KeycloakSession session;
|
private volatile transient KeycloakSession session;
|
||||||
|
|
||||||
private final Map<String, ClientModel> allApps = new HashMap<String, ClientModel>();
|
private final Map<String, ClientModel> allApps = new HashMap<String, ClientModel>();
|
||||||
|
@ -287,6 +289,29 @@ public class RealmAdapter implements RealmModel {
|
||||||
realm.setPasswordPolicy(policy.toString());
|
realm.setPasswordPolicy(policy.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OTPPolicy getOTPPolicy() {
|
||||||
|
if (otpPolicy == null) {
|
||||||
|
otpPolicy = new OTPPolicy();
|
||||||
|
otpPolicy.setDigits(realm.getOtpPolicyDigits());
|
||||||
|
otpPolicy.setAlgorithm(realm.getOtpPolicyAlgorithm());
|
||||||
|
otpPolicy.setInitialCounter(realm.getOtpPolicyInitialCounter());
|
||||||
|
otpPolicy.setLookAheadWindow(realm.getOtpPolicyLookAheadWindow());
|
||||||
|
otpPolicy.setType(realm.getOtpPolicyType());
|
||||||
|
}
|
||||||
|
return otpPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setOTPPolicy(OTPPolicy policy) {
|
||||||
|
realm.setOtpPolicyAlgorithm(policy.getAlgorithm());
|
||||||
|
realm.setOtpPolicyDigits(policy.getDigits());
|
||||||
|
realm.setOtpPolicyInitialCounter(policy.getInitialCounter());
|
||||||
|
realm.setOtpPolicyLookAheadWindow(policy.getLookAheadWindow());
|
||||||
|
realm.setOtpPolicyType(policy.getType());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getNotBefore() {
|
public int getNotBefore() {
|
||||||
return realm.getNotBefore();
|
return realm.getNotBefore();
|
||||||
|
|
|
@ -22,6 +22,7 @@ import org.keycloak.models.ClientModel;
|
||||||
import static org.keycloak.models.utils.Pbkdf2PasswordEncoder.getSalt;
|
import static org.keycloak.models.utils.Pbkdf2PasswordEncoder.getSalt;
|
||||||
|
|
||||||
import org.keycloak.models.ModelDuplicateException;
|
import org.keycloak.models.ModelDuplicateException;
|
||||||
|
import org.keycloak.models.OTPPolicy;
|
||||||
import org.keycloak.models.UserConsentModel;
|
import org.keycloak.models.UserConsentModel;
|
||||||
import org.keycloak.models.PasswordPolicy;
|
import org.keycloak.models.PasswordPolicy;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
|
@ -256,12 +257,12 @@ public class UserAdapter implements UserModel, Comparable {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isTotp() {
|
public boolean isOtpEnabled() {
|
||||||
return user.isTotp();
|
return user.isTotp();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setTotp(boolean totp) {
|
public void setOtpEnabled(boolean totp) {
|
||||||
user.setTotp(totp);
|
user.setTotp(totp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -270,6 +271,9 @@ public class UserAdapter implements UserModel, Comparable {
|
||||||
|
|
||||||
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
|
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
|
||||||
updatePasswordCredential(cred);
|
updatePasswordCredential(cred);
|
||||||
|
} else if (UserCredentialModel.isOtp(cred.getType())){
|
||||||
|
updateOtpCredential(cred);
|
||||||
|
|
||||||
}else {
|
}else {
|
||||||
CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType());
|
CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType());
|
||||||
|
|
||||||
|
@ -283,6 +287,27 @@ public class UserAdapter implements UserModel, Comparable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateOtpCredential(UserCredentialModel cred) {
|
||||||
|
CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType());
|
||||||
|
|
||||||
|
if (credentialEntity == null) {
|
||||||
|
credentialEntity = setCredentials(user, cred);
|
||||||
|
credentialEntity.setValue(cred.getValue());
|
||||||
|
OTPPolicy otpPolicy = realm.getOTPPolicy();
|
||||||
|
credentialEntity.setAlgorithm(otpPolicy.getAlgorithm());
|
||||||
|
credentialEntity.setDigits(otpPolicy.getDigits());
|
||||||
|
credentialEntity.setCounter(otpPolicy.getInitialCounter());
|
||||||
|
user.getCredentials().add(credentialEntity);
|
||||||
|
} else {
|
||||||
|
credentialEntity.setValue(cred.getValue());
|
||||||
|
OTPPolicy policy = realm.getOTPPolicy();
|
||||||
|
credentialEntity.setDigits(policy.getDigits());
|
||||||
|
credentialEntity.setCounter(policy.getInitialCounter());
|
||||||
|
credentialEntity.setAlgorithm(policy.getAlgorithm());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private void updatePasswordCredential(UserCredentialModel cred) {
|
private void updatePasswordCredential(UserCredentialModel cred) {
|
||||||
CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType());
|
CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType());
|
||||||
|
|
||||||
|
@ -390,6 +415,9 @@ public class UserAdapter implements UserModel, Comparable {
|
||||||
credModel.setValue(credEntity.getValue());
|
credModel.setValue(credEntity.getValue());
|
||||||
credModel.setSalt(credEntity.getSalt());
|
credModel.setSalt(credEntity.getSalt());
|
||||||
credModel.setHashIterations(credEntity.getHashIterations());
|
credModel.setHashIterations(credEntity.getHashIterations());
|
||||||
|
credModel.setCounter(credEntity.getCounter());
|
||||||
|
credModel.setAlgorithm(credEntity.getAlgorithm());
|
||||||
|
credModel.setDigits(credEntity.getDigits());
|
||||||
|
|
||||||
result.add(credModel);
|
result.add(credModel);
|
||||||
}
|
}
|
||||||
|
@ -414,6 +442,9 @@ public class UserAdapter implements UserModel, Comparable {
|
||||||
credentialEntity.setSalt(credModel.getSalt());
|
credentialEntity.setSalt(credModel.getSalt());
|
||||||
credentialEntity.setDevice(credModel.getDevice());
|
credentialEntity.setDevice(credModel.getDevice());
|
||||||
credentialEntity.setHashIterations(credModel.getHashIterations());
|
credentialEntity.setHashIterations(credModel.getHashIterations());
|
||||||
|
credentialEntity.setCounter(credModel.getCounter());
|
||||||
|
credentialEntity.setAlgorithm(credModel.getAlgorithm());
|
||||||
|
credentialEntity.setDigits(credModel.getDigits());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -8,6 +8,7 @@ import org.keycloak.models.AuthenticatorConfigModel;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.IdentityProviderMapperModel;
|
import org.keycloak.models.IdentityProviderMapperModel;
|
||||||
import org.keycloak.models.IdentityProviderModel;
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
|
import org.keycloak.models.OTPPolicy;
|
||||||
import org.keycloak.models.PasswordPolicy;
|
import org.keycloak.models.PasswordPolicy;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.RequiredActionProviderModel;
|
import org.keycloak.models.RequiredActionProviderModel;
|
||||||
|
@ -452,6 +453,19 @@ public class RealmAdapter implements RealmModel {
|
||||||
updated.setPasswordPolicy(policy);
|
updated.setPasswordPolicy(policy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OTPPolicy getOTPPolicy() {
|
||||||
|
if (updated != null) return updated.getOTPPolicy();
|
||||||
|
return cached.getOtpPolicy();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setOTPPolicy(OTPPolicy policy) {
|
||||||
|
getDelegateForUpdate();
|
||||||
|
updated.setOTPPolicy(policy);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RoleModel getRoleById(String id) {
|
public RoleModel getRoleById(String id) {
|
||||||
if (updated != null) return updated.getRoleById(id);
|
if (updated != null) return updated.getRoleById(id);
|
||||||
|
|
|
@ -80,8 +80,8 @@ public class UserAdapter implements UserModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isTotp() {
|
public boolean isOtpEnabled() {
|
||||||
if (updated != null) return updated.isTotp();
|
if (updated != null) return updated.isOtpEnabled();
|
||||||
return cached.isTotp();
|
return cached.isTotp();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -208,9 +208,9 @@ public class UserAdapter implements UserModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setTotp(boolean totp) {
|
public void setOtpEnabled(boolean totp) {
|
||||||
getDelegateForUpdate();
|
getDelegateForUpdate();
|
||||||
updated.setTotp(totp);
|
updated.setOtpEnabled(totp);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -7,6 +7,7 @@ import org.keycloak.models.AuthenticatorConfigModel;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.IdentityProviderMapperModel;
|
import org.keycloak.models.IdentityProviderMapperModel;
|
||||||
import org.keycloak.models.IdentityProviderModel;
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
|
import org.keycloak.models.OTPPolicy;
|
||||||
import org.keycloak.models.PasswordPolicy;
|
import org.keycloak.models.PasswordPolicy;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.RealmProvider;
|
import org.keycloak.models.RealmProvider;
|
||||||
|
@ -63,6 +64,7 @@ public class CachedRealm implements Serializable {
|
||||||
private int accessCodeLifespanLogin;
|
private int accessCodeLifespanLogin;
|
||||||
private int notBefore;
|
private int notBefore;
|
||||||
private PasswordPolicy passwordPolicy;
|
private PasswordPolicy passwordPolicy;
|
||||||
|
private OTPPolicy otpPolicy;
|
||||||
|
|
||||||
private String publicKeyPem;
|
private String publicKeyPem;
|
||||||
private String privateKeyPem;
|
private String privateKeyPem;
|
||||||
|
@ -137,6 +139,7 @@ public class CachedRealm implements Serializable {
|
||||||
accessCodeLifespanLogin = model.getAccessCodeLifespanLogin();
|
accessCodeLifespanLogin = model.getAccessCodeLifespanLogin();
|
||||||
notBefore = model.getNotBefore();
|
notBefore = model.getNotBefore();
|
||||||
passwordPolicy = model.getPasswordPolicy();
|
passwordPolicy = model.getPasswordPolicy();
|
||||||
|
otpPolicy = model.getOTPPolicy();
|
||||||
|
|
||||||
publicKeyPem = model.getPublicKeyPem();
|
publicKeyPem = model.getPublicKeyPem();
|
||||||
privateKeyPem = model.getPrivateKeyPem();
|
privateKeyPem = model.getPrivateKeyPem();
|
||||||
|
@ -456,4 +459,8 @@ public class CachedRealm implements Serializable {
|
||||||
public Map<String, RequiredActionProviderModel> getRequiredActionProvidersByAlias() {
|
public Map<String, RequiredActionProviderModel> getRequiredActionProvidersByAlias() {
|
||||||
return requiredActionProvidersByAlias;
|
return requiredActionProvidersByAlias;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public OTPPolicy getOtpPolicy() {
|
||||||
|
return otpPolicy;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,11 +7,9 @@ import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.util.MultivaluedHashMap;
|
import org.keycloak.util.MultivaluedHashMap;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -48,7 +46,7 @@ public class CachedUser implements Serializable {
|
||||||
this.emailVerified = user.isEmailVerified();
|
this.emailVerified = user.isEmailVerified();
|
||||||
this.credentials.addAll(user.getCredentialsDirectly());
|
this.credentials.addAll(user.getCredentialsDirectly());
|
||||||
this.enabled = user.isEnabled();
|
this.enabled = user.isEnabled();
|
||||||
this.totp = user.isTotp();
|
this.totp = user.isOtpEnabled();
|
||||||
this.federationLink = user.getFederationLink();
|
this.federationLink = user.getFederationLink();
|
||||||
this.serviceAccountClientLink = user.getServiceAccountClientLink();
|
this.serviceAccountClientLink = user.getServiceAccountClientLink();
|
||||||
this.requiredActions.addAll(user.getRequiredActions());
|
this.requiredActions.addAll(user.getRequiredActions());
|
||||||
|
|
|
@ -9,6 +9,7 @@ import org.keycloak.models.IdentityProviderMapperModel;
|
||||||
import org.keycloak.models.IdentityProviderModel;
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.ModelDuplicateException;
|
import org.keycloak.models.ModelDuplicateException;
|
||||||
|
import org.keycloak.models.OTPPolicy;
|
||||||
import org.keycloak.models.PasswordPolicy;
|
import org.keycloak.models.PasswordPolicy;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.RequiredActionProviderModel;
|
import org.keycloak.models.RequiredActionProviderModel;
|
||||||
|
@ -65,6 +66,7 @@ public class RealmAdapter implements RealmModel {
|
||||||
protected volatile transient Key codeSecretKey;
|
protected volatile transient Key codeSecretKey;
|
||||||
protected KeycloakSession session;
|
protected KeycloakSession session;
|
||||||
private PasswordPolicy passwordPolicy;
|
private PasswordPolicy passwordPolicy;
|
||||||
|
private OTPPolicy otpPolicy;
|
||||||
|
|
||||||
public RealmAdapter(KeycloakSession session, EntityManager em, RealmEntity realm) {
|
public RealmAdapter(KeycloakSession session, EntityManager em, RealmEntity realm) {
|
||||||
this.session = session;
|
this.session = session;
|
||||||
|
@ -1017,6 +1019,30 @@ public class RealmAdapter implements RealmModel {
|
||||||
em.flush();
|
em.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OTPPolicy getOTPPolicy() {
|
||||||
|
if (otpPolicy == null) {
|
||||||
|
otpPolicy = new OTPPolicy();
|
||||||
|
otpPolicy.setDigits(realm.getOtpPolicyDigits());
|
||||||
|
otpPolicy.setAlgorithm(realm.getOtpPolicyAlgorithm());
|
||||||
|
otpPolicy.setInitialCounter(realm.getOtpPolicyInitialCounter());
|
||||||
|
otpPolicy.setLookAheadWindow(realm.getOtpPolicyLookAheadWindow());
|
||||||
|
otpPolicy.setType(realm.getOtpPolicyType());
|
||||||
|
}
|
||||||
|
return otpPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setOTPPolicy(OTPPolicy policy) {
|
||||||
|
realm.setOtpPolicyAlgorithm(policy.getAlgorithm());
|
||||||
|
realm.setOtpPolicyDigits(policy.getDigits());
|
||||||
|
realm.setOtpPolicyInitialCounter(policy.getInitialCounter());
|
||||||
|
realm.setOtpPolicyLookAheadWindow(policy.getLookAheadWindow());
|
||||||
|
realm.setOtpPolicyType(policy.getType());
|
||||||
|
em.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
if (this == o) return true;
|
if (this == o) return true;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package org.keycloak.models.jpa;
|
package org.keycloak.models.jpa;
|
||||||
|
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.OTPPolicy;
|
||||||
import org.keycloak.models.ProtocolMapperModel;
|
import org.keycloak.models.ProtocolMapperModel;
|
||||||
import org.keycloak.models.UserConsentModel;
|
import org.keycloak.models.UserConsentModel;
|
||||||
import org.keycloak.models.ModelDuplicateException;
|
import org.keycloak.models.ModelDuplicateException;
|
||||||
|
@ -94,7 +95,7 @@ public class UserAdapter implements UserModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isTotp() {
|
public boolean isOtpEnabled() {
|
||||||
return user.isTotp();
|
return user.isTotp();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -282,7 +283,7 @@ public class UserAdapter implements UserModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setTotp(boolean totp) {
|
public void setOtpEnabled(boolean totp) {
|
||||||
user.setTotp(totp);
|
user.setTotp(totp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -291,6 +292,9 @@ public class UserAdapter implements UserModel {
|
||||||
|
|
||||||
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
|
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
|
||||||
updatePasswordCredential(cred);
|
updatePasswordCredential(cred);
|
||||||
|
} else if (UserCredentialModel.isOtp(cred.getType())){
|
||||||
|
updateOtpCredential(cred);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType());
|
CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType());
|
||||||
|
|
||||||
|
@ -306,6 +310,31 @@ public class UserAdapter implements UserModel {
|
||||||
em.flush();
|
em.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateOtpCredential(UserCredentialModel cred) {
|
||||||
|
CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType());
|
||||||
|
|
||||||
|
if (credentialEntity == null) {
|
||||||
|
credentialEntity = setCredentials(user, cred);
|
||||||
|
|
||||||
|
credentialEntity.setValue(cred.getValue());
|
||||||
|
OTPPolicy otpPolicy = realm.getOTPPolicy();
|
||||||
|
credentialEntity.setAlgorithm(otpPolicy.getAlgorithm());
|
||||||
|
credentialEntity.setDigits(otpPolicy.getDigits());
|
||||||
|
credentialEntity.setCounter(otpPolicy.getInitialCounter());
|
||||||
|
em.persist(credentialEntity);
|
||||||
|
user.getCredentials().add(credentialEntity);
|
||||||
|
} else {
|
||||||
|
OTPPolicy policy = realm.getOTPPolicy();
|
||||||
|
credentialEntity.setDigits(policy.getDigits());
|
||||||
|
credentialEntity.setCounter(policy.getInitialCounter());
|
||||||
|
credentialEntity.setAlgorithm(policy.getAlgorithm());
|
||||||
|
credentialEntity.setValue(cred.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private void updatePasswordCredential(UserCredentialModel cred) {
|
private void updatePasswordCredential(UserCredentialModel cred) {
|
||||||
CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType());
|
CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType());
|
||||||
|
|
||||||
|
@ -418,6 +447,9 @@ public class UserAdapter implements UserModel {
|
||||||
credModel.setCreatedDate(credEntity.getCreatedDate());
|
credModel.setCreatedDate(credEntity.getCreatedDate());
|
||||||
credModel.setSalt(credEntity.getSalt());
|
credModel.setSalt(credEntity.getSalt());
|
||||||
credModel.setHashIterations(credEntity.getHashIterations());
|
credModel.setHashIterations(credEntity.getHashIterations());
|
||||||
|
credModel.setCounter(credEntity.getCounter());
|
||||||
|
credModel.setAlgorithm(credEntity.getAlgorithm());
|
||||||
|
credModel.setDigits(credEntity.getDigits());
|
||||||
|
|
||||||
result.add(credModel);
|
result.add(credModel);
|
||||||
}
|
}
|
||||||
|
@ -444,6 +476,9 @@ public class UserAdapter implements UserModel {
|
||||||
credentialEntity.setSalt(credModel.getSalt());
|
credentialEntity.setSalt(credModel.getSalt());
|
||||||
credentialEntity.setDevice(credModel.getDevice());
|
credentialEntity.setDevice(credModel.getDevice());
|
||||||
credentialEntity.setHashIterations(credModel.getHashIterations());
|
credentialEntity.setHashIterations(credModel.getHashIterations());
|
||||||
|
credentialEntity.setCounter(credModel.getCounter());
|
||||||
|
credentialEntity.setAlgorithm(credModel.getAlgorithm());
|
||||||
|
credentialEntity.setDigits(credModel.getDigits());
|
||||||
|
|
||||||
em.flush();
|
em.flush();
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,15 @@ public class CredentialEntity {
|
||||||
@JoinColumn(name="USER_ID")
|
@JoinColumn(name="USER_ID")
|
||||||
protected UserEntity user;
|
protected UserEntity user;
|
||||||
|
|
||||||
|
@Column(name="COUNTER")
|
||||||
|
protected int counter;
|
||||||
|
|
||||||
|
@Column(name="ALGORITHM")
|
||||||
|
protected String algorithm;
|
||||||
|
@Column(name="DIGITS")
|
||||||
|
protected int digits;
|
||||||
|
|
||||||
|
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
@ -108,4 +117,27 @@ public class CredentialEntity {
|
||||||
this.createdDate = createdDate;
|
this.createdDate = createdDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getCounter() {
|
||||||
|
return counter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCounter(int counter) {
|
||||||
|
this.counter = counter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAlgorithm() {
|
||||||
|
return algorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAlgorithm(String algorithm) {
|
||||||
|
this.algorithm = algorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDigits() {
|
||||||
|
return digits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDigits(int digits) {
|
||||||
|
this.digits = digits;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,8 +55,22 @@ public class RealmEntity {
|
||||||
protected boolean resetPasswordAllowed;
|
protected boolean resetPasswordAllowed;
|
||||||
@Column(name="REMEMBER_ME")
|
@Column(name="REMEMBER_ME")
|
||||||
protected boolean rememberMe;
|
protected boolean rememberMe;
|
||||||
|
|
||||||
@Column(name="PASSWORD_POLICY")
|
@Column(name="PASSWORD_POLICY")
|
||||||
protected String passwordPolicy;
|
protected String passwordPolicy;
|
||||||
|
|
||||||
|
@Column(name="OTP_POLICY_TYPE")
|
||||||
|
protected String otpPolicyType;
|
||||||
|
@Column(name="OTP_POLICY_ALG")
|
||||||
|
protected String otpPolicyAlgorithm;
|
||||||
|
@Column(name="OTP_POLICY_COUNTER")
|
||||||
|
protected int otpPolicyInitialCounter;
|
||||||
|
@Column(name="OTP_POLICY_DIGITS")
|
||||||
|
protected int otpPolicyDigits;
|
||||||
|
@Column(name="OTP_POLICY_WINDOW")
|
||||||
|
protected int otpPolicyLookAheadWindow;
|
||||||
|
|
||||||
|
|
||||||
@Column(name="EDIT_USERNAME_ALLOWED")
|
@Column(name="EDIT_USERNAME_ALLOWED")
|
||||||
protected boolean editUsernameAllowed;
|
protected boolean editUsernameAllowed;
|
||||||
|
|
||||||
|
@ -580,5 +594,44 @@ public class RealmEntity {
|
||||||
this.authenticationFlows = authenticationFlows;
|
this.authenticationFlows = authenticationFlows;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getOtpPolicyType() {
|
||||||
|
return otpPolicyType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOtpPolicyType(String otpPolicyType) {
|
||||||
|
this.otpPolicyType = otpPolicyType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOtpPolicyAlgorithm() {
|
||||||
|
return otpPolicyAlgorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOtpPolicyAlgorithm(String otpPolicyAlgorithm) {
|
||||||
|
this.otpPolicyAlgorithm = otpPolicyAlgorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getOtpPolicyInitialCounter() {
|
||||||
|
return otpPolicyInitialCounter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOtpPolicyInitialCounter(int otpPolicyInitialCounter) {
|
||||||
|
this.otpPolicyInitialCounter = otpPolicyInitialCounter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getOtpPolicyDigits() {
|
||||||
|
return otpPolicyDigits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOtpPolicyDigits(int otpPolicyDigits) {
|
||||||
|
this.otpPolicyDigits = otpPolicyDigits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getOtpPolicyLookAheadWindow() {
|
||||||
|
return otpPolicyLookAheadWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOtpPolicyLookAheadWindow(int otpPolicyLookAheadWindow) {
|
||||||
|
this.otpPolicyLookAheadWindow = otpPolicyLookAheadWindow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import org.keycloak.models.IdentityProviderMapperModel;
|
||||||
import org.keycloak.models.IdentityProviderModel;
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.ModelDuplicateException;
|
import org.keycloak.models.ModelDuplicateException;
|
||||||
|
import org.keycloak.models.OTPPolicy;
|
||||||
import org.keycloak.models.PasswordPolicy;
|
import org.keycloak.models.PasswordPolicy;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.RealmProvider;
|
import org.keycloak.models.RealmProvider;
|
||||||
|
@ -66,6 +67,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
|
||||||
protected volatile transient X509Certificate certificate;
|
protected volatile transient X509Certificate certificate;
|
||||||
protected volatile transient Key codeSecretKey;
|
protected volatile transient Key codeSecretKey;
|
||||||
|
|
||||||
|
private volatile transient OTPPolicy otpPolicy;
|
||||||
private volatile transient PasswordPolicy passwordPolicy;
|
private volatile transient PasswordPolicy passwordPolicy;
|
||||||
private volatile transient KeycloakSession session;
|
private volatile transient KeycloakSession session;
|
||||||
|
|
||||||
|
@ -272,6 +274,30 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
|
||||||
updateRealm();
|
updateRealm();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OTPPolicy getOTPPolicy() {
|
||||||
|
if (otpPolicy == null) {
|
||||||
|
otpPolicy = new OTPPolicy();
|
||||||
|
otpPolicy.setDigits(realm.getOtpPolicyDigits());
|
||||||
|
otpPolicy.setAlgorithm(realm.getOtpPolicyAlgorithm());
|
||||||
|
otpPolicy.setInitialCounter(realm.getOtpPolicyInitialCounter());
|
||||||
|
otpPolicy.setLookAheadWindow(realm.getOtpPolicyLookAheadWindow());
|
||||||
|
otpPolicy.setType(realm.getOtpPolicyType());
|
||||||
|
}
|
||||||
|
return otpPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setOTPPolicy(OTPPolicy policy) {
|
||||||
|
realm.setOtpPolicyAlgorithm(policy.getAlgorithm());
|
||||||
|
realm.setOtpPolicyDigits(policy.getDigits());
|
||||||
|
realm.setOtpPolicyInitialCounter(policy.getInitialCounter());
|
||||||
|
realm.setOtpPolicyLookAheadWindow(policy.getLookAheadWindow());
|
||||||
|
realm.setOtpPolicyType(policy.getType());
|
||||||
|
updateRealm();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getNotBefore() {
|
public int getNotBefore() {
|
||||||
return realm.getNotBefore();
|
return realm.getNotBefore();
|
||||||
|
|
|
@ -7,6 +7,7 @@ import com.mongodb.QueryBuilder;
|
||||||
|
|
||||||
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
|
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.OTPPolicy;
|
||||||
import org.keycloak.models.ProtocolMapperModel;
|
import org.keycloak.models.ProtocolMapperModel;
|
||||||
import org.keycloak.models.UserConsentModel;
|
import org.keycloak.models.UserConsentModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
@ -227,12 +228,12 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isTotp() {
|
public boolean isOtpEnabled() {
|
||||||
return user.isTotp();
|
return user.isTotp();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setTotp(boolean totp) {
|
public void setOtpEnabled(boolean totp) {
|
||||||
user.setTotp(totp);
|
user.setTotp(totp);
|
||||||
updateUser();
|
updateUser();
|
||||||
}
|
}
|
||||||
|
@ -242,6 +243,8 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
|
||||||
|
|
||||||
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
|
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
|
||||||
updatePasswordCredential(cred);
|
updatePasswordCredential(cred);
|
||||||
|
} else if (UserCredentialModel.isOtp(cred.getType())){
|
||||||
|
updateOtpCredential(cred);
|
||||||
} else {
|
} else {
|
||||||
CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType());
|
CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType());
|
||||||
|
|
||||||
|
@ -256,6 +259,27 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
|
||||||
getMongoStore().updateEntity(user, invocationContext);
|
getMongoStore().updateEntity(user, invocationContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateOtpCredential(UserCredentialModel cred) {
|
||||||
|
CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType());
|
||||||
|
|
||||||
|
if (credentialEntity == null) {
|
||||||
|
credentialEntity = setCredentials(user, cred);
|
||||||
|
credentialEntity.setValue(cred.getValue());
|
||||||
|
OTPPolicy otpPolicy = realm.getOTPPolicy();
|
||||||
|
credentialEntity.setAlgorithm(otpPolicy.getAlgorithm());
|
||||||
|
credentialEntity.setDigits(otpPolicy.getDigits());
|
||||||
|
credentialEntity.setCounter(otpPolicy.getInitialCounter());
|
||||||
|
user.getCredentials().add(credentialEntity);
|
||||||
|
} else {
|
||||||
|
credentialEntity.setValue(cred.getValue());
|
||||||
|
OTPPolicy policy = realm.getOTPPolicy();
|
||||||
|
credentialEntity.setDigits(policy.getDigits());
|
||||||
|
credentialEntity.setCounter(policy.getInitialCounter());
|
||||||
|
credentialEntity.setAlgorithm(policy.getAlgorithm());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private void updatePasswordCredential(UserCredentialModel cred) {
|
private void updatePasswordCredential(UserCredentialModel cred) {
|
||||||
CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType());
|
CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType());
|
||||||
|
|
||||||
|
@ -362,6 +386,9 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
|
||||||
credModel.setValue(credEntity.getValue());
|
credModel.setValue(credEntity.getValue());
|
||||||
credModel.setSalt(credEntity.getSalt());
|
credModel.setSalt(credEntity.getSalt());
|
||||||
credModel.setHashIterations(credEntity.getHashIterations());
|
credModel.setHashIterations(credEntity.getHashIterations());
|
||||||
|
credModel.setCounter(credEntity.getCounter());
|
||||||
|
credModel.setAlgorithm(credEntity.getAlgorithm());
|
||||||
|
credModel.setDigits(credEntity.getDigits());
|
||||||
|
|
||||||
result.add(credModel);
|
result.add(credModel);
|
||||||
}
|
}
|
||||||
|
@ -384,6 +411,9 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
|
||||||
credentialEntity.setSalt(credModel.getSalt());
|
credentialEntity.setSalt(credModel.getSalt());
|
||||||
credentialEntity.setDevice(credModel.getDevice());
|
credentialEntity.setDevice(credModel.getDevice());
|
||||||
credentialEntity.setHashIterations(credModel.getHashIterations());
|
credentialEntity.setHashIterations(credModel.getHashIterations());
|
||||||
|
credentialEntity.setCounter(credModel.getCounter());
|
||||||
|
credentialEntity.setAlgorithm(credModel.getAlgorithm());
|
||||||
|
credentialEntity.setDigits(credModel.getDigits());
|
||||||
|
|
||||||
|
|
||||||
getMongoStore().updateEntity(user, invocationContext);
|
getMongoStore().updateEntity(user, invocationContext);
|
||||||
|
|
|
@ -45,7 +45,7 @@ public class OTPFormAuthenticator extends AbstractFormAuthenticator implements A
|
||||||
context.challenge(challengeResponse);
|
context.challenge(challengeResponse);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
credentials.add(UserCredentialModel.totp(password));
|
credentials.add(UserCredentialModel.otp(context.getRealm().getOTPPolicy().getType(), password));
|
||||||
boolean valid = context.getSession().users().validCredentials(context.getRealm(), context.getUser(), credentials);
|
boolean valid = context.getSession().users().validCredentials(context.getRealm(), context.getUser(), credentials);
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
context.getEvent().user(context.getUser())
|
context.getEvent().user(context.getUser())
|
||||||
|
@ -75,7 +75,7 @@ public class OTPFormAuthenticator extends AbstractFormAuthenticator implements A
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
|
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||||
return session.users().configuredForCredentialType(UserCredentialModel.TOTP, realm, user) && user.isTotp();
|
return session.users().configuredForCredentialType(realm.getOTPPolicy().getType(), realm, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -50,7 +50,7 @@ public class ValidateOTP extends AbstractDirectGrantAuthenticator {
|
||||||
context.failure(AuthenticationProcessor.Error.INVALID_USER, challengeResponse);
|
context.failure(AuthenticationProcessor.Error.INVALID_USER, challengeResponse);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
credentials.add(UserCredentialModel.totp(otp));
|
credentials.add(UserCredentialModel.otp(context.getRealm().getOTPPolicy().getType(), otp));
|
||||||
boolean valid = context.getSession().users().validCredentials(context.getRealm(), context.getUser(), credentials);
|
boolean valid = context.getSession().users().validCredentials(context.getRealm(), context.getUser(), credentials);
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
context.getEvent().user(context.getUser());
|
context.getEvent().user(context.getUser());
|
||||||
|
@ -74,7 +74,7 @@ public class ValidateOTP extends AbstractDirectGrantAuthenticator {
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isConfigured(KeycloakSession session, RealmModel realm, UserModel user) {
|
private boolean isConfigured(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||||
return session.users().configuredForCredentialType(UserCredentialModel.TOTP, realm, user) && user.isTotp();
|
return session.users().configuredForCredentialType(realm.getOTPPolicy().getType(), realm, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -11,6 +11,7 @@ import org.keycloak.models.BrowserSecurityHeaders;
|
||||||
import org.keycloak.models.Constants;
|
import org.keycloak.models.Constants;
|
||||||
import org.keycloak.models.ImpersonationConstants;
|
import org.keycloak.models.ImpersonationConstants;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.OTPPolicy;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.RealmProvider;
|
import org.keycloak.models.RealmProvider;
|
||||||
import org.keycloak.models.RoleModel;
|
import org.keycloak.models.RoleModel;
|
||||||
|
@ -150,6 +151,7 @@ public class RealmManager {
|
||||||
realm.setMaxDeltaTimeSeconds(60 * 60 * 12); // 12 hours
|
realm.setMaxDeltaTimeSeconds(60 * 60 * 12); // 12 hours
|
||||||
realm.setFailureFactor(30);
|
realm.setFailureFactor(30);
|
||||||
realm.setSslRequired(SslRequired.EXTERNAL);
|
realm.setSslRequired(SslRequired.EXTERNAL);
|
||||||
|
realm.setOTPPolicy(OTPPolicy.DEFAULT_POLICY);
|
||||||
|
|
||||||
realm.setEventsListeners(Collections.singleton("jboss-logging"));
|
realm.setEventsListeners(Collections.singleton("jboss-logging"));
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,11 +22,6 @@
|
||||||
package org.keycloak.services.resources;
|
package org.keycloak.services.resources;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.jboss.resteasy.spi.BadRequestException;
|
|
||||||
import org.jboss.resteasy.spi.HttpRequest;
|
|
||||||
import org.keycloak.AbstractOAuthClient;
|
|
||||||
import org.keycloak.ClientConnection;
|
|
||||||
import org.keycloak.OAuth2Constants;
|
|
||||||
import org.keycloak.account.AccountPages;
|
import org.keycloak.account.AccountPages;
|
||||||
import org.keycloak.account.AccountProvider;
|
import org.keycloak.account.AccountProvider;
|
||||||
import org.keycloak.events.Details;
|
import org.keycloak.events.Details;
|
||||||
|
@ -38,10 +33,8 @@ import org.keycloak.login.LoginFormsProvider;
|
||||||
import org.keycloak.models.AccountRoles;
|
import org.keycloak.models.AccountRoles;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.ClientSessionModel;
|
import org.keycloak.models.ClientSessionModel;
|
||||||
import org.keycloak.models.Constants;
|
|
||||||
import org.keycloak.models.FederatedIdentityModel;
|
import org.keycloak.models.FederatedIdentityModel;
|
||||||
import org.keycloak.models.IdentityProviderModel;
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
|
||||||
import org.keycloak.models.ModelDuplicateException;
|
import org.keycloak.models.ModelDuplicateException;
|
||||||
import org.keycloak.models.ModelException;
|
import org.keycloak.models.ModelException;
|
||||||
import org.keycloak.models.ModelReadOnlyException;
|
import org.keycloak.models.ModelReadOnlyException;
|
||||||
|
@ -50,11 +43,11 @@ import org.keycloak.models.UserCredentialModel;
|
||||||
import org.keycloak.models.UserCredentialValueModel;
|
import org.keycloak.models.UserCredentialValueModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.UserSessionModel;
|
import org.keycloak.models.UserSessionModel;
|
||||||
|
import org.keycloak.models.utils.CredentialValidation;
|
||||||
import org.keycloak.models.utils.FormMessage;
|
import org.keycloak.models.utils.FormMessage;
|
||||||
import org.keycloak.models.utils.ModelToRepresentation;
|
import org.keycloak.models.utils.ModelToRepresentation;
|
||||||
import org.keycloak.models.utils.TimeBasedOTP;
|
import org.keycloak.models.utils.TimeBasedOTP;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
|
|
||||||
import org.keycloak.protocol.oidc.utils.RedirectUtils;
|
import org.keycloak.protocol.oidc.utils.RedirectUtils;
|
||||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||||
import org.keycloak.representations.idm.UserRepresentation;
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
|
@ -65,7 +58,6 @@ import org.keycloak.services.managers.Auth;
|
||||||
import org.keycloak.services.managers.AuthenticationManager;
|
import org.keycloak.services.managers.AuthenticationManager;
|
||||||
import org.keycloak.services.managers.ClientSessionCode;
|
import org.keycloak.services.managers.ClientSessionCode;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
import org.keycloak.services.util.CookieHelper;
|
|
||||||
import org.keycloak.services.util.ResolveRelative;
|
import org.keycloak.services.util.ResolveRelative;
|
||||||
import org.keycloak.services.validation.Validation;
|
import org.keycloak.services.validation.Validation;
|
||||||
import org.keycloak.util.UriUtils;
|
import org.keycloak.util.UriUtils;
|
||||||
|
@ -76,12 +68,8 @@ import javax.ws.rs.OPTIONS;
|
||||||
import javax.ws.rs.POST;
|
import javax.ws.rs.POST;
|
||||||
import javax.ws.rs.Path;
|
import javax.ws.rs.Path;
|
||||||
import javax.ws.rs.QueryParam;
|
import javax.ws.rs.QueryParam;
|
||||||
import javax.ws.rs.core.Context;
|
|
||||||
import javax.ws.rs.core.Cookie;
|
|
||||||
import javax.ws.rs.core.HttpHeaders;
|
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.MultivaluedMap;
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
import javax.ws.rs.core.NewCookie;
|
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import javax.ws.rs.core.UriBuilder;
|
import javax.ws.rs.core.UriBuilder;
|
||||||
import javax.ws.rs.core.UriInfo;
|
import javax.ws.rs.core.UriInfo;
|
||||||
|
@ -441,7 +429,7 @@ public class AccountService extends AbstractSecuredLocalService {
|
||||||
csrfCheck(stateChecker);
|
csrfCheck(stateChecker);
|
||||||
|
|
||||||
UserModel user = auth.getUser();
|
UserModel user = auth.getUser();
|
||||||
user.setTotp(false);
|
user.setOtpEnabled(false);
|
||||||
|
|
||||||
event.event(EventType.REMOVE_TOTP).client(auth.getClient()).user(auth.getUser()).success();
|
event.event(EventType.REMOVE_TOTP).client(auth.getClient()).user(auth.getUser()).success();
|
||||||
|
|
||||||
|
@ -552,17 +540,23 @@ public class AccountService extends AbstractSecuredLocalService {
|
||||||
if (Validation.isBlank(totp)) {
|
if (Validation.isBlank(totp)) {
|
||||||
setReferrerOnPage();
|
setReferrerOnPage();
|
||||||
return account.setError(Messages.MISSING_TOTP).createResponse(AccountPages.TOTP);
|
return account.setError(Messages.MISSING_TOTP).createResponse(AccountPages.TOTP);
|
||||||
} else if (!new TimeBasedOTP().validate(totp, totpSecret.getBytes())) {
|
} else if (!CredentialValidation.validOTP(realm, totp, totpSecret)) {
|
||||||
setReferrerOnPage();
|
setReferrerOnPage();
|
||||||
return account.setError(Messages.INVALID_TOTP).createResponse(AccountPages.TOTP);
|
return account.setError(Messages.INVALID_TOTP).createResponse(AccountPages.TOTP);
|
||||||
}
|
}
|
||||||
|
|
||||||
UserCredentialModel credentials = new UserCredentialModel();
|
UserCredentialModel credentials = new UserCredentialModel();
|
||||||
credentials.setType(CredentialRepresentation.TOTP);
|
credentials.setType(realm.getOTPPolicy().getType());
|
||||||
credentials.setValue(totpSecret);
|
credentials.setValue(totpSecret);
|
||||||
session.users().updateCredential(realm, user, credentials);
|
session.users().updateCredential(realm, user, credentials);
|
||||||
|
|
||||||
user.setTotp(true);
|
user.setOtpEnabled(true);
|
||||||
|
|
||||||
|
// to update counter
|
||||||
|
UserCredentialModel cred = new UserCredentialModel();
|
||||||
|
cred.setType(realm.getOTPPolicy().getType());
|
||||||
|
cred.setValue(totp);
|
||||||
|
session.users().validCredentials(realm, user, cred);
|
||||||
|
|
||||||
event.event(EventType.UPDATE_TOTP).client(auth.getClient()).user(auth.getUser()).success();
|
event.event(EventType.UPDATE_TOTP).client(auth.getClient()).user(auth.getUser()).success();
|
||||||
|
|
||||||
|
|
|
@ -47,6 +47,7 @@ import org.keycloak.models.UserCredentialModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.UserModel.RequiredAction;
|
import org.keycloak.models.UserModel.RequiredAction;
|
||||||
import org.keycloak.models.UserSessionModel;
|
import org.keycloak.models.UserSessionModel;
|
||||||
|
import org.keycloak.models.utils.CredentialValidation;
|
||||||
import org.keycloak.models.utils.DefaultAuthenticationFlows;
|
import org.keycloak.models.utils.DefaultAuthenticationFlows;
|
||||||
import org.keycloak.models.utils.FormMessage;
|
import org.keycloak.models.utils.FormMessage;
|
||||||
import org.keycloak.models.utils.TimeBasedOTP;
|
import org.keycloak.models.utils.TimeBasedOTP;
|
||||||
|
@ -563,18 +564,25 @@ public class LoginActionsService {
|
||||||
return loginForms.setError(Messages.MISSING_TOTP)
|
return loginForms.setError(Messages.MISSING_TOTP)
|
||||||
.setClientSessionCode(accessCode.getCode())
|
.setClientSessionCode(accessCode.getCode())
|
||||||
.createResponse(RequiredAction.CONFIGURE_TOTP);
|
.createResponse(RequiredAction.CONFIGURE_TOTP);
|
||||||
} else if (!new TimeBasedOTP().validate(totp, totpSecret.getBytes())) {
|
} else if (!CredentialValidation.validOTP(realm, totp, totpSecret)) {
|
||||||
return loginForms.setError(Messages.INVALID_TOTP)
|
return loginForms.setError(Messages.INVALID_TOTP)
|
||||||
.setClientSessionCode(accessCode.getCode())
|
.setClientSessionCode(accessCode.getCode())
|
||||||
.createResponse(RequiredAction.CONFIGURE_TOTP);
|
.createResponse(RequiredAction.CONFIGURE_TOTP);
|
||||||
}
|
}
|
||||||
|
|
||||||
UserCredentialModel credentials = new UserCredentialModel();
|
UserCredentialModel credentials = new UserCredentialModel();
|
||||||
credentials.setType(CredentialRepresentation.TOTP);
|
credentials.setType(realm.getOTPPolicy().getType());
|
||||||
credentials.setValue(totpSecret);
|
credentials.setValue(totpSecret);
|
||||||
session.users().updateCredential(realm, user, credentials);
|
session.users().updateCredential(realm, user, credentials);
|
||||||
|
|
||||||
user.setTotp(true);
|
|
||||||
|
// if type is HOTP, to update counter we execute validation based on supplied token
|
||||||
|
UserCredentialModel cred = new UserCredentialModel();
|
||||||
|
cred.setType(realm.getOTPPolicy().getType());
|
||||||
|
cred.setValue(totp);
|
||||||
|
session.users().validCredentials(realm, user, cred);
|
||||||
|
|
||||||
|
user.setOtpEnabled(true);
|
||||||
|
|
||||||
user.removeRequiredAction(RequiredAction.CONFIGURE_TOTP);
|
user.removeRequiredAction(RequiredAction.CONFIGURE_TOTP);
|
||||||
|
|
||||||
|
|
|
@ -69,7 +69,6 @@ import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
@ -213,7 +212,7 @@ public class UsersResource {
|
||||||
user.setLastName(rep.getLastName());
|
user.setLastName(rep.getLastName());
|
||||||
|
|
||||||
user.setEnabled(rep.isEnabled());
|
user.setEnabled(rep.isEnabled());
|
||||||
user.setTotp(rep.isTotp());
|
user.setOtpEnabled(rep.isTotp());
|
||||||
user.setEmailVerified(rep.isEmailVerified());
|
user.setEmailVerified(rep.isEmailVerified());
|
||||||
|
|
||||||
List<String> reqActions = rep.getRequiredActions();
|
List<String> reqActions = rep.getRequiredActions();
|
||||||
|
@ -821,7 +820,7 @@ public class UsersResource {
|
||||||
throw new NotFoundException("User not found");
|
throw new NotFoundException("User not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
user.setTotp(false);
|
user.setOtpEnabled(false);
|
||||||
adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo).success();
|
adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo).success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,7 @@ public class TotpGenerator {
|
||||||
public void run() {
|
public void run() {
|
||||||
String google = new String(Base32.decode(secret));
|
String google = new String(Base32.decode(secret));
|
||||||
TimeBasedOTP otp = new TimeBasedOTP();
|
TimeBasedOTP otp = new TimeBasedOTP();
|
||||||
System.out.println(otp.generate(google));
|
System.out.println(otp.generateTOTP(google));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -167,7 +167,7 @@ public class AccountTest {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//@Test
|
@Test
|
||||||
public void ideTesting() throws Exception {
|
public void ideTesting() throws Exception {
|
||||||
Thread.sleep(100000000);
|
Thread.sleep(100000000);
|
||||||
}
|
}
|
||||||
|
@ -517,11 +517,11 @@ public class AccountTest {
|
||||||
Assert.assertFalse(driver.getPageSource().contains("Remove Google"));
|
Assert.assertFalse(driver.getPageSource().contains("Remove Google"));
|
||||||
|
|
||||||
// Error with false code
|
// Error with false code
|
||||||
totpPage.configure(totp.generate(totpPage.getTotpSecret() + "123"));
|
totpPage.configure(totp.generateTOTP(totpPage.getTotpSecret() + "123"));
|
||||||
|
|
||||||
Assert.assertEquals("Invalid authenticator code.", profilePage.getError());
|
Assert.assertEquals("Invalid authenticator code.", profilePage.getError());
|
||||||
|
|
||||||
totpPage.configure(totp.generate(totpPage.getTotpSecret()));
|
totpPage.configure(totp.generateTOTP(totpPage.getTotpSecret()));
|
||||||
|
|
||||||
Assert.assertEquals("Mobile authenticator configured.", profilePage.getSuccess());
|
Assert.assertEquals("Mobile authenticator configured.", profilePage.getSuccess());
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,6 @@ import org.junit.Assert;
|
||||||
import org.junit.ClassRule;
|
import org.junit.ClassRule;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.authentication.requiredactions.UpdateTotp;
|
|
||||||
import org.keycloak.events.Details;
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.events.Event;
|
import org.keycloak.events.Event;
|
||||||
import org.keycloak.events.EventType;
|
import org.keycloak.events.EventType;
|
||||||
|
@ -113,7 +112,7 @@ public class RequiredActionTotpSetupTest {
|
||||||
|
|
||||||
totpPage.assertCurrent();
|
totpPage.assertCurrent();
|
||||||
|
|
||||||
totpPage.configure(totp.generate(totpPage.getTotpSecret()));
|
totpPage.configure(totp.generateTOTP(totpPage.getTotpSecret()));
|
||||||
|
|
||||||
String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setuptotp").assertEvent().getSessionId();
|
String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setuptotp").assertEvent().getSessionId();
|
||||||
|
|
||||||
|
@ -131,7 +130,7 @@ public class RequiredActionTotpSetupTest {
|
||||||
|
|
||||||
String totpSecret = totpPage.getTotpSecret();
|
String totpSecret = totpPage.getTotpSecret();
|
||||||
|
|
||||||
totpPage.configure(totp.generate(totpSecret));
|
totpPage.configure(totp.generateTOTP(totpSecret));
|
||||||
|
|
||||||
String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent().getSessionId();
|
String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent().getSessionId();
|
||||||
|
|
||||||
|
@ -146,7 +145,7 @@ public class RequiredActionTotpSetupTest {
|
||||||
loginPage.open();
|
loginPage.open();
|
||||||
loginPage.login("test-user@localhost", "password");
|
loginPage.login("test-user@localhost", "password");
|
||||||
String src = driver.getPageSource();
|
String src = driver.getPageSource();
|
||||||
loginTotpPage.login(totp.generate(totpSecret));
|
loginTotpPage.login(totp.generateTOTP(totpSecret));
|
||||||
|
|
||||||
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
|
|
||||||
|
@ -166,7 +165,7 @@ public class RequiredActionTotpSetupTest {
|
||||||
totpPage.assertCurrent();
|
totpPage.assertCurrent();
|
||||||
|
|
||||||
String totpCode = totpPage.getTotpSecret();
|
String totpCode = totpPage.getTotpSecret();
|
||||||
totpPage.configure(totp.generate(totpCode));
|
totpPage.configure(totp.generateTOTP(totpCode));
|
||||||
|
|
||||||
// After totp config, user should be on the app page
|
// After totp config, user should be on the app page
|
||||||
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
|
@ -184,11 +183,13 @@ public class RequiredActionTotpSetupTest {
|
||||||
loginPage.login("setupTotp2", "password2");
|
loginPage.login("setupTotp2", "password2");
|
||||||
|
|
||||||
// Totp is already configured, thus one-time password is needed, login page should be loaded
|
// Totp is already configured, thus one-time password is needed, login page should be loaded
|
||||||
|
String uri = driver.getCurrentUrl();
|
||||||
|
String src = driver.getPageSource();
|
||||||
Assert.assertTrue(loginPage.isCurrent());
|
Assert.assertTrue(loginPage.isCurrent());
|
||||||
Assert.assertFalse(totpPage.isCurrent());
|
Assert.assertFalse(totpPage.isCurrent());
|
||||||
|
|
||||||
// Login with one-time password
|
// Login with one-time password
|
||||||
loginTotpPage.login(totp.generate(totpCode));
|
loginTotpPage.login(totp.generateTOTP(totpCode));
|
||||||
|
|
||||||
loginEvent = events.expectLogin().user(userId).detail(Details.USERNAME, "setuptotp2").assertEvent();
|
loginEvent = events.expectLogin().user(userId).detail(Details.USERNAME, "setuptotp2").assertEvent();
|
||||||
|
|
||||||
|
@ -211,7 +212,7 @@ public class RequiredActionTotpSetupTest {
|
||||||
|
|
||||||
// Since the authentificator was removed, it has to be set up again
|
// Since the authentificator was removed, it has to be set up again
|
||||||
totpPage.assertCurrent();
|
totpPage.assertCurrent();
|
||||||
totpPage.configure(totp.generate(totpPage.getTotpSecret()));
|
totpPage.configure(totp.generateTOTP(totpPage.getTotpSecret()));
|
||||||
|
|
||||||
String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setuptotp2").assertEvent().getSessionId();
|
String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setuptotp2").assertEvent().getSessionId();
|
||||||
|
|
||||||
|
|
|
@ -72,7 +72,7 @@ public class BruteForceTest {
|
||||||
credentials.setValue("totpSecret");
|
credentials.setValue("totpSecret");
|
||||||
user.updateCredential(credentials);
|
user.updateCredential(credentials);
|
||||||
|
|
||||||
user.setTotp(true);
|
user.setOtpEnabled(true);
|
||||||
appRealm.setEventsListeners(Collections.singleton("dummy"));
|
appRealm.setEventsListeners(Collections.singleton("dummy"));
|
||||||
|
|
||||||
appRealm.setBruteForceProtected(true);
|
appRealm.setBruteForceProtected(true);
|
||||||
|
@ -158,14 +158,14 @@ public class BruteForceTest {
|
||||||
@Test
|
@Test
|
||||||
public void testGrantInvalidPassword() throws Exception {
|
public void testGrantInvalidPassword() throws Exception {
|
||||||
{
|
{
|
||||||
String totpSecret = totp.generate("totpSecret");
|
String totpSecret = totp.generateTOTP("totpSecret");
|
||||||
OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret);
|
OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret);
|
||||||
Assert.assertNotNull(response.getAccessToken());
|
Assert.assertNotNull(response.getAccessToken());
|
||||||
Assert.assertNull(response.getError());
|
Assert.assertNull(response.getError());
|
||||||
events.clear();
|
events.clear();
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
String totpSecret = totp.generate("totpSecret");
|
String totpSecret = totp.generateTOTP("totpSecret");
|
||||||
OAuthClient.AccessTokenResponse response = getTestToken("invalid", totpSecret);
|
OAuthClient.AccessTokenResponse response = getTestToken("invalid", totpSecret);
|
||||||
Assert.assertNull(response.getAccessToken());
|
Assert.assertNull(response.getAccessToken());
|
||||||
Assert.assertEquals(response.getError(), "invalid_grant");
|
Assert.assertEquals(response.getError(), "invalid_grant");
|
||||||
|
@ -173,7 +173,7 @@ public class BruteForceTest {
|
||||||
events.clear();
|
events.clear();
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
String totpSecret = totp.generate("totpSecret");
|
String totpSecret = totp.generateTOTP("totpSecret");
|
||||||
OAuthClient.AccessTokenResponse response = getTestToken("invalid", totpSecret);
|
OAuthClient.AccessTokenResponse response = getTestToken("invalid", totpSecret);
|
||||||
Assert.assertNull(response.getAccessToken());
|
Assert.assertNull(response.getAccessToken());
|
||||||
Assert.assertEquals(response.getError(), "invalid_grant");
|
Assert.assertEquals(response.getError(), "invalid_grant");
|
||||||
|
@ -181,7 +181,7 @@ public class BruteForceTest {
|
||||||
events.clear();
|
events.clear();
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
String totpSecret = totp.generate("totpSecret");
|
String totpSecret = totp.generateTOTP("totpSecret");
|
||||||
OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret);
|
OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret);
|
||||||
Assert.assertNull(response.getAccessToken());
|
Assert.assertNull(response.getAccessToken());
|
||||||
Assert.assertNotNull(response.getError());
|
Assert.assertNotNull(response.getError());
|
||||||
|
@ -191,7 +191,7 @@ public class BruteForceTest {
|
||||||
}
|
}
|
||||||
clearUserFailures();
|
clearUserFailures();
|
||||||
{
|
{
|
||||||
String totpSecret = totp.generate("totpSecret");
|
String totpSecret = totp.generateTOTP("totpSecret");
|
||||||
OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret);
|
OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret);
|
||||||
Assert.assertNotNull(response.getAccessToken());
|
Assert.assertNotNull(response.getAccessToken());
|
||||||
Assert.assertNull(response.getError());
|
Assert.assertNull(response.getError());
|
||||||
|
@ -203,7 +203,7 @@ public class BruteForceTest {
|
||||||
@Test
|
@Test
|
||||||
public void testGrantInvalidOtp() throws Exception {
|
public void testGrantInvalidOtp() throws Exception {
|
||||||
{
|
{
|
||||||
String totpSecret = totp.generate("totpSecret");
|
String totpSecret = totp.generateTOTP("totpSecret");
|
||||||
OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret);
|
OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret);
|
||||||
Assert.assertNotNull(response.getAccessToken());
|
Assert.assertNotNull(response.getAccessToken());
|
||||||
Assert.assertNull(response.getError());
|
Assert.assertNull(response.getError());
|
||||||
|
@ -224,7 +224,7 @@ public class BruteForceTest {
|
||||||
events.clear();
|
events.clear();
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
String totpSecret = totp.generate("totpSecret");
|
String totpSecret = totp.generateTOTP("totpSecret");
|
||||||
OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret);
|
OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret);
|
||||||
Assert.assertNull(response.getAccessToken());
|
Assert.assertNull(response.getAccessToken());
|
||||||
Assert.assertNotNull(response.getError());
|
Assert.assertNotNull(response.getError());
|
||||||
|
@ -234,7 +234,7 @@ public class BruteForceTest {
|
||||||
}
|
}
|
||||||
clearUserFailures();
|
clearUserFailures();
|
||||||
{
|
{
|
||||||
String totpSecret = totp.generate("totpSecret");
|
String totpSecret = totp.generateTOTP("totpSecret");
|
||||||
OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret);
|
OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret);
|
||||||
Assert.assertNotNull(response.getAccessToken());
|
Assert.assertNotNull(response.getAccessToken());
|
||||||
Assert.assertNull(response.getError());
|
Assert.assertNull(response.getError());
|
||||||
|
@ -244,7 +244,7 @@ public class BruteForceTest {
|
||||||
} @Test
|
} @Test
|
||||||
public void testGrantMissingOtp() throws Exception {
|
public void testGrantMissingOtp() throws Exception {
|
||||||
{
|
{
|
||||||
String totpSecret = totp.generate("totpSecret");
|
String totpSecret = totp.generateTOTP("totpSecret");
|
||||||
OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret);
|
OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret);
|
||||||
Assert.assertNotNull(response.getAccessToken());
|
Assert.assertNotNull(response.getAccessToken());
|
||||||
Assert.assertNull(response.getError());
|
Assert.assertNull(response.getError());
|
||||||
|
@ -265,7 +265,7 @@ public class BruteForceTest {
|
||||||
events.clear();
|
events.clear();
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
String totpSecret = totp.generate("totpSecret");
|
String totpSecret = totp.generateTOTP("totpSecret");
|
||||||
OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret);
|
OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret);
|
||||||
Assert.assertNull(response.getAccessToken());
|
Assert.assertNull(response.getAccessToken());
|
||||||
Assert.assertNotNull(response.getError());
|
Assert.assertNotNull(response.getError());
|
||||||
|
@ -275,7 +275,7 @@ public class BruteForceTest {
|
||||||
}
|
}
|
||||||
clearUserFailures();
|
clearUserFailures();
|
||||||
{
|
{
|
||||||
String totpSecret = totp.generate("totpSecret");
|
String totpSecret = totp.generateTOTP("totpSecret");
|
||||||
OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret);
|
OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret);
|
||||||
Assert.assertNotNull(response.getAccessToken());
|
Assert.assertNotNull(response.getAccessToken());
|
||||||
Assert.assertNull(response.getError());
|
Assert.assertNull(response.getError());
|
||||||
|
@ -353,7 +353,7 @@ public class BruteForceTest {
|
||||||
|
|
||||||
loginTotpPage.assertCurrent();
|
loginTotpPage.assertCurrent();
|
||||||
|
|
||||||
String totpSecret = totp.generate("totpSecret");
|
String totpSecret = totp.generateTOTP("totpSecret");
|
||||||
loginTotpPage.login(totpSecret);
|
loginTotpPage.login(totpSecret);
|
||||||
|
|
||||||
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
|
|
|
@ -43,7 +43,6 @@ import org.keycloak.testsuite.rule.KeycloakRule;
|
||||||
import org.keycloak.testsuite.rule.KeycloakRule.KeycloakSetup;
|
import org.keycloak.testsuite.rule.KeycloakRule.KeycloakSetup;
|
||||||
import org.keycloak.testsuite.rule.WebResource;
|
import org.keycloak.testsuite.rule.WebResource;
|
||||||
import org.keycloak.testsuite.rule.WebRule;
|
import org.keycloak.testsuite.rule.WebRule;
|
||||||
import org.keycloak.util.Time;
|
|
||||||
import org.openqa.selenium.WebDriver;
|
import org.openqa.selenium.WebDriver;
|
||||||
|
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
|
@ -66,7 +65,7 @@ public class LoginTotpTest {
|
||||||
credentials.setValue("totpSecret");
|
credentials.setValue("totpSecret");
|
||||||
user.updateCredential(credentials);
|
user.updateCredential(credentials);
|
||||||
|
|
||||||
user.setTotp(true);
|
user.setOtpEnabled(true);
|
||||||
appRealm.setEventsListeners(Collections.singleton("dummy"));
|
appRealm.setEventsListeners(Collections.singleton("dummy"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,7 +146,7 @@ public class LoginTotpTest {
|
||||||
|
|
||||||
loginTotpPage.assertCurrent();
|
loginTotpPage.assertCurrent();
|
||||||
|
|
||||||
loginTotpPage.login(totp.generate("totpSecret"));
|
loginTotpPage.login(totp.generateTOTP("totpSecret"));
|
||||||
|
|
||||||
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue