This commit is contained in:
Bill Burke 2013-09-19 17:40:44 -04:00
commit 935f6455c0
72 changed files with 2375 additions and 411 deletions

View file

@ -14,10 +14,12 @@ public class RealmRepresentation {
protected String realm; protected String realm;
protected int tokenLifespan; protected int tokenLifespan;
protected int accessCodeLifespan; protected int accessCodeLifespan;
protected int accessCodeLifespanUserAction;
protected boolean enabled; protected boolean enabled;
protected boolean sslNotRequired; protected boolean sslNotRequired;
protected boolean cookieLoginAllowed; protected boolean cookieLoginAllowed;
protected boolean registrationAllowed; protected boolean registrationAllowed;
protected boolean verifyEmail;
protected boolean social; protected boolean social;
protected boolean automaticRegistrationAfterSocialLogin; protected boolean automaticRegistrationAfterSocialLogin;
protected String privateKey; protected String privateKey;
@ -190,6 +192,14 @@ public class RealmRepresentation {
this.accessCodeLifespan = accessCodeLifespan; this.accessCodeLifespan = accessCodeLifespan;
} }
public int getAccessCodeLifespanUserAction() {
return accessCodeLifespanUserAction;
}
public void setAccessCodeLifespanUserAction(int accessCodeLifespanUserAction) {
this.accessCodeLifespanUserAction = accessCodeLifespanUserAction;
}
public List<RoleRepresentation> getRoles() { public List<RoleRepresentation> getRoles() {
return roles; return roles;
} }
@ -230,6 +240,14 @@ public class RealmRepresentation {
this.registrationAllowed = registrationAllowed; this.registrationAllowed = registrationAllowed;
} }
public boolean isVerifyEmail() {
return verifyEmail;
}
public void setVerifyEmail(boolean verifyEmail) {
this.verifyEmail = verifyEmail;
}
public boolean isSocial() { public boolean isSocial() {
return social; return social;
} }

View file

@ -13,12 +13,13 @@ public class UserRepresentation {
protected String self; // link protected String self; // link
protected String username; protected String username;
protected boolean enabled; protected String status;
protected String firstName; protected String firstName;
protected String lastName; protected String lastName;
protected String email; protected String email;
protected Map<String, String> attributes; protected Map<String, String> attributes;
protected List<CredentialRepresentation> credentials; protected List<CredentialRepresentation> credentials;
protected List<String> requiredActions;
public String getSelf() { public String getSelf() {
return self; return self;
@ -91,11 +92,19 @@ public class UserRepresentation {
return this; return this;
} }
public boolean isEnabled() { public String getStatus() {
return enabled; return status;
} }
public void setEnabled(boolean enabled) { public void setStatus(String status) {
this.enabled = enabled; this.status = status;
}
public List<String> getRequiredActions() {
return requiredActions;
}
public void setRequiredActions(List<String> requiredActions) {
this.requiredActions = requiredActions;
} }
} }

View file

@ -3,6 +3,7 @@
"enabled": true, "enabled": true,
"tokenLifespan": 300, "tokenLifespan": 300,
"accessCodeLifespan": 10, "accessCodeLifespan": 10,
"accessCodeLifespanUserAction": 600,
"sslNotRequired": true, "sslNotRequired": true,
"cookieLoginAllowed": true, "cookieLoginAllowed": true,
"registrationAllowed": true, "registrationAllowed": true,
@ -17,7 +18,7 @@
"users" : [ "users" : [
{ {
"username" : "bburke@redhat.com", "username" : "bburke@redhat.com",
"enabled" : true, "status": "ENABLED",
"attributes" : { "attributes" : {
"email" : "bburke@redhat.com" "email" : "bburke@redhat.com"
}, },
@ -28,7 +29,7 @@
}, },
{ {
"username" : "third-party", "username" : "third-party",
"enabled" : true, "status": "ENABLED",
"credentials" : [ "credentials" : [
{ "type" : "password", { "type" : "password",
"value" : "password" } "value" : "password" }

View file

@ -30,20 +30,6 @@
</div> </div>
</div> </div>
<div class="form-group clearfix block">
<label class="control-label">Enabled</label>
<div class="onoffswitch">
<input type="checkbox" data-ng-model="user.enabled" class="onoffswitch-checkbox"
name="enabled" id="enabled">
<label for="enabled" class="onoffswitch-label">
<span class="onoffswitch-inner">
<span class="onoffswitch-active">ON</span>
<span class="onoffswitch-inactive">OFF</span>
</span>
<span class="onoffswitch-switch"></span>
</label>
</div>
</div>
<div class="form-group"> <div class="form-group">
<label for="email" class="control-label">Email</label> <label for="email" class="control-label">Email</label>

View file

@ -9,7 +9,7 @@
</ul> </ul>
</div> </div>
<div id="content"> <div id="content">
<h2 class="pull-left">Users</h2> <h2 class="pull-left">Query Users</h2>
<table> <table>
<caption>Table of realm users</caption> <caption>Table of realm users</caption>
<thead> <thead>
@ -27,7 +27,6 @@
</tr> </tr>
<tr> <tr>
<tr> <tr>
<th>Enabled</th>
<th>Username</th> <th>Username</th>
<th>Last Name</th> <th>Last Name</th>
<th>First Name</th> <th>First Name</th>
@ -37,7 +36,7 @@
</thead> </thead>
<tfoot> <tfoot>
<tr> <tr>
<td colspan="3"> <td colspan="4">
<div class="table-nav"> <div class="table-nav">
<a href="#" class="first disabled">First page</a><a href="#" class="prev disabled">Previous <a href="#" class="first disabled">First page</a><a href="#" class="prev disabled">Previous
page</a><span><strong>1-8</strong> of <strong>10</strong></span><a href="#" page</a><span><strong>1-8</strong> of <strong>10</strong></span><a href="#"
@ -49,7 +48,6 @@
</tfoot> </tfoot>
<tbody class="selectable-rows"> <tbody class="selectable-rows">
<tr ng-repeat="user in users"> <tr ng-repeat="user in users">
<td>{{user.enabled}}</td>
<td><a href="#/realms/{{realm.id}}/users/{{user.username}}">{{user.username}}</a></td> <td><a href="#/realms/{{realm.id}}/users/{{user.username}}">{{user.username}}</a></td>
<td>{{user.lastName</td> <td>{{user.lastName</td>
<td>{{user.firstName</td> <td>{{user.firstName</td>

View file

@ -69,7 +69,7 @@ public class TotpBean {
} }
public boolean isEnabled() { public boolean isEnabled() {
return "ENABLED".equals(user.getUser().getAttribute("KEYCLOAK_TOTP")); return user.getUser().isTotp();
} }
public String getTotpSecret() { public String getTotpSecret() {

View file

@ -31,6 +31,7 @@ import javax.faces.context.FacesContext;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriBuilder;
import org.keycloak.services.resources.flows.FormFlows;
import org.keycloak.services.resources.flows.Urls; import org.keycloak.services.resources.flows.Urls;
/** /**
@ -56,6 +57,11 @@ public class UrlBean {
UriBuilder b = UriBuilder.fromUri(request.getRequestURI()).replaceQuery(request.getQueryString()) UriBuilder b = UriBuilder.fromUri(request.getRequestURI()).replaceQuery(request.getQueryString())
.replacePath(request.getContextPath()).path("rest"); .replacePath(request.getContextPath()).path("rest");
if (request.getAttribute(FormFlows.CODE) != null) {
b.queryParam("code", request.getAttribute(FormFlows.CODE));
}
baseURI = b.build(); baseURI = b.build();
} }

View file

@ -2,6 +2,7 @@
<ui:composition xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" <ui:composition xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:c="http://java.sun.com/jstl/core" template="template-login.xhtml"> xmlns:c="http://java.sun.com/jstl/core" template="template-login.xhtml">
<ui:define name="title">Log in to #{realm.name}</ui:define>
<ui:define name="header">Log in to <strong>#{realm.name}</strong></ui:define> <ui:define name="header">Log in to <strong>#{realm.name}</strong></ui:define>
<ui:define name="form"> <ui:define name="form">

View file

@ -2,6 +2,7 @@
<ui:composition xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" <ui:composition xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:c="http://java.sun.com/jstl/core" template="template-login.xhtml"> xmlns:c="http://java.sun.com/jstl/core" template="template-login.xhtml">
<ui:define name="title">Log in to #{realm.name}</ui:define>
<ui:define name="header">Log in to <strong>#{realm.name}</strong></ui:define> <ui:define name="header">Log in to <strong>#{realm.name}</strong></ui:define>
<ui:define name="form"> <ui:define name="form">

View file

@ -4,6 +4,7 @@
<ui:param name="bodyClass" value="register" /> <ui:param name="bodyClass" value="register" />
<ui:define name="title">#{messages.registerWith} #{realm.name}</ui:define>
<ui:define name="header">#{messages.registerWith} <strong>#{realm.name}</strong></ui:define> <ui:define name="header">#{messages.registerWith} <strong>#{realm.name}</strong></ui:define>
<ui:define name="form"> <ui:define name="form">

View file

@ -3,7 +3,7 @@
<h:head> <h:head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>#{messages.logInTo} #{realm.name}</title> <title><ui:insert name="title" /></title>
<link href="#{template.themeConfig['styles']}" rel="stylesheet" /> <link href="#{template.themeConfig['styles']}" rel="stylesheet" />
<style> <style>
body { body {
@ -34,7 +34,7 @@ body {
<h:panelGroup rendered="#{not empty error.summary}"> <h:panelGroup rendered="#{not empty error.summary}">
<div class="feedback error bottom-left show"> <div class="feedback error bottom-left show">
<p> <p>
<strong>#{messages[error.summary]}</strong> <strong id="loginError">#{messages[error.summary]}</strong>
</p> </p>
</div> </div>
</h:panelGroup> </h:panelGroup>

View file

@ -0,0 +1,6 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<body>
Please verify your email address
</body>
</html>

View file

@ -226,6 +226,11 @@
<artifactId>javase</artifactId> <artifactId>javase</artifactId>
<version>2.2</version> <version>2.2</version>
</dependency> </dependency>
<dependency>
<groupId>com.icegreen</groupId>
<artifactId>greenmail</artifactId>
<version>1.3.1b</version>
</dependency>
<dependency> <dependency>
<groupId>org.jboss.arquillian</groupId> <groupId>org.jboss.arquillian</groupId>

View file

@ -156,6 +156,11 @@
<version>3.6.6.Final</version> <version>3.6.6.Final</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>com.icegreen</groupId>
<artifactId>greenmail</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>
<plugins> <plugins>

View file

@ -0,0 +1,123 @@
/*
* JBoss, Home of Professional Open Source.
* Copyright 2012, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.keycloak.services.email;
import java.net.URI;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import java.util.List;
import java.util.Properties;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import org.jboss.resteasy.logging.Logger;
import org.keycloak.services.models.RealmModel;
import org.keycloak.services.models.UserModel;
import org.keycloak.services.resources.AccountService;
import org.keycloak.services.resources.flows.Urls;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class EmailSender {
private static final Logger log = Logger.getLogger(EmailSender.class);
private Properties properties;
public EmailSender() {
properties = new Properties();
for (Entry<Object, Object> e : System.getProperties().entrySet()) {
String key = (String) e.getKey();
if (key.startsWith("keycloak.mail.smtp.")) {
key = key.replace("keycloak.mail.smtp.", "mail.smtp.");
properties.put(key, e.getValue());
}
}
}
public void send(String address, String subject, String body) throws AddressException, MessagingException {
Session session = Session.getDefaultInstance(properties);
Message msg = new MimeMessage(session);
msg.setFrom(new InternetAddress(properties.getProperty("mail.smtp.from")));
msg.setSubject(subject);
msg.setText(body);
msg.saveChanges();
Transport transport = session.getTransport("smtp");
transport.connect(properties.getProperty("mail.smtp.user"), properties.getProperty("mail.smtp.password"));
transport.sendMessage(msg, new InternetAddress[] { new InternetAddress(address) });
}
public void sendEmailVerification(UserModel user, RealmModel realm, String code, UriInfo uriInfo) {
UriBuilder builder = Urls.accountBase(uriInfo.getBaseUri()).path(AccountService.class, "processEmailVerification");
for (Entry<String, List<String>> e : uriInfo.getQueryParameters().entrySet()) {
builder.queryParam(e.getKey(), e.getValue().toArray());
}
builder.queryParam("code", code);
URI uri = builder.build(realm.getId());
StringBuilder sb = new StringBuilder();
sb.append(uri.toString());
sb.append("\n");
sb.append("Expires in " + TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction()));
try {
send(user.getEmail(), "Verify email", sb.toString());
} catch (Exception e1) {
log.warn("Failed to send email verification");
}
}
public void sendPasswordReset(UserModel user, RealmModel realm, String code, UriInfo uriInfo) {
UriBuilder builder = Urls.accountBase(uriInfo.getBaseUri()).path(AccountService.class, "passwordPage");
for (Entry<String, List<String>> e : uriInfo.getQueryParameters().entrySet()) {
builder.queryParam(e.getKey(), e.getValue().toArray());
}
builder.queryParam("code", code);
URI uri = builder.build(realm.getId());
StringBuilder sb = new StringBuilder();
sb.append(uri.toString());
sb.append("\n");
sb.append("Expires in " + TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction()));
try {
send(user.getEmail(), "Reset password link", sb.toString());
} catch (Exception e) {
log.warn("Failed to send reset password link", e);
}
}
}

View file

@ -12,6 +12,8 @@ import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.models.RealmModel; import org.keycloak.services.models.RealmModel;
import org.keycloak.services.models.RequiredCredentialModel; import org.keycloak.services.models.RequiredCredentialModel;
import org.keycloak.services.models.UserModel; import org.keycloak.services.models.UserModel;
import org.keycloak.services.models.UserModel.RequiredAction;
import org.keycloak.services.models.UserModel.Status;
import org.keycloak.services.resources.RealmsResource; import org.keycloak.services.resources.RealmsResource;
import org.keycloak.services.resources.SaasService; import org.keycloak.services.resources.SaasService;
@ -197,7 +199,17 @@ public class AuthenticationManager {
} }
} }
public boolean authenticateForm(RealmModel realm, UserModel user, MultivaluedMap<String, String> formData) { public AuthenticationStatus authenticateForm(RealmModel realm, UserModel user, MultivaluedMap<String, String> formData) {
if (user == null) {
logger.info("Not Authenticated! Incorrect user name");
return AuthenticationStatus.INVALID_USER;
}
if (!user.isEnabled() && user.getStatus() == Status.DISABLED) {
logger.info("Account is disabled, contact admin.");
return AuthenticationStatus.ACCOUNT_DISABLED;
}
Set<String> types = new HashSet<String>(); Set<String> types = new HashSet<String>();
List<RequiredCredentialModel> requiredCredentials = null; List<RequiredCredentialModel> requiredCredentials = null;
@ -207,11 +219,8 @@ public class AuthenticationManager {
requiredCredentials = realm.getRequiredOAuthClientCredentials(); requiredCredentials = realm.getRequiredOAuthClientCredentials();
} else { } else {
requiredCredentials = realm.getRequiredCredentials(); requiredCredentials = realm.getRequiredCredentials();
if (!types.contains(CredentialRepresentation.TOTP) && "ENABLED".equals(user.getAttribute("KEYCLOAK_TOTP"))) {
types.add(CredentialRepresentation.TOTP);
}
} }
for (RequiredCredentialModel credential : requiredCredentials) { for (RequiredCredentialModel credential : requiredCredentials) {
types.add(credential.getType()); types.add(credential.getType());
} }
@ -220,24 +229,39 @@ public class AuthenticationManager {
String password = formData.getFirst(CredentialRepresentation.PASSWORD); String password = formData.getFirst(CredentialRepresentation.PASSWORD);
if (password == null) { if (password == null) {
logger.warn("Password not provided"); logger.warn("Password not provided");
return false; return AuthenticationStatus.MISSING_PASSWORD;
} }
if (types.contains(CredentialRepresentation.TOTP)) { if (user.isTotp()) {
String token = formData.getFirst(CredentialRepresentation.TOTP); String token = formData.getFirst(CredentialRepresentation.TOTP);
if (token == null) { if (token == null) {
logger.warn("TOTP token not provided"); logger.warn("TOTP token not provided");
return false; return AuthenticationStatus.MISSING_TOTP;
} }
logger.info("validating TOTP"); logger.info("validating TOTP");
return realm.validateTOTP(user, password, token); if (!realm.validateTOTP(user, password, token)) {
return AuthenticationStatus.INVALID_CREDENTIALS;
}
} else { } else {
logger.info("validating password for user: " + user.getLoginName()); logger.info("validating password for user: " + user.getLoginName());
return realm.validatePassword(user, password); if (!realm.validatePassword(user, password)) {
return AuthenticationStatus.INVALID_CREDENTIALS;
}
}
if (user.getStatus() == Status.ACTIONS_REQUIRED) {
return AuthenticationStatus.ACTIONS_REQUIRED;
} else {
return AuthenticationStatus.SUCCESS;
} }
} else { } else {
logger.warn("Do not know how to authenticate user"); logger.warn("Do not know how to authenticate user");
return false; return AuthenticationStatus.FAILED;
} }
} }
public enum AuthenticationStatus {
SUCCESS, ACCOUNT_DISABLED, ACTIONS_REQUIRED, INVALID_USER, INVALID_CREDENTIALS, MISSING_PASSWORD, MISSING_TOTP, FAILED
}
} }

View file

@ -3,6 +3,7 @@ package org.keycloak.services.managers;
import org.jboss.resteasy.logging.Logger; import org.jboss.resteasy.logging.Logger;
import org.keycloak.representations.idm.*; import org.keycloak.representations.idm.*;
import org.keycloak.services.models.*; import org.keycloak.services.models.*;
import org.keycloak.services.models.UserModel.RequiredAction;
import java.security.KeyPair; import java.security.KeyPair;
import java.security.KeyPairGenerator; import java.security.KeyPairGenerator;
@ -71,9 +72,11 @@ public class RealmManager {
realm.setSocial(rep.isSocial()); realm.setSocial(rep.isSocial());
realm.setCookieLoginAllowed(rep.isCookieLoginAllowed()); realm.setCookieLoginAllowed(rep.isCookieLoginAllowed());
realm.setRegistrationAllowed(rep.isRegistrationAllowed()); realm.setRegistrationAllowed(rep.isRegistrationAllowed());
realm.setVerifyEmail(rep.isVerifyEmail());
realm.setAutomaticRegistrationAfterSocialLogin(rep.isAutomaticRegistrationAfterSocialLogin()); realm.setAutomaticRegistrationAfterSocialLogin(rep.isAutomaticRegistrationAfterSocialLogin());
realm.setSslNotRequired((rep.isSslNotRequired())); realm.setSslNotRequired((rep.isSslNotRequired()));
realm.setAccessCodeLifespan(rep.getAccessCodeLifespan()); realm.setAccessCodeLifespan(rep.getAccessCodeLifespan());
realm.setAccessCodeLifespanUserAction(rep.getAccessCodeLifespanUserAction());
realm.setTokenLifespan(rep.getTokenLifespan()); realm.setTokenLifespan(rep.getTokenLifespan());
if (rep.getRequiredOAuthClientCredentials() != null) { if (rep.getRequiredOAuthClientCredentials() != null) {
realm.updateRequiredOAuthClientCredentials(rep.getRequiredOAuthClientCredentials()); realm.updateRequiredOAuthClientCredentials(rep.getRequiredOAuthClientCredentials());
@ -104,9 +107,11 @@ public class RealmManager {
newRealm.setSocial(rep.isSocial()); newRealm.setSocial(rep.isSocial());
newRealm.setTokenLifespan(rep.getTokenLifespan()); newRealm.setTokenLifespan(rep.getTokenLifespan());
newRealm.setAccessCodeLifespan(rep.getAccessCodeLifespan()); newRealm.setAccessCodeLifespan(rep.getAccessCodeLifespan());
newRealm.setAccessCodeLifespanUserAction(rep.getAccessCodeLifespanUserAction());
newRealm.setSslNotRequired(rep.isSslNotRequired()); newRealm.setSslNotRequired(rep.isSslNotRequired());
newRealm.setCookieLoginAllowed(rep.isCookieLoginAllowed()); newRealm.setCookieLoginAllowed(rep.isCookieLoginAllowed());
newRealm.setRegistrationAllowed(rep.isRegistrationAllowed()); newRealm.setRegistrationAllowed(rep.isRegistrationAllowed());
newRealm.setVerifyEmail(rep.isVerifyEmail());
newRealm.setAutomaticRegistrationAfterSocialLogin(rep.isAutomaticRegistrationAfterSocialLogin()); newRealm.setAutomaticRegistrationAfterSocialLogin(rep.isAutomaticRegistrationAfterSocialLogin());
if (rep.getPrivateKey() == null || rep.getPublicKey() == null) { if (rep.getPrivateKey() == null || rep.getPublicKey() == null) {
generateRealmKeys(newRealm); generateRealmKeys(newRealm);
@ -203,12 +208,18 @@ public class RealmManager {
public UserModel createUser(RealmModel newRealm, UserRepresentation userRep) { public UserModel createUser(RealmModel newRealm, UserRepresentation userRep) {
UserModel user = newRealm.addUser(userRep.getUsername()); UserModel user = newRealm.addUser(userRep.getUsername());
user.setEnabled(userRep.isEnabled()); user.setStatus(UserModel.Status.valueOf(userRep.getStatus()));
user.setEmail(userRep.getEmail());
if (userRep.getAttributes() != null) { if (userRep.getAttributes() != null) {
for (Map.Entry<String, String> entry : userRep.getAttributes().entrySet()) { for (Map.Entry<String, String> entry : userRep.getAttributes().entrySet()) {
user.setAttribute(entry.getKey(), entry.getValue()); user.setAttribute(entry.getKey(), entry.getValue());
} }
} }
if (userRep.getRequiredActions() != null) {
for (String requiredAction : userRep.getRequiredActions()) {
user.addRequiredAction(RequiredAction.valueOf(requiredAction));
}
}
if (userRep.getCredentials() != null) { if (userRep.getCredentials() != null) {
for (CredentialRepresentation cred : userRep.getCredentials()) { for (CredentialRepresentation cred : userRep.getCredentials()) {
UserCredentialModel credential = new UserCredentialModel(); UserCredentialModel credential = new UserCredentialModel();
@ -306,7 +317,6 @@ public class RealmManager {
rep.setLastName(user.getLastName()); rep.setLastName(user.getLastName());
rep.setFirstName(user.getFirstName()); rep.setFirstName(user.getFirstName());
rep.setEmail(user.getEmail()); rep.setEmail(user.getEmail());
rep.setEnabled(user.isEnabled());
Map<String, String> attrs = new HashMap<String, String>(); Map<String, String> attrs = new HashMap<String, String>();
attrs.putAll(user.getAttributes()); attrs.putAll(user.getAttributes());
rep.setAttributes(attrs); rep.setAttributes(attrs);
@ -331,8 +341,11 @@ public class RealmManager {
rep.setSslNotRequired(realm.isSslNotRequired()); rep.setSslNotRequired(realm.isSslNotRequired());
rep.setCookieLoginAllowed(realm.isCookieLoginAllowed()); rep.setCookieLoginAllowed(realm.isCookieLoginAllowed());
rep.setPublicKey(realm.getPublicKeyPem()); rep.setPublicKey(realm.getPublicKeyPem());
rep.setRegistrationAllowed(realm.isRegistrationAllowed());
rep.setVerifyEmail(realm.isVerifyEmail());
rep.setTokenLifespan(realm.getTokenLifespan()); rep.setTokenLifespan(realm.getTokenLifespan());
rep.setAccessCodeLifespan(realm.getAccessCodeLifespan()); rep.setAccessCodeLifespan(realm.getAccessCodeLifespan());
rep.setAccessCodeLifespanUserAction(realm.getAccessCodeLifespanUserAction());
List<RequiredCredentialModel> requiredCredentialModels = realm.getRequiredCredentials(); List<RequiredCredentialModel> requiredCredentialModels = realm.getRequiredCredentials();
if (requiredCredentialModels.size() > 0) { if (requiredCredentialModels.size() > 0) {
rep.setRequiredCredentials(new HashSet<String>()); rep.setRequiredCredentials(new HashSet<String>());

View file

@ -19,7 +19,7 @@ public class UserManager {
rep.setEmail(user.getEmail()); rep.setEmail(user.getEmail());
rep.setLastName(user.getLastName()); rep.setLastName(user.getLastName());
rep.setFirstName(user.getFirstName()); rep.setFirstName(user.getFirstName());
rep.setEnabled(user.isEnabled()); rep.setStatus(user.getStatus().name());
rep.setUsername(user.getLoginName()); rep.setUsername(user.getLoginName());
for (Map.Entry<String, String> entry : user.getAttributes().entrySet()) { for (Map.Entry<String, String> entry : user.getAttributes().entrySet()) {
rep.attribute(entry.getKey(), entry.getValue()); rep.attribute(entry.getKey(), entry.getValue());
@ -29,7 +29,7 @@ public class UserManager {
public UserModel createUser(RealmModel newRealm, UserRepresentation userRep) { public UserModel createUser(RealmModel newRealm, UserRepresentation userRep) {
UserModel user = newRealm.addUser(userRep.getUsername()); UserModel user = newRealm.addUser(userRep.getUsername());
user.setEnabled(userRep.isEnabled()); user.setStatus(UserModel.Status.valueOf(userRep.getStatus()));
user.setEmail(userRep.getEmail()); user.setEmail(userRep.getEmail());
user.setFirstName(userRep.getFirstName()); user.setFirstName(userRep.getFirstName());
user.setLastName(userRep.getLastName()); user.setLastName(userRep.getLastName());
@ -56,7 +56,7 @@ public class UserManager {
* @param userRep * @param userRep
*/ */
public void updateUserAsAdmin(UserModel user, UserRepresentation userRep) { public void updateUserAsAdmin(UserModel user, UserRepresentation userRep) {
user.setEnabled(userRep.isEnabled()); user.setStatus(UserModel.Status.valueOf(userRep.getStatus()));
user.setEmail(userRep.getEmail()); user.setEmail(userRep.getEmail());
user.setFirstName(userRep.getFirstName()); user.setFirstName(userRep.getFirstName());
user.setLastName(userRep.getLastName()); user.setLastName(userRep.getLastName());
@ -65,6 +65,11 @@ public class UserManager {
user.setAttribute(entry.getKey(), entry.getValue()); user.setAttribute(entry.getKey(), entry.getValue());
} }
} }
if (userRep.getAttributes() != null) {
for (Map.Entry<String, String> entry : userRep.getAttributes().entrySet()) {
user.setAttribute(entry.getKey(), entry.getValue());
}
}
} }
/** /**
@ -83,6 +88,11 @@ public class UserManager {
user.setAttribute(entry.getKey(), entry.getValue()); user.setAttribute(entry.getKey(), entry.getValue());
} }
} }
if (userRep.getAttributes() != null) {
for (Map.Entry<String, String> entry : userRep.getAttributes().entrySet()) {
user.setAttribute(entry.getKey(), entry.getValue());
}
}
} }

View file

@ -35,6 +35,10 @@ public interface RealmModel {
void setRegistrationAllowed(boolean registrationAllowed); void setRegistrationAllowed(boolean registrationAllowed);
boolean isVerifyEmail();
void setVerifyEmail(boolean verifyEmail);
int getTokenLifespan(); int getTokenLifespan();
void setTokenLifespan(int tokenLifespan); void setTokenLifespan(int tokenLifespan);
@ -43,6 +47,10 @@ public interface RealmModel {
void setAccessCodeLifespan(int accessCodeLifespan); void setAccessCodeLifespan(int accessCodeLifespan);
int getAccessCodeLifespanUserAction();
void setAccessCodeLifespanUserAction(int accessCodeLifespanUserAction);
String getPublicKeyPem(); String getPublicKeyPem();
void setPublicKeyPem(String publicKeyPem); void setPublicKeyPem(String publicKeyPem);

View file

@ -1,5 +1,6 @@
package org.keycloak.services.models; package org.keycloak.services.models;
import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
@ -16,7 +17,11 @@ public interface UserModel {
boolean isEnabled(); boolean isEnabled();
void setEnabled(boolean enabled); boolean isTotp();
Status getStatus();
void setStatus(Status status);
void setAttribute(String name, String value); void setAttribute(String name, String value);
@ -26,6 +31,12 @@ public interface UserModel {
Map<String, String> getAttributes(); Map<String, String> getAttributes();
List<RequiredAction> getRequiredActions();
void addRequiredAction(RequiredAction action);
void removeRequiredAction(RequiredAction action);
String getFirstName(); String getFirstName();
void setFirstName(String firstName); void setFirstName(String firstName);
@ -37,4 +48,18 @@ public interface UserModel {
String getEmail(); String getEmail();
void setEmail(String email); void setEmail(String email);
}
boolean isEmailVerified();
void setEmailVerified(boolean verified);
void setTotp(boolean totp);
public static enum Status {
ENABLED, DISABLED, ACTIONS_REQUIRED
}
public static enum RequiredAction {
VERIFY_EMAIL, UPDATE_PROFILE, CONFIGURE_TOTP, RESET_PASSWORD
}
}

View file

@ -166,6 +166,17 @@ public class RealmAdapter implements RealmModel {
updateRealm(); updateRealm();
} }
@Override
public boolean isVerifyEmail() {
return realm.isVerifyEmail();
}
@Override
public void setVerifyEmail(boolean verifyEmail) {
realm.setVerifyEmail(verifyEmail);
updateRealm();
}
@Override @Override
public int getTokenLifespan() { public int getTokenLifespan() {
return realm.getTokenLifespan(); return realm.getTokenLifespan();
@ -188,6 +199,17 @@ public class RealmAdapter implements RealmModel {
updateRealm(); updateRealm();
} }
@Override
public int getAccessCodeLifespanUserAction() {
return realm.getAccessCodeLifespanUserAction();
}
@Override
public void setAccessCodeLifespanUserAction(int accessCodeLifespanUserAction) {
realm.setAccessCodeLifespanUserAction(accessCodeLifespanUserAction);
updateRealm();
}
@Override @Override
public String getPublicKeyPem() { public String getPublicKeyPem() {
return realm.getPublicKeyPem(); return realm.getPublicKeyPem();

View file

@ -1,18 +1,27 @@
package org.keycloak.services.models.picketlink; package org.keycloak.services.models.picketlink;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.keycloak.services.models.UserModel; import org.keycloak.services.models.UserModel;
import org.keycloak.services.models.utils.ArrayUtils;
import org.picketlink.idm.IdentityManager; import org.picketlink.idm.IdentityManager;
import org.picketlink.idm.model.Attribute; import org.picketlink.idm.model.Attribute;
import org.picketlink.idm.model.sample.User; import org.picketlink.idm.model.sample.User;
import java.util.HashMap;
import java.util.Map;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class UserAdapter implements UserModel { public class UserAdapter implements UserModel {
private static final String EMAIL_VERIFIED_ATTR = "emailVerified";
private static final String KEYCLOAK_TOTP_ATTR = "totpEnabled";
private static final String REQUIRED_ACTIONS_ATTR = "requiredActions";
private static final String STATUS_ATTR = "status";
protected User user; protected User user;
protected IdentityManager idm; protected IdentityManager idm;
@ -35,9 +44,23 @@ public class UserAdapter implements UserModel {
return user.isEnabled(); return user.isEnabled();
} }
public UserModel.Status getStatus() {
Attribute<UserModel.Status> a = user.getAttribute(STATUS_ATTR);
if (a != null) {
return a.getValue();
} else {
return user.isEnabled() ? UserModel.Status.ENABLED : UserModel.Status.DISABLED;
}
}
@Override @Override
public void setEnabled(boolean enabled) { public void setStatus(UserModel.Status status) {
user.setEnabled(enabled); user.setAttribute(new Attribute<UserModel.Status>(STATUS_ATTR, status));
if (status == UserModel.Status.DISABLED) {
user.setEnabled(false);
} else {
user.setEnabled(true);
}
idm.update(user); idm.update(user);
} }
@ -74,6 +97,18 @@ public class UserAdapter implements UserModel {
idm.update(user); idm.update(user);
} }
@Override
public boolean isEmailVerified() {
Attribute<Boolean> a = user.getAttribute(EMAIL_VERIFIED_ATTR);
return a != null ? a.getValue() : false;
}
@Override
public void setEmailVerified(boolean verified) {
user.setAttribute(new Attribute<Boolean>(EMAIL_VERIFIED_ATTR, verified));
idm.update(user);
}
@Override @Override
public void setAttribute(String name, String value) { public void setAttribute(String name, String value) {
user.setAttribute(new Attribute<String>(name, value)); user.setAttribute(new Attribute<String>(name, value));
@ -101,4 +136,75 @@ public class UserAdapter implements UserModel {
} }
return attributes; return attributes;
} }
private RequiredAction[] getRequiredActionsArray() {
Attribute<?> a = user.getAttribute(REQUIRED_ACTIONS_ATTR);
if (a == null) {
return null;
}
Object o = a.getValue();
if (o instanceof RequiredAction) {
return new RequiredAction[] { (RequiredAction) o };
} else {
return (RequiredAction[]) o;
}
}
@Override
public List<RequiredAction> getRequiredActions() {
RequiredAction[] actions = getRequiredActionsArray();
if (actions == null) {
return null;
} else {
return Collections.unmodifiableList(Arrays.asList(actions));
}
}
@Override
public void addRequiredAction(RequiredAction action) {
RequiredAction[] actions = getRequiredActionsArray();
if (actions == null) {
actions = new RequiredAction[] { action };
} else {
actions = ArrayUtils.add(actions, action);
}
Attribute<RequiredAction[]> a = new Attribute<RequiredAction[]>(REQUIRED_ACTIONS_ATTR, actions);
user.setAttribute(a);
idm.update(user);
}
@Override
public void removeRequiredAction(RequiredAction action) {
RequiredAction[] actions = getRequiredActionsArray();
if (actions != null) {
if (Arrays.binarySearch(actions, action) >= 0) {
actions = ArrayUtils.remove(actions, action);
if (actions.length == 0) {
user.removeAttribute(REQUIRED_ACTIONS_ATTR);
} else {
Attribute<RequiredAction[]> a = new Attribute<RequiredAction[]>(REQUIRED_ACTIONS_ATTR, actions);
user.setAttribute(a);
}
idm.update(user);
}
}
}
@Override
public boolean isTotp() {
Attribute<Boolean> a = user.getAttribute(KEYCLOAK_TOTP_ATTR);
return a != null ? a.getValue() : false;
}
@Override
public void setTotp(boolean totp) {
user.setAttribute(new Attribute<Boolean>(KEYCLOAK_TOTP_ATTR, totp));
idm.update(user);
}
} }

View file

@ -13,10 +13,12 @@ public class RealmData extends AbstractPartition {
private boolean sslNotRequired; private boolean sslNotRequired;
private boolean cookieLoginAllowed; private boolean cookieLoginAllowed;
private boolean registrationAllowed; private boolean registrationAllowed;
private boolean verifyEmail;
private boolean social; private boolean social;
private boolean automaticRegistrationAfterSocialLogin; private boolean automaticRegistrationAfterSocialLogin;
private int tokenLifespan; private int tokenLifespan;
private int accessCodeLifespan; private int accessCodeLifespan;
private int accessCodeLifespanUserAction;
private String publicKeyPem; private String publicKeyPem;
private String privateKeyPem; private String privateKeyPem;
private String[] defaultRoles; private String[] defaultRoles;
@ -91,6 +93,14 @@ public class RealmData extends AbstractPartition {
this.registrationAllowed = registrationAllowed; this.registrationAllowed = registrationAllowed;
} }
public boolean isVerifyEmail() {
return verifyEmail;
}
public void setVerifyEmail(boolean verifyEmail) {
this.verifyEmail = verifyEmail;
}
@AttributeProperty @AttributeProperty
public int getTokenLifespan() { public int getTokenLifespan() {
return tokenLifespan; return tokenLifespan;
@ -109,6 +119,15 @@ public class RealmData extends AbstractPartition {
this.accessCodeLifespan = accessCodeLifespan; this.accessCodeLifespan = accessCodeLifespan;
} }
@AttributeProperty
public int getAccessCodeLifespanUserAction() {
return accessCodeLifespanUserAction;
}
public void setAccessCodeLifespanUserAction(int accessCodeLifespanUserAction) {
this.accessCodeLifespanUserAction = accessCodeLifespanUserAction;
}
@AttributeProperty @AttributeProperty
public String getPublicKeyPem() { public String getPublicKeyPem() {
return publicKeyPem; return publicKeyPem;

View file

@ -35,6 +35,8 @@ public class RealmEntity implements Serializable {
@AttributeValue @AttributeValue
private boolean registrationAllowed; private boolean registrationAllowed;
@AttributeValue @AttributeValue
private boolean verifyEmail;
@AttributeValue
private boolean social; private boolean social;
@AttributeValue @AttributeValue
private boolean automaticRegistrationAfterSocialLogin; private boolean automaticRegistrationAfterSocialLogin;
@ -43,6 +45,8 @@ public class RealmEntity implements Serializable {
@AttributeValue @AttributeValue
private int accessCodeLifespan; private int accessCodeLifespan;
@AttributeValue @AttributeValue
private int accessCodeLifespanUserAction;
@AttributeValue
@Column(length = 2048) @Column(length = 2048)
private String publicKeyPem; private String publicKeyPem;
@AttributeValue @AttributeValue
@ -100,6 +104,14 @@ public class RealmEntity implements Serializable {
this.registrationAllowed = registrationAllowed; this.registrationAllowed = registrationAllowed;
} }
public boolean isVerifyEmail() {
return verifyEmail;
}
public void setVerifyEmail(boolean verifyEmail) {
this.verifyEmail = verifyEmail;
}
public boolean isSocial() { public boolean isSocial() {
return social; return social;
} }
@ -132,6 +144,14 @@ public class RealmEntity implements Serializable {
this.accessCodeLifespan = accessCodeLifespan; this.accessCodeLifespan = accessCodeLifespan;
} }
public int getAccessCodeLifespanUserAction() {
return accessCodeLifespanUserAction;
}
public void setAccessCodeLifespanUserAction(int accessCodeLifespanUserAction) {
this.accessCodeLifespanUserAction = accessCodeLifespanUserAction;
}
public String getPublicKeyPem() { public String getPublicKeyPem() {
return publicKeyPem; return publicKeyPem;
} }

View file

@ -0,0 +1,31 @@
package org.keycloak.services.models.utils;
import java.lang.reflect.Array;
import java.util.Arrays;
public class ArrayUtils {
public static <T> T[] add(T[] src, T o) {
T[] dst = Arrays.copyOf(src, src.length + 1);
dst[src.length] = o;
return dst;
}
public static <T> T[] remove(T[] src, T o) {
int l = Arrays.binarySearch(src, o);
if (l < 0) {
return src;
}
T[] dst = newInstance(o, src.length - 1);
System.arraycopy(src, 0, dst, 0, l);
System.arraycopy(src, l + 1, dst, l, dst.length - l);
return dst;
}
@SuppressWarnings("unchecked")
private static <T extends Object> T[] newInstance(T type, int length) {
return (T[]) Array.newInstance(type.getClass(), length);
}
}

View file

@ -21,24 +21,39 @@
*/ */
package org.keycloak.services.resources; package org.keycloak.services.resources;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.models.RealmModel;
import org.keycloak.services.models.UserCredentialModel;
import org.keycloak.services.models.UserModel;
import org.keycloak.services.resources.flows.Flows;
import org.keycloak.services.resources.flows.FormFlows;
import org.keycloak.services.validation.Validation;
import org.picketlink.idm.credential.util.TimeBasedOTP;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.POST; import javax.ws.rs.POST;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.core.*; import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.Providers;
import org.jboss.resteasy.jose.jws.JWSInput;
import org.jboss.resteasy.jose.jws.crypto.RSAProvider;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.email.EmailSender;
import org.keycloak.services.managers.AccessCodeEntry;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.ResourceAdminManager;
import org.keycloak.services.managers.TokenManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.models.RealmModel;
import org.keycloak.services.models.UserCredentialModel;
import org.keycloak.services.models.UserModel;
import org.keycloak.services.models.UserModel.RequiredAction;
import org.keycloak.services.resources.flows.Flows;
import org.keycloak.services.resources.flows.FormFlows;
import org.keycloak.services.resources.flows.OAuthFlows;
import org.keycloak.services.validation.Validation;
import org.picketlink.idm.credential.util.TimeBasedOTP;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -56,18 +71,24 @@ public class AccountService {
@Context @Context
private UriInfo uriInfo; private UriInfo uriInfo;
@Context
protected Providers providers;
protected AuthenticationManager authManager = new AuthenticationManager(); protected AuthenticationManager authManager = new AuthenticationManager();
public AccountService(RealmModel realm) { private TokenManager tokenManager;
public AccountService(RealmModel realm, TokenManager tokenManager) {
this.realm = realm; this.realm = realm;
this.tokenManager = tokenManager;
} }
@Path("access") @Path("access")
@GET @GET
public Response accessPage() { public Response accessPage() {
UserModel user = authManager.authenticateIdentityCookie(realm, uriInfo, headers); UserModel user = getUserFromAuthManager();
if (user != null) { if (user != null) {
return Flows.forms(realm, request).setUser(user).forwardToAccess(); return Flows.forms(realm, request, uriInfo).setUser(user).forwardToAccess();
} else { } else {
return Response.status(Status.FORBIDDEN).build(); return Response.status(Status.FORBIDDEN).build();
} }
@ -77,25 +98,81 @@ public class AccountService {
@POST @POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response processAccountUpdate(final MultivaluedMap<String, String> formData) { public Response processAccountUpdate(final MultivaluedMap<String, String> formData) {
UserModel user = authManager.authenticateIdentityCookie(realm, uriInfo, headers); UserModel user = getUser(RequiredAction.UPDATE_PROFILE);
if (user != null) { if (user != null) {
user.setFirstName(formData.getFirst("firstName")); user.setFirstName(formData.getFirst("firstName"));
user.setLastName(formData.getFirst("lastName")); user.setLastName(formData.getFirst("lastName"));
user.setEmail(formData.getFirst("email")); user.setEmail(formData.getFirst("email"));
return Flows.forms(realm, request).setUser(user).forwardToAccount(); Response response = redirectOauth();
if (response != null) {
return response;
} else {
return Flows.forms(realm, request, uriInfo).setUser(user).forwardToAccount();
}
} else { } else {
return Response.status(Status.FORBIDDEN).build(); return Response.status(Status.FORBIDDEN).build();
} }
} }
private UserModel getUser(RequiredAction action) {
if (uriInfo.getQueryParameters().containsKey(FormFlows.CODE)) {
AccessCodeEntry accessCodeEntry = getAccessCodeEntry(uriInfo.getQueryParameters().getFirst(FormFlows.CODE));
if (accessCodeEntry == null) {
return null;
}
String loginName = accessCodeEntry.getUser().getLoginName();
UserModel user = realm.getUser(loginName);
if (!user.getRequiredActions().contains(action)) {
return null;
}
if (!accessCodeEntry.getUser().getRequiredActions().contains(action)) {
return null;
}
return user;
} else {
return getUserFromAuthManager();
}
}
private UserModel getUserFromAuthManager() {
return authManager.authenticateIdentityCookie(realm, uriInfo, headers);
}
private AccessCodeEntry getAccessCodeEntry(String code) {
JWSInput input = new JWSInput(code, providers);
boolean verifiedCode = false;
try {
verifiedCode = RSAProvider.verify(input, realm.getPublicKey());
} catch (Exception ignored) {
return null;
}
if (!verifiedCode) {
return null;
}
String key = input.readContent(String.class);
AccessCodeEntry accessCodeEntry = tokenManager.getAccessCode(key);
if (accessCodeEntry == null) {
return null;
}
if (accessCodeEntry.isExpired()) {
return null;
}
return accessCodeEntry;
}
@Path("totp") @Path("totp")
@POST @POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response processTotpUpdate(final MultivaluedMap<String, String> formData) { public Response processTotpUpdate(final MultivaluedMap<String, String> formData) {
UserModel user = authManager.authenticateIdentityCookie(realm, uriInfo, headers); UserModel user = getUser(RequiredAction.CONFIGURE_TOTP);
if (user != null) { if (user != null) {
FormFlows forms = Flows.forms(realm, request); FormFlows forms = Flows.forms(realm, request, uriInfo);
String totp = formData.getFirst("totp"); String totp = formData.getFirst("totp");
String totpSecret = formData.getFirst("totpSecret"); String totpSecret = formData.getFirst("totpSecret");
@ -109,7 +186,7 @@ public class AccountService {
} }
if (error != null) { if (error != null) {
return forms.setError(error).forwardToTotp(); return forms.setError(error).setUser(user).forwardToTotp();
} }
UserCredentialModel credentials = new UserCredentialModel(); UserCredentialModel credentials = new UserCredentialModel();
@ -117,25 +194,61 @@ public class AccountService {
credentials.setValue(formData.getFirst("totpSecret")); credentials.setValue(formData.getFirst("totpSecret"));
realm.updateCredential(user, credentials); realm.updateCredential(user, credentials);
if (!user.isEnabled() && "REQUIRED".equals(user.getAttribute("KEYCLOAK_TOTP"))) { user.removeRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP);
user.setEnabled(true);
user.setTotp(true);
Response response = redirectOauth();
if (response != null) {
return response;
} else {
return Flows.forms(realm, request, uriInfo).setUser(user).forwardToTotp();
} }
user.setAttribute("KEYCLOAK_TOTP", "ENABLED");
return Flows.forms(realm, request).setUser(user).forwardToTotp();
} else { } else {
return Response.status(Status.FORBIDDEN).build(); return Response.status(Status.FORBIDDEN).build();
} }
} }
@Path("email-verify")
@GET
public Response processEmailVerification(@QueryParam("code") String code) {
AccessCodeEntry accessCodeEntry = getAccessCodeEntry(code);
String loginName = accessCodeEntry.getUser().getLoginName();
UserModel user = realm.getUser(loginName);
if (user != null) {
user.setEmailVerified(true);
user.removeRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL);
Response response = redirectOauth();
if (response != null) {
return response;
} else {
return Flows.forms(realm, request, uriInfo).setUser(user).forwardToVerifyEmail();
}
} else {
return Response.status(Status.FORBIDDEN).build();
}
}
private Response redirectOauth() {
String redirect = uriInfo.getQueryParameters().getFirst("redirect_uri");
if (redirect != null) {
AccessCodeEntry accessCode = getAccessCodeEntry(uriInfo.getQueryParameters().getFirst(FormFlows.CODE));
String state = uriInfo.getQueryParameters().getFirst("state");
return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).redirectAccessCode(accessCode, state,
redirect);
} else {
return null;
}
}
@Path("password") @Path("password")
@POST @POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response processPasswordUpdate(final MultivaluedMap<String, String> formData) { public Response processPasswordUpdate(final MultivaluedMap<String, String> formData) {
UserModel user = authManager.authenticateIdentityCookie(realm, uriInfo, headers); UserModel user = getUser(RequiredAction.RESET_PASSWORD);
if (user != null) { if (user != null) {
FormFlows forms = Flows.forms(realm, request).setUser(user); FormFlows forms = Flows.forms(realm, request, uriInfo).setUser(user);
String password = formData.getFirst("password"); String password = formData.getFirst("password");
String passwordNew = formData.getFirst("password-new"); String passwordNew = formData.getFirst("password-new");
@ -143,14 +256,18 @@ public class AccountService {
String error = null; String error = null;
if (Validation.isEmpty(password)) { if (Validation.isEmpty(passwordNew)) {
error = Messages.MISSING_PASSWORD;
} else if (Validation.isEmpty(passwordNew)) {
error = Messages.MISSING_PASSWORD; error = Messages.MISSING_PASSWORD;
} else if (!passwordNew.equals(passwordConfirm)) { } else if (!passwordNew.equals(passwordConfirm)) {
error = Messages.INVALID_PASSWORD_CONFIRM; error = Messages.INVALID_PASSWORD_CONFIRM;
} else if (!realm.validatePassword(user, password)) { }
error = Messages.INVALID_PASSWORD_EXISTING;
if (user.getRequiredActions() == null || !user.getRequiredActions().contains(RequiredAction.RESET_PASSWORD)) {
if (Validation.isEmpty(password)) {
error = Messages.MISSING_PASSWORD;
} else if (!realm.validatePassword(user, password)) {
error = Messages.INVALID_PASSWORD_EXISTING;
}
} }
if (error != null) { if (error != null) {
@ -163,7 +280,13 @@ public class AccountService {
realm.updateCredential(user, credentials); realm.updateCredential(user, credentials);
return Flows.forms(realm, request).setUser(user).forwardToPassword(); user.removeRequiredAction(RequiredAction.RESET_PASSWORD);
user.setStatus(UserModel.Status.ENABLED);
authManager.expireIdentityCookie(realm, uriInfo);
new ResourceAdminManager().singleLogOut(realm, user.getLoginName());
return Flows.forms(realm, request, uriInfo).forwardToLogin();
} else { } else {
return Response.status(Status.FORBIDDEN).build(); return Response.status(Status.FORBIDDEN).build();
} }
@ -172,9 +295,9 @@ public class AccountService {
@Path("") @Path("")
@GET @GET
public Response accountPage() { public Response accountPage() {
UserModel user = authManager.authenticateIdentityCookie(realm, uriInfo, headers); UserModel user = getUserFromAuthManager();
if (user != null) { if (user != null) {
return Flows.forms(realm, request).setUser(user).forwardToAccount(); return Flows.forms(realm, request, uriInfo).setUser(user).forwardToAccount();
} else { } else {
return Response.status(Status.FORBIDDEN).build(); return Response.status(Status.FORBIDDEN).build();
} }
@ -183,9 +306,9 @@ public class AccountService {
@Path("social") @Path("social")
@GET @GET
public Response socialPage() { public Response socialPage() {
UserModel user = authManager.authenticateIdentityCookie(realm, uriInfo, headers); UserModel user = getUserFromAuthManager();
if (user != null) { if (user != null) {
return Flows.forms(realm, request).setUser(user).forwardToSocial(); return Flows.forms(realm, request, uriInfo).setUser(user).forwardToSocial();
} else { } else {
return Response.status(Status.FORBIDDEN).build(); return Response.status(Status.FORBIDDEN).build();
} }
@ -194,9 +317,9 @@ public class AccountService {
@Path("totp") @Path("totp")
@GET @GET
public Response totpPage() { public Response totpPage() {
UserModel user = authManager.authenticateIdentityCookie(realm, uriInfo, headers); UserModel user = getUserFromAuthManager();
if (user != null) { if (user != null) {
return Flows.forms(realm, request).setUser(user).forwardToTotp(); return Flows.forms(realm, request, uriInfo).setUser(user).forwardToTotp();
} else { } else {
return Response.status(Status.FORBIDDEN).build(); return Response.status(Status.FORBIDDEN).build();
} }
@ -205,11 +328,48 @@ public class AccountService {
@Path("password") @Path("password")
@GET @GET
public Response passwordPage() { public Response passwordPage() {
UserModel user = authManager.authenticateIdentityCookie(realm, uriInfo, headers); UserModel user = getUser(RequiredAction.RESET_PASSWORD);
if (user != null) { if (user != null) {
return Flows.forms(realm, request).setUser(user).forwardToPassword(); return Flows.forms(realm, request, uriInfo).setUser(user).forwardToPassword();
} else { } else {
return Response.status(Status.FORBIDDEN).build(); return Response.status(Status.FORBIDDEN).build();
} }
} }
@Path("password-reset")
@GET
public Response resetPassword(@QueryParam("username") final String username,
@QueryParam("client_id") final String clientId, @QueryParam("scope") final String scopeParam,
@QueryParam("state") final String state, @QueryParam("redirect_uri") final String redirect) {
OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager);
if (!realm.isEnabled()) {
return oauth.forwardToSecurityFailure("Realm not enabled.");
}
UserModel client = realm.getUser(clientId);
if (client == null) {
return oauth.forwardToSecurityFailure("Unknown login requester.");
}
if (!client.isEnabled()) {
return oauth.forwardToSecurityFailure("Login requester not enabled.");
}
// String username = formData.getFirst("username");
UserModel user = realm.getUser(username);
user.addRequiredAction(RequiredAction.RESET_PASSWORD);
user.setStatus(UserModel.Status.ACTIONS_REQUIRED);
AccessCodeEntry accessCode = tokenManager.createAccessCode(scopeParam, state, redirect, realm, client, user);
accessCode.setExpiration(System.currentTimeMillis() / 1000 + realm.getAccessCodeLifespanUserAction());
if (user.getEmail() == null) {
return oauth.forwardToSecurityFailure("Email address not set, contact admin");
}
new EmailSender().sendPasswordReset(user, realm, accessCode.getCode(), uriInfo);
// TODO Add info message
return Flows.forms(realm, request, uriInfo).forwardToLogin();
}
} }

View file

@ -66,7 +66,7 @@ public class RealmsResource {
logger.debug("realm not found"); logger.debug("realm not found");
throw new NotFoundException(); throw new NotFoundException();
} }
AccountService accountService = new AccountService(realm); AccountService accountService = new AccountService(realm, tokenManager);
resourceContext.initResource(accountService); resourceContext.initResource(accountService);
return accountService; return accountService;
} }

View file

@ -8,6 +8,7 @@ import org.jboss.resteasy.spi.NotImplementedYetException;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationManager.AuthenticationStatus;
import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.services.models.*; import org.keycloak.services.models.*;
@ -169,7 +170,7 @@ public class SaasService {
RealmModel realm = realmManager.defaultRealm(); RealmModel realm = realmManager.defaultRealm();
authManager.expireSaasIdentityCookie(uriInfo); authManager.expireSaasIdentityCookie(uriInfo);
Flows.forms(realm, request).forwardToLogin(); Flows.forms(realm, request, uriInfo).forwardToLogin();
} }
@Path("registrations") @Path("registrations")
@ -180,7 +181,7 @@ public class SaasService {
RealmModel realm = realmManager.defaultRealm(); RealmModel realm = realmManager.defaultRealm();
authManager.expireSaasIdentityCookie(uriInfo); authManager.expireSaasIdentityCookie(uriInfo);
Flows.forms(realm, request).forwardToRegistration(); Flows.forms(realm, request, uriInfo).forwardToRegistration();
} }
@Path("logout") @Path("logout")
@ -191,7 +192,7 @@ public class SaasService {
RealmModel realm = realmManager.defaultRealm(); RealmModel realm = realmManager.defaultRealm();
authManager.expireSaasIdentityCookie(uriInfo); authManager.expireSaasIdentityCookie(uriInfo);
Flows.forms(realm, request).forwardToLogin(); Flows.forms(realm, request, uriInfo).forwardToLogin();
} }
@Path("logout-cookie") @Path("logout-cookie")
@ -217,29 +218,22 @@ public class SaasService {
} }
String username = formData.getFirst("username"); String username = formData.getFirst("username");
UserModel user = realm.getUser(username); UserModel user = realm.getUser(username);
if (user == null) {
logger.info("Not Authenticated! Incorrect user name");
return Flows.forms(realm, request).setError(Messages.INVALID_USER).setFormData(formData) AuthenticationStatus status = authManager.authenticateForm(realm, user, formData);
.forwardToLogin();
switch (status) {
case SUCCESS:
NewCookie cookie = authManager.createSaasIdentityCookie(realm, user, uriInfo);
return Response.status(302).cookie(cookie).location(contextRoot(uriInfo).path(adminPath).build()).build();
case ACCOUNT_DISABLED:
return Flows.forms(realm, request, uriInfo).setError(Messages.ACCOUNT_DISABLED).setFormData(formData)
.forwardToLogin();
case ACTIONS_REQUIRED:
return Flows.forms(realm, request, uriInfo).forwardToAction(user.getRequiredActions().get(0));
default:
return Flows.forms(realm, request, uriInfo).setError(Messages.INVALID_USER).setFormData(formData)
.forwardToLogin();
} }
if (!user.isEnabled()) {
logger.info("Account is disabled, contact admin.");
return Flows.forms(realm, request).setError(Messages.ACCOUNT_DISABLED)
.setFormData(formData).forwardToLogin();
}
boolean authenticated = authManager.authenticateForm(realm, user, formData);
if (!authenticated) {
logger.info("Not Authenticated! Invalid credentials");
return Flows.forms(realm, request).setError(Messages.INVALID_PASSWORD).setFormData(formData)
.forwardToLogin();
}
NewCookie cookie = authManager.createSaasIdentityCookie(realm, user, uriInfo);
return Response.status(302).cookie(cookie).location(contextRoot(uriInfo).path(adminPath).build()).build();
} }
@Path("registrations") @Path("registrations")
@ -270,7 +264,7 @@ public class SaasService {
String error = Validation.validateRegistrationForm(formData, requiredCredentialTypes); String error = Validation.validateRegistrationForm(formData, requiredCredentialTypes);
if (error != null) { if (error != null) {
return Flows.forms(defaultRealm, request).setError(error).setFormData(formData) return Flows.forms(defaultRealm, request, uriInfo).setError(error).setFormData(formData)
.forwardToRegistration(); .forwardToRegistration();
} }
@ -312,7 +306,7 @@ public class SaasService {
UserModel user = registerMe(defaultRealm, newUser); UserModel user = registerMe(defaultRealm, newUser);
if (user == null) { if (user == null) {
return Flows.forms(defaultRealm, request).setError(Messages.USERNAME_EXISTS) return Flows.forms(defaultRealm, request, uriInfo).setError(Messages.USERNAME_EXISTS)
.setFormData(formData).forwardToRegistration(); .setFormData(formData).forwardToRegistration();
} }

View file

@ -201,7 +201,8 @@ public class SocialResource {
cookiePath, null, "Added social cookie", NewCookie.DEFAULT_MAX_AGE, secureOnly); cookiePath, null, "Added social cookie", NewCookie.DEFAULT_MAX_AGE, secureOnly);
response.addNewCookie(newCookie); response.addNewCookie(newCookie);
return Flows.forms(realm, request).setFormData(formData).setSocialRegistration(true).forwardToRegistration(); return Flows.forms(realm, request, uriInfo).setFormData(formData).setSocialRegistration(true)
.forwardToRegistration();
} }
} }

View file

@ -11,21 +11,21 @@ import org.jboss.resteasy.spi.HttpResponse;
import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.SkeletonKeyToken; import org.keycloak.representations.SkeletonKeyToken;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.managers.AccessCodeEntry; import org.keycloak.services.managers.AccessCodeEntry;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationManager.AuthenticationStatus;
import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.managers.ResourceAdminManager; import org.keycloak.services.managers.ResourceAdminManager;
import org.keycloak.services.managers.TokenManager; import org.keycloak.services.managers.TokenManager;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.services.models.*; import org.keycloak.services.models.*;
import org.keycloak.services.models.UserModel.RequiredAction;
import org.keycloak.services.models.UserModel.Status;
import org.keycloak.services.resources.flows.Flows; import org.keycloak.services.resources.flows.Flows;
import org.keycloak.services.resources.flows.OAuthFlows; import org.keycloak.services.resources.flows.OAuthFlows;
import org.keycloak.services.validation.Validation; import org.keycloak.services.validation.Validation;
import org.picketlink.idm.credential.util.TimeBasedOTP;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.ForbiddenException;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.NotAuthorizedException; import javax.ws.rs.NotAuthorizedException;
import javax.ws.rs.POST; import javax.ws.rs.POST;
@ -36,7 +36,6 @@ import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders; 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.SecurityContext; import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriBuilder;
@ -132,15 +131,12 @@ public class TokenService {
throw new NotAuthorizedException("Disabled realm"); throw new NotAuthorizedException("Disabled realm");
} }
UserModel user = realm.getUser(username); UserModel user = realm.getUser(username);
if (user == null) {
throw new NotAuthorizedException("No user"); AuthenticationStatus status = authManager.authenticateForm(realm, user, form);
} if (status != AuthenticationStatus.SUCCESS) {
if (!user.isEnabled()) { throw new NotAuthorizedException(status);
throw new NotAuthorizedException("Disabled user.");
}
if (!authManager.authenticateForm(realm, user, form)) {
throw new NotAuthorizedException("FORM");
} }
tokenManager = new TokenManager(); tokenManager = new TokenManager();
SkeletonKeyToken token = authManager.createIdentityToken(realm, username); SkeletonKeyToken token = authManager.createIdentityToken(realm, username);
String encoded = tokenManager.encodeToken(realm, token); String encoded = tokenManager.encodeToken(realm, token);
@ -167,7 +163,7 @@ public class TokenService {
if (!user.isEnabled()) { if (!user.isEnabled()) {
throw new NotAuthorizedException("Disabled user."); throw new NotAuthorizedException("Disabled user.");
} }
if (authManager.authenticateForm(realm, user, form)) { if (authManager.authenticateForm(realm, user, form) != AuthenticationStatus.SUCCESS) {
throw new NotAuthorizedException("Auth failed"); throw new NotAuthorizedException("Auth failed");
} }
SkeletonKeyToken token = tokenManager.createAccessToken(realm, user); SkeletonKeyToken token = tokenManager.createAccessToken(realm, user);
@ -194,38 +190,46 @@ public class TokenService {
if (!client.isEnabled()) { if (!client.isEnabled()) {
return oauth.forwardToSecurityFailure("Login requester not enabled."); return oauth.forwardToSecurityFailure("Login requester not enabled.");
} }
String username = formData.getFirst("username"); String username = formData.getFirst("username");
UserModel user = realm.getUser(username); UserModel user = realm.getUser(username);
if (user == null) {
logger.error("Incorrect user name.");
return Flows.forms(realm, request).setError(Messages.INVALID_USER).setFormData(formData) isTotpConfigurationRequired(user);
.forwardToLogin(); isEmailVerificationRequired(user);
AuthenticationStatus status = authManager.authenticateForm(realm, user, formData);
switch (status) {
case SUCCESS:
case ACTIONS_REQUIRED:
return oauth.processAccessCode(scopeParam, state, redirect, client, user);
case ACCOUNT_DISABLED:
return Flows.forms(realm, request, uriInfo).setError(Messages.ACCOUNT_DISABLED).setFormData(formData)
.forwardToLogin();
case MISSING_TOTP:
return Flows.forms(realm, request, uriInfo).setFormData(formData).forwardToLoginTotp();
default:
return Flows.forms(realm, request, uriInfo).setError(Messages.INVALID_USER).setFormData(formData)
.forwardToLogin();
} }
}
if (!user.isEnabled()) { private void isTotpConfigurationRequired(UserModel user) {
return oauth.forwardToSecurityFailure("Your account is not enabled."); for (RequiredCredentialModel c : realm.getRequiredCredentials()) {
} if (c.getType().equals(CredentialRepresentation.TOTP) && !user.isTotp()) {
user.addRequiredAction(RequiredAction.CONFIGURE_TOTP);
if ("ENABLED".equals(user.getAttribute("KEYCLOAK_TOTP")) && Validation.isEmpty(formData.getFirst("totp"))) { user.setStatus(Status.ACTIONS_REQUIRED);
return Flows.forms(realm, request).setFormData(formData).forwardToLoginTotp(); logger.info("User is required to configure totp");
} else {
for (RequiredCredentialModel c : realm.getRequiredCredentials()) {
if (c.getType().equals(CredentialRepresentation.TOTP)) {
return Flows.forms(realm, request).forwardToTotp();
}
} }
} }
}
boolean authenticated = authManager.authenticateForm(realm, user, formData); private void isEmailVerificationRequired(UserModel user) {
if (!authenticated) { if (realm.isVerifyEmail() && !user.isEmailVerified()) {
logger.error("Authentication failed"); user.addRequiredAction(RequiredAction.VERIFY_EMAIL);
user.setStatus(Status.ACTIONS_REQUIRED);
return Flows.forms(realm, request).setError(Messages.INVALID_PASSWORD).setFormData(formData) logger.info("User is required to verify email");
.forwardToLogin();
} }
return oauth.processAccessCode(scopeParam, state, redirect, client, user);
} }
@Path("registrations") @Path("registrations")
@ -272,7 +276,7 @@ public class TokenService {
String error = Validation.validateRegistrationForm(formData, requiredCredentialTypes); String error = Validation.validateRegistrationForm(formData, requiredCredentialTypes);
if (error != null) { if (error != null) {
return Flows.forms(realm, request).setError(error).setFormData(formData) return Flows.forms(realm, request, uriInfo).setError(error).setFormData(formData)
.setSocialRegistration(isSocialRegistration).forwardToRegistration(); .setSocialRegistration(isSocialRegistration).forwardToRegistration();
} }
@ -280,7 +284,7 @@ public class TokenService {
UserModel user = realm.getUser(username); UserModel user = realm.getUser(username);
if (user != null) { if (user != null) {
return Flows.forms(realm, request).setError(Messages.USERNAME_EXISTS).setFormData(formData) return Flows.forms(realm, request, uriInfo).setError(Messages.USERNAME_EXISTS).setFormData(formData)
.setSocialRegistration(isSocialRegistration).forwardToRegistration(); .setSocialRegistration(isSocialRegistration).forwardToRegistration();
} }
@ -369,8 +373,8 @@ public class TokenService {
return Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build(); return Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build();
} }
boolean authenticated = authManager.authenticateForm(realm, client, formData); AuthenticationStatus status = authManager.authenticateForm(realm, client, formData);
if (!authenticated) { if (status != AuthenticationStatus.SUCCESS) {
Map<String, String> error = new HashMap<String, String>(); Map<String, String> error = new HashMap<String, String>();
error.put("error", "unauthorized_client"); error.put("error", "unauthorized_client");
return Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build(); return Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build();
@ -489,7 +493,7 @@ public class TokenService {
return oauth.processAccessCode(scopeParam, state, redirect, client, user); return oauth.processAccessCode(scopeParam, state, redirect, client, user);
} }
return Flows.forms(realm, request).forwardToLogin(); return Flows.forms(realm, request, uriInfo).forwardToLogin();
} }
@Path("registrations") @Path("registrations")
@ -517,7 +521,7 @@ public class TokenService {
authManager.expireIdentityCookie(realm, uriInfo); authManager.expireIdentityCookie(realm, uriInfo);
return Flows.forms(realm, request).forwardToRegistration(); return Flows.forms(realm, request, uriInfo).forwardToRegistration();
} }
@Path("logout") @Path("logout")

View file

@ -128,7 +128,6 @@ public class RealmAdminResource {
throw new NotFoundException(); throw new NotFoundException();
} }
user.setEmail(rep.getEmail()); user.setEmail(rep.getEmail());
user.setEnabled(rep.isEnabled());
user.setFirstName(rep.getFirstName()); user.setFirstName(rep.getFirstName());
user.setLastName(rep.getLastName()); user.setLastName(rep.getLastName());
for (Map.Entry<String, String> attr : rep.getAttributes().entrySet()) { for (Map.Entry<String, String> attr : rep.getAttributes().entrySet()) {
@ -148,7 +147,6 @@ public class RealmAdminResource {
throw new NotFoundException(); throw new NotFoundException();
} }
user.setEmail(rep.getEmail()); user.setEmail(rep.getEmail());
user.setEnabled(rep.isEnabled());
user.setFirstName(rep.getFirstName()); user.setFirstName(rep.getFirstName());
user.setLastName(rep.getLastName()); user.setLastName(rep.getLastName());
if (rep.getAttributes() != null) { if (rep.getAttributes() != null) {

View file

@ -40,8 +40,8 @@ public class Flows {
return new PageFlows(request); return new PageFlows(request);
} }
public static FormFlows forms(RealmModel realm, HttpRequest request) { public static FormFlows forms(RealmModel realm, HttpRequest request, UriInfo uriInfo) {
return new FormFlows(realm, request); return new FormFlows(realm, request, uriInfo);
} }
public static OAuthFlows oauth(RealmModel realm, HttpRequest request, UriInfo uriInfo, AuthenticationManager authManager, public static OAuthFlows oauth(RealmModel realm, HttpRequest request, UriInfo uriInfo, AuthenticationManager authManager,

View file

@ -22,12 +22,15 @@
package org.keycloak.services.resources.flows; package org.keycloak.services.resources.flows;
import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.services.email.EmailSender;
import org.keycloak.services.models.RealmModel; import org.keycloak.services.models.RealmModel;
import org.keycloak.services.models.UserModel; import org.keycloak.services.models.UserModel;
import org.keycloak.services.models.UserModel.RequiredAction;
import org.picketlink.idm.model.sample.Realm; import org.picketlink.idm.model.sample.Realm;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -39,6 +42,7 @@ public class FormFlows {
public static final String REALM = Realm.class.getName(); public static final String REALM = Realm.class.getName();
public static final String USER = UserModel.class.getName(); public static final String USER = UserModel.class.getName();
public static final String SOCIAL_REGISTRATION = "socialRegistration"; public static final String SOCIAL_REGISTRATION = "socialRegistration";
public static final String CODE = "code";
private String error; private String error;
private MultivaluedMap<String, String> formData; private MultivaluedMap<String, String> formData;
@ -49,10 +53,28 @@ public class FormFlows {
private UserModel userModel; private UserModel userModel;
private boolean socialRegistration; private boolean socialRegistration;
private String code;
private UriInfo uriInfo;
FormFlows(RealmModel realm, HttpRequest request) { FormFlows(RealmModel realm, HttpRequest request, UriInfo uriInfo) {
this.realm = realm; this.realm = realm;
this.request = request; this.request = request;
this.uriInfo = uriInfo;
}
public Response forwardToAction(RequiredAction action) {
switch (action) {
case CONFIGURE_TOTP:
return forwardToTotp();
case UPDATE_PROFILE:
return forwardToAccount();
case RESET_PASSWORD:
return forwardToPassword();
case VERIFY_EMAIL:
return forwardToVerifyEmail();
default:
return null; // TODO
}
} }
public Response forwardToAccess() { public Response forwardToAccess() {
@ -78,6 +100,10 @@ public class FormFlows {
request.setAttribute(USER, userModel); request.setAttribute(USER, userModel);
} }
if (code != null) {
request.setAttribute(CODE, code);
}
request.setAttribute(SOCIAL_REGISTRATION, socialRegistration); request.setAttribute(SOCIAL_REGISTRATION, socialRegistration);
request.forward(form); request.forward(form);
@ -108,6 +134,16 @@ public class FormFlows {
return forwardToForm(Pages.TOTP); return forwardToForm(Pages.TOTP);
} }
public Response forwardToVerifyEmail() {
new EmailSender().sendEmailVerification(userModel, realm, code, uriInfo);
return forwardToForm(Pages.VERIFY_EMAIL);
}
public FormFlows setCode(String code) {
this.code = code;
return this;
}
public FormFlows setError(String error) { public FormFlows setError(String error) {
this.error = error; this.error = error;
return this; return this;

View file

@ -90,10 +90,21 @@ public class OAuthFlows {
.size() > 0))); .size() > 0)));
if (!isResource if (!isResource
&& (accessCode.getRealmRolesRequested().size() > 0 || accessCode.getResourceRolesRequested().size() > 0)) { && (accessCode.getRealmRolesRequested().size() > 0 || accessCode.getResourceRolesRequested().size() > 0)) {
accessCode.setExpiration(System.currentTimeMillis() / 1000 + realm.getAccessCodeLifespanUserAction());
return oauthGrantPage(accessCode, client); return oauthGrantPage(accessCode, client);
} }
return redirectAccessCode(accessCode, state, redirect); if (user.getRequiredActions() != null) {
accessCode.setExpiration(System.currentTimeMillis() / 1000 + realm.getAccessCodeLifespanUserAction());
return Flows.forms(realm, request, uriInfo).setCode(accessCode.getCode()).setUser(user)
.forwardToAction(user.getRequiredActions().get(0));
}
if (redirect != null) {
return redirectAccessCode(accessCode, state, redirect);
} else {
return null;
}
} }
public Response oauthGrantPage(AccessCodeEntry accessCode, UserModel client) { public Response oauthGrantPage(AccessCodeEntry accessCode, UserModel client) {

View file

@ -46,4 +46,6 @@ public class Pages {
public final static String TOTP = "/forms/totp.xhtml"; public final static String TOTP = "/forms/totp.xhtml";
public final static String VERIFY_EMAIL = "/forms/verify-email.xhtml";
} }

View file

@ -35,7 +35,7 @@ public class Urls {
return accountBase(baseUri).path(AccountService.class, "accessPage").build(realmId); return accountBase(baseUri).path(AccountService.class, "accessPage").build(realmId);
} }
private static UriBuilder accountBase(URI baseUri) { public static UriBuilder accountBase(URI baseUri) {
return realmBase(baseUri).path(RealmsResource.class, "getAccountService"); return realmBase(baseUri).path(RealmsResource.class, "getAccountService");
} }

View file

@ -0,0 +1,14 @@
import org.picketlink.common.util.Base32;
import org.picketlink.idm.credential.util.TimeBasedOTP;
public class TotpUtil {
public static void main(String[] args) {
String google = "PJBX GURY NZIT C2JX I44T S3D2 JBKD G6SB";
google = google.replace(" ", "");
google = new String(Base32.decode(google));
TimeBasedOTP otp = new TimeBasedOTP();
System.out.println(otp.generate(google));
}
}

View file

@ -0,0 +1,55 @@
package org.keycloak.services.email;
import java.io.IOException;
import javax.mail.MessagingException;
import javax.mail.internet.AddressException;
import javax.mail.internet.MimeMessage;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import com.icegreen.greenmail.util.GreenMail;
import com.icegreen.greenmail.util.ServerSetup;
public class EmailSenderTest {
private GreenMail greenMail;
@Before
public void before() {
ServerSetup setup = new ServerSetup(3025, "localhost", "smtp");
greenMail = new GreenMail(setup);
greenMail.start();
System.setProperty("keycloak.mail.smtp.from", "auto@keycloak.org");
System.setProperty("keycloak.mail.smtp.host", "localhost");
System.setProperty("keycloak.mail.smtp.port", "3025");
}
@After
public void after() throws InterruptedException {
if (greenMail != null) {
greenMail.stop();
}
}
@Test
public void sendMail() throws AddressException, MessagingException, IOException {
EmailSender emailSender = new EmailSender();
emailSender.send("test@test.com", "Test subject", "Test body");
MimeMessage[] receivedMessages = greenMail.getReceivedMessages();
Assert.assertEquals(1, receivedMessages.length);
MimeMessage msg = receivedMessages[0];
Assert.assertEquals(1, msg.getFrom().length);
Assert.assertEquals("auto@keycloak.org", msg.getFrom()[0].toString());
Assert.assertEquals("Test subject", msg.getSubject());
Assert.assertEquals("Test body", ((String) msg.getContent()).trim());
}
}

View file

@ -0,0 +1,178 @@
package org.keycloak.services.managers;
import java.util.UUID;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.managers.AuthenticationManager.AuthenticationStatus;
import org.keycloak.services.models.KeycloakSession;
import org.keycloak.services.models.KeycloakSessionFactory;
import org.keycloak.services.models.RealmModel;
import org.keycloak.services.models.UserCredentialModel;
import org.keycloak.services.models.UserModel;
import org.keycloak.services.models.UserModel.RequiredAction;
import org.keycloak.services.models.UserModel.Status;
import org.keycloak.services.resources.KeycloakApplication;
import org.picketlink.idm.credential.util.TimeBasedOTP;
public class AuthenticationManagerTest {
private RealmManager adapter;
private AuthenticationManager am;
private KeycloakSessionFactory factory;
private MultivaluedMap<String, String> formData;
private KeycloakSession identitySession;
private TimeBasedOTP otp;
private RealmModel realm;
private UserModel user;
@After
public void after() throws Exception {
identitySession.getTransaction().commit();
identitySession.close();
factory.close();
}
@Test
public void authForm() {
AuthenticationStatus status = am.authenticateForm(realm, user, formData);
Assert.assertEquals(AuthenticationStatus.SUCCESS, status);
}
@Test
public void authFormInvalidPassword() {
formData.remove(CredentialRepresentation.PASSWORD);
formData.add(CredentialRepresentation.PASSWORD, "invalid");
AuthenticationStatus status = am.authenticateForm(realm, user, formData);
Assert.assertEquals(AuthenticationStatus.INVALID_CREDENTIALS, status);
}
@Test
public void authFormMissingPassword() {
formData.remove(CredentialRepresentation.PASSWORD);
AuthenticationStatus status = am.authenticateForm(realm, user, formData);
Assert.assertEquals(AuthenticationStatus.MISSING_PASSWORD, status);
}
@Test
public void authFormRequiredAction() {
realm.addRequiredCredential(CredentialRepresentation.TOTP);
user.addRequiredAction(RequiredAction.CONFIGURE_TOTP);
user.setStatus(Status.ACTIONS_REQUIRED);
AuthenticationStatus status = am.authenticateForm(realm, user, formData);
Assert.assertEquals(AuthenticationStatus.ACTIONS_REQUIRED, status);
}
@Test
public void authFormUserDisabled() {
user.setStatus(Status.DISABLED);
AuthenticationStatus status = am.authenticateForm(realm, user, formData);
Assert.assertEquals(AuthenticationStatus.ACCOUNT_DISABLED, status);
}
@Test
public void authFormUserRequiredActions() {
user.setStatus(Status.ACTIONS_REQUIRED);
AuthenticationStatus status = am.authenticateForm(realm, user, formData);
Assert.assertEquals(AuthenticationStatus.ACTIONS_REQUIRED, status);
}
@Test
public void authFormWithTotp() {
realm.addRequiredCredential(CredentialRepresentation.TOTP);
String totpSecret = UUID.randomUUID().toString();
UserCredentialModel credential = new UserCredentialModel();
credential.setType(CredentialRepresentation.TOTP);
credential.setValue(totpSecret);
realm.updateCredential(user, credential);
user.setTotp(true);
String token = otp.generate(totpSecret);
formData.add(CredentialRepresentation.TOTP, token);
AuthenticationStatus status = am.authenticateForm(realm, user, formData);
Assert.assertEquals(AuthenticationStatus.SUCCESS, status);
}
@Test
public void authFormWithTotpInvalidPassword() {
authFormWithTotp();
formData.remove(CredentialRepresentation.PASSWORD);
formData.add(CredentialRepresentation.PASSWORD, "invalid");
AuthenticationStatus status = am.authenticateForm(realm, user, formData);
Assert.assertEquals(AuthenticationStatus.INVALID_CREDENTIALS, status);
}
@Test
public void authFormWithTotpInvalidTotp() {
authFormWithTotp();
formData.remove(CredentialRepresentation.TOTP);
formData.add(CredentialRepresentation.TOTP, "invalid");
AuthenticationStatus status = am.authenticateForm(realm, user, formData);
Assert.assertEquals(AuthenticationStatus.INVALID_CREDENTIALS, status);
}
@Test
public void authFormWithTotpMissingTotp() {
authFormWithTotp();
formData.remove(CredentialRepresentation.TOTP);
AuthenticationStatus status = am.authenticateForm(realm, user, formData);
Assert.assertEquals(AuthenticationStatus.MISSING_TOTP, status);
}
@Before
public void before() throws Exception {
factory = KeycloakApplication.buildSessionFactory();
identitySession = factory.createSession();
identitySession.getTransaction().begin();
adapter = new RealmManager(identitySession);
realm = adapter.createRealm("Test");
realm.setAccessCodeLifespan(100);
realm.setCookieLoginAllowed(true);
realm.setEnabled(true);
realm.setName("Test");
realm.setPrivateKeyPem("0234234");
realm.setPublicKeyPem("0234234");
realm.setTokenLifespan(1000);
realm.addRequiredCredential(CredentialRepresentation.PASSWORD);
am = new AuthenticationManager();
user = realm.addUser("test");
UserCredentialModel credential = new UserCredentialModel();
credential.setType(CredentialRepresentation.PASSWORD);
credential.setValue("password");
realm.updateCredential(user, credential);
formData = new MultivaluedHashMap<String, String>();
formData.add(CredentialRepresentation.PASSWORD, "password");
otp = new TimeBasedOTP();
}
}

View file

@ -0,0 +1,35 @@
package org.keycloak.services.models.utils;
import org.junit.Assert;
import org.junit.Test;
public class ArrayUtilsTest {
@Test
public void add() {
String[] a = new String[] { "a" };
a = ArrayUtils.add(a, "b");
Assert.assertArrayEquals(new String[] { "a", "b" }, a);
a = ArrayUtils.add(a, "c");
Assert.assertArrayEquals(new String[] { "a", "b", "c" }, a);
}
@Test
public void remove() {
String[] a = new String[] { "a", "b", "c", "d" };
a = ArrayUtils.remove(a, "b");
Assert.assertArrayEquals(new String[] { "a", "c", "d" }, a);
a = ArrayUtils.remove(a, "d");
Assert.assertArrayEquals(new String[] { "a", "c" }, a);
a = ArrayUtils.remove(a, "a");
Assert.assertArrayEquals(new String[] { "c" }, a);
a = ArrayUtils.remove(a, "c");
Assert.assertArrayEquals(new String[] {}, a);
}
}

View file

@ -18,6 +18,7 @@ import org.keycloak.services.models.UserCredentialModel;
import org.keycloak.services.resources.KeycloakApplication; import org.keycloak.services.resources.KeycloakApplication;
import java.util.Arrays;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -69,6 +70,7 @@ public class AdapterTest {
public void test1CreateRealm() throws Exception { public void test1CreateRealm() throws Exception {
realmModel = adapter.createRealm("JUGGLER"); realmModel = adapter.createRealm("JUGGLER");
realmModel.setAccessCodeLifespan(100); realmModel.setAccessCodeLifespan(100);
realmModel.setAccessCodeLifespanUserAction(600);
realmModel.setCookieLoginAllowed(true); realmModel.setCookieLoginAllowed(true);
realmModel.setEnabled(true); realmModel.setEnabled(true);
realmModel.setName("JUGGLER"); realmModel.setName("JUGGLER");
@ -82,6 +84,7 @@ public class AdapterTest {
realmModel = adapter.getRealm(realmModel.getId()); realmModel = adapter.getRealm(realmModel.getId());
Assert.assertNotNull(realmModel); Assert.assertNotNull(realmModel);
Assert.assertEquals(realmModel.getAccessCodeLifespan(), 100); Assert.assertEquals(realmModel.getAccessCodeLifespan(), 100);
Assert.assertEquals(600, realmModel.getAccessCodeLifespanUserAction());
Assert.assertEquals(realmModel.getTokenLifespan(), 1000); Assert.assertEquals(realmModel.getTokenLifespan(), 1000);
Assert.assertEquals(realmModel.isEnabled(), true); Assert.assertEquals(realmModel.isEnabled(), true);
Assert.assertEquals(realmModel.getName(), "JUGGLER"); Assert.assertEquals(realmModel.getName(), "JUGGLER");
@ -261,5 +264,54 @@ public class AdapterTest {
Assert.assertEquals("user", role.getName()); Assert.assertEquals("user", role.getName());
} }
@Test
public void testUserStatus() throws Exception {
test1CreateRealm();
UserModel user = realmModel.addUser("bburke");
Assert.assertTrue(user.isEnabled());
Assert.assertEquals(UserModel.Status.ENABLED, user.getStatus());
user.setStatus(UserModel.Status.DISABLED);
user = realmModel.getUser("bburke");
Assert.assertFalse(user.isEnabled());
Assert.assertEquals(UserModel.Status.DISABLED, user.getStatus());
user.setStatus(UserModel.Status.ACTIONS_REQUIRED);
user = realmModel.getUser("bburke");
Assert.assertTrue(user.isEnabled());
Assert.assertEquals(UserModel.Status.ACTIONS_REQUIRED, user.getStatus());
}
@Test
public void testUserRequiredActions() throws Exception {
test1CreateRealm();
UserModel user = realmModel.addUser("bburke");
Assert.assertNull(user.getRequiredActions());
user.addRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP);
user = realmModel.getUser("bburke");
Assert.assertEquals(Arrays.asList(UserModel.RequiredAction.CONFIGURE_TOTP), user.getRequiredActions());
user.addRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL);
user = realmModel.getUser("bburke");
Assert.assertEquals(Arrays.asList(UserModel.RequiredAction.CONFIGURE_TOTP, UserModel.RequiredAction.VERIFY_EMAIL),
user.getRequiredActions());
user.removeRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP);
user = realmModel.getUser("bburke");
Assert.assertEquals(Arrays.asList(UserModel.RequiredAction.VERIFY_EMAIL), user.getRequiredActions());
user.removeRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL);
user = realmModel.getUser("bburke");
Assert.assertNull(user.getRequiredActions());
}
} }

View file

@ -56,6 +56,7 @@ public class ImportTest {
defaultRealm.setEnabled(true); defaultRealm.setEnabled(true);
defaultRealm.setTokenLifespan(300); defaultRealm.setTokenLifespan(300);
defaultRealm.setAccessCodeLifespan(60); defaultRealm.setAccessCodeLifespan(60);
defaultRealm.setAccessCodeLifespanUserAction(600);
defaultRealm.setSslNotRequired(false); defaultRealm.setSslNotRequired(false);
defaultRealm.setCookieLoginAllowed(true); defaultRealm.setCookieLoginAllowed(true);
defaultRealm.setRegistrationAllowed(true); defaultRealm.setRegistrationAllowed(true);
@ -71,6 +72,8 @@ public class ImportTest {
manager.importRealm(rep, realm); manager.importRealm(rep, realm);
realm.addRealmAdmin(admin); realm.addRealmAdmin(admin);
Assert.assertTrue(realm.isVerifyEmail());
Assert.assertFalse(realm.isAutomaticRegistrationAfterSocialLogin()); Assert.assertFalse(realm.isAutomaticRegistrationAfterSocialLogin());
List<RequiredCredentialModel> creds = realm.getRequiredCredentials(); List<RequiredCredentialModel> creds = realm.getRequiredCredentials();
Assert.assertEquals(1, creds.size()); Assert.assertEquals(1, creds.size());
@ -131,6 +134,7 @@ public class ImportTest {
defaultRealm.setEnabled(true); defaultRealm.setEnabled(true);
defaultRealm.setTokenLifespan(300); defaultRealm.setTokenLifespan(300);
defaultRealm.setAccessCodeLifespan(60); defaultRealm.setAccessCodeLifespan(60);
defaultRealm.setAccessCodeLifespanUserAction(600);
defaultRealm.setSslNotRequired(false); defaultRealm.setSslNotRequired(false);
defaultRealm.setCookieLoginAllowed(true); defaultRealm.setCookieLoginAllowed(true);
defaultRealm.setRegistrationAllowed(true); defaultRealm.setRegistrationAllowed(true);
@ -147,6 +151,7 @@ public class ImportTest {
realm.addRealmAdmin(admin); realm.addRealmAdmin(admin);
Assert.assertTrue(realm.isAutomaticRegistrationAfterSocialLogin()); Assert.assertTrue(realm.isAutomaticRegistrationAfterSocialLogin());
Assert.assertEquals(600, realm.getAccessCodeLifespanUserAction());
verifyRequiredCredentials(realm.getRequiredCredentials(), "password"); verifyRequiredCredentials(realm.getRequiredCredentials(), "password");
verifyRequiredCredentials(realm.getRequiredApplicationCredentials(), "totp"); verifyRequiredCredentials(realm.getRequiredApplicationCredentials(), "totp");
verifyRequiredCredentials(realm.getRequiredOAuthClientCredentials(), "cert"); verifyRequiredCredentials(realm.getRequiredOAuthClientCredentials(), "cert");

View file

@ -18,6 +18,7 @@ public class InstallationManager {
defaultRealm.setEnabled(true); defaultRealm.setEnabled(true);
defaultRealm.setTokenLifespan(300); defaultRealm.setTokenLifespan(300);
defaultRealm.setAccessCodeLifespan(60); defaultRealm.setAccessCodeLifespan(60);
defaultRealm.setAccessCodeLifespanUserAction(600);
defaultRealm.setSslNotRequired(false); defaultRealm.setSslNotRequired(false);
defaultRealm.setCookieLoginAllowed(true); defaultRealm.setCookieLoginAllowed(true);
defaultRealm.setRegistrationAllowed(true); defaultRealm.setRegistrationAllowed(true);

View file

@ -3,6 +3,7 @@
"enabled": true, "enabled": true,
"tokenLifespan": 300, "tokenLifespan": 300,
"accessCodeLifespan": 10, "accessCodeLifespan": 10,
"accessCodeLifespanUserAction": 600,
"sslNotRequired": true, "sslNotRequired": true,
"cookieLoginAllowed": true, "cookieLoginAllowed": true,
"automaticRegistrationAfterSocialLogin": true, "automaticRegistrationAfterSocialLogin": true,
@ -14,7 +15,7 @@
"users" : [ "users" : [
{ {
"username" : "bburke@redhat.com", "username" : "bburke@redhat.com",
"enabled" : true, "status": "ENABLED",
"attributes" : { "attributes" : {
"email" : "bburke@redhat.com" "email" : "bburke@redhat.com"
}, },
@ -25,7 +26,7 @@
}, },
{ {
"username" : "third-party", "username" : "third-party",
"enabled" : true, "status": "ENABLED",
"credentials" : [ "credentials" : [
{ "type" : "Password", { "type" : "Password",
"value" : "password" } "value" : "password" }

View file

@ -3,14 +3,16 @@
"enabled": true, "enabled": true,
"tokenLifespan": 6000, "tokenLifespan": 6000,
"accessCodeLifespan": 30, "accessCodeLifespan": 30,
"accessCodeLifespanUserAction": 600,
"requiredCredentials": [ "password" ], "requiredCredentials": [ "password" ],
"requiredApplicationCredentials": [ "password" ], "requiredApplicationCredentials": [ "password" ],
"requiredOAuthClientCredentials": [ "password" ], "requiredOAuthClientCredentials": [ "password" ],
"defaultRoles": [ "foo", "bar" ], "defaultRoles": [ "foo", "bar" ],
"verifyEmail" : "true",
"users": [ "users": [
{ {
"username": "wburke", "username": "wburke",
"enabled": true, "status": "ENABLED",
"attributes": { "attributes": {
"email": "bburke@redhat.com" "email": "bburke@redhat.com"
}, },
@ -23,7 +25,7 @@
}, },
{ {
"username": "loginclient", "username": "loginclient",
"enabled": true, "status": "ENABLED",
"credentials": [ "credentials": [
{ {
"type": "password", "type": "password",
@ -33,7 +35,7 @@
}, },
{ {
"username": "admin", "username": "admin",
"enabled": true, "status": "ENABLED",
"credentials": [ "credentials": [
{ {
"type": "password", "type": "password",
@ -43,7 +45,7 @@
}, },
{ {
"username": "oauthclient", "username": "oauthclient",
"enabled": true, "status": "ENABLED",
"credentials": [ "credentials": [
{ {
"type": "password", "type": "password",
@ -53,7 +55,7 @@
}, },
{ {
"username": "mySocialUser", "username": "mySocialUser",
"enabled": true "status": "ENABLED"
} }
], ],
"roleMappings": [ "roleMappings": [

24
testsuite/README.md Normal file
View file

@ -0,0 +1,24 @@
Executing testsuite
===================
Currently the testsuite is not executed as part of a regular build. To run the testsuite first you need to do a "mvn clean install" on the project. Then you need to install JBoss AS 7.1.1.Final and upgrade Resteasy (see descriptions in examples/as7-eap-demo/README.md).
The tests can either be run in remote (JBoss AS already running) or managed (tests starts/stops JBoss AS).
To run tests in remote mode:
mvn clean install -Pjboss-remote
To run tests in managed mode:
export JBOSS_HOME=<path to JBoss AS 7.1.1.Final with upgraded Resteasy>
mvn clean install -Pjboss-managed
When running tests in the testsuite from an IDE it is best to use the remote mode.
Browser
-------
The testsuite uses Arquillian Drone and Graphene. By default it uses the headless PhantomJS webkit, but it is also possible to run it with other browsers. For example using Firefox or Chrome is good if you want to step-through a test to see what's actually going in.
To run the tests with Firefox add `-Dbrowser=firefox` or for Chrome add `-Dbrowser=chrome`

View file

@ -120,40 +120,28 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.jboss.arquillian.extension</groupId> <groupId>org.jboss.arquillian.extension</groupId>
<artifactId>arquillian-drone-impl</artifactId> <artifactId>arquillian-drone-webdriver-depchain</artifactId>
<type>pom</type>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.jboss.arquillian.extension</groupId> <groupId>org.jboss.arquillian.graphene</groupId>
<artifactId>arquillian-drone-selenium</artifactId> <artifactId>graphene-webdriver</artifactId>
<scope>test</scope> <type>pom</type>
</dependency>
<dependency>
<groupId>org.jboss.arquillian.extension</groupId>
<artifactId>arquillian-drone-selenium-server</artifactId>
<scope>test</scope> <scope>test</scope>
<version>2.0.0.Beta1</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.jboss.shrinkwrap.resolver</groupId> <groupId>org.jboss.shrinkwrap.resolver</groupId>
<artifactId>shrinkwrap-resolver-impl-maven</artifactId> <artifactId>shrinkwrap-resolver-impl-maven</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.seleniumhq.selenium</groupId> <groupId>com.icegreen</groupId>
<artifactId>selenium-java</artifactId> <artifactId>greenmail</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-server</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.mortbay.jetty</groupId>
<artifactId>servlet-api-2.5</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies> </dependencies>
<build> <build>
<plugins> <plugins>

View file

@ -21,20 +21,15 @@
*/ */
package org.keycloak.testsuite; package org.keycloak.testsuite;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import org.jboss.arquillian.container.test.api.Deployment; import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.drone.api.annotation.Drone; import org.jboss.arquillian.drone.api.annotation.Drone;
import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.arquillian.graphene.page.Page;
import org.jboss.shrinkwrap.api.spec.WebArchive; import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.jboss.shrinkwrap.resolver.api.maven.Maven;
import org.junit.After; import org.junit.After;
import org.junit.Assert; import org.keycloak.testsuite.pages.AppPage;
import org.junit.Before; import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.RegisterPage;
import com.thoughtworks.selenium.DefaultSelenium; import org.openqa.selenium.WebDriver;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -43,112 +38,33 @@ public abstract class AbstractDroneTest {
@Deployment(name = "app", testable = false, order = 2) @Deployment(name = "app", testable = false, order = 2)
public static WebArchive appDeployment() { public static WebArchive appDeployment() {
File[] libs = Maven.resolver().loadPomFromFile("pom.xml") return Deployments.appDeployment();
.resolve("org.keycloak:keycloak-core", "org.keycloak:keycloak-as7-adapter").withoutTransitivity().asFile();
WebArchive archive = ShrinkWrap.create(WebArchive.class, "app.war").addClasses(TestApplication.class)
.addAsLibraries(libs).addAsWebInfResource("jboss-deployment-structure.xml")
.addAsWebInfResource("app-web.xml", "web.xml").addAsWebInfResource("app-jboss-web.xml", "jboss-web.xml")
.addAsWebInfResource("app-resteasy-oauth.json", "resteasy-oauth.json").addAsWebResource("user.jsp");
return archive;
} }
@Deployment(name = "auth-server", testable = false, order = 1) @Deployment(name = "auth-server", testable = false, order = 1)
public static WebArchive deployment() { public static WebArchive deployment() {
File[] libs = Maven.resolver().loadPomFromFile("pom.xml").importRuntimeDependencies().resolve().withTransitivity() return Deployments.deployment();
.asFile();
WebArchive archive = ShrinkWrap.create(WebArchive.class, "auth-server.war").addClasses(TestApplication.class)
.addAsLibraries(libs).addAsWebInfResource("jboss-deployment-structure.xml").addAsWebInfResource("web.xml")
.addAsResource("persistence.xml", "META-INF/persistence.xml")
.addAsResource("testrealm.json", "META-INF/testrealm.json");
return archive;
} }
URL appUrl; @Page
protected AppPage appPage;
URL authServerUrl;
String DEFAULT_WAIT = "10000";
@Drone @Drone
DefaultSelenium selenium; protected WebDriver browser;
@Page
protected LoginPage loginPage;
@Page
protected RegisterPage registerPage;
@After @After
public void after() { public void after() {
logout(); appPage.open();
} if (appPage.isCurrent()) {
appPage.logout();
@Before
public void before() throws MalformedURLException {
authServerUrl = new URL("http://localhost:8080/auth-server");
appUrl = new URL("http://localhost:8080/app/user.jsp");
}
public void login(String username, String password) {
login(username, password, null);
}
public void login(String username, String password, String expectErrorMessage) {
selenium.open(appUrl.toString());
selenium.waitForPageToLoad(DEFAULT_WAIT);
Assert.assertEquals("Log in to demo", selenium.getTitle());
if (username != null) {
selenium.type("id=username", username);
}
if (password != null) {
selenium.type("id=password", password);
}
selenium.click("css=input[type=\"submit\"]");
selenium.waitForPageToLoad(DEFAULT_WAIT);
if (expectErrorMessage == null) {
Assert.assertEquals(username, selenium.getText("id=user"));
} else {
Assert.assertTrue(selenium.isTextPresent(expectErrorMessage));
}
}
public void logout() {
selenium.open(authServerUrl + "/rest/realms/demo/tokens/logout?redirect_uri=" + appUrl);
selenium.waitForPageToLoad(DEFAULT_WAIT);
Assert.assertEquals("Log in to demo", selenium.getTitle());
}
public void registerUser(String username, String password) {
registerUser(username, password, null);
}
public void registerUser(String username, String password, String expectErrorMessage) {
selenium.open(appUrl.toString());
selenium.waitForPageToLoad(DEFAULT_WAIT);
selenium.click("link=Register");
selenium.waitForPageToLoad(DEFAULT_WAIT);
selenium.type("id=name", "Test User");
selenium.type("id=email", "test@user.com");
if (username != null) {
selenium.type("id=username", username);
}
if (password != null) {
selenium.type("id=password", password);
selenium.type("id=password-confirm", password);
}
selenium.click("css=input[type=\"submit\"]");
selenium.waitForPageToLoad(DEFAULT_WAIT);
if (expectErrorMessage == null) {
Assert.assertEquals(username, selenium.getText("id=user"));
} else {
Assert.assertTrue(selenium.isTextPresent(expectErrorMessage));
} }
browser.manage().deleteAllCookies();
} }
} }

View file

@ -21,10 +21,13 @@
*/ */
package org.keycloak.testsuite; package org.keycloak.testsuite;
import org.jboss.arquillian.graphene.page.Page;
import org.jboss.arquillian.junit.Arquillian; import org.jboss.arquillian.junit.Arquillian;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.keycloak.testsuite.pages.ChangePasswordPage;
import org.keycloak.testsuite.pages.UpdateProfilePage;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -32,44 +35,52 @@ import org.junit.runner.RunWith;
@RunWith(Arquillian.class) @RunWith(Arquillian.class)
public class AccountTest extends AbstractDroneTest { public class AccountTest extends AbstractDroneTest {
@Page
protected ChangePasswordPage changePasswordPage;
@Page
protected UpdateProfilePage profilePage;
@Test @Test
public void changePassword() { public void changePassword() {
registerUser("changePassword", "password"); appPage.open();
loginPage.register();
registerPage.register("name", "email", "changePassword", "password", "password");
selenium.open(authServerUrl + "/rest/realms/demo/account/password"); changePasswordPage.open();
selenium.waitForPageToLoad(DEFAULT_WAIT); changePasswordPage.changePassword("password", "new-password", "new-password");
Assert.assertTrue(selenium.isTextPresent("Change Password")); appPage.open();
selenium.type("id=password", "password"); Assert.assertTrue(loginPage.isCurrent());
selenium.type("id=password-new", "newpassword");
selenium.type("id=password-confirm", "newpassword");
selenium.click("css=input[type=\"submit\"]");
selenium.waitForPageToLoad(DEFAULT_WAIT);
logout(); loginPage.login("changePassword", "password");
login("changePassword", "password", "Invalid username or password"); Assert.assertEquals("Invalid username or password", loginPage.getError());
login("changePassword", "newpassword");
loginPage.login("changePassword", "new-password");
Assert.assertTrue(appPage.isCurrent());
Assert.assertEquals("changePassword", appPage.getUser());
} }
@Test @Test
public void changeProfile() { public void changeProfile() {
registerUser("changeProfile", "password"); appPage.open();
loginPage.register();
registerPage.register("first last", "old@email.com", "changeProfile", "password", "password");
selenium.open(authServerUrl + "/rest/realms/demo/account"); profilePage.open();
selenium.waitForPageToLoad(DEFAULT_WAIT);
selenium.type("id=firstName", "Newfirst"); Assert.assertEquals("first", profilePage.getFirstName());
selenium.type("id=lastName", "Newlast"); Assert.assertEquals("last", profilePage.getLastName());
selenium.type("id=email", "new@email.com"); Assert.assertEquals("old@email.com", profilePage.getEmail());
selenium.click("css=input[type=\"submit\"]"); profilePage.updateProfile("New first", "New last", "new@email.com");
selenium.waitForPageToLoad(DEFAULT_WAIT);
Assert.assertEquals("Newfirst", selenium.getValue("id=firstName")); Assert.assertEquals("New first", profilePage.getFirstName());
Assert.assertEquals("Newlast", selenium.getValue("id=lastName")); Assert.assertEquals("New last", profilePage.getLastName());
Assert.assertEquals("new@email.com", selenium.getValue("id=email")); Assert.assertEquals("new@email.com", profilePage.getEmail());
} }
} }

View file

@ -0,0 +1,9 @@
package org.keycloak.testsuite;
public class Constants {
public static String APP_ROOT = "http://localhost:8080/app";
public static String AUTH_SERVER_ROOT = "http://localhost:8080/auth-server";
}

View file

@ -0,0 +1,32 @@
package org.keycloak.testsuite;
import java.io.File;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.jboss.shrinkwrap.resolver.api.maven.Maven;
public class Deployments {
public static WebArchive appDeployment() {
File[] libs = Maven.resolver().loadPomFromFile("pom.xml")
.resolve("org.keycloak:keycloak-core", "org.keycloak:keycloak-as7-adapter").withoutTransitivity().asFile();
WebArchive archive = ShrinkWrap.create(WebArchive.class, "app.war").addClasses(TestApplication.class)
.addAsLibraries(libs).addAsWebInfResource("jboss-deployment-structure.xml")
.addAsWebInfResource("app-web.xml", "web.xml").addAsWebInfResource("app-jboss-web.xml", "jboss-web.xml")
.addAsWebInfResource("app-resteasy-oauth.json", "resteasy-oauth.json").addAsWebResource("user.jsp");
return archive;
}
public static WebArchive deployment() {
File[] libs = Maven.resolver().loadPomFromFile("pom.xml").importRuntimeDependencies().resolve().withTransitivity()
.asFile();
WebArchive archive = ShrinkWrap.create(WebArchive.class, "auth-server.war").addClasses(TestApplication.class)
.addAsLibraries(libs).addAsWebInfResource("jboss-deployment-structure.xml").addAsWebInfResource("web.xml")
.addAsResource("persistence.xml", "META-INF/persistence.xml")
.addAsResource("testrealm.json", "META-INF/testrealm.json");
return archive;
}
}

View file

@ -22,6 +22,7 @@
package org.keycloak.testsuite; package org.keycloak.testsuite;
import org.jboss.arquillian.junit.Arquillian; import org.jboss.arquillian.junit.Arquillian;
import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -33,17 +34,42 @@ public class LoginTest extends AbstractDroneTest {
@Test @Test
public void loginInvalidPassword() { public void loginInvalidPassword() {
login("invalid", "password", "Invalid username or password"); appPage.open();
Assert.assertTrue(loginPage.isCurrent());
loginPage.login("bburke@redhat.com", "invalid");
Assert.assertEquals("Invalid username or password", loginPage.getError());
} }
@Test @Test
public void loginInvalidUsername() { public void loginInvalidUsername() {
login("invalid", "password", "Invalid username or password"); appPage.open();
Assert.assertTrue(loginPage.isCurrent());
loginPage.login("invalid", "password");
Assert.assertEquals("Invalid username or password", loginPage.getError());
} }
@Test @Test
public void loginSuccess() { public void loginSuccess() {
login("bburke@redhat.com", "password"); appPage.open();
Assert.assertTrue(loginPage.isCurrent());
loginPage.login("bburke@redhat.com", "password");
Assert.assertTrue(appPage.isCurrent());
Assert.assertEquals("bburke@redhat.com", appPage.getUser());
}
@Test
public void logout() {
loginSuccess();
appPage.logout();
appPage.open();
Assert.assertTrue(loginPage.isCurrent());
} }
} }

View file

@ -22,6 +22,7 @@
package org.keycloak.testsuite; package org.keycloak.testsuite;
import org.jboss.arquillian.junit.Arquillian; import org.jboss.arquillian.junit.Arquillian;
import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -31,19 +32,67 @@ import org.junit.runner.RunWith;
@RunWith(Arquillian.class) @RunWith(Arquillian.class)
public class RegisterTest extends AbstractDroneTest { public class RegisterTest extends AbstractDroneTest {
@Test
public void registerExistingUser() {
appPage.open();
loginPage.register();
Assert.assertTrue(registerPage.isCurrent());
registerPage.register("name", "email", "username", null, null);
Assert.assertTrue(registerPage.isCurrent());
Assert.assertEquals("Please specify password", registerPage.getError());
}
@Test
public void registerUserInvalidPasswordConfirm() {
appPage.open();
loginPage.register();
Assert.assertTrue(registerPage.isCurrent());
registerPage.register("name", "email", "bburke@redhat.com", "password", "invalid");
Assert.assertTrue(registerPage.isCurrent());
Assert.assertEquals("Password confirmation doesn't match", registerPage.getError());
}
@Test @Test
public void registerUserMissingPassword() { public void registerUserMissingPassword() {
registerUser("registerUserMissingPassword", null, "Please specify password"); appPage.open();
loginPage.register();
Assert.assertTrue(registerPage.isCurrent());
registerPage.register("name", "email", "username", null, null);
Assert.assertTrue(registerPage.isCurrent());
Assert.assertEquals("Please specify password", registerPage.getError());
} }
@Test @Test
public void registerUserMissingUsername() { public void registerUserMissingUsername() {
registerUser(null, "password", "Please specify username"); appPage.open();
loginPage.register();
Assert.assertTrue(registerPage.isCurrent());
registerPage.register("name", "email", null, "password", "password");
Assert.assertTrue(registerPage.isCurrent());
Assert.assertEquals("Please specify username", registerPage.getError());
} }
@Test @Test
public void registerUserSuccess() { public void registerUserSuccess() {
registerUser("registerUserSuccess", "password"); appPage.open();
loginPage.register();
Assert.assertTrue(registerPage.isCurrent());
registerPage.register("name", "email", "username", "password", "password");
Assert.assertTrue(appPage.isCurrent());
} }
} }

View file

@ -0,0 +1,123 @@
/*
* JBoss, Home of Professional Open Source.
* Copyright 2012, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.keycloak.testsuite;
import java.io.IOException;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.drone.api.annotation.Drone;
import org.jboss.arquillian.graphene.page.Page;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.RegisterPage;
import org.openqa.selenium.WebDriver;
import com.icegreen.greenmail.util.GreenMail;
import com.icegreen.greenmail.util.ServerSetup;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@RunWith(Arquillian.class)
public class RequiredActionEmailVerificationTest {
@Deployment(name = "app", testable = false, order = 3)
public static WebArchive appDeployment() {
return Deployments.appDeployment();
}
@Deployment(name = "auth-server", testable = false, order = 2)
public static WebArchive deployment() {
return Deployments.deployment().addAsResource("testrealm-email.json", "META-INF/testrealm.json");
}
@Deployment(name = "properties", testable = false, order = 1)
public static WebArchive propertiesDeployment() {
return ShrinkWrap.create(WebArchive.class, "properties.war").addClass(SystemPropertiesSetter.class)
.addAsWebInfResource("web-properties-email-verfication.xml", "web.xml");
}
@Page
protected AppPage appPage;
@Drone
protected WebDriver browser;
@Page
protected LoginPage loginPage;
@Page
protected RegisterPage registerPage;
private GreenMail greenMail;
@Before
public void before() {
ServerSetup setup = new ServerSetup(3025, "localhost", "smtp");
greenMail = new GreenMail(setup);
greenMail.start();
}
@After
public void after() {
appPage.open();
if (appPage.isCurrent()) {
appPage.logout();
}
if (greenMail != null) {
greenMail.stop();
}
}
@Test
public void verifyEmail() throws IOException, MessagingException {
appPage.open();
loginPage.register();
registerPage.register("name", "email", "verifyEmail", "password", "password");
Assert.assertTrue(browser.getPageSource().contains("Please verify your email address"));
MimeMessage message = greenMail.getReceivedMessages()[0];
String body = (String) message.getContent();
String verificationUrl = body.split("\n")[0];
browser.navigate().to(verificationUrl.trim());
Assert.assertTrue(appPage.isCurrent());
Assert.assertEquals("verifyEmail", appPage.getUser());
}
}

View file

@ -0,0 +1,104 @@
/*
* JBoss, Home of Professional Open Source.
* Copyright 2012, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.keycloak.testsuite;
import java.net.MalformedURLException;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.drone.api.annotation.Drone;
import org.jboss.arquillian.graphene.page.Page;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.RegisterPage;
import org.keycloak.testsuite.pages.TotpPage;
import org.openqa.selenium.WebDriver;
import org.picketlink.idm.credential.util.TimeBasedOTP;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@RunWith(Arquillian.class)
public class RequiredActionTotpSetupTest {
@Deployment(name = "app", testable = false, order = 2)
public static WebArchive appDeployment() {
return Deployments.appDeployment();
}
@Deployment(name = "auth-server", testable = false, order = 1)
public static WebArchive deployment() {
return Deployments.deployment().addAsResource("testrealm-totp.json", "META-INF/testrealm.json");
}
@Page
protected AppPage appPage;
@Drone
protected WebDriver browser;
@Page
protected TotpPage totpPage;
@Page
protected LoginPage loginPage;
@Page
protected RegisterPage registerPage;
protected TimeBasedOTP totp;
@Before
public void before() throws MalformedURLException {
totp = new TimeBasedOTP();
}
@After
public void after() {
appPage.open();
if (appPage.isCurrent()) {
appPage.logout();
}
}
@Test
public void setupTotp() {
appPage.open();
loginPage.register();
registerPage.register("name", "email", "setupTotp", "password", "password");
Assert.assertTrue(totpPage.isCurrent());
totpPage.configure(totp.generate(totpPage.getTotpSecret()));
Assert.assertTrue(appPage.isCurrent());
Assert.assertEquals("setupTotp", appPage.getUser());
}
}

View file

@ -0,0 +1,109 @@
/*
* JBoss, Home of Professional Open Source.
* Copyright 2012, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.keycloak.testsuite;
import java.io.IOException;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.graphene.page.Page;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.keycloak.testsuite.pages.ChangePasswordPage;
import com.icegreen.greenmail.util.GreenMail;
import com.icegreen.greenmail.util.ServerSetup;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@RunWith(Arquillian.class)
public class ResetPasswordTest extends AbstractDroneTest {
@Deployment(name = "properties", testable = false, order = 1)
public static WebArchive propertiesDeployment() {
return ShrinkWrap.create(WebArchive.class, "properties.war").addClass(SystemPropertiesSetter.class)
.addAsWebInfResource("web-properties-email-verfication.xml", "web.xml");
}
private GreenMail greenMail;
@Page
protected ChangePasswordPage changePasswordPage;
@Before
public void before() {
ServerSetup setup = new ServerSetup(3025, "localhost", "smtp");
greenMail = new GreenMail(setup);
greenMail.start();
}
@After
public void after() {
if (greenMail != null) {
greenMail.stop();
}
}
@Test
public void resetPassword() throws IOException, MessagingException {
appPage.open();
Assert.assertTrue(loginPage.isCurrent());
String url = browser.getCurrentUrl();
url = url.replace("tokens/login", "account/password-reset");
url = url + "&username=bburke@redhat.com";
browser.navigate().to(url);
MimeMessage message = greenMail.getReceivedMessages()[0];
String body = (String) message.getContent();
String changePasswordUrl = body.split("\n")[0];
browser.navigate().to(changePasswordUrl.trim());
changePasswordPage.changePassword("new-password", "new-password");
Assert.assertTrue(loginPage.isCurrent());
loginPage.login("bburke@redhat.com", "password");
Assert.assertTrue(loginPage.isCurrent());
Assert.assertEquals("Invalid username or password", loginPage.getError());
loginPage.login("bburke@redhat.com", "new-password");
Assert.assertTrue(appPage.isCurrent());
Assert.assertEquals("bburke@redhat.com", appPage.getUser());
}
}

View file

@ -0,0 +1,55 @@
package org.keycloak.testsuite;
import java.io.IOException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
public class SystemPropertiesSetter implements Servlet {
@Override
public void destroy() {
for (Entry<String, String> e : original.entrySet()) {
System.setProperty(e.getKey(), e.getValue());
System.out.println("RESET " + e.getKey() + "=" + e.getValue());
}
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public String getServletInfo() {
return null;
}
private Map<String, String> original = new HashMap<String, String>();
@Override
public void init(ServletConfig arg0) throws ServletException {
Enumeration<String> n = arg0.getInitParameterNames();
while (n.hasMoreElements()) {
String k = n.nextElement();
String v = arg0.getInitParameter(k);
original.put(k, v);
System.setProperty(k, v);
System.out.println("SET " + k + "=" + v);
}
}
@Override
public void service(ServletRequest arg0, ServletResponse arg1) throws ServletException, IOException {
}
}

View file

@ -25,6 +25,9 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import javax.servlet.ServletContext;
import javax.ws.rs.core.Context;
import org.jboss.resteasy.jwt.JsonSerialization; import org.jboss.resteasy.jwt.JsonSerialization;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
@ -40,27 +43,8 @@ import org.keycloak.services.resources.SaasService;
*/ */
public class TestApplication extends KeycloakApplication { public class TestApplication extends KeycloakApplication {
public static RealmRepresentation loadJson(String path) public TestApplication(@Context ServletContext servletContext) {
{ super(servletContext);
InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(path);
ByteArrayOutputStream os = new ByteArrayOutputStream();
int c;
try {
while ( (c = is.read()) != -1)
{
os.write(c);
}
byte[] bytes = os.toByteArray();
//System.out.println(new String(bytes));
return JsonSerialization.fromBytes(RealmRepresentation.class, bytes);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public TestApplication() {
super();
KeycloakSession session = factory.createSession(); KeycloakSession session = factory.createSession();
session.getTransaction().begin(); session.getTransaction().begin();
RealmManager realmManager = new RealmManager(session); RealmManager realmManager = new RealmManager(session);
@ -90,5 +74,21 @@ public class TestApplication extends KeycloakApplication {
} }
public static RealmRepresentation loadJson(String path) {
InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(path);
ByteArrayOutputStream os = new ByteArrayOutputStream();
int c;
try {
while ((c = is.read()) != -1) {
os.write(c);
}
byte[] bytes = os.toByteArray();
// System.out.println(new String(bytes));
return JsonSerialization.fromBytes(RealmRepresentation.class, bytes);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
} }

View file

@ -23,11 +23,14 @@ package org.keycloak.testsuite;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import org.jboss.arquillian.graphene.page.Page;
import org.jboss.arquillian.junit.Arquillian; import org.jboss.arquillian.junit.Arquillian;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.keycloak.testsuite.pages.LoginTotpPage;
import org.keycloak.testsuite.pages.TotpPage;
import org.picketlink.idm.credential.util.TimeBasedOTP; import org.picketlink.idm.credential.util.TimeBasedOTP;
/** /**
@ -36,80 +39,77 @@ import org.picketlink.idm.credential.util.TimeBasedOTP;
@RunWith(Arquillian.class) @RunWith(Arquillian.class)
public class TotpTest extends AbstractDroneTest { public class TotpTest extends AbstractDroneTest {
@Page
private LoginTotpPage loginTotpPage;
private TimeBasedOTP totp; private TimeBasedOTP totp;
private String totpSecret;
@Page
protected TotpPage totpPage;
@Before @Before
public void before() throws MalformedURLException { public void before() throws MalformedURLException {
super.before();
totp = new TimeBasedOTP(); totp = new TimeBasedOTP();
} }
public void configureTotp() {
selenium.open(authServerUrl + "/rest/realms/demo/account/totp");
selenium.waitForPageToLoad("10000");
Assert.assertTrue(selenium.isTextPresent("To setup Google Authenticator"));
totpSecret = selenium.getValue("totpSecret");
String code = totp.generate(totpSecret);
selenium.type("id=totp", code);
selenium.click("css=input[type=\"submit\"]");
selenium.waitForPageToLoad("30000");
Assert.assertTrue(selenium.isTextPresent("Google Authenticator enabled"));
}
@Test @Test
public void loginWithTotpFailure() { public void loginWithTotpFailure() {
registerUser("loginWithTotpFailure", "password"); appPage.open();
configureTotp(); loginPage.register();
logout(); registerPage.register("name", "email", "loginWithTotpFailure", "password", "password");
totpPage.open();
selenium.type("id=username", "loginWithTotpFailure"); String totpSecret = totpPage.getTotpSecret();
selenium.type("id=password", "password"); totpPage.configure(totp.generate(totpSecret));
selenium.click("css=input[type=\"submit\"]"); appPage.open();
selenium.waitForPageToLoad(DEFAULT_WAIT); appPage.logout();
Assert.assertEquals("Log in to demo", selenium.getTitle()); loginPage.login("loginWithTotpSuccess", "password");
selenium.type("id=totp", "123456"); Assert.assertFalse(appPage.isCurrent());
selenium.click("css=input[type=\"submit\"]"); loginTotpPage.login("123456");
selenium.waitForPageToLoad(DEFAULT_WAIT);
Assert.assertTrue(selenium.isTextPresent("Invalid username or password")); Assert.assertTrue(loginTotpPage.isCurrent());
Assert.assertEquals("Invalid username or password", loginTotpPage.getError());
} }
@Test @Test
public void loginWithTotpSuccess() { public void loginWithTotpSuccess() {
registerUser("loginWithTotpSuccess", "password"); appPage.open();
configureTotp(); loginPage.register();
logout(); registerPage.register("name", "email", "loginWithTotpSuccess", "password", "password");
totpPage.open();
selenium.type("id=username", "loginWithTotpSuccess"); String totpSecret = totpPage.getTotpSecret();
selenium.type("id=password", "password"); totpPage.configure(totp.generate(totpSecret));
selenium.click("css=input[type=\"submit\"]"); appPage.open();
selenium.waitForPageToLoad(DEFAULT_WAIT); appPage.logout();
Assert.assertEquals("Log in to demo", selenium.getTitle()); loginPage.login("loginWithTotpSuccess", "password");
selenium.type("id=totp", totp.generate(totpSecret)); Assert.assertFalse(appPage.isCurrent());
selenium.click("css=input[type=\"submit\"]"); loginTotpPage.login(totp.generate(totpSecret));
selenium.waitForPageToLoad(DEFAULT_WAIT);
Assert.assertEquals("loginWithTotpSuccess", selenium.getText("id=user")); Assert.assertTrue(appPage.isCurrent());
} }
@Test @Test
public void setupTotp() { public void setupTotp() {
registerUser("setupTotp", "password"); appPage.open();
configureTotp(); loginPage.register();
registerPage.register("name", "email", "setupTotp", "password", "password");
totpPage.open();
Assert.assertTrue(totpPage.isCurrent());
totpPage.configure(totp.generate(totpPage.getTotpSecret()));
Assert.assertTrue(browser.getPageSource().contains("Google Authenticator enabled"));
} }
} }

View file

@ -0,0 +1,38 @@
package org.keycloak.testsuite.pages;
import org.jboss.arquillian.drone.api.annotation.Drone;
import org.keycloak.testsuite.Constants;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
public class AppPage {
private static String PATH = Constants.APP_ROOT + "/user.jsp";
@Drone
private WebDriver browser;
@FindBy(id = "logout")
private WebElement logoutLink;
@FindBy(id = "user")
private WebElement user;
public void open() {
browser.navigate().to(PATH);
}
public String getUser() {
return user.getText();
}
public boolean isCurrent() {
return browser.getCurrentUrl().equals(PATH);
}
public void logout() {
logoutLink.click();
}
}

View file

@ -0,0 +1,51 @@
package org.keycloak.testsuite.pages;
import org.jboss.arquillian.drone.api.annotation.Drone;
import org.keycloak.testsuite.Constants;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
public class ChangePasswordPage {
private static String PATH = Constants.AUTH_SERVER_ROOT + "/rest/realms/demo/account/password";
@Drone
private WebDriver browser;
@FindBy(id = "password")
private WebElement passwordInput;
@FindBy(id = "password-new")
private WebElement newPasswordInput;
@FindBy(id = "password-confirm")
private WebElement passwordConfirmInput;
@FindBy(css = "input[type=\"submit\"]")
private WebElement submitButton;
public void changePassword(String newPassword, String passwordConfirm) {
newPasswordInput.sendKeys(newPassword);
passwordConfirmInput.sendKeys(passwordConfirm);
submitButton.click();
}
public void changePassword(String password, String newPassword, String passwordConfirm) {
passwordInput.sendKeys(password);
newPasswordInput.sendKeys(newPassword);
passwordConfirmInput.sendKeys(passwordConfirm);
submitButton.click();
}
public boolean isCurrent() {
return browser.getPageSource().contains("Change Password");
}
public void open() {
browser.navigate().to(PATH);
}
}

View file

@ -0,0 +1,50 @@
package org.keycloak.testsuite.pages;
import org.jboss.arquillian.drone.api.annotation.Drone;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
public class LoginPage {
@Drone
private WebDriver browser;
@FindBy(id = "username")
private WebElement usernameInput;
@FindBy(id = "password")
private WebElement passwordInput;
@FindBy(css = "input[type=\"submit\"]")
private WebElement submitButton;
@FindBy(linkText = "Register")
private WebElement registerLink;
@FindBy(id = "loginError")
private WebElement loginErrorMessage;
public void login(String username, String password) {
usernameInput.clear();
usernameInput.sendKeys(username);
passwordInput.clear();
passwordInput.sendKeys(password);
submitButton.click();
}
public String getError() {
return loginErrorMessage != null ? loginErrorMessage.getText() : null;
}
public boolean isCurrent() {
return browser.getTitle().equals("Log in to demo");
}
public void register() {
registerLink.click();
}
}

View file

@ -0,0 +1,39 @@
package org.keycloak.testsuite.pages;
import org.jboss.arquillian.drone.api.annotation.Drone;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
public class LoginTotpPage {
@Drone
private WebDriver browser;
@FindBy(id = "totp")
private WebElement totpInput;
@FindBy(css = "input[type=\"submit\"]")
private WebElement submitButton;
@FindBy(linkText = "Register")
private WebElement registerLink;
@FindBy(id = "loginError")
private WebElement loginErrorMessage;
public void login(String totp) {
totpInput.sendKeys(totp);
submitButton.click();
}
public String getError() {
return loginErrorMessage != null ? loginErrorMessage.getText() : null;
}
public boolean isCurrent() {
return browser.getTitle().equals("Log in to demo");
}
}

View file

@ -0,0 +1,66 @@
package org.keycloak.testsuite.pages;
import org.jboss.arquillian.drone.api.annotation.Drone;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
public class RegisterPage {
@Drone
private WebDriver browser;
@FindBy(id = "name")
private WebElement nameInput;
@FindBy(id = "email")
private WebElement emailInput;
@FindBy(id = "username")
private WebElement usernameInput;
@FindBy(id = "password")
private WebElement passwordInput;
@FindBy(id = "password-confirm")
private WebElement passwordConfirmInput;
@FindBy(css = "input[type=\"submit\"]")
private WebElement submitButton;
@FindBy(id = "loginError")
private WebElement loginErrorMessage;
public void register(String name, String email, String username, String password, String passwordConfirm) {
if (name != null) {
nameInput.sendKeys(name);
}
if (email != null) {
emailInput.sendKeys(email);
}
if (username != null) {
usernameInput.sendKeys(username);
}
if (password != null) {
passwordInput.sendKeys(password);
}
if (passwordConfirm != null) {
passwordConfirmInput.sendKeys(passwordConfirm);
}
submitButton.click();
}
public String getError() {
return loginErrorMessage != null ? loginErrorMessage.getText() : null;
}
public boolean isCurrent() {
return browser.getTitle().equals("Register with demo");
}
}

View file

@ -0,0 +1,42 @@
package org.keycloak.testsuite.pages;
import org.jboss.arquillian.drone.api.annotation.Drone;
import org.keycloak.testsuite.Constants;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
public class TotpPage {
private static String PATH = Constants.AUTH_SERVER_ROOT + "/rest/realms/demo/account/totp";
@Drone
private WebDriver browser;
@FindBy(id = "totpSecret")
private WebElement totpSecret;
@FindBy(id = "totp")
private WebElement totpInput;
@FindBy(css = "input[type=\"submit\"]")
private WebElement submitButton;
public void configure(String totp) {
totpInput.sendKeys(totp);
submitButton.click();
}
public String getTotpSecret() {
return totpSecret.getAttribute("value");
}
public boolean isCurrent() {
return browser.getPageSource().contains("Google Authenticator Setup");
}
public void open() {
browser.navigate().to(PATH);
}
}

View file

@ -0,0 +1,59 @@
package org.keycloak.testsuite.pages;
import org.jboss.arquillian.drone.api.annotation.Drone;
import org.keycloak.testsuite.Constants;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
public class UpdateProfilePage {
private static String PATH = Constants.AUTH_SERVER_ROOT + "/rest/realms/demo/account";
@Drone
private WebDriver browser;
@FindBy(id = "firstName")
private WebElement firstNameInput;
@FindBy(id = "lastName")
private WebElement lastNameInput;
@FindBy(id = "email")
private WebElement emailInput;
@FindBy(css = "input[type=\"submit\"]")
private WebElement submitButton;
public void updateProfile(String firstName, String lastName, String email) {
firstNameInput.clear();
firstNameInput.sendKeys(firstName);
lastNameInput.clear();
lastNameInput.sendKeys(lastName);
emailInput.clear();
emailInput.sendKeys(email);
submitButton.click();
}
public String getFirstName() {
return firstNameInput.getAttribute("value");
}
public String getLastName() {
return lastNameInput.getAttribute("value");
}
public String getEmail() {
return emailInput.getAttribute("value");
}
public boolean isCurrent() {
return browser.getPageSource().contains("Edit Account");
}
public void open() {
browser.navigate().to(PATH);
}
}

View file

@ -1,9 +1,9 @@
<arquillian xmlns="http://jboss.org/schema/arquillian" <arquillian xmlns="http://jboss.org/schema/arquillian"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation=" xsi:schemaLocation="
http://jboss.org/schema/arquillian http://jboss.org/schema/arquillian
http://jboss.org/schema/arquillian/arquillian_1_0.xsd"> http://jboss.org/schema/arquillian/arquillian_1_0.xsd">
<extension qualifier="selenium"> <extension qualifier="webdriver">
<property name="browser">*googlechrome</property> <property name="browser">${browser:phantomjs}</property>
</extension> </extension>
</arquillian> </arquillian>

View file

@ -0,0 +1,76 @@
{
"realm": "demo",
"enabled": true,
"tokenLifespan": 300,
"accessCodeLifespan": 10,
"accessCodeLifespanUserAction": 600,
"sslNotRequired": true,
"cookieLoginAllowed": true,
"registrationAllowed": true,
"verifyEmail": true,
"privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"requiredCredentials": [ "password" ],
"requiredApplicationCredentials": [ "password" ],
"requiredOAuthClientCredentials": [ "password" ],
"defaultRoles": [ "user" ],
"users" : [
{
"username" : "bburke@redhat.com",
"status": "ENABLED",
"email" : "bburke@redhat.com",
"credentials" : [
{ "type" : "password",
"value" : "password" }
]
},
{
"username" : "third-party",
"status": "ENABLED",
"credentials" : [
{ "type" : "password",
"value" : "password" }
]
}
],
"roles": [
{
"name": "user",
"description": "Have User privileges"
},
{
"name": "admin",
"description": "Have Administrator privileges"
}
],
"roleMappings": [
{
"username": "bburke@redhat.com",
"roles": ["user"]
},
{
"username": "third-party",
"roles": ["KEYCLOAK_IDENTITY_REQUESTER"]
}
],
"scopeMappings": [
{
"username": "third-party",
"roles": ["user"]
}
],
"applications": [
{
"name": "customer-portal",
"enabled": true,
"adminUrl": "http://localhost:8080/app/j_admin_request",
"useRealmMappings": true,
"credentials": [
{
"type": "password",
"value": "password"
}
]
}
]
}

View file

@ -0,0 +1,75 @@
{
"realm": "demo",
"enabled": true,
"tokenLifespan": 300,
"accessCodeLifespan": 10,
"accessCodeLifespanUserAction": 600,
"sslNotRequired": true,
"cookieLoginAllowed": true,
"registrationAllowed": true,
"privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"requiredCredentials": [ "password", "totp" ],
"requiredApplicationCredentials": [ "password" ],
"requiredOAuthClientCredentials": [ "password" ],
"defaultRoles": [ "user" ],
"users" : [
{
"username" : "bburke@redhat.com",
"status": "ENABLED",
"email" : "bburke@redhat.com",
"credentials" : [
{ "type" : "password",
"value" : "password" }
]
},
{
"username" : "third-party",
"status": "ENABLED",
"credentials" : [
{ "type" : "password",
"value" : "password" }
]
}
],
"roles": [
{
"name": "user",
"description": "Have User privileges"
},
{
"name": "admin",
"description": "Have Administrator privileges"
}
],
"roleMappings": [
{
"username": "bburke@redhat.com",
"roles": ["user"]
},
{
"username": "third-party",
"roles": ["KEYCLOAK_IDENTITY_REQUESTER"]
}
],
"scopeMappings": [
{
"username": "third-party",
"roles": ["user"]
}
],
"applications": [
{
"name": "customer-portal",
"enabled": true,
"adminUrl": "http://localhost:8080/app/j_admin_request",
"useRealmMappings": true,
"credentials": [
{
"type": "password",
"value": "password"
}
]
}
]
}

View file

@ -3,6 +3,7 @@
"enabled": true, "enabled": true,
"tokenLifespan": 300, "tokenLifespan": 300,
"accessCodeLifespan": 10, "accessCodeLifespan": 10,
"accessCodeLifespanUserAction": 600,
"sslNotRequired": true, "sslNotRequired": true,
"cookieLoginAllowed": true, "cookieLoginAllowed": true,
"registrationAllowed": true, "registrationAllowed": true,
@ -15,10 +16,8 @@
"users" : [ "users" : [
{ {
"username" : "bburke@redhat.com", "username" : "bburke@redhat.com",
"enabled" : true, "status": "ENABLED",
"attributes" : { "email" : "bburke@redhat.com",
"email" : "bburke@redhat.com"
},
"credentials" : [ "credentials" : [
{ "type" : "password", { "type" : "password",
"value" : "password" } "value" : "password" }
@ -26,7 +25,7 @@
}, },
{ {
"username" : "third-party", "username" : "third-party",
"enabled" : true, "status": "ENABLED",
"credentials" : [ "credentials" : [
{ "type" : "password", { "type" : "password",
"value" : "password" } "value" : "password" }

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<module-name>properties</module-name>
<servlet>
<servlet-name>SystemPropertiesSetter</servlet-name>
<servlet-class>org.keycloak.testsuite.SystemPropertiesSetter</servlet-class>
<init-param>
<param-name>keycloak.mail.smtp.from</param-name>
<param-value>auto@keycloak.org</param-value>
</init-param>
<init-param>
<param-name>keycloak.mail.smtp.host</param-name>
<param-value>localhost</param-value>
</init-param>
<init-param>
<param-name>keycloak.mail.smtp.port</param-name>
<param-value>3025</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
</web-app>

View file

@ -21,6 +21,16 @@
<async-supported>true</async-supported> <async-supported>true</async-supported>
</servlet> </servlet>
<filter>
<filter-name>Keycloak Session Management</filter-name>
<filter-class>org.keycloak.services.filters.KeycloakSessionServletFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>Keycloak Session Management</filter-name>
<url-pattern>/rest/*</url-pattern>
</filter-mapping>
<servlet-mapping> <servlet-mapping>
<servlet-name>Resteasy</servlet-name> <servlet-name>Resteasy</servlet-name>
<url-pattern>/rest/*</url-pattern> <url-pattern>/rest/*</url-pattern>