Merge pull request #318 from mposolda/ldap

Basic support for registration of new users in AuthenticationProvider (like LDAP). Other improvements in auth SPI
This commit is contained in:
Stian Thorgersen 2014-04-07 09:35:35 +01:00
commit a0cd2bf5a7
14 changed files with 192 additions and 33 deletions

View file

@ -31,5 +31,5 @@ public interface Account {
Account setEvents(List<Event> events); Account setEvents(List<Event> events);
Account setFeatures(boolean social, boolean audit); Account setFeatures(boolean social, boolean audit, boolean passwordUpdateSupported);
} }

View file

@ -44,6 +44,7 @@ public class FreeMarkerAccount implements Account {
private List<Event> events; private List<Event> events;
private boolean social; private boolean social;
private boolean audit; private boolean audit;
private boolean passwordUpdateSupported;
public static enum MessageType {SUCCESS, WARNING, ERROR} public static enum MessageType {SUCCESS, WARNING, ERROR}
@ -95,7 +96,7 @@ public class FreeMarkerAccount implements Account {
attributes.put("url", new UrlBean(realm, theme, baseUri)); attributes.put("url", new UrlBean(realm, theme, baseUri));
attributes.put("features", new FeaturesBean(social, audit)); attributes.put("features", new FeaturesBean(social, audit, passwordUpdateSupported));
switch (page) { switch (page) {
case ACCOUNT: case ACCOUNT:
@ -172,9 +173,10 @@ public class FreeMarkerAccount implements Account {
} }
@Override @Override
public Account setFeatures(boolean social, boolean audit) { public Account setFeatures(boolean social, boolean audit, boolean passwordUpdateSupported) {
this.social = social; this.social = social;
this.audit = audit; this.audit = audit;
this.passwordUpdateSupported = passwordUpdateSupported;
return this; return this;
} }
} }

View file

@ -7,10 +7,12 @@ public class FeaturesBean {
private final boolean social; private final boolean social;
private final boolean log; private final boolean log;
private final boolean passwordUpdateSupported;
public FeaturesBean(boolean social, boolean log) { public FeaturesBean(boolean social, boolean log, boolean passwordUpdateSupported) {
this.social = social; this.social = social;
this.log = log; this.log = log;
this.passwordUpdateSupported = passwordUpdateSupported;
} }
public boolean isSocial() { public boolean isSocial() {
@ -21,4 +23,7 @@ public class FeaturesBean {
return log; return log;
} }
public boolean isPasswordUpdateSupported() {
return passwordUpdateSupported;
}
} }

View file

@ -40,7 +40,7 @@
<div class="bs-sidebar col-sm-3 ng-scope"> <div class="bs-sidebar col-sm-3 ng-scope">
<ul> <ul>
<li class="<#if active=='account'>active</#if>"><a href="${url.accountUrl}">Account</a></li> <li class="<#if active=='account'>active</#if>"><a href="${url.accountUrl}">Account</a></li>
<li class="<#if active=='password'>active</#if>"><a href="${url.passwordUrl}">Password</a></li> <#if features.passwordUpdateSupported><li class="<#if active=='password'>active</#if>"><a href="${url.passwordUrl}">Password</a></li></#if>
<li class="<#if active=='totp'>active</#if>"><a href="${url.totpUrl}">Authenticator</a></li> <li class="<#if active=='totp'>active</#if>"><a href="${url.totpUrl}">Authenticator</a></li>
<#if features.social><li class="<#if active=='social'>active</#if>"><a href="${url.socialUrl}">Social</a></li></#if> <#if features.social><li class="<#if active=='social'>active</#if>"><a href="${url.socialUrl}">Social</a></li></#if>
<#if features.log><li class="<#if active=='log'>active</#if>"><a href="${url.logUrl}">Log</a></li></#if> <#if features.log><li class="<#if active=='log'>active</#if>"><a href="${url.logUrl}">Log</a></li></#if>

View file

@ -231,6 +231,7 @@ public class AuthenticationManager {
user.setLastName(authUser.getLastName()); user.setLastName(authUser.getLastName());
user.setEmail(authUser.getEmail()); user.setEmail(authUser.getEmail());
realm.setAuthenticationLink(user, new AuthenticationLinkModel(authUser.getProviderName(), authUser.getId())); realm.setAuthenticationLink(user, new AuthenticationLinkModel(authUser.getProviderName(), authUser.getId()));
logger.info("User " + authUser.getUsername() + " created and linked with provider " + authUser.getProviderName());
} else { } else {
logger.warn("User " + username + " not found"); logger.warn("User " + username + " not found");
return AuthenticationStatus.INVALID_USER; return AuthenticationStatus.INVALID_USER;

View file

@ -35,6 +35,8 @@ import org.keycloak.audit.Events;
import org.keycloak.jaxrs.JaxrsOAuthClient; import org.keycloak.jaxrs.JaxrsOAuthClient;
import org.keycloak.models.AccountRoles; import org.keycloak.models.AccountRoles;
import org.keycloak.models.ApplicationModel; import org.keycloak.models.ApplicationModel;
import org.keycloak.models.AuthenticationLinkModel;
import org.keycloak.models.AuthenticationProviderModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
@ -143,14 +145,23 @@ public class AccountService {
public void init() { public void init() {
auditProvider = providers.getProvider(AuditProvider.class); auditProvider = providers.getProvider(AuditProvider.class);
account = AccountLoader.load().createAccount(uriInfo).setRealm(realm).setFeatures(realm.isSocial(), auditProvider != null); account = AccountLoader.load().createAccount(uriInfo).setRealm(realm);
boolean passwordUpdateSupported = false;
auth = authManager.authenticate(realm, headers); auth = authManager.authenticate(realm, headers);
if (auth != null) { if (auth != null) {
account.setUser(auth.getUser()); account.setUser(auth.getUser());
AuthenticationLinkModel authLinkModel = realm.getAuthenticationLink(auth.getUser());
if (authLinkModel != null) {
AuthenticationProviderModel authProviderModel = AuthenticationProviderManager.getConfiguredProviderModel(realm, authLinkModel.getAuthProvider());
passwordUpdateSupported = authProviderModel.isPasswordUpdateSupported();
} }
} }
account.setFeatures(realm.isSocial(), auditProvider != null, passwordUpdateSupported);
}
public static UriBuilder accountServiceBaseUrl(UriInfo uriInfo) { public static UriBuilder accountServiceBaseUrl(UriInfo uriInfo) {
UriBuilder base = uriInfo.getBaseUriBuilder().path(RealmsResource.class).path(RealmsResource.class, "getAccountService"); UriBuilder base = uriInfo.getBaseUriBuilder().path(RealmsResource.class).path(RealmsResource.class, "getAccountService");
return base; return base;

View file

@ -383,13 +383,15 @@ public class TokenService {
return Flows.forms(realm, request, uriInfo).setError(error).setFormData(formData).createRegistration(); return Flows.forms(realm, request, uriInfo).setError(error).setFormData(formData).createRegistration();
} }
UserModel user = realm.getUser(username); AuthenticationProviderManager authenticationProviderManager = AuthenticationProviderManager.getManager(realm);
if (user != null) {
// Validate that user with this username doesn't exist in realm or any authentication provider
if (realm.getUser(username) != null || authenticationProviderManager.getUser(username) != null) {
audit.error(Errors.USERNAME_IN_USE); audit.error(Errors.USERNAME_IN_USE);
return Flows.forms(realm, request, uriInfo).setError(Messages.USERNAME_EXISTS).setFormData(formData).createRegistration(); return Flows.forms(realm, request, uriInfo).setError(Messages.USERNAME_EXISTS).setFormData(formData).createRegistration();
} }
user = realm.addUser(username); UserModel user = realm.addUser(username);
user.setEnabled(true); user.setEnabled(true);
user.setFirstName(formData.getFirst("firstName")); user.setFirstName(formData.getFirst("firstName"));
user.setLastName(formData.getFirst("lastName")); user.setLastName(formData.getFirst("lastName"));

View file

@ -29,6 +29,14 @@ public abstract class AbstractModelAuthenticationProvider implements Authenticat
return user == null ? null : createAuthenticatedUserInstance(user); return user == null ? null : createAuthenticatedUserInstance(user);
} }
@Override
public String registerUser(RealmModel currentRealm, Map<String, String> config, String username) throws AuthenticationProviderException {
RealmModel realm = getRealm(currentRealm, config);
UserModel user = currentRealm.addUser(username);
user.setEnabled(true);
return user.getId();
}
@Override @Override
public AuthProviderStatus validatePassword(RealmModel currentRealm, Map<String, String> config, String username, String password) throws AuthenticationProviderException { public AuthProviderStatus validatePassword(RealmModel currentRealm, Map<String, String> config, String username, String password) throws AuthenticationProviderException {
RealmModel realm = getRealm(currentRealm, config); RealmModel realm = getRealm(currentRealm, config);

View file

@ -14,6 +14,7 @@ import org.keycloak.spi.authentication.AuthenticationProvider;
import org.keycloak.spi.authentication.AuthenticationProviderException; import org.keycloak.spi.authentication.AuthenticationProviderException;
import org.keycloak.spi.picketlink.PartitionManagerProvider; import org.keycloak.spi.picketlink.PartitionManagerProvider;
import org.keycloak.util.ProviderLoader; import org.keycloak.util.ProviderLoader;
import org.picketlink.idm.IdentityManagementException;
import org.picketlink.idm.IdentityManager; import org.picketlink.idm.IdentityManager;
import org.picketlink.idm.PartitionManager; import org.picketlink.idm.PartitionManager;
import org.picketlink.idm.credential.Credentials; import org.picketlink.idm.credential.Credentials;
@ -44,17 +45,36 @@ public class PicketlinkAuthenticationProvider implements AuthenticationProvider
@Override @Override
public AuthUser getUser(RealmModel realm, Map<String, String> configuration, String username) throws AuthenticationProviderException { public AuthUser getUser(RealmModel realm, Map<String, String> configuration, String username) throws AuthenticationProviderException {
IdentityManager identityManager = getIdentityManager(realm); IdentityManager identityManager = getIdentityManager(realm);
try {
User picketlinkUser = BasicModel.getUser(identityManager, username); User picketlinkUser = BasicModel.getUser(identityManager, username);
return picketlinkUser == null ? null : new AuthUser(picketlinkUser.getId(), picketlinkUser.getLoginName(), getName()) return picketlinkUser == null ? null : new AuthUser(picketlinkUser.getId(), picketlinkUser.getLoginName(), getName())
.setName(picketlinkUser.getFirstName(), picketlinkUser.getLastName()) .setName(picketlinkUser.getFirstName(), picketlinkUser.getLastName())
.setEmail(picketlinkUser.getEmail()) .setEmail(picketlinkUser.getEmail())
.setProviderName(getName()); .setProviderName(getName());
} catch (IdentityManagementException ie) {
throw convertIDMException(ie);
}
}
@Override
public String registerUser(RealmModel realm, Map<String, String> configuration, String username) throws AuthenticationProviderException {
IdentityManager identityManager = getIdentityManager(realm);
try {
User picketlinkUser = new User(username);
identityManager.add(picketlinkUser);
return picketlinkUser.getId();
} catch (IdentityManagementException ie) {
throw convertIDMException(ie);
}
} }
@Override @Override
public AuthProviderStatus validatePassword(RealmModel realm, Map<String, String> configuration, String username, String password) throws AuthenticationProviderException { public AuthProviderStatus validatePassword(RealmModel realm, Map<String, String> configuration, String username, String password) throws AuthenticationProviderException {
IdentityManager identityManager = getIdentityManager(realm); IdentityManager identityManager = getIdentityManager(realm);
try {
UsernamePasswordCredentials credential = new UsernamePasswordCredentials(); UsernamePasswordCredentials credential = new UsernamePasswordCredentials();
credential.setUsername(username); credential.setUsername(username);
credential.setPassword(new Password(password.toCharArray())); credential.setPassword(new Password(password.toCharArray()));
@ -64,12 +84,16 @@ public class PicketlinkAuthenticationProvider implements AuthenticationProvider
} else { } else {
return AuthProviderStatus.INVALID_CREDENTIALS; return AuthProviderStatus.INVALID_CREDENTIALS;
} }
} catch (IdentityManagementException ie) {
throw convertIDMException(ie);
}
} }
@Override @Override
public boolean updateCredential(RealmModel realm, Map<String, String> configuration, String username, String password) throws AuthenticationProviderException { public boolean updateCredential(RealmModel realm, Map<String, String> configuration, String username, String password) throws AuthenticationProviderException {
IdentityManager identityManager = getIdentityManager(realm); IdentityManager identityManager = getIdentityManager(realm);
try {
User picketlinkUser = BasicModel.getUser(identityManager, username); User picketlinkUser = BasicModel.getUser(identityManager, username);
if (picketlinkUser == null) { if (picketlinkUser == null) {
logger.debugf("User '%s' doesn't exists. Skip password update", username); logger.debugf("User '%s' doesn't exists. Skip password update", username);
@ -78,6 +102,9 @@ public class PicketlinkAuthenticationProvider implements AuthenticationProvider
identityManager.updateCredential(picketlinkUser, new Password(password.toCharArray())); identityManager.updateCredential(picketlinkUser, new Password(password.toCharArray()));
return true; return true;
} catch (IdentityManagementException ie) {
throw convertIDMException(ie);
}
} }
public IdentityManager getIdentityManager(RealmModel realm) throws AuthenticationProviderException { public IdentityManager getIdentityManager(RealmModel realm) throws AuthenticationProviderException {
@ -103,4 +130,14 @@ public class PicketlinkAuthenticationProvider implements AuthenticationProvider
} }
return identityManager; return identityManager;
} }
private AuthenticationProviderException convertIDMException(IdentityManagementException ie) {
Throwable realCause = ie;
while (realCause.getCause() != null) {
realCause = realCause.getCause();
}
// Use the message from the realCause
return new AuthenticationProviderException(realCause.getMessage(), ie);
}
} }

