KEYCLOAK-186 Password policies
This commit is contained in:
parent
551ade29e8
commit
7f499b2833
23 changed files with 451 additions and 18 deletions
|
@ -44,6 +44,13 @@
|
||||||
<input id="oauth" type="text" ui-select2="userCredentialOptions" ng-model="realm.requiredOAuthClientCredentials" placeholder="Type a role and enter">
|
<input id="oauth" type="text" ui-select2="userCredentialOptions" ng-model="realm.requiredOAuthClientCredentials" placeholder="Type a role and enter">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="policy">Password Policy <span class="required" data-ng-show="createRealm">*</span></label>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<input class="xlarge" type="text" id="policy" name="policy" data-ng-model="realm.passwordPolicy" autofocus required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="submit" kc-save class="primary" data-ng-show="changed">Save
|
<button type="submit" kc-save class="primary" data-ng-show="changed">Save
|
||||||
|
|
|
@ -32,6 +32,7 @@ public class RealmRepresentation {
|
||||||
protected Set<String> requiredCredentials;
|
protected Set<String> requiredCredentials;
|
||||||
protected Set<String> requiredApplicationCredentials;
|
protected Set<String> requiredApplicationCredentials;
|
||||||
protected Set<String> requiredOAuthClientCredentials;
|
protected Set<String> requiredOAuthClientCredentials;
|
||||||
|
protected String passwordPolicy;
|
||||||
protected List<UserRepresentation> users;
|
protected List<UserRepresentation> users;
|
||||||
protected List<UserRoleMappingRepresentation> roleMappings;
|
protected List<UserRoleMappingRepresentation> roleMappings;
|
||||||
protected List<ScopeMappingRepresentation> scopeMappings;
|
protected List<ScopeMappingRepresentation> scopeMappings;
|
||||||
|
@ -199,6 +200,14 @@ public class RealmRepresentation {
|
||||||
this.requiredOAuthClientCredentials = requiredOAuthClientCredentials;
|
this.requiredOAuthClientCredentials = requiredOAuthClientCredentials;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getPasswordPolicy() {
|
||||||
|
return passwordPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPasswordPolicy(String passwordPolicy) {
|
||||||
|
this.passwordPolicy = passwordPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
public Integer getAccessCodeLifespan() {
|
public Integer getAccessCodeLifespan() {
|
||||||
return accessCodeLifespan;
|
return accessCodeLifespan;
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,8 @@ package org.keycloak.forms;
|
||||||
|
|
||||||
import org.keycloak.services.resources.flows.FormFlows;
|
import org.keycloak.services.resources.flows.FormFlows;
|
||||||
|
|
||||||
|
import java.util.ResourceBundle;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
*/
|
*/
|
||||||
|
@ -32,13 +34,12 @@ public class MessageBean {
|
||||||
|
|
||||||
private FormFlows.MessageType type;
|
private FormFlows.MessageType type;
|
||||||
|
|
||||||
// Message is considered ERROR by default
|
public MessageBean(String summary, FormFlows.MessageType type, ResourceBundle rb) {
|
||||||
public MessageBean(String summary) {
|
if (rb.containsKey(summary)) {
|
||||||
this(summary, FormFlows.MessageType.ERROR);
|
this.summary = rb.getString(summary);
|
||||||
}
|
} else {
|
||||||
|
this.summary = summary;
|
||||||
public MessageBean(String summary, FormFlows.MessageType type) {
|
}
|
||||||
this.summary = summary;
|
|
||||||
this.type = type;
|
this.type = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -79,19 +79,18 @@ public class FormServiceImpl implements FormService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public String process(String pageId, FormServiceDataBean dataBean){
|
public String process(String pageId, FormServiceDataBean dataBean){
|
||||||
|
|
||||||
Map<String, Object> attributes = new HashMap<String, Object>();
|
Map<String, Object> attributes = new HashMap<String, Object>();
|
||||||
|
|
||||||
|
ResourceBundle rb = ResourceBundle.getBundle(BUNDLE);
|
||||||
|
attributes.put("rb", rb);
|
||||||
|
|
||||||
if (dataBean.getMessage() != null){
|
if (dataBean.getMessage() != null){
|
||||||
attributes.put("message", new MessageBean(dataBean.getMessage(), dataBean.getMessageType()));
|
attributes.put("message", new MessageBean(dataBean.getMessage(), dataBean.getMessageType(), rb));
|
||||||
}
|
}
|
||||||
|
|
||||||
RealmBean realm = new RealmBean(dataBean.getRealm());
|
RealmBean realm = new RealmBean(dataBean.getRealm());
|
||||||
attributes.put("template", new TemplateBean(realm, dataBean.getContextPath()));
|
attributes.put("template", new TemplateBean(realm, dataBean.getContextPath()));
|
||||||
|
|
||||||
ResourceBundle rb = ResourceBundle.getBundle(BUNDLE);
|
|
||||||
attributes.put("rb", rb);
|
|
||||||
|
|
||||||
if (commandMap.containsKey(pageId)){
|
if (commandMap.containsKey(pageId)){
|
||||||
commandMap.get(pageId).exec(attributes, dataBean);
|
commandMap.get(pageId).exec(attributes, dataBean);
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
<div class="feedback-aligner">
|
<div class="feedback-aligner">
|
||||||
<#if message?has_content && message.warning>
|
<#if message?has_content && message.warning>
|
||||||
<div class="feedback warning show">
|
<div class="feedback warning show">
|
||||||
<p><strong>${rb.getString('actionWarningHeader')} ${rb.getString(message.summary)}</strong><br/>${rb.getString('actionFollow')}</p>
|
<p><strong>${rb.getString('actionWarningHeader')} ${message.summary}</strong><br/>${rb.getString('actionFollow')}</p>
|
||||||
</div>
|
</div>
|
||||||
</#if>
|
</#if>
|
||||||
</div>
|
</div>
|
||||||
|
@ -47,13 +47,13 @@
|
||||||
<#if message.error>
|
<#if message.error>
|
||||||
<div class="feedback error bottom-left show">
|
<div class="feedback error bottom-left show">
|
||||||
<p>
|
<p>
|
||||||
<strong id="loginError">${rb.getString(message.summary)}</strong><br/>${rb.getString('emailErrorInfo')}
|
<strong id="loginError">${message.summary}</strong><br/>${rb.getString('emailErrorInfo')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<#elseif message.success>
|
<#elseif message.success>
|
||||||
<div class="feedback success bottom-left show">
|
<div class="feedback success bottom-left show">
|
||||||
<p>
|
<p>
|
||||||
<strong>${rb.getString('successHeader')}</strong> ${rb.getString(message.summary)}
|
<strong>${rb.getString('successHeader')}</strong> ${message.summary}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</#if>
|
</#if>
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
<#if message?has_content && message.error>
|
<#if message?has_content && message.error>
|
||||||
<div class="feedback error bottom-left show">
|
<div class="feedback error bottom-left show">
|
||||||
<p>
|
<p>
|
||||||
<strong id="loginError">${rb.getString(message.summary)}</strong><br/>${rb.getString('emailErrorInfo')}
|
<strong id="loginError">${message.summary}</strong><br/>${rb.getString('emailErrorInfo')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</#if>
|
</#if>
|
||||||
|
|
|
@ -31,10 +31,10 @@
|
||||||
<#if message?has_content>
|
<#if message?has_content>
|
||||||
<div class="feedback-aligner">
|
<div class="feedback-aligner">
|
||||||
<#if message.success>
|
<#if message.success>
|
||||||
<div class="feedback success show"><p><strong>${rb.getString('successHeader')}</strong> ${rb.getString(message.summary)}</p></div>
|
<div class="feedback success show"><p><strong>${rb.getString('successHeader')}</strong> ${message.summary}</p></div>
|
||||||
</#if>
|
</#if>
|
||||||
<#if message.error>
|
<#if message.error>
|
||||||
<div class="feedback error show"><p><strong>${rb.getString('errorHeader')}</strong> ${rb.getString(message.summary)}</p></div>
|
<div class="feedback error show"><p><strong>${rb.getString('errorHeader')}</strong> ${message.summary}</p></div>
|
||||||
</#if>
|
</#if>
|
||||||
</div>
|
</div>
|
||||||
</#if>
|
</#if>
|
||||||
|
|
|
@ -13,6 +13,11 @@
|
||||||
<description/>
|
<description/>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
|
|
182
model/api/src/main/java/org/keycloak/models/PasswordPolicy.java
Normal file
182
model/api/src/main/java/org/keycloak/models/PasswordPolicy.java
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
package org.keycloak.models;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
public class PasswordPolicy {
|
||||||
|
|
||||||
|
private List<Policy> policies;
|
||||||
|
private String policyString;
|
||||||
|
|
||||||
|
public PasswordPolicy(String policyString) {
|
||||||
|
if (policyString == null || policyString.length() == 0) {
|
||||||
|
this.policyString = null;
|
||||||
|
policies = Collections.emptyList();
|
||||||
|
} else {
|
||||||
|
this.policyString = policyString;
|
||||||
|
policies = parse(policyString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Policy> parse(String policyString) {
|
||||||
|
List<Policy> list = new LinkedList<Policy>();
|
||||||
|
String[] policies = policyString.split(" and ");
|
||||||
|
for (String policy : policies) {
|
||||||
|
policy = policy.trim();
|
||||||
|
|
||||||
|
String name;
|
||||||
|
String[] args = null;
|
||||||
|
|
||||||
|
int i = policy.indexOf('(');
|
||||||
|
if (i == -1) {
|
||||||
|
name = policy.trim();
|
||||||
|
} else {
|
||||||
|
name = policy.substring(0, i).trim();
|
||||||
|
args = policy.substring(i + 1, policy.length() - 1).split(",");
|
||||||
|
for (int j = 0; j < args.length; j++) {
|
||||||
|
args[j] = args[j].trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.equals(Length.NAME)) {
|
||||||
|
list.add(new Length(args));
|
||||||
|
} else if (name.equals(Digits.NAME)) {
|
||||||
|
list.add(new Digits(args));
|
||||||
|
} else if (name.equals(LowerCase.NAME)) {
|
||||||
|
list.add(new LowerCase(args));
|
||||||
|
} else if (name.equals(UpperCase.NAME)) {
|
||||||
|
list.add(new UpperCase(args));
|
||||||
|
} else if (name.equals(SpecialChars.NAME)) {
|
||||||
|
list.add(new SpecialChars(args));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String validate(String password) {
|
||||||
|
for (Policy p : policies) {
|
||||||
|
String error = p.validate(password);
|
||||||
|
if (error != null) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static interface Policy {
|
||||||
|
public String validate(String password);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Length implements Policy {
|
||||||
|
private static final String NAME = "length";
|
||||||
|
private int min;
|
||||||
|
|
||||||
|
public Length(String[] args) {
|
||||||
|
min = intArg(NAME, 8, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String validate(String password) {
|
||||||
|
return password.length() < min ? "Invalid password: minimum length " + min : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Digits implements Policy {
|
||||||
|
private static final String NAME = "digits";
|
||||||
|
private int min;
|
||||||
|
|
||||||
|
public Digits(String[] args) {
|
||||||
|
min = intArg(NAME, 1, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String validate(String password) {
|
||||||
|
int count = 0;
|
||||||
|
for (char c : password.toCharArray()) {
|
||||||
|
if (Character.isDigit(c)) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count < min ? "Invalid password: must contain at least " + count + " numerical digits" : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class LowerCase implements Policy {
|
||||||
|
private static final String NAME = "lowerCase";
|
||||||
|
private int min;
|
||||||
|
|
||||||
|
public LowerCase(String[] args) {
|
||||||
|
min = intArg(NAME, 1, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String validate(String password) {
|
||||||
|
int count = 0;
|
||||||
|
for (char c : password.toCharArray()) {
|
||||||
|
if (Character.isLowerCase(c)) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count < min ? "Invalid password: must contain at least " + count + " lower case characters": null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class UpperCase implements Policy {
|
||||||
|
private static final String NAME = "upperCase";
|
||||||
|
private int min;
|
||||||
|
|
||||||
|
public UpperCase(String[] args) {
|
||||||
|
min = intArg(NAME, 1, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String validate(String password) {
|
||||||
|
int count = 0;
|
||||||
|
for (char c : password.toCharArray()) {
|
||||||
|
if (Character.isUpperCase(c)) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count < min ? "Invalid password: must contain at least " + count + " upper case characters" : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class SpecialChars implements Policy {
|
||||||
|
private static final String NAME = "specialChars";
|
||||||
|
private int min;
|
||||||
|
|
||||||
|
public SpecialChars(String[] args) {
|
||||||
|
min = intArg(NAME, 1, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String validate(String password) {
|
||||||
|
int count = 0;
|
||||||
|
for (char c : password.toCharArray()) {
|
||||||
|
if (!Character.isLetterOrDigit(c)) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count < min ? "Invalid password: must contain at least " + count + " special characters" : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int intArg(String policy, int defaultValue, String... args) {
|
||||||
|
if (args == null || args.length == 0) {
|
||||||
|
return defaultValue;
|
||||||
|
} else if (args.length == 1) {
|
||||||
|
return Integer.parseInt(args[0]);
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Invalid arguments to " + policy + ", expect no argument or single integer");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return policyString;
|
||||||
|
}
|
||||||
|
}
|
|
@ -74,6 +74,10 @@ public interface RealmModel extends RoleContainerModel, RoleMapperModel, ScopeMa
|
||||||
|
|
||||||
void addRequiredCredential(String cred);
|
void addRequiredCredential(String cred);
|
||||||
|
|
||||||
|
PasswordPolicy getPasswordPolicy();
|
||||||
|
|
||||||
|
void setPasswordPolicy(PasswordPolicy policy);
|
||||||
|
|
||||||
boolean validatePassword(UserModel user, String password);
|
boolean validatePassword(UserModel user, String password);
|
||||||
|
|
||||||
boolean validateTOTP(UserModel user, String password, String token);
|
boolean validateTOTP(UserModel user, String password, String token);
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
package org.keycloak.models;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
public class PasswordPolicyTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLength() {
|
||||||
|
PasswordPolicy policy = new PasswordPolicy("length");
|
||||||
|
Assert.assertNotNull(policy.validate("1234567"));
|
||||||
|
Assert.assertNull(policy.validate("12345678"));
|
||||||
|
|
||||||
|
policy = new PasswordPolicy("length(4)");
|
||||||
|
Assert.assertNotNull(policy.validate("123"));
|
||||||
|
Assert.assertNull(policy.validate("1234"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDigits() {
|
||||||
|
PasswordPolicy policy = new PasswordPolicy("digits");
|
||||||
|
Assert.assertNotNull(policy.validate("abcd"));
|
||||||
|
Assert.assertNull(policy.validate("abcd1"));
|
||||||
|
|
||||||
|
policy = new PasswordPolicy("digits(2)");
|
||||||
|
Assert.assertNotNull(policy.validate("abcd1"));
|
||||||
|
Assert.assertNull(policy.validate("abcd12"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLowerCase() {
|
||||||
|
PasswordPolicy policy = new PasswordPolicy("lowerCase");
|
||||||
|
Assert.assertNotNull(policy.validate("ABCD1234"));
|
||||||
|
Assert.assertNull(policy.validate("ABcD1234"));
|
||||||
|
|
||||||
|
policy = new PasswordPolicy("lowerCase(2)");
|
||||||
|
Assert.assertNotNull(policy.validate("ABcD1234"));
|
||||||
|
Assert.assertNull(policy.validate("aBcD1234"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUpperCase() {
|
||||||
|
PasswordPolicy policy = new PasswordPolicy("upperCase");
|
||||||
|
Assert.assertNotNull(policy.validate("abcd1234"));
|
||||||
|
Assert.assertNull(policy.validate("abCd1234"));
|
||||||
|
|
||||||
|
policy = new PasswordPolicy("upperCase(2)");
|
||||||
|
Assert.assertNotNull(policy.validate("abCd1234"));
|
||||||
|
Assert.assertNull(policy.validate("AbCd1234"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSpecialChars() {
|
||||||
|
PasswordPolicy policy = new PasswordPolicy("specialChars");
|
||||||
|
Assert.assertNotNull(policy.validate("abcd1234"));
|
||||||
|
Assert.assertNull(policy.validate("ab&d1234"));
|
||||||
|
|
||||||
|
policy = new PasswordPolicy("specialChars(2)");
|
||||||
|
Assert.assertNotNull(policy.validate("ab&d1234"));
|
||||||
|
Assert.assertNull(policy.validate("ab&d-234"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testComplex() {
|
||||||
|
PasswordPolicy policy = new PasswordPolicy("length(8) and digits(2) and lowerCase(2) and upperCase(2) and specialChars(2)");
|
||||||
|
Assert.assertNotNull(policy.validate("12aaBB&"));
|
||||||
|
Assert.assertNotNull(policy.validate("aaaaBB&-"));
|
||||||
|
Assert.assertNotNull(policy.validate("12AABB&-"));
|
||||||
|
Assert.assertNotNull(policy.validate("12aabb&-"));
|
||||||
|
Assert.assertNotNull(policy.validate("12aaBBcc"));
|
||||||
|
|
||||||
|
Assert.assertNull(policy.validate("12aaBB&-"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import org.bouncycastle.openssl.PEMWriter;
|
||||||
import org.keycloak.PemUtils;
|
import org.keycloak.PemUtils;
|
||||||
import org.keycloak.models.ApplicationModel;
|
import org.keycloak.models.ApplicationModel;
|
||||||
import org.keycloak.models.OAuthClientModel;
|
import org.keycloak.models.OAuthClientModel;
|
||||||
|
import org.keycloak.models.PasswordPolicy;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.RequiredCredentialModel;
|
import org.keycloak.models.RequiredCredentialModel;
|
||||||
import org.keycloak.models.RoleModel;
|
import org.keycloak.models.RoleModel;
|
||||||
|
@ -37,6 +38,7 @@ public class RealmAdapter implements RealmModel {
|
||||||
protected EntityManager em;
|
protected EntityManager em;
|
||||||
protected volatile transient PublicKey publicKey;
|
protected volatile transient PublicKey publicKey;
|
||||||
protected volatile transient PrivateKey privateKey;
|
protected volatile transient PrivateKey privateKey;
|
||||||
|
private PasswordPolicy passwordPolicy;
|
||||||
|
|
||||||
public RealmAdapter(EntityManager em, RealmEntity realm) {
|
public RealmAdapter(EntityManager em, RealmEntity realm) {
|
||||||
this.em = em;
|
this.em = em;
|
||||||
|
@ -1037,4 +1039,18 @@ public class RealmAdapter implements RealmModel {
|
||||||
em.flush();
|
em.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PasswordPolicy getPasswordPolicy() {
|
||||||
|
if (passwordPolicy == null) {
|
||||||
|
passwordPolicy = new PasswordPolicy(realm.getPasswordPolicy());
|
||||||
|
}
|
||||||
|
return passwordPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setPasswordPolicy(PasswordPolicy policy) {
|
||||||
|
this.passwordPolicy = policy;
|
||||||
|
realm.setPasswordPolicy(policy.toString());
|
||||||
|
em.flush();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ public class RealmEntity {
|
||||||
protected boolean resetPasswordAllowed;
|
protected boolean resetPasswordAllowed;
|
||||||
protected boolean social;
|
protected boolean social;
|
||||||
protected boolean automaticRegistrationAfterSocialLogin;
|
protected boolean automaticRegistrationAfterSocialLogin;
|
||||||
|
protected String passwordPolicy;
|
||||||
|
|
||||||
protected int tokenLifespan;
|
protected int tokenLifespan;
|
||||||
protected int accessCodeLifespan;
|
protected int accessCodeLifespan;
|
||||||
|
@ -269,4 +270,13 @@ public class RealmEntity {
|
||||||
public void setDefaultRoles(Collection<RoleEntity> defaultRoles) {
|
public void setDefaultRoles(Collection<RoleEntity> defaultRoles) {
|
||||||
this.defaultRoles = defaultRoles;
|
this.defaultRoles = defaultRoles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getPasswordPolicy() {
|
||||||
|
return passwordPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPasswordPolicy(String passwordPolicy) {
|
||||||
|
this.passwordPolicy = passwordPolicy;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import org.keycloak.models.ApplicationModel;
|
||||||
import org.keycloak.models.IdGenerator;
|
import org.keycloak.models.IdGenerator;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.OAuthClientModel;
|
import org.keycloak.models.OAuthClientModel;
|
||||||
|
import org.keycloak.models.PasswordPolicy;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.RequiredCredentialModel;
|
import org.keycloak.models.RequiredCredentialModel;
|
||||||
import org.keycloak.models.RoleModel;
|
import org.keycloak.models.RoleModel;
|
||||||
|
@ -62,6 +63,7 @@ public class RealmAdapter implements RealmModel {
|
||||||
protected PartitionManager partitionManager;
|
protected PartitionManager partitionManager;
|
||||||
protected RelationshipManager relationshipManager;
|
protected RelationshipManager relationshipManager;
|
||||||
protected KeycloakSession session;
|
protected KeycloakSession session;
|
||||||
|
private PasswordPolicy passwordPolicy;
|
||||||
|
|
||||||
public RealmAdapter(KeycloakSession session, RealmData realm, PartitionManager partitionManager) {
|
public RealmAdapter(KeycloakSession session, RealmData realm, PartitionManager partitionManager) {
|
||||||
this.session = session;
|
this.session = session;
|
||||||
|
@ -977,4 +979,19 @@ public class RealmAdapter implements RealmModel {
|
||||||
realm.setSocialConfig(socialConfig);
|
realm.setSocialConfig(socialConfig);
|
||||||
updateRealm();
|
updateRealm();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PasswordPolicy getPasswordPolicy() {
|
||||||
|
if (passwordPolicy == null) {
|
||||||
|
passwordPolicy = new PasswordPolicy(realm.getPasswordPolicy());
|
||||||
|
}
|
||||||
|
return passwordPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setPasswordPolicy(PasswordPolicy policy) {
|
||||||
|
this.passwordPolicy = policy;
|
||||||
|
realm.setPasswordPolicy(policy.toString());
|
||||||
|
updateRealm();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ public class RealmData extends AbstractPartition {
|
||||||
private String[] defaultRoles;
|
private String[] defaultRoles;
|
||||||
private Map<String, String> smtpConfig;
|
private Map<String, String> smtpConfig;
|
||||||
private Map<String, String> socialConfig;
|
private Map<String, String> socialConfig;
|
||||||
|
private String passwordPolicy;
|
||||||
|
|
||||||
public RealmData() {
|
public RealmData() {
|
||||||
super(null);
|
super(null);
|
||||||
|
@ -185,4 +186,13 @@ public class RealmData extends AbstractPartition {
|
||||||
public void setSocialConfig(Map<String, String> socialConfig) {
|
public void setSocialConfig(Map<String, String> socialConfig) {
|
||||||
this.socialConfig = socialConfig;
|
this.socialConfig = socialConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AttributeProperty
|
||||||
|
public String getPasswordPolicy() {
|
||||||
|
return passwordPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPasswordPolicy(String passwordPolicy) {
|
||||||
|
this.passwordPolicy = passwordPolicy;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import org.keycloak.models.ApplicationModel;
|
||||||
import org.keycloak.models.Constants;
|
import org.keycloak.models.Constants;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.OAuthClientModel;
|
import org.keycloak.models.OAuthClientModel;
|
||||||
|
import org.keycloak.models.PasswordPolicy;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.RequiredCredentialModel;
|
import org.keycloak.models.RequiredCredentialModel;
|
||||||
import org.keycloak.models.RoleModel;
|
import org.keycloak.models.RoleModel;
|
||||||
|
@ -110,6 +111,9 @@ public class RealmManager {
|
||||||
if (rep.getRequiredApplicationCredentials() != null) {
|
if (rep.getRequiredApplicationCredentials() != null) {
|
||||||
realm.updateRequiredApplicationCredentials(rep.getRequiredApplicationCredentials());
|
realm.updateRequiredApplicationCredentials(rep.getRequiredApplicationCredentials());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
realm.setPasswordPolicy(new PasswordPolicy(rep.getPasswordPolicy()));
|
||||||
|
|
||||||
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()]));
|
||||||
}
|
}
|
||||||
|
@ -222,6 +226,8 @@ public class RealmManager {
|
||||||
addOAuthClientRequiredCredential(newRealm, CredentialRepresentation.PASSWORD);
|
addOAuthClientRequiredCredential(newRealm, CredentialRepresentation.PASSWORD);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newRealm.setPasswordPolicy(new PasswordPolicy(rep.getPasswordPolicy()));
|
||||||
|
|
||||||
if (rep.getUsers() != null) {
|
if (rep.getUsers() != null) {
|
||||||
for (UserRepresentation userRep : rep.getUsers()) {
|
for (UserRepresentation userRep : rep.getUsers()) {
|
||||||
UserModel user = createUser(newRealm, userRep);
|
UserModel user = createUser(newRealm, userRep);
|
||||||
|
@ -473,6 +479,9 @@ public class RealmManager {
|
||||||
rep.setAccessCodeLifespanUserAction(realm.getAccessCodeLifespanUserAction());
|
rep.setAccessCodeLifespanUserAction(realm.getAccessCodeLifespanUserAction());
|
||||||
rep.setSmtpServer(realm.getSmtpConfig());
|
rep.setSmtpServer(realm.getSmtpConfig());
|
||||||
rep.setSocialProviders(realm.getSocialConfig());
|
rep.setSocialProviders(realm.getSocialConfig());
|
||||||
|
if (realm.getPasswordPolicy() != null) {
|
||||||
|
rep.setPasswordPolicy(realm.getPasswordPolicy().toString());
|
||||||
|
}
|
||||||
|
|
||||||
ApplicationModel accountManagementApplication = realm.getApplicationNameMap().get(Constants.ACCOUNT_APPLICATION);
|
ApplicationModel accountManagementApplication = realm.getApplicationNameMap().get(Constants.ACCOUNT_APPLICATION);
|
||||||
rep.setAccountManagement(accountManagementApplication != null && accountManagementApplication.isEnabled());
|
rep.setAccountManagement(accountManagementApplication != null && accountManagementApplication.isEnabled());
|
||||||
|
|
|
@ -255,6 +255,11 @@ public class AccountService {
|
||||||
return forms.setError(Messages.INVALID_PASSWORD_EXISTING).forwardToPassword();
|
return forms.setError(Messages.INVALID_PASSWORD_EXISTING).forwardToPassword();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String error = Validation.validatePassword(formData, realm.getPasswordPolicy());
|
||||||
|
if (error != null) {
|
||||||
|
return forms.setError(error).forwardToPassword();
|
||||||
|
}
|
||||||
|
|
||||||
UserCredentialModel credentials = new UserCredentialModel();
|
UserCredentialModel credentials = new UserCredentialModel();
|
||||||
credentials.setType(CredentialRepresentation.PASSWORD);
|
credentials.setType(CredentialRepresentation.PASSWORD);
|
||||||
credentials.setValue(passwordNew);
|
credentials.setValue(passwordNew);
|
||||||
|
|
|
@ -297,6 +297,10 @@ public class TokenService {
|
||||||
}
|
}
|
||||||
|
|
||||||
String error = Validation.validateRegistrationForm(formData, requiredCredentialTypes);
|
String error = Validation.validateRegistrationForm(formData, requiredCredentialTypes);
|
||||||
|
if (error == null) {
|
||||||
|
error = Validation.validatePassword(formData, realm.getPasswordPolicy());
|
||||||
|
}
|
||||||
|
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
return Flows.forms(realm, request, uriInfo).setError(error).setFormData(formData)
|
return Flows.forms(realm, request, uriInfo).setError(error).setFormData(formData)
|
||||||
.setSocialRegistration(isSocialRegistration).forwardToRegistration();
|
.setSocialRegistration(isSocialRegistration).forwardToRegistration();
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package org.keycloak.services.validation;
|
package org.keycloak.services.validation;
|
||||||
|
|
||||||
|
import org.keycloak.models.PasswordPolicy;
|
||||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
|
|
||||||
|
@ -38,6 +39,10 @@ public class Validation {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String validatePassword(MultivaluedMap<String, String> formData, PasswordPolicy policy) {
|
||||||
|
return policy.validate(formData.getFirst("password"));
|
||||||
|
}
|
||||||
|
|
||||||
public static String validateUpdateProfileForm(MultivaluedMap<String, String> formData) {
|
public static String validateUpdateProfileForm(MultivaluedMap<String, String> formData) {
|
||||||
if (isEmpty(formData.getFirst("firstName"))) {
|
if (isEmpty(formData.getFirst("firstName"))) {
|
||||||
return Messages.MISSING_FIRST_NAME;
|
return Messages.MISSING_FIRST_NAME;
|
||||||
|
|
|
@ -6,6 +6,7 @@ import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
import org.keycloak.models.PasswordPolicy;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.RoleModel;
|
import org.keycloak.models.RoleModel;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
@ -46,6 +47,7 @@ public class ModelTest extends AbstractKeycloakServerTest {
|
||||||
realm.setSslNotRequired(true);
|
realm.setSslNotRequired(true);
|
||||||
realm.setVerifyEmail(true);
|
realm.setVerifyEmail(true);
|
||||||
realm.setTokenLifespan(1000);
|
realm.setTokenLifespan(1000);
|
||||||
|
realm.setPasswordPolicy(new PasswordPolicy("length"));
|
||||||
realm.setAccessCodeLifespan(1001);
|
realm.setAccessCodeLifespan(1001);
|
||||||
realm.setAccessCodeLifespanUserAction(1002);
|
realm.setAccessCodeLifespanUserAction(1002);
|
||||||
realm.setPublicKeyPem("0234234");
|
realm.setPublicKeyPem("0234234");
|
||||||
|
|
|
@ -163,6 +163,36 @@ public class AccountTest {
|
||||||
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void changePasswordWithPasswordPolicy() {
|
||||||
|
keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
|
||||||
|
@Override
|
||||||
|
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
|
||||||
|
appRealm.setPasswordPolicy(new PasswordPolicy("length"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
changePasswordPage.open();
|
||||||
|
loginPage.login("test-user@localhost", "password");
|
||||||
|
|
||||||
|
changePasswordPage.changePassword("", "new", "new");
|
||||||
|
|
||||||
|
Assert.assertTrue(profilePage.isError());
|
||||||
|
|
||||||
|
changePasswordPage.changePassword("password", "new-password", "new-password");
|
||||||
|
|
||||||
|
Assert.assertTrue(profilePage.isSuccess());
|
||||||
|
} finally {
|
||||||
|
keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
|
||||||
|
@Override
|
||||||
|
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
|
||||||
|
appRealm.setPasswordPolicy(new PasswordPolicy(null));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void changeProfile() {
|
public void changeProfile() {
|
||||||
profilePage.open();
|
profilePage.open();
|
||||||
|
|
|
@ -25,6 +25,9 @@ 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.models.PasswordPolicy;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.services.managers.RealmManager;
|
||||||
import org.keycloak.testsuite.pages.AppPage;
|
import org.keycloak.testsuite.pages.AppPage;
|
||||||
import org.keycloak.testsuite.pages.AppPage.RequestType;
|
import org.keycloak.testsuite.pages.AppPage.RequestType;
|
||||||
import org.keycloak.testsuite.pages.LoginPage;
|
import org.keycloak.testsuite.pages.LoginPage;
|
||||||
|
@ -93,6 +96,37 @@ public class RegisterTest {
|
||||||
Assert.assertEquals("Please specify password.", registerPage.getError());
|
Assert.assertEquals("Please specify password.", registerPage.getError());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void registerPasswordPolicy() {
|
||||||
|
keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
|
||||||
|
@Override
|
||||||
|
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
|
||||||
|
appRealm.setPasswordPolicy(new PasswordPolicy("length"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
loginPage.open();
|
||||||
|
loginPage.clickRegister();
|
||||||
|
registerPage.assertCurrent();
|
||||||
|
|
||||||
|
registerPage.register("firstName", "lastName", "email", "registerPasswordPolicy", "pass", "pass");
|
||||||
|
|
||||||
|
registerPage.assertCurrent();
|
||||||
|
Assert.assertEquals("Invalid password: minimum length 8", registerPage.getError());
|
||||||
|
|
||||||
|
registerPage.register("firstName", "lastName", "email", "registerPasswordPolicy", "password", "password");
|
||||||
|
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
|
} finally {
|
||||||
|
keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
|
||||||
|
@Override
|
||||||
|
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
|
||||||
|
appRealm.setPasswordPolicy(new PasswordPolicy(null));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void registerUserMissingUsername() {
|
public void registerUserMissingUsername() {
|
||||||
loginPage.open();
|
loginPage.open();
|
||||||
|
|
|
@ -54,26 +54,32 @@ public class RegisterPage extends AbstractPage {
|
||||||
private WebElement loginErrorMessage;
|
private WebElement loginErrorMessage;
|
||||||
|
|
||||||
public void register(String firstName, String lastName, String email, String username, String password, String passwordConfirm) {
|
public void register(String firstName, String lastName, String email, String username, String password, String passwordConfirm) {
|
||||||
|
firstNameInput.clear();
|
||||||
if (firstName != null) {
|
if (firstName != null) {
|
||||||
firstNameInput.sendKeys(firstName);
|
firstNameInput.sendKeys(firstName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lastNameInput.clear();
|
||||||
if (lastName != null) {
|
if (lastName != null) {
|
||||||
lastNameInput.sendKeys(lastName);
|
lastNameInput.sendKeys(lastName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emailInput.clear();
|
||||||
if (email != null) {
|
if (email != null) {
|
||||||
emailInput.sendKeys(email);
|
emailInput.sendKeys(email);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
usernameInput.clear();
|
||||||
if (username != null) {
|
if (username != null) {
|
||||||
usernameInput.sendKeys(username);
|
usernameInput.sendKeys(username);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
passwordInput.clear();
|
||||||
if (password != null) {
|
if (password != null) {
|
||||||
passwordInput.sendKeys(password);
|
passwordInput.sendKeys(password);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
passwordConfirmInput.clear();
|
||||||
if (passwordConfirm != null) {
|
if (passwordConfirm != null) {
|
||||||
passwordConfirmInput.sendKeys(passwordConfirm);
|
passwordConfirmInput.sendKeys(passwordConfirm);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue