This commit is contained in:
Bill Burke 2013-09-11 09:44:36 -04:00
commit bfe9beae72
36 changed files with 741 additions and 98 deletions

View file

@ -19,6 +19,7 @@ public class RealmRepresentation {
protected boolean cookieLoginAllowed; protected boolean cookieLoginAllowed;
protected boolean registrationAllowed; protected boolean registrationAllowed;
protected boolean social; protected boolean social;
protected boolean automaticRegistrationAfterSocialLogin;
protected String privateKey; protected String privateKey;
protected String publicKey; protected String publicKey;
protected List<RoleRepresentation> roles; protected List<RoleRepresentation> roles;
@ -29,6 +30,7 @@ public class RealmRepresentation {
protected List<UserRepresentation> users; protected List<UserRepresentation> users;
protected List<RoleMappingRepresentation> roleMappings; protected List<RoleMappingRepresentation> roleMappings;
protected List<ScopeMappingRepresentation> scopeMappings; protected List<ScopeMappingRepresentation> scopeMappings;
protected List<SocialMappingRepresentation> socialMappings;
protected List<ApplicationRepresentation> applications; protected List<ApplicationRepresentation> applications;
@ -144,6 +146,18 @@ public class RealmRepresentation {
return mapping; return mapping;
} }
public List<SocialMappingRepresentation> getSocialMappings() {
return socialMappings;
}
public SocialMappingRepresentation socialMapping(String username) {
SocialMappingRepresentation mapping = new SocialMappingRepresentation();
mapping.setUsername(username);
if (socialMappings == null) socialMappings = new ArrayList<SocialMappingRepresentation>();
socialMappings.add(mapping);
return mapping;
}
public Set<String> getRequiredCredentials() { public Set<String> getRequiredCredentials() {
return requiredCredentials; return requiredCredentials;
} }
@ -223,4 +237,12 @@ public class RealmRepresentation {
public void setSocial(boolean social) { public void setSocial(boolean social) {
this.social = social; this.social = social;
} }
public boolean isAutomaticRegistrationAfterSocialLogin() {
return automaticRegistrationAfterSocialLogin;
}
public void setAutomaticRegistrationAfterSocialLogin(boolean automaticRegistrationAfterSocialLogin) {
this.automaticRegistrationAfterSocialLogin = automaticRegistrationAfterSocialLogin;
}
} }

View file

@ -0,0 +1,26 @@
package org.keycloak.representations.idm;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class SocialLinkRepresentation {
protected String socialProvider;
protected String socialUsername;
public String getSocialProvider() {
return socialProvider;
}
public void setSocialProvider(String socialProvider) {
this.socialProvider = socialProvider;
}
public String getSocialUsername() {
return socialUsername;
}
public void setSocialUsername(String socialUsername) {
this.socialUsername = socialUsername;
}
}

View file

@ -0,0 +1,43 @@
package org.keycloak.representations.idm;
import java.util.ArrayList;
import java.util.List;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class SocialMappingRepresentation {
protected String self; // link
protected String username;
protected List<SocialLinkRepresentation> socialLinks;
public String getSelf() {
return self;
}
public void setSelf(String self) {
this.self = self;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public List<SocialLinkRepresentation> getSocialLinks() {
return socialLinks;
}
public SocialLinkRepresentation socialLink(String socialProvider, String socialUsername) {
SocialLinkRepresentation link = new SocialLinkRepresentation();
link.setSocialProvider(socialProvider);
link.setSocialUsername(socialUsername);
if (socialLinks == null) socialLinks = new ArrayList<SocialLinkRepresentation>();
socialLinks.add(link);
return link;
}
}

View file

@ -6,6 +6,8 @@
"sslNotRequired": true, "sslNotRequired": true,
"cookieLoginAllowed": true, "cookieLoginAllowed": true,
"registrationAllowed": true, "registrationAllowed": true,
"social": true,
"automaticRegistrationAfterSocialLogin": false,
"privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=", "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", "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"requiredCredentials": [ "password" ], "requiredCredentials": [ "password" ],

View file

@ -26,6 +26,7 @@ import java.util.Map;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import javax.faces.bean.ManagedBean; import javax.faces.bean.ManagedBean;
import javax.faces.bean.ManagedProperty;
import javax.faces.bean.RequestScoped; import javax.faces.bean.RequestScoped;
import javax.faces.context.FacesContext; import javax.faces.context.FacesContext;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@ -42,6 +43,8 @@ public class RegisterBean {
private HashMap<String, String> formData; private HashMap<String, String> formData;
private boolean socialRegistration;
@PostConstruct @PostConstruct
public void init() { public void init() {
FacesContext ctx = FacesContext.getCurrentInstance(); FacesContext ctx = FacesContext.getCurrentInstance();
@ -49,6 +52,9 @@ public class RegisterBean {
this.formData = new HashMap<String, String>(); this.formData = new HashMap<String, String>();
Boolean socialRegistrationAttr = (Boolean)request.getAttribute(FormFlows.SOCIAL_REGISTRATION);
this.socialRegistration = socialRegistrationAttr != null && socialRegistrationAttr;
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
MultivaluedMap<String, String> formData = (MultivaluedMap<String, String>) request.getAttribute(FormFlows.DATA); MultivaluedMap<String, String> formData = (MultivaluedMap<String, String>) request.getAttribute(FormFlows.DATA);
if (formData != null) { if (formData != null) {
@ -62,4 +68,8 @@ public class RegisterBean {
return formData; return formData;
} }
public boolean isSocialRegistration() {
return socialRegistration;
}
} }

View file

@ -46,6 +46,9 @@ public class SocialBean {
@ManagedProperty(value = "#{realm}") @ManagedProperty(value = "#{realm}")
private RealmBean realm; private RealmBean realm;
@ManagedProperty(value = "#{register}")
private RegisterBean registerBean;
@ManagedProperty(value = "#{url}") @ManagedProperty(value = "#{url}")
private UrlBean url; private UrlBean url;
@ -73,4 +76,32 @@ public class SocialBean {
return providers; return providers;
} }
// Display panel with social providers just in case that social is enabled for realm, but we are not in the middle of registration with social
public boolean isDisplaySocialProviders() {
return realm.isSocial() && !registerBean.isSocialRegistration();
}
public RealmBean getRealm() {
return realm;
}
public void setRealm(RealmBean realm) {
this.realm = realm;
}
public UrlBean getUrl() {
return url;
}
public void setUrl(UrlBean url) {
this.url = url;
}
public RegisterBean getRegisterBean() {
return registerBean;
}
public void setRegisterBean(RegisterBean registerBean) {
this.registerBean = registerBean;
}
} }

View file

@ -45,6 +45,9 @@ public class UrlBean {
@ManagedProperty(value = "#{realm}") @ManagedProperty(value = "#{realm}")
private RealmBean realm; private RealmBean realm;
@ManagedProperty(value = "#{register}")
private RegisterBean registerBean;
@PostConstruct @PostConstruct
public void init() { public void init() {
FacesContext ctx = FacesContext.getCurrentInstance(); FacesContext ctx = FacesContext.getCurrentInstance();
@ -64,6 +67,14 @@ public class UrlBean {
this.realm = realm; this.realm = realm;
} }
public RegisterBean getRegisterBean() {
return registerBean;
}
public void setRegisterBean(RegisterBean registerBean) {
this.registerBean = registerBean;
}
public String getAccessUrl() { public String getAccessUrl() {
return Urls.accountAccessPage(baseURI, realm.getId()).toString(); return Urls.accountAccessPage(baseURI, realm.getId()).toString();
} }
@ -98,7 +109,10 @@ public class UrlBean {
public String getRegistrationAction() { public String getRegistrationAction() {
if (realm.isSaas()) { if (realm.isSaas()) {
// TODO: saas social registration
return Urls.saasRegisterAction(baseURI).toString(); return Urls.saasRegisterAction(baseURI).toString();
} else if (registerBean.isSocialRegistration()) {
return Urls.socialRegisterAction(baseURI, realm.getId()).toString();
} else { } else {
return Urls.realmRegisterAction(baseURI, realm.getId()).toString(); return Urls.realmRegisterAction(baseURI, realm.getId()).toString();
} }

View file

@ -39,6 +39,8 @@
</ui:define> </ui:define>
<ui:define name="info"> <ui:define name="info">
<p>#{messages.alreadyHaveAccount} <a href="#{url.loginUrl}">#{messages.logIn}</a>.</p> <h:panelGroup rendered="#{not register.socialRegistration}">
<p>#{messages.alreadyHaveAccount} <a href="#{url.loginUrl}">#{messages.logIn}</a>.</p>
</h:panelGroup>
</ui:define> </ui:define>
</ui:composition> </ui:composition>

View file

@ -39,12 +39,12 @@ body {
</div> </div>
</h:panelGroup> </h:panelGroup>
<h:panelGroup rendered="#{realm.social}"> <h:panelGroup rendered="#{social.displaySocialProviders}">
<section class="social-login"> <span>or</span> <section class="social-login"> <span>or</span>
<h3>Social login area</h3> <h3>Social login area</h3>
<p>#{messages.logInWith}</p> <p>#{messages.logInWith}</p>
<ul> <ul>
<ui:repeat var="p" value="#{forms.providers}"> <ui:repeat var="p" value="#{social.providers}">
<li><a href="#{p.loginUrl}" class="zocial #{p.id}"> <span class="text">#{p.name}</span></a></li> <li><a href="#{p.loginUrl}" class="zocial #{p.id}"> <span class="text">#{p.name}</span></a></li>
</ui:repeat> </ui:repeat>
</ul> </ul>

View file

@ -236,7 +236,7 @@
<dependency> <dependency>
<groupId>org.jboss.arquillian.extension</groupId> <groupId>org.jboss.arquillian.extension</groupId>
<artifactId>arquillian-drone-bom</artifactId> <artifactId>arquillian-drone-bom</artifactId>
<version>1.2.0.Alpha3</version> <version>1.2.0.Beta1</version>
<type>pom</type> <type>pom</type>
<scope>import</scope> <scope>import</scope>
</dependency> </dependency>

View file

@ -74,6 +74,7 @@ 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.setAutomaticRegistrationAfterSocialLogin(rep.isAutomaticRegistrationAfterSocialLogin());
realm.setSslNotRequired((rep.isSslNotRequired())); realm.setSslNotRequired((rep.isSslNotRequired()));
realm.setAccessCodeLifespan(rep.getAccessCodeLifespan()); realm.setAccessCodeLifespan(rep.getAccessCodeLifespan());
realm.setTokenLifespan(rep.getTokenLifespan()); realm.setTokenLifespan(rep.getTokenLifespan());
@ -109,6 +110,7 @@ public class RealmManager {
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.setAutomaticRegistrationAfterSocialLogin(rep.isAutomaticRegistrationAfterSocialLogin());
if (rep.getPrivateKey() == null || rep.getPublicKey() == null) { if (rep.getPrivateKey() == null || rep.getPublicKey() == null) {
generateRealmKeys(newRealm); generateRealmKeys(newRealm);
} else { } else {
@ -185,6 +187,16 @@ public class RealmManager {
} }
} }
if (rep.getSocialMappings() != null) {
for (SocialMappingRepresentation socialMapping : rep.getSocialMappings()) {
UserModel user = userMap.get(socialMapping.getUsername());
for (SocialLinkRepresentation link : socialMapping.getSocialLinks()) {
SocialLinkModel mappingModel = new SocialLinkModel(link.getSocialProvider(), link.getSocialUsername());
newRealm.addSocialLink(user, mappingModel);
}
}
}
} }
public void createRole(RealmModel newRealm, RoleRepresentation roleRep) { public void createRole(RealmModel newRealm, RoleRepresentation roleRep) {
@ -245,6 +257,7 @@ public class RealmManager {
rep.setRealm(realm.getName()); rep.setRealm(realm.getName());
rep.setEnabled(realm.isEnabled()); rep.setEnabled(realm.isEnabled());
rep.setSocial(realm.isSocial()); rep.setSocial(realm.isSocial());
rep.setAutomaticRegistrationAfterSocialLogin(realm.isAutomaticRegistrationAfterSocialLogin());
rep.setSslNotRequired(realm.isSslNotRequired()); rep.setSslNotRequired(realm.isSslNotRequired());
rep.setCookieLoginAllowed(realm.isCookieLoginAllowed()); rep.setCookieLoginAllowed(realm.isCookieLoginAllowed());
rep.setPublicKey(realm.getPublicKeyPem()); rep.setPublicKey(realm.getPublicKeyPem());

View file

@ -127,9 +127,19 @@ public interface RealmModel {
void updateRequiredApplicationCredentials(Set<String> creds); void updateRequiredApplicationCredentials(Set<String> creds);
UserModel getUserBySocialLink(SocialLinkModel socialLink);
Set<SocialLinkModel> getSocialLinks(UserModel user);
void addSocialLink(UserModel user, SocialLinkModel socialLink);
void removeSocialLink(UserModel user, SocialLinkModel socialLink);
boolean isSocial(); boolean isSocial();
void setSocial(boolean social); void setSocial(boolean social);
List<UserModel> queryUsers(Map<String, String> parameters); public boolean isAutomaticRegistrationAfterSocialLogin();
public void setAutomaticRegistrationAfterSocialLogin(boolean automaticRegistrationAfterSocialLogin);
} }

View file

@ -0,0 +1,31 @@
package org.keycloak.services.models;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class SocialLinkModel {
private String socialUsername;
private String socialProvider;
public SocialLinkModel(String socialProvider, String socialUsername) {
this.socialUsername = socialUsername;
this.socialProvider = socialProvider;
}
public String getSocialUsername() {
return socialUsername;
}
public void setSocialUsername(String socialUsername) {
this.socialUsername = socialUsername;
}
public String getSocialProvider() {
return socialProvider;
}
public void setSocialProvider(String socialProvider) {
this.socialProvider = socialProvider;
}
}

View file

@ -5,15 +5,27 @@ import org.jboss.resteasy.logging.Logger;
import org.jboss.resteasy.security.PemUtils; import org.jboss.resteasy.security.PemUtils;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.models.*; import org.keycloak.services.models.KeycloakSession;
import org.keycloak.services.models.picketlink.mappings.ApplicationData; import org.keycloak.services.models.RealmModel;
import org.keycloak.services.models.RequiredCredentialModel;
import org.keycloak.services.models.ApplicationModel;
import org.keycloak.services.models.RoleModel;
import org.keycloak.services.models.SocialLinkModel;
import org.keycloak.services.models.UserCredentialModel;
import org.keycloak.services.models.UserModel;
import org.keycloak.services.models.picketlink.mappings.RealmData; import org.keycloak.services.models.picketlink.mappings.RealmData;
import org.keycloak.services.models.picketlink.mappings.ApplicationData;
import org.keycloak.services.models.picketlink.relationships.*; import org.keycloak.services.models.picketlink.relationships.*;
import org.keycloak.services.models.picketlink.relationships.RequiredApplicationCredentialRelationship;
import org.picketlink.idm.IdentityManager; import org.picketlink.idm.IdentityManager;
import org.picketlink.idm.PartitionManager; import org.picketlink.idm.PartitionManager;
import org.picketlink.idm.RelationshipManager; import org.picketlink.idm.RelationshipManager;
import org.picketlink.idm.credential.*; import org.picketlink.idm.credential.Credentials;
import org.picketlink.idm.model.AttributedType; import org.picketlink.idm.credential.Password;
import org.picketlink.idm.credential.TOTPCredential;
import org.picketlink.idm.credential.TOTPCredentials;
import org.picketlink.idm.credential.UsernamePasswordCredentials;
import org.picketlink.idm.credential.X509CertificateCredentials;
import org.picketlink.idm.model.IdentityType; import org.picketlink.idm.model.IdentityType;
import org.picketlink.idm.model.sample.Grant; import org.picketlink.idm.model.sample.Grant;
import org.picketlink.idm.model.sample.Role; import org.picketlink.idm.model.sample.Role;
@ -27,7 +39,13 @@ import java.io.StringWriter;
import java.security.PrivateKey; import java.security.PrivateKey;
import java.security.PublicKey; import java.security.PublicKey;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.*; import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/** /**
* Meant to be a per-request object * Meant to be a per-request object
@ -104,6 +122,17 @@ public class RealmAdapter implements RealmModel {
realm.setSocial(social); realm.setSocial(social);
} }
@Override
public boolean isAutomaticRegistrationAfterSocialLogin() {
return realm.isAutomaticRegistrationAfterSocialLogin();
}
@Override
public void setAutomaticRegistrationAfterSocialLogin(boolean automaticRegistrationAfterSocialLogin) {
realm.setAutomaticRegistrationAfterSocialLogin(automaticRegistrationAfterSocialLogin);
updateRealm();
}
@Override @Override
public boolean isSslNotRequired() { public boolean isSslNotRequired() {
return realm.isSslNotRequired(); return realm.isSslNotRequired();
@ -459,21 +488,6 @@ public class RealmAdapter implements RealmModel {
return new UserAdapter(user, getIdm()); return new UserAdapter(user, getIdm());
} }
@Override
public List<UserModel> queryUsers(Map<String, String> parameters) {
IdentityQuery<User> userQuery = getIdm().createIdentityQuery(User.class);
for (Map.Entry<String, String> entry : parameters.entrySet()) {
userQuery.setParameter(AttributedType.QUERY_ATTRIBUTE.byName(entry.getKey()), entry.getValue());
}
List<User> users = userQuery.getResultList();
List<UserModel> userModels = new ArrayList<UserModel>();
for (User user : users) {
userModels.add(new UserAdapter(user, getIdm()));
}
return userModels;
}
@Override @Override
public RoleAdapter getRole(String name) { public RoleAdapter getRole(String name) {
Role role = SampleModel.getRole(getIdm(), name); Role role = SampleModel.getRole(getIdm(), name);
@ -691,4 +705,54 @@ public class RealmAdapter implements RealmModel {
realm.setDefaultRoles(defaultRoles); realm.setDefaultRoles(defaultRoles);
updateRealm(); updateRealm();
} }
@Override
public UserModel getUserBySocialLink(SocialLinkModel socialLink) {
RelationshipQuery<SocialLinkRelationship> query = getRelationshipManager().createRelationshipQuery(SocialLinkRelationship.class);
query.setParameter(SocialLinkRelationship.SOCIAL_PROVIDER, socialLink.getSocialProvider());
query.setParameter(SocialLinkRelationship.SOCIAL_USERNAME, socialLink.getSocialUsername());
List<SocialLinkRelationship> results = query.getResultList();
if (results.isEmpty()) {
return null;
} else if (results.size() > 1) {
throw new IllegalStateException("More results found for socialProvider=" + socialLink.getSocialProvider() +
", socialUsername=" + socialLink.getSocialUsername() + ", results=" + results);
} else {
User user = results.get(0).getUser();
return new UserAdapter(user, getIdm());
}
}
@Override
public Set<SocialLinkModel> getSocialLinks(UserModel user) {
RelationshipQuery<SocialLinkRelationship> query = getRelationshipManager().createRelationshipQuery(SocialLinkRelationship.class);
query.setParameter(SocialLinkRelationship.USER, ((UserAdapter)user).getUser());
List<SocialLinkRelationship> plSocialLinks = query.getResultList();
Set<SocialLinkModel> results = new HashSet<SocialLinkModel>();
for (SocialLinkRelationship relationship : plSocialLinks) {
results.add(new SocialLinkModel(relationship.getSocialProvider(), relationship.getSocialUsername()));
}
return results;
}
@Override
public void addSocialLink(UserModel user, SocialLinkModel socialLink) {
SocialLinkRelationship relationship = new SocialLinkRelationship();
relationship.setUser(((UserAdapter)user).getUser());
relationship.setSocialProvider(socialLink.getSocialProvider());
relationship.setSocialUsername(socialLink.getSocialUsername());
getRelationshipManager().add(relationship);
}
@Override
public void removeSocialLink(UserModel user, SocialLinkModel socialLink) {
SocialLinkRelationship relationship = new SocialLinkRelationship();
relationship.setUser(((UserAdapter)user).getUser());
relationship.setSocialProvider(socialLink.getSocialProvider());
relationship.setSocialUsername(socialLink.getSocialUsername());
getRelationshipManager().remove(relationship);
}
} }

View file

@ -14,6 +14,7 @@ public class RealmData extends AbstractPartition {
private boolean cookieLoginAllowed; private boolean cookieLoginAllowed;
private boolean registrationAllowed; private boolean registrationAllowed;
private boolean social; private boolean social;
private boolean automaticRegistrationAfterSocialLogin;
private int tokenLifespan; private int tokenLifespan;
private int accessCodeLifespan; private int accessCodeLifespan;
private String publicKeyPem; private String publicKeyPem;
@ -54,6 +55,15 @@ public class RealmData extends AbstractPartition {
this.social = social; this.social = social;
} }
@AttributeProperty
public boolean isAutomaticRegistrationAfterSocialLogin() {
return automaticRegistrationAfterSocialLogin;
}
public void setAutomaticRegistrationAfterSocialLogin(boolean automaticRegistrationAfterSocialLogin) {
this.automaticRegistrationAfterSocialLogin = automaticRegistrationAfterSocialLogin;
}
@AttributeProperty @AttributeProperty
public boolean isSslNotRequired() { public boolean isSslNotRequired() {
return sslNotRequired; return sslNotRequired;

View file

@ -37,6 +37,8 @@ public class RealmEntity implements Serializable {
@AttributeValue @AttributeValue
private boolean social; private boolean social;
@AttributeValue @AttributeValue
private boolean automaticRegistrationAfterSocialLogin;
@AttributeValue
private int tokenLifespan; private int tokenLifespan;
@AttributeValue @AttributeValue
private int accessCodeLifespan; private int accessCodeLifespan;
@ -106,6 +108,14 @@ public class RealmEntity implements Serializable {
this.social = social; this.social = social;
} }
public boolean isAutomaticRegistrationAfterSocialLogin() {
return automaticRegistrationAfterSocialLogin;
}
public void setAutomaticRegistrationAfterSocialLogin(boolean automaticRegistrationAfterSocialLogin) {
this.automaticRegistrationAfterSocialLogin = automaticRegistrationAfterSocialLogin;
}
public int getTokenLifespan() { public int getTokenLifespan() {
return tokenLifespan; return tokenLifespan;
} }

View file

@ -0,0 +1,57 @@
package org.keycloak.services.models.picketlink.relationships;
import org.picketlink.idm.model.AbstractAttributedType;
import org.picketlink.idm.model.Attribute;
import org.picketlink.idm.model.Relationship;
import org.picketlink.idm.model.sample.User;
import org.picketlink.idm.query.AttributeParameter;
import org.picketlink.idm.query.RelationshipQueryParameter;
/**
* Binding between user and his social username for particular Social provider
*
* Example: Keycloak user "john" has username "john123" in social provider "facebook"
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class SocialLinkRelationship extends AbstractAttributedType implements Relationship {
private static final long serialVersionUID = 154879L;
public static final AttributeParameter SOCIAL_PROVIDER = new AttributeParameter("socialProvider");
public static final AttributeParameter SOCIAL_USERNAME = new AttributeParameter("socialUsername");
public static final RelationshipQueryParameter USER = new RelationshipQueryParameter() {
@Override
public String getName() {
return "user";
}
};
private User user;
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public String getSocialProvider() {
return (String)getAttribute("socialProvider").getValue();
}
public void setSocialProvider(String socialProvider) {
setAttribute(new Attribute<String>("socialProvider", socialProvider));
}
public String getSocialUsername() {
return (String)getAttribute("socialUsername").getValue();
}
public void setSocialUsername(String socialProviderUserId) {
setAttribute(new Attribute<String>("socialUsername", socialProviderUserId));
}
}

View file

@ -21,36 +21,56 @@
*/ */
package org.keycloak.services.resources; package org.keycloak.services.resources;
import org.jboss.resteasy.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.managers.TokenManager;
import org.keycloak.services.models.KeycloakSession;
import org.keycloak.services.models.RealmModel;
import org.keycloak.services.models.RoleModel;
import org.keycloak.services.models.UserModel;
import org.keycloak.services.resources.flows.Flows;
import org.keycloak.services.resources.flows.OAuthFlows;
import org.keycloak.services.resources.flows.Urls;
import org.keycloak.social.*;
import javax.imageio.spi.ServiceRegistry;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.UUID;
import javax.imageio.spi.ServiceRegistry;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.container.ResourceContext;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.NewCookie;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
import org.jboss.resteasy.logging.Logger;
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.HttpResponse;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.managers.TokenManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.models.*;
import org.keycloak.services.resources.flows.Flows;
import org.keycloak.services.resources.flows.OAuthFlows;
import org.keycloak.services.resources.flows.PageFlows;
import org.keycloak.services.resources.flows.Urls;
import org.keycloak.social.AuthCallback;
import org.keycloak.social.AuthRequest;
import org.keycloak.social.RequestDetails;
import org.keycloak.social.RequestDetailsBuilder;
import org.keycloak.social.SocialConstants;
import org.keycloak.social.SocialProvider;
import org.keycloak.social.SocialProviderConfig;
import org.keycloak.social.SocialProviderException;
import org.keycloak.social.SocialRequestManager;
import org.keycloak.social.SocialUser;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -69,14 +89,21 @@ public class SocialResource {
@Context @Context
private HttpRequest request; private HttpRequest request;
@Context
private HttpResponse response;
@Context
ResourceContext resourceContext;
@Context @Context
protected KeycloakSession session; protected KeycloakSession session;
protected SocialRequestManager socialRequestManager;
protected TokenManager tokenManager; private SocialRequestManager socialRequestManager;
protected AuthenticationManager authManager = new AuthenticationManager(); private TokenManager tokenManager;
private AuthenticationManager authManager = new AuthenticationManager();
public SocialResource(TokenManager tokenManager, SocialRequestManager socialRequestManager) { public SocialResource(TokenManager tokenManager, SocialRequestManager socialRequestManager) {
this.tokenManager = tokenManager; this.tokenManager = tokenManager;
@ -102,10 +129,6 @@ public class SocialResource {
return oauth.forwardToSecurityFailure("Realm not enabled."); return oauth.forwardToSecurityFailure("Realm not enabled.");
} }
if (!realm.isEnabled()) {
return oauth.forwardToSecurityFailure("Realm not enabled.");
}
String clientId = requestData.getClientAttributes().get("clientId"); String clientId = requestData.getClientAttributes().get("clientId");
UserModel client = realm.getUser(clientId); UserModel client = realm.getUser(clientId);
@ -131,21 +154,54 @@ public class SocialResource {
return oauth.forwardToSecurityFailure("Failed to process social callback"); return oauth.forwardToSecurityFailure("Failed to process social callback");
} }
// TODO Lookup user based on attribute for provider id - this is so a user can have a friendly username + link a SocialLinkModel socialLink = new SocialLinkModel(provider.getId(), socialUser.getUsername());
// user to UserModel user = realm.getUserBySocialLink(socialLink);
// multiple social logins
UserModel user = realm.getUser(provider.getId() + "." + socialUser.getId());
if (user == null) { if (user == null) {
if (!realm.isRegistrationAllowed()) { if (!realm.isRegistrationAllowed()) {
return oauth.forwardToSecurityFailure("Registration not allowed"); return oauth.forwardToSecurityFailure("Registration not allowed");
} }
user = realm.addUser(provider.getId() + "." + socialUser.getId()); // Automatically register user into realm with his social username (don't redirect to registration screen)
user.setAttribute(provider.getId() + ".id", socialUser.getId()); if (realm.isAutomaticRegistrationAfterSocialLogin()) {
for (RoleModel role : realm.getDefaultRoles()) { if (realm.getUser(socialUser.getUsername()) != null) {
realm.grantRole(user, role); // TODO: Username is already in realm. Show message and let user to bind accounts after he re-authenticate
throw new IllegalStateException("Username " + socialUser.getUsername() +
" already registered in the realm. TODO: bind accounts...");
// TODO: Maybe we should search also by email and bind accounts if user with this email is
// already registered. But actually Keycloak allows duplicate emails
} else {
user = realm.addUser(socialUser.getUsername());
user.setFirstName(socialUser.getFirstName());
user.setLastName(socialUser.getLastName());
user.setEmail(socialUser.getEmail());
}
realm.addSocialLink(user, socialLink);
for (RoleModel role : realm.getDefaultRoles()) {
realm.grantRole(user, role);
}
} else {
// Redirect user to registration screen with prefilled data from social provider
MultivaluedMap<String, String> formData = fillRegistrationFormWithSocialData(socialUser);
RequestDetailsBuilder reqDetailsBuilder = RequestDetailsBuilder.createFromRequestDetails(requestData);
reqDetailsBuilder.putSocialAttribute(SocialConstants.ATTR_SOCIAL_LINK, socialLink);
String requestId = UUID.randomUUID().toString();
socialRequestManager.addRequest(requestId, reqDetailsBuilder.build());
boolean secureOnly = !realm.isSslNotRequired();
String cookiePath = Urls.socialBase(uriInfo.getBaseUri()).build().getPath();
logger.info("creating cookie for social registration - name: " + SocialConstants.SOCIAL_REGISTRATION_COOKIE
+ " path: " + cookiePath);
NewCookie newCookie = new NewCookie(SocialConstants.SOCIAL_REGISTRATION_COOKIE, requestId,
cookiePath, null, "Added social cookie", NewCookie.DEFAULT_MAX_AGE, secureOnly);
response.addNewCookie(newCookie);
return Flows.forms(realm, request).setFormData(formData).setSocialRegistration(true).forwardToRegistration();
} }
} }
@ -193,6 +249,66 @@ public class SocialResource {
} }
} }
@POST
@Path("{realm}/socialRegistration")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response socialRegistration(@PathParam("realm") final String realmId,
final MultivaluedMap<String, String> formData) {
PageFlows pageFlows = Flows.pages(request);
Cookie cookie = headers.getCookies().get(SocialConstants.SOCIAL_REGISTRATION_COOKIE);
if (cookie == null) {
return pageFlows.forwardToSecurityFailure("Social registration cookie not found");
}
String requestId = cookie.getValue();
if (!socialRequestManager.isRequestId(requestId)) {
logger.error("Unknown requestId found in cookie. Maybe it's expired. requestId=" + requestId);
return pageFlows.forwardToSecurityFailure("Unknown requestId found in cookie. Maybe it's expired.");
}
RequestDetails requestData = socialRequestManager.getData(requestId);
RealmManager realmManager = new RealmManager(session);
RealmModel realm = realmManager.getRealm(realmId);
if (realm == null || !realm.isEnabled()) {
return pageFlows.forwardToSecurityFailure("Realm doesn't exists or is not enabled.");
}
TokenService tokenService = new TokenService(realm, tokenManager);
resourceContext.initResource(tokenService);
String clientId = requestData.getClientAttribute("clientId");
String scope = requestData.getClientAttribute("scope");
String state = requestData.getClientAttribute("state");
String redirectUri = requestData.getClientAttribute("redirectUri");
SocialLinkModel socialLink = (SocialLinkModel)requestData.getSocialAttribute(SocialConstants.ATTR_SOCIAL_LINK);
Response response1 = tokenService.processRegisterImpl(clientId, scope, state, redirectUri, formData, true);
// Some error occured during registration
if (response1 != null || request.wasForwarded()) {
logger.warn("Registration attempt wasn't successful. Request already forwarded or redirected.");
return response1;
}
String username = formData.getFirst("username");
UserModel user = realm.getUser(username);
if (user == null) {
// Normally shouldn't happen
throw new IllegalStateException("User " + username + " not found in the realm");
}
realm.addSocialLink(user, socialLink);
// Expire cookie and invalidate requestData
String cookiePath = Urls.socialBase(uriInfo.getBaseUri()).build().getPath();
NewCookie newCookie = new NewCookie(SocialConstants.SOCIAL_REGISTRATION_COOKIE, "", cookiePath, null,
"Expire social cookie", 0, false);
logger.info("Expiring social registration cookie: " + SocialConstants.SOCIAL_REGISTRATION_COOKIE + ", path: " + cookiePath);
response.addNewCookie(newCookie);
socialRequestManager.retrieveData(requestId);
return tokenService.processLogin(clientId, scope, state, redirectUri, formData);
}
private RequestDetails getRequestDetails(Map<String, String[]> queryParams) { private RequestDetails getRequestDetails(Map<String, String[]> queryParams) {
Iterator<SocialProvider> itr = ServiceRegistry.lookupProviders(SocialProvider.class); Iterator<SocialProvider> itr = ServiceRegistry.lookupProviders(SocialProvider.class);
@ -231,4 +347,25 @@ public class SocialResource {
return queryParams; return queryParams;
} }
protected MultivaluedMap<String, String> fillRegistrationFormWithSocialData(SocialUser socialUser) {
MultivaluedMap<String, String> formData = new MultivaluedMapImpl<String, String>();
formData.putSingle("username", socialUser.getUsername());
if (socialUser.getEmail() != null) {
formData.putSingle("email", socialUser.getEmail());
}
String fullName = null;
if (socialUser.getFirstName() == null) {
fullName = socialUser.getLastName();
} else if (socialUser.getLastName() == null) {
fullName = socialUser.getFirstName();
} else {
fullName = socialUser.getFirstName() + " " + socialUser.getLastName();
}
formData.putSingle("name", fullName);
return formData;
}
} }

View file

@ -11,18 +11,44 @@ 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.services.managers.*; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.managers.AccessCodeEntry;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.managers.ResourceAdminManager;
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.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.*; import javax.ws.rs.Consumes;
import javax.ws.rs.core.*; import javax.ws.rs.ForbiddenException;
import javax.ws.rs.GET;
import javax.ws.rs.NotAuthorizedException;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
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.NewCookie;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.Providers; import javax.ws.rs.ext.Providers;
import java.security.PrivateKey; import java.security.PrivateKey;
import java.util.*; import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -38,25 +64,18 @@ public class TokenService {
@Context @Context
protected Providers providers; protected Providers providers;
@Context @Context
protected SecurityContext securityContext; protected SecurityContext securityContext;
@Context @Context
protected UriInfo uriInfo; protected UriInfo uriInfo;
@Context @Context
protected HttpHeaders headers; protected HttpHeaders headers;
@Context @Context
protected HttpRequest request; protected HttpRequest request;
@Context @Context
protected HttpResponse response; protected HttpResponse response;
@Context @Context
protected KeycloakSession session; protected KeycloakSession session;
@Context @Context
protected KeycloakTransaction transaction; protected KeycloakTransaction transaction;
@ -215,6 +234,19 @@ public class TokenService {
public Response processRegister(@QueryParam("client_id") final String clientId, public Response processRegister(@QueryParam("client_id") final String clientId,
@QueryParam("scope") final String scopeParam, @QueryParam("state") final String state, @QueryParam("scope") final String scopeParam, @QueryParam("state") final String state,
@QueryParam("redirect_uri") final String redirect, final MultivaluedMap<String, String> formData) { @QueryParam("redirect_uri") final String redirect, final MultivaluedMap<String, String> formData) {
Response registrationResponse = processRegisterImpl(clientId, scopeParam, state, redirect, formData, false);
// If request has been already forwarded (either due to security or validation error) then we won't continue with login
if (registrationResponse != null || request.wasForwarded()) {
logger.warn("Registration attempt wasn't successful. Request already forwarded or redirected.");
return registrationResponse;
} else {
return processLogin(clientId, scopeParam, state, redirect, formData);
}
}
public Response processRegisterImpl(String clientId, String scopeParam, String state, String redirect,
MultivaluedMap<String, String> formData, boolean isSocialRegistration) {
OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager); OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager);
if (!realm.isEnabled()) { if (!realm.isEnabled()) {
@ -240,7 +272,8 @@ 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).forwardToRegistration(); return Flows.forms(realm, request).setError(error).setFormData(formData)
.setSocialRegistration(isSocialRegistration).forwardToRegistration();
} }
String username = formData.getFirst("username"); String username = formData.getFirst("username");
@ -248,7 +281,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).setError(Messages.USERNAME_EXISTS).setFormData(formData)
.forwardToRegistration(); .setSocialRegistration(isSocialRegistration).forwardToRegistration();
} }
user = realm.addUser(username); user = realm.addUser(username);
@ -290,7 +323,7 @@ public class TokenService {
realm.grantRole(user, role); realm.grantRole(user, role);
} }
return processLogin(clientId, scopeParam, state, redirect, formData); return null;
} }
@Path("access/codes") @Path("access/codes")

View file

@ -38,6 +38,7 @@ public class FormFlows {
public static final String ERROR_MESSAGE = "KEYCLOAK_FORMS_ERROR_MESSAGE"; public static final String ERROR_MESSAGE = "KEYCLOAK_FORMS_ERROR_MESSAGE";
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";
private String error; private String error;
private MultivaluedMap<String, String> formData; private MultivaluedMap<String, String> formData;
@ -47,6 +48,8 @@ public class FormFlows {
private HttpRequest request; private HttpRequest request;
private UserModel userModel; private UserModel userModel;
private boolean socialRegistration;
FormFlows(RealmModel realm, HttpRequest request) { FormFlows(RealmModel realm, HttpRequest request) {
this.realm = realm; this.realm = realm;
this.request = request; this.request = request;
@ -75,6 +78,8 @@ public class FormFlows {
request.setAttribute(USER, userModel); request.setAttribute(USER, userModel);
} }
request.setAttribute(SOCIAL_REGISTRATION, socialRegistration);
request.forward(form); request.forward(form);
return null; return null;
} }
@ -113,6 +118,12 @@ public class FormFlows {
return this; return this;
} }
// Set flag whether user registration is triggered from social login
public FormFlows setSocialRegistration(boolean socialRegistration) {
this.socialRegistration = socialRegistration;
return this;
}
public FormFlows setFormData(MultivaluedMap<String, String> formData) { public FormFlows setFormData(MultivaluedMap<String, String> formData) {
this.formData = formData; this.formData = formData;
return this; return this;

View file

@ -95,7 +95,7 @@ public class Urls {
return saasBase(baseUri).path(SaasService.class, "registerPage").build(); return saasBase(baseUri).path(SaasService.class, "registerPage").build();
} }
private static UriBuilder socialBase(URI baseUri) { public static UriBuilder socialBase(URI baseUri) {
return UriBuilder.fromUri(baseUri).path(SocialResource.class); return UriBuilder.fromUri(baseUri).path(SocialResource.class);
} }
@ -111,4 +111,8 @@ public class Urls {
private static UriBuilder tokenBase(URI baseUri) { private static UriBuilder tokenBase(URI baseUri) {
return realmBase(baseUri).path(RealmsResource.class, "getTokenService"); return realmBase(baseUri).path(RealmsResource.class, "getTokenService");
} }
public static URI socialRegisterAction(URI baseUri, String realmId) {
return socialBase(baseUri).path(SocialResource.class, "socialRegistration").build(realmId);
}
} }