View file

@ -30,6 +30,17 @@ public interface AuthenticationProvider {
*/ */
AuthUser getUser(RealmModel realm, Map<String, String> configuration, String username) throws AuthenticationProviderException; AuthUser getUser(RealmModel realm, Map<String, String> configuration, String username) throws AuthenticationProviderException;
/**
* Try to register user with this authentication provider
*
* @param realm
* @param configuration
* @param username
* @return ID of newly created user (For example ID from LDAP)
* @throws AuthenticationProviderException if user creation couldn't happen
*/
String registerUser(RealmModel realm, Map<String, String> configuration, String username) throws AuthenticationProviderException;
/** /**
* Standard Authentication flow * Standard Authentication flow
* *

View file

@ -73,7 +73,11 @@ public class AuthenticationProviderManager {
public AuthProviderStatus validatePassword(UserModel user, String password) { public AuthProviderStatus validatePassword(UserModel user, String password) {
AuthenticationLinkModel authLink = realm.getAuthenticationLink(user); AuthenticationLinkModel authLink = realm.getAuthenticationLink(user);
if (authLink == null) { if (authLink == null) {
authLink = new AuthenticationLinkModel(AuthenticationProviderModel.DEFAULT_PROVIDER.getProviderName(), user.getId()); // User not yet linked with any authenticationProvider. Find provider with biggest priority where he is and link
AuthUser authUser = getUser(user.getLoginName());
authLink = new AuthenticationLinkModel(authUser.getProviderName(), authUser.getId());
realm.setAuthenticationLink(user, authLink);
logger.infof("User '%s' linked with provider '%s'", authUser.getUsername(), authUser.getProviderName());
} }
String providerName = authLink.getAuthProvider(); String providerName = authLink.getAuthProvider();
@ -99,7 +103,38 @@ public class AuthenticationProviderManager {
public boolean updatePassword(UserModel user, String password) throws AuthenticationProviderException { public boolean updatePassword(UserModel user, String password) throws AuthenticationProviderException {
AuthenticationLinkModel authLink = realm.getAuthenticationLink(user); AuthenticationLinkModel authLink = realm.getAuthenticationLink(user);
if (authLink == null) { if (authLink == null) {
authLink = new AuthenticationLinkModel(AuthenticationProviderModel.DEFAULT_PROVIDER.getProviderName(), user.getId()); // Find provider with biggest priority where password update is supported. Then register user here and link him
List<AuthenticationProviderModel> configuredProviders = getConfiguredProviderModels(realm);
for (AuthenticationProviderModel providerModel : configuredProviders) {
if (providerModel.isPasswordUpdateSupported()) {
AuthenticationProvider delegate = getProvider(providerModel.getProviderName());
if (delegate != null) {
AuthUser authUser = delegate.getUser(realm, providerModel.getConfig(), user.getLoginName());
if (authUser != null) {
// Linking existing user supported just for "model" provider. In other cases throw exception
if (providerModel.getProviderName().equals(AuthenticationProviderModel.DEFAULT_PROVIDER.getProviderName())) {
authLink = new AuthenticationLinkModel(providerModel.getProviderName(), authUser.getId());
realm.setAuthenticationLink(user, authLink);
logger.infof("User '%s' linked with provider '%s'", authUser.getUsername(), authUser.getProviderName());
} else {
throw new AuthenticationProviderException("User " + authUser.getUsername() + " exists in provider "
+ authUser.getProviderName() + " but is not linked with model user");
}
} else {
String userIdInProvider = delegate.registerUser(realm, providerModel.getConfig(), user.getLoginName());
authLink = new AuthenticationLinkModel(providerModel.getProviderName(), userIdInProvider);
realm.setAuthenticationLink(user, authLink);
logger.infof("User '%s' registered in provider '%s' and linked", user.getLoginName(), providerModel.getProviderName());
}
break;
}
}
}
if (authLink == null) {
logger.warnf("No providers found where password update is supported for user '%s'", user.getLoginName());
return false;
}
} }
String providerName = authLink.getAuthProvider(); String providerName = authLink.getAuthProvider();
@ -147,7 +182,7 @@ public class AuthenticationProviderManager {
return delegate; return delegate;
} }
private List<AuthenticationProviderModel> getConfiguredProviderModels(RealmModel realm) { private static List<AuthenticationProviderModel> getConfiguredProviderModels(RealmModel realm) {
List<AuthenticationProviderModel> configuredProviders = realm.getAuthenticationProviders(); List<AuthenticationProviderModel> configuredProviders = realm.getAuthenticationProviders();
// Use model based authentication of current realm by default // Use model based authentication of current realm by default
@ -159,7 +194,7 @@ public class AuthenticationProviderManager {
return configuredProviders; return configuredProviders;
} }
private AuthenticationProviderModel getConfiguredProviderModel(RealmModel realm, String providerName) { public static AuthenticationProviderModel getConfiguredProviderModel(RealmModel realm, String providerName) {
List<AuthenticationProviderModel> providers = getConfiguredProviderModels(realm); List<AuthenticationProviderModel> providers = getConfiguredProviderModels(realm);
for (AuthenticationProviderModel provider : providers) { for (AuthenticationProviderModel provider : providers) {
if (providerName.equals(provider.getProviderName())) { if (providerName.equals(provider.getProviderName())) {

View file

@ -23,6 +23,7 @@ import org.keycloak.testsuite.pages.AccountPasswordPage;
import org.keycloak.testsuite.pages.AccountUpdateProfilePage; import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.RegisterPage;
import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.testsuite.rule.LDAPRule; import org.keycloak.testsuite.rule.LDAPRule;
import org.keycloak.testsuite.rule.WebResource; import org.keycloak.testsuite.rule.WebResource;
@ -82,6 +83,9 @@ public class AuthProvidersIntegrationTest {
@WebResource @WebResource
protected AppPage appPage; protected AppPage appPage;
@WebResource
protected RegisterPage registerPage;
@WebResource @WebResource
protected LoginPage loginPage; protected LoginPage loginPage;
@ -111,6 +115,9 @@ public class AuthProvidersIntegrationTest {
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
profilePage.open();
Assert.assertFalse(profilePage.isPasswordUpdateSupported());
} }
@Test @Test
@ -120,6 +127,9 @@ public class AuthProvidersIntegrationTest {
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
profilePage.open();
Assert.assertTrue(profilePage.isPasswordUpdateSupported());
} }
@Test @Test
@ -131,6 +141,7 @@ public class AuthProvidersIntegrationTest {
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
profilePage.open(); profilePage.open();
Assert.assertTrue(profilePage.isPasswordUpdateSupported());
Assert.assertEquals("John", profilePage.getFirstName()); Assert.assertEquals("John", profilePage.getFirstName());
Assert.assertEquals("Doe", profilePage.getLastName()); Assert.assertEquals("Doe", profilePage.getLastName());
Assert.assertEquals("john@email.org", profilePage.getEmail()); Assert.assertEquals("john@email.org", profilePage.getEmail());
@ -191,4 +202,26 @@ public class AuthProvidersIntegrationTest {
loginPage.login("john", "new-password"); loginPage.login("john", "new-password");
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
} }
@Test
public void registerExistingLdapUser() {
loginPage.open();
loginPage.clickRegister();
registerPage.assertCurrent();
registerPage.register("firstName", "lastName", "email", "existing", "password", "password");
registerPage.assertCurrent();
Assert.assertEquals("Username already exists", registerPage.getError());
}
@Test
public void registerUserLdapSuccess() {
loginPage.open();
loginPage.clickRegister();
registerPage.assertCurrent();
registerPage.register("firstName", "lastName", "email", "registerUserSuccess", "password", "password");
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
}
} }

View file

@ -96,4 +96,8 @@ public class AccountUpdateProfilePage extends AbstractAccountPage {
public String getError() { public String getError() {
return errorMessage.getText(); return errorMessage.getText();
} }
public boolean isPasswordUpdateSupported() {
return driver.getPageSource().contains(PATH + "/password");
}
} }

View file

@ -28,3 +28,13 @@ uid: john
cn: John cn: John
sn: Doe sn: Doe
mail: john@email.org mail: john@email.org
dn: uid=existing,ou=People,dc=keycloak,dc=org
objectclass: top
objectclass: uidObject
objectclass: person
objectclass: inetOrgPerson
uid: existing
cn: Existing
sn: Foo
mail: existing@email.org