View file

@ -75,6 +75,7 @@ public class AdapterTest {
realmModel.setPrivateKeyPem("0234234"); realmModel.setPrivateKeyPem("0234234");
realmModel.setPublicKeyPem("0234234"); realmModel.setPublicKeyPem("0234234");
realmModel.setTokenLifespan(1000); realmModel.setTokenLifespan(1000);
realmModel.setAutomaticRegistrationAfterSocialLogin(true);
realmModel.addDefaultRole("foo"); realmModel.addDefaultRole("foo");
System.out.println(realmModel.getId()); System.out.println(realmModel.getId());
@ -86,6 +87,7 @@ public class AdapterTest {
Assert.assertEquals(realmModel.getName(), "JUGGLER"); Assert.assertEquals(realmModel.getName(), "JUGGLER");
Assert.assertEquals(realmModel.getPrivateKeyPem(), "0234234"); Assert.assertEquals(realmModel.getPrivateKeyPem(), "0234234");
Assert.assertEquals(realmModel.getPublicKeyPem(), "0234234"); Assert.assertEquals(realmModel.getPublicKeyPem(), "0234234");
Assert.assertEquals(realmModel.isAutomaticRegistrationAfterSocialLogin(), true);
Assert.assertEquals(1, realmModel.getDefaultRoles().size()); Assert.assertEquals(1, realmModel.getDefaultRoles().size());
Assert.assertEquals("foo", realmModel.getDefaultRoles().get(0).getName()); Assert.assertEquals("foo", realmModel.getDefaultRoles().get(0).getName());
} }

View file

@ -15,6 +15,7 @@ import org.keycloak.services.models.RealmModel;
import org.keycloak.services.models.RequiredCredentialModel; import org.keycloak.services.models.RequiredCredentialModel;
import org.keycloak.services.models.ApplicationModel; import org.keycloak.services.models.ApplicationModel;
import org.keycloak.services.models.RoleModel; import org.keycloak.services.models.RoleModel;
import org.keycloak.services.models.SocialLinkModel;
import org.keycloak.services.models.UserModel; import org.keycloak.services.models.UserModel;
import org.keycloak.services.resources.KeycloakApplication; import org.keycloak.services.resources.KeycloakApplication;
import org.keycloak.services.resources.SaasService; import org.keycloak.services.resources.SaasService;
@ -58,6 +59,7 @@ public class ImportTest {
defaultRealm.setSslNotRequired(false); defaultRealm.setSslNotRequired(false);
defaultRealm.setCookieLoginAllowed(true); defaultRealm.setCookieLoginAllowed(true);
defaultRealm.setRegistrationAllowed(true); defaultRealm.setRegistrationAllowed(true);
defaultRealm.setAutomaticRegistrationAfterSocialLogin(false);
manager.generateRealmKeys(defaultRealm); manager.generateRealmKeys(defaultRealm);
defaultRealm.addRequiredCredential(CredentialRepresentation.PASSWORD); defaultRealm.addRequiredCredential(CredentialRepresentation.PASSWORD);
RoleModel role = defaultRealm.addRole(SaasService.REALM_CREATOR_ROLE); RoleModel role = defaultRealm.addRole(SaasService.REALM_CREATOR_ROLE);
@ -68,6 +70,8 @@ public class ImportTest {
RealmModel realm = manager.createRealm("demo", rep.getRealm()); RealmModel realm = manager.createRealm("demo", rep.getRealm());
manager.importRealm(rep, realm); manager.importRealm(rep, realm);
realm.addRealmAdmin(admin); realm.addRealmAdmin(admin);
Assert.assertFalse(realm.isAutomaticRegistrationAfterSocialLogin());
List<RequiredCredentialModel> creds = realm.getRequiredCredentials(); List<RequiredCredentialModel> creds = realm.getRequiredCredentials();
Assert.assertEquals(1, creds.size()); Assert.assertEquals(1, creds.size());
RequiredCredentialModel cred = creds.get(0); RequiredCredentialModel cred = creds.get(0);
@ -82,6 +86,8 @@ public class ImportTest {
Set<String> scopes = realm.getScope(user); Set<String> scopes = realm.getScope(user);
System.out.println("Scopes size: " + scopes.size()); System.out.println("Scopes size: " + scopes.size());
Assert.assertTrue(scopes.contains("*")); Assert.assertTrue(scopes.contains("*"));
Assert.assertEquals(0, realm.getSocialLinks(user).size());
List<ApplicationModel> resources = realm.getApplications(); List<ApplicationModel> resources = realm.getApplications();
Assert.assertEquals(2, resources.size()); Assert.assertEquals(2, resources.size());
List<RealmModel> realms = identitySession.getRealms(admin); List<RealmModel> realms = identitySession.getRealms(admin);
@ -94,6 +100,28 @@ public class ImportTest {
Assert.assertNotNull(oauthClient); Assert.assertNotNull(oauthClient);
Set<String> appScopes = application.getScope(oauthClient); Set<String> appScopes = application.getScope(oauthClient);
Assert.assertTrue(appScopes.contains("user")); Assert.assertTrue(appScopes.contains("user"));
// Test social linking
UserModel socialUser = realm.getUser("mySocialUser");
Set<SocialLinkModel> socialLinks = realm.getSocialLinks(socialUser);
Assert.assertEquals(3, socialLinks.size());
int facebookCount = 0;
int googleCount = 0;
for (SocialLinkModel socialLinkModel : socialLinks) {
if ("facebook".equals(socialLinkModel.getSocialProvider())) {
facebookCount++;
} else if ("google".equals(socialLinkModel.getSocialProvider())) {
googleCount++;
Assert.assertEquals(socialLinkModel.getSocialUsername(), "mySocialUser@gmail.com");
}
}
Assert.assertEquals(2, facebookCount);
Assert.assertEquals(1, googleCount);
UserModel foundSocialUser = realm.getUserBySocialLink(new SocialLinkModel("facebook", "fbuser1"));
Assert.assertEquals(foundSocialUser.getLoginName(), socialUser.getLoginName());
Assert.assertNull(realm.getUserBySocialLink(new SocialLinkModel("facebook", "not-existing")));
} }
@Test @Test
@ -106,6 +134,7 @@ public class ImportTest {
defaultRealm.setSslNotRequired(false); defaultRealm.setSslNotRequired(false);
defaultRealm.setCookieLoginAllowed(true); defaultRealm.setCookieLoginAllowed(true);
defaultRealm.setRegistrationAllowed(true); defaultRealm.setRegistrationAllowed(true);
defaultRealm.setAutomaticRegistrationAfterSocialLogin(false);
manager.generateRealmKeys(defaultRealm); manager.generateRealmKeys(defaultRealm);
defaultRealm.addRequiredCredential(CredentialRepresentation.PASSWORD); defaultRealm.addRequiredCredential(CredentialRepresentation.PASSWORD);
RoleModel role = defaultRealm.addRole(SaasService.REALM_CREATOR_ROLE); RoleModel role = defaultRealm.addRole(SaasService.REALM_CREATOR_ROLE);
@ -117,6 +146,7 @@ public class ImportTest {
manager.importRealm(rep, realm); manager.importRealm(rep, realm);
realm.addRealmAdmin(admin); realm.addRealmAdmin(admin);
Assert.assertTrue(realm.isAutomaticRegistrationAfterSocialLogin());
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

@ -5,6 +5,7 @@
"accessCodeLifespan": 10, "accessCodeLifespan": 10,
"sslNotRequired": true, "sslNotRequired": true,
"cookieLoginAllowed": true, "cookieLoginAllowed": true,
"automaticRegistrationAfterSocialLogin": true,
"privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=", "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", "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"requiredCredentials": [ "password" ], "requiredCredentials": [ "password" ],

View file

@ -50,6 +50,10 @@
"value": "clientpassword" "value": "clientpassword"
} }
] ]
},
{
"username": "mySocialUser",
"enabled": true
} }
], ],
"roleMappings": [ "roleMappings": [
@ -64,6 +68,25 @@
"roles": ["*"] "roles": ["*"]
} }
], ],
"socialMappings": [
{
"username": "mySocialUser",
"socialLinks": [
{
"socialProvider": "facebook",
"socialUsername": "fbuser1"
},
{
"socialProvider": "facebook",
"socialUsername": "fbuser2"
},
{
"socialProvider": "google",
"socialUsername": "mySocialUser@gmail.com"
}
]
}
],
"applications": [ "applications": [
{ {
"name": "Application", "name": "Application",

View file

@ -28,16 +28,16 @@ import java.util.Map;
*/ */
public class AuthCallback { public class AuthCallback {
private Map<String, String> attributes; private Map<String, Object> attributes;
private Map<String, String[]> queryParams; private Map<String, String[]> queryParams;
public AuthCallback(Map<String, String> attributes, Map<String, String[]> queryParams) { public AuthCallback(Map<String, Object> attributes, Map<String, String[]> queryParams) {
this.attributes = attributes; this.attributes = attributes;
this.queryParams = queryParams; this.queryParams = queryParams;
} }
public String getAttribute(String name) { public Object getAttribute(String name) {
return attributes.get(name); return attributes.get(name);
} }

View file

@ -33,9 +33,9 @@ public class AuthRequest {
private URI authUri; private URI authUri;
private Map<String, String> attributes; private Map<String, Object> attributes;
AuthRequest(String id, URI authUri, Map<String, String> attributes) { AuthRequest(String id, URI authUri, Map<String, Object> attributes) {
this.id = id; this.id = id;
this.authUri = authUri; this.authUri = authUri;
this.attributes = attributes; this.attributes = attributes;
@ -49,7 +49,7 @@ public class AuthRequest {
return authUri; return authUri;
} }
public Map<String, String> getAttributes() { public Map<String, Object> getAttributes() {
return attributes; return attributes;
} }

View file

@ -33,7 +33,7 @@ public class AuthRequestBuilder {
private UriBuilder b; private UriBuilder b;
private Map<String, String> attributes; private Map<String, Object> attributes;
private String id; private String id;
@ -44,7 +44,7 @@ public class AuthRequestBuilder {
AuthRequestBuilder req = new AuthRequestBuilder(); AuthRequestBuilder req = new AuthRequestBuilder();
req.id = id; req.id = id;
req.b = UriBuilder.fromUri(path); req.b = UriBuilder.fromUri(path);
req.attributes = new HashMap<String, String>(); req.attributes = new HashMap<String, Object>();
return req; return req;
} }
@ -53,7 +53,7 @@ public class AuthRequestBuilder {
return this; return this;
} }
public AuthRequestBuilder setAttribute(String name, String value) { public AuthRequestBuilder setAttribute(String name, Object value) {
attributes.put(name, value); attributes.put(name, value);
return this; return this;
} }

View file

@ -32,9 +32,9 @@ public class RequestDetails {
private Map<String, String> clientAttributes; private Map<String, String> clientAttributes;
private Map<String, String> socialAttributes; private Map<String, Object> socialAttributes;
RequestDetails(String providerId, Map<String, String> clientAttributes, Map<String, String> socialAttributes) { RequestDetails(String providerId, Map<String, String> clientAttributes, Map<String, Object> socialAttributes) {
this.providerId = providerId; this.providerId = providerId;
this.clientAttributes = clientAttributes; this.clientAttributes = clientAttributes;
this.socialAttributes = socialAttributes; this.socialAttributes = socialAttributes;
@ -52,11 +52,11 @@ public class RequestDetails {
return clientAttributes; return clientAttributes;
} }
public String getSocialAttribute(String name) { public Object getSocialAttribute(String name) {
return socialAttributes.get(name); return socialAttributes.get(name);
} }
public Map<String, String> getSocialAttributes() { public Map<String, Object> getSocialAttributes() {
return socialAttributes; return socialAttributes;
} }

View file

@ -33,7 +33,7 @@ public class RequestDetailsBuilder {
private Map<String, String> clientAttributes; private Map<String, String> clientAttributes;
private Map<String, String> socialAttributes; private Map<String, Object> socialAttributes;
private RequestDetailsBuilder() { private RequestDetailsBuilder() {
} }
@ -42,7 +42,17 @@ public class RequestDetailsBuilder {
RequestDetailsBuilder req = new RequestDetailsBuilder(); RequestDetailsBuilder req = new RequestDetailsBuilder();
req.providerId = providerId; req.providerId = providerId;
req.clientAttributes = new HashMap<String, String>(); req.clientAttributes = new HashMap<String, String>();
req.socialAttributes = new HashMap<String, String>(); req.socialAttributes = new HashMap<String, Object>();
return req;
}
public static RequestDetailsBuilder createFromRequestDetails(RequestDetails from) {
RequestDetailsBuilder req = new RequestDetailsBuilder();
req.providerId = from.getProviderId();
req.clientAttributes = new HashMap<String, String>();
req.clientAttributes.putAll(from.getClientAttributes());
req.socialAttributes = new HashMap<String, Object>();
req.socialAttributes.putAll(from.getSocialAttributes());
return req; return req;
} }
@ -56,12 +66,12 @@ public class RequestDetailsBuilder {
return this; return this;
} }
public RequestDetailsBuilder putSocialAttribute(String name, String value) { public RequestDetailsBuilder putSocialAttribute(String name, Object value) {
socialAttributes.put(name, value); socialAttributes.put(name, value);
return this; return this;
} }
public RequestDetailsBuilder putSocialAttributes(Map<String, String> attributes) { public RequestDetailsBuilder putSocialAttributes(Map<String, Object> attributes) {
socialAttributes.putAll(attributes); socialAttributes.putAll(attributes);
return this; return this;
} }

View file

@ -0,0 +1,11 @@
package org.keycloak.social;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class SocialConstants {
public static final String ATTR_SOCIAL_LINK = "ATTR_SOCIAL_LINK";
public static final String SOCIAL_REGISTRATION_COOKIE = "SOCIAL_REGISTRATION_COOKIE";
}

View file

@ -57,6 +57,11 @@ public class SocialRequestManager {
return details; return details;
} }
// Just obtain data without expiring it
public synchronized RequestDetails getData(String requestId) {
return map.get(requestId);
}
private void pruneExpired() { private void pruneExpired() {
long currentTime = System.currentTimeMillis(); long currentTime = System.currentTimeMillis();

View file

@ -3,6 +3,7 @@ package org.keycloak.social;
public class SocialUser { public class SocialUser {
private String id; private String id;
private String username;
private String firstName; private String firstName;
private String lastName; private String lastName;
private String email; private String email;
@ -19,6 +20,14 @@ public class SocialUser {
this.id = id; this.id = id;
} }
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getFirstName() { public String getFirstName() {
return firstName; return firstName;
} }

View file

@ -82,6 +82,13 @@ public class FacebookProvider implements SocialProvider {
FacebookUser facebookUser = loadUser(accessToken, client); FacebookUser facebookUser = loadUser(accessToken, client);
SocialUser socialUser = new SocialUser(facebookUser.getId()); SocialUser socialUser = new SocialUser(facebookUser.getId());
socialUser.setUsername(facebookUser.getUsername());
// This could happen with Facebook testing users
if (facebookUser.getUsername() == null || facebookUser.getUsername().length() == 0) {
socialUser.setUsername(facebookUser.getId());
}
socialUser.setEmail(facebookUser.getEmail()); socialUser.setEmail(facebookUser.getEmail());
socialUser.setLastName(facebookUser.getLastName()); socialUser.setLastName(facebookUser.getLastName());
socialUser.setFirstName(facebookUser.getFirstName()); socialUser.setFirstName(facebookUser.getFirstName());

View file

@ -106,6 +106,10 @@ public class GoogleProvider implements SocialProvider {
Userinfo userInfo = oauth2.userinfo().get().execute(); Userinfo userInfo = oauth2.userinfo().get().execute();
SocialUser user = new SocialUser(userInfo.getId()); SocialUser user = new SocialUser(userInfo.getId());
// Use email as username for Google
user.setUsername(userInfo.getEmail());
user.setFirstName(userInfo.getGivenName()); user.setFirstName(userInfo.getGivenName());
user.setLastName(userInfo.getFamilyName()); user.setLastName(userInfo.getFamilyName());
user.setEmail(userInfo.getEmail()); user.setEmail(userInfo.getEmail());

View file

@ -71,13 +71,24 @@ public class TwitterProvider implements SocialProvider {
twitter.setOAuthConsumer(config.getKey(), config.getSecret()); twitter.setOAuthConsumer(config.getKey(), config.getSecret());
String verifier = callback.getQueryParam("oauth_verifier"); String verifier = callback.getQueryParam("oauth_verifier");
RequestToken requestToken = new RequestToken(callback.getAttribute("token"), callback.getAttribute("tokenSecret")); RequestToken requestToken = new RequestToken((String)callback.getAttribute("token"), (String)callback.getAttribute("tokenSecret"));
twitter.getOAuthAccessToken(requestToken, verifier); twitter.getOAuthAccessToken(requestToken, verifier);
twitter4j.User twitterUser = twitter.verifyCredentials(); twitter4j.User twitterUser = twitter.verifyCredentials();
SocialUser user = new SocialUser(Long.toString(twitterUser.getId())); SocialUser user = new SocialUser(Long.toString(twitterUser.getId()));
user.setFirstName(twitterUser.getName());
// Use screenName as username for Twitter
user.setUsername(twitterUser.getScreenName());
String twitterName = twitterUser.getName();
int spaceIndex = twitterName.lastIndexOf(' ');
if (spaceIndex != -1) {
user.setFirstName(twitterName.substring(0, spaceIndex));
user.setLastName(twitterName.substring(spaceIndex + 1));
} else {
user.setFirstName(twitterName);
}
return user; return user;
} catch (Exception e) { } catch (Exception e) {