keycloak-memoizeit
Changes
services/src/main/java/org/keycloak/services/models/picketlink/relationships/SocialLinkRelationship.java 57(+57 -0)
services/src/test/resources/testrealm.json 23(+23 -0)
Details
diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
index e365a61..92c3f10 100755
--- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
@@ -19,6 +19,7 @@ public class RealmRepresentation {
protected boolean cookieLoginAllowed;
protected boolean registrationAllowed;
protected boolean social;
+ protected boolean automaticRegistrationAfterSocialLogin;
protected String privateKey;
protected String publicKey;
protected List<RoleRepresentation> roles;
@@ -29,6 +30,7 @@ public class RealmRepresentation {
protected List<UserRepresentation> users;
protected List<RoleMappingRepresentation> roleMappings;
protected List<ScopeMappingRepresentation> scopeMappings;
+ protected List<SocialMappingRepresentation> socialMappings;
protected List<ApplicationRepresentation> applications;
@@ -144,6 +146,18 @@ public class RealmRepresentation {
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() {
return requiredCredentials;
}
@@ -223,4 +237,12 @@ public class RealmRepresentation {
public void setSocial(boolean social) {
this.social = social;
}
+
+ public boolean isAutomaticRegistrationAfterSocialLogin() {
+ return automaticRegistrationAfterSocialLogin;
+ }
+
+ public void setAutomaticRegistrationAfterSocialLogin(boolean automaticRegistrationAfterSocialLogin) {
+ this.automaticRegistrationAfterSocialLogin = automaticRegistrationAfterSocialLogin;
+ }
}
diff --git a/core/src/main/java/org/keycloak/representations/idm/SocialLinkRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/SocialLinkRepresentation.java
new file mode 100644
index 0000000..a6e1838
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/idm/SocialLinkRepresentation.java
@@ -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;
+ }
+}
diff --git a/core/src/main/java/org/keycloak/representations/idm/SocialMappingRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/SocialMappingRepresentation.java
new file mode 100644
index 0000000..57dd874
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/idm/SocialMappingRepresentation.java
@@ -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;
+ }
+}
diff --git a/examples/as7-eap-demo/server/src/main/resources/META-INF/testrealm.json b/examples/as7-eap-demo/server/src/main/resources/META-INF/testrealm.json
index f92e34c..bb918ef 100755
--- a/examples/as7-eap-demo/server/src/main/resources/META-INF/testrealm.json
+++ b/examples/as7-eap-demo/server/src/main/resources/META-INF/testrealm.json
@@ -6,6 +6,8 @@
"sslNotRequired": true,
"cookieLoginAllowed": 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=",
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"requiredCredentials": [ "password" ],
diff --git a/forms/src/main/java/org/keycloak/forms/RegisterBean.java b/forms/src/main/java/org/keycloak/forms/RegisterBean.java
index 60d247a..77d6a8c 100755
--- a/forms/src/main/java/org/keycloak/forms/RegisterBean.java
+++ b/forms/src/main/java/org/keycloak/forms/RegisterBean.java
@@ -26,6 +26,7 @@ import java.util.Map;
import javax.annotation.PostConstruct;
import javax.faces.bean.ManagedBean;
+import javax.faces.bean.ManagedProperty;
import javax.faces.bean.RequestScoped;
import javax.faces.context.FacesContext;
import javax.servlet.http.HttpServletRequest;
@@ -42,6 +43,8 @@ public class RegisterBean {
private HashMap<String, String> formData;
+ private boolean socialRegistration;
+
@PostConstruct
public void init() {
FacesContext ctx = FacesContext.getCurrentInstance();
@@ -49,6 +52,9 @@ public class RegisterBean {
this.formData = new HashMap<String, String>();
+ Boolean socialRegistrationAttr = (Boolean)request.getAttribute(FormFlows.SOCIAL_REGISTRATION);
+ this.socialRegistration = socialRegistrationAttr != null && socialRegistrationAttr;
+
@SuppressWarnings("unchecked")
MultivaluedMap<String, String> formData = (MultivaluedMap<String, String>) request.getAttribute(FormFlows.DATA);
if (formData != null) {
@@ -62,4 +68,8 @@ public class RegisterBean {
return formData;
}
+ public boolean isSocialRegistration() {
+ return socialRegistration;
+ }
+
}
diff --git a/forms/src/main/java/org/keycloak/forms/SocialBean.java b/forms/src/main/java/org/keycloak/forms/SocialBean.java
index b007083..fab91b1 100644
--- a/forms/src/main/java/org/keycloak/forms/SocialBean.java
+++ b/forms/src/main/java/org/keycloak/forms/SocialBean.java
@@ -46,6 +46,9 @@ public class SocialBean {
@ManagedProperty(value = "#{realm}")
private RealmBean realm;
+ @ManagedProperty(value = "#{register}")
+ private RegisterBean registerBean;
+
@ManagedProperty(value = "#{url}")
private UrlBean url;
@@ -73,4 +76,32 @@ public class SocialBean {
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;
+ }
}
diff --git a/forms/src/main/java/org/keycloak/forms/UrlBean.java b/forms/src/main/java/org/keycloak/forms/UrlBean.java
index ba00609..62017d0 100644
--- a/forms/src/main/java/org/keycloak/forms/UrlBean.java
+++ b/forms/src/main/java/org/keycloak/forms/UrlBean.java
@@ -45,6 +45,9 @@ public class UrlBean {
@ManagedProperty(value = "#{realm}")
private RealmBean realm;
+ @ManagedProperty(value = "#{register}")
+ private RegisterBean registerBean;
+
@PostConstruct
public void init() {
FacesContext ctx = FacesContext.getCurrentInstance();
@@ -64,6 +67,14 @@ public class UrlBean {
this.realm = realm;
}
+ public RegisterBean getRegisterBean() {
+ return registerBean;
+ }
+
+ public void setRegisterBean(RegisterBean registerBean) {
+ this.registerBean = registerBean;
+ }
+
public String getAccessUrl() {
return Urls.accountAccessPage(baseURI, realm.getId()).toString();
}
@@ -98,7 +109,10 @@ public class UrlBean {
public String getRegistrationAction() {
if (realm.isSaas()) {
+ // TODO: saas social registration
return Urls.saasRegisterAction(baseURI).toString();
+ } else if (registerBean.isSocialRegistration()) {
+ return Urls.socialRegisterAction(baseURI, realm.getId()).toString();
} else {
return Urls.realmRegisterAction(baseURI, realm.getId()).toString();
}
diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/register.xhtml b/forms/src/main/resources/META-INF/resources/forms/theme/default/register.xhtml
index 0078271..ca9d8b6 100755
--- a/forms/src/main/resources/META-INF/resources/forms/theme/default/register.xhtml
+++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/register.xhtml
@@ -39,6 +39,8 @@
</ui:define>
<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:composition>
\ No newline at end of file
diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login.xhtml b/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login.xhtml
index 05ee4fb..e99a442 100644
--- a/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login.xhtml
+++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login.xhtml
@@ -39,12 +39,12 @@ body {
</div>
</h:panelGroup>
- <h:panelGroup rendered="#{realm.social}">
+ <h:panelGroup rendered="#{social.displaySocialProviders}">
<section class="social-login"> <span>or</span>
<h3>Social login area</h3>
<p>#{messages.logInWith}</p>
<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>
</ui:repeat>
</ul>
diff --git a/services/src/main/java/org/keycloak/services/managers/RealmManager.java b/services/src/main/java/org/keycloak/services/managers/RealmManager.java
index afd2406..603fc56 100755
--- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java
@@ -71,6 +71,7 @@ public class RealmManager {
realm.setSocial(rep.isSocial());
realm.setCookieLoginAllowed(rep.isCookieLoginAllowed());
realm.setRegistrationAllowed(rep.isRegistrationAllowed());
+ realm.setAutomaticRegistrationAfterSocialLogin(rep.isAutomaticRegistrationAfterSocialLogin());
realm.setSslNotRequired((rep.isSslNotRequired()));
realm.setAccessCodeLifespan(rep.getAccessCodeLifespan());
realm.setTokenLifespan(rep.getTokenLifespan());
@@ -106,6 +107,7 @@ public class RealmManager {
newRealm.setSslNotRequired(rep.isSslNotRequired());
newRealm.setCookieLoginAllowed(rep.isCookieLoginAllowed());
newRealm.setRegistrationAllowed(rep.isRegistrationAllowed());
+ newRealm.setAutomaticRegistrationAfterSocialLogin(rep.isAutomaticRegistrationAfterSocialLogin());
if (rep.getPrivateKey() == null || rep.getPublicKey() == null) {
generateRealmKeys(newRealm);
} else {
@@ -182,6 +184,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) {
@@ -242,6 +254,7 @@ public class RealmManager {
rep.setRealm(realm.getName());
rep.setEnabled(realm.isEnabled());
rep.setSocial(realm.isSocial());
+ rep.setAutomaticRegistrationAfterSocialLogin(realm.isAutomaticRegistrationAfterSocialLogin());
rep.setSslNotRequired(realm.isSslNotRequired());
rep.setCookieLoginAllowed(realm.isCookieLoginAllowed());
rep.setPublicKey(realm.getPublicKeyPem());
diff --git a/services/src/main/java/org/keycloak/services/models/picketlink/mappings/RealmData.java b/services/src/main/java/org/keycloak/services/models/picketlink/mappings/RealmData.java
index 1fc6b7f..aeb0ad7 100755
--- a/services/src/main/java/org/keycloak/services/models/picketlink/mappings/RealmData.java
+++ b/services/src/main/java/org/keycloak/services/models/picketlink/mappings/RealmData.java
@@ -15,6 +15,7 @@ public class RealmData extends AbstractPartition {
private boolean cookieLoginAllowed;
private boolean registrationAllowed;
private boolean social;
+ private boolean automaticRegistrationAfterSocialLogin;
private int tokenLifespan;
private int accessCodeLifespan;
private String publicKeyPem;
@@ -56,6 +57,15 @@ public class RealmData extends AbstractPartition {
}
@AttributeProperty
+ public boolean isAutomaticRegistrationAfterSocialLogin() {
+ return automaticRegistrationAfterSocialLogin;
+ }
+
+ public void setAutomaticRegistrationAfterSocialLogin(boolean automaticRegistrationAfterSocialLogin) {
+ this.automaticRegistrationAfterSocialLogin = automaticRegistrationAfterSocialLogin;
+ }
+
+ @AttributeProperty
public boolean isSslNotRequired() {
return sslNotRequired;
}
diff --git a/services/src/main/java/org/keycloak/services/models/picketlink/mappings/RealmEntity.java b/services/src/main/java/org/keycloak/services/models/picketlink/mappings/RealmEntity.java
index e7e0883..0d64e86 100755
--- a/services/src/main/java/org/keycloak/services/models/picketlink/mappings/RealmEntity.java
+++ b/services/src/main/java/org/keycloak/services/models/picketlink/mappings/RealmEntity.java
@@ -37,6 +37,8 @@ public class RealmEntity implements Serializable {
@AttributeValue
private boolean social;
@AttributeValue
+ private boolean automaticRegistrationAfterSocialLogin;
+ @AttributeValue
private int tokenLifespan;
@AttributeValue
private int accessCodeLifespan;
@@ -106,6 +108,14 @@ public class RealmEntity implements Serializable {
this.social = social;
}
+ public boolean isAutomaticRegistrationAfterSocialLogin() {
+ return automaticRegistrationAfterSocialLogin;
+ }
+
+ public void setAutomaticRegistrationAfterSocialLogin(boolean automaticRegistrationAfterSocialLogin) {
+ this.automaticRegistrationAfterSocialLogin = automaticRegistrationAfterSocialLogin;
+ }
+
public int getTokenLifespan() {
return tokenLifespan;
}
diff --git a/services/src/main/java/org/keycloak/services/models/picketlink/RealmAdapter.java b/services/src/main/java/org/keycloak/services/models/picketlink/RealmAdapter.java
index dc8d6da..4641f09 100755
--- a/services/src/main/java/org/keycloak/services/models/picketlink/RealmAdapter.java
+++ b/services/src/main/java/org/keycloak/services/models/picketlink/RealmAdapter.java
@@ -10,6 +10,7 @@ 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;
@@ -26,7 +27,6 @@ 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.annotation.AttributeProperty;
import org.picketlink.idm.model.sample.Grant;
import org.picketlink.idm.model.sample.Role;
import org.picketlink.idm.model.sample.SampleModel;
@@ -123,6 +123,17 @@ public class RealmAdapter implements RealmModel {
}
@Override
+ public boolean isAutomaticRegistrationAfterSocialLogin() {
+ return realm.isAutomaticRegistrationAfterSocialLogin();
+ }
+
+ @Override
+ public void setAutomaticRegistrationAfterSocialLogin(boolean automaticRegistrationAfterSocialLogin) {
+ realm.setAutomaticRegistrationAfterSocialLogin(automaticRegistrationAfterSocialLogin);
+ updateRealm();
+ }
+
+ @Override
public boolean isSslNotRequired() {
return realm.isSslNotRequired();
}
@@ -694,4 +705,54 @@ public class RealmAdapter implements RealmModel {
realm.setDefaultRoles(defaultRoles);
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);
+ }
}
diff --git a/services/src/main/java/org/keycloak/services/models/picketlink/relationships/SocialLinkRelationship.java b/services/src/main/java/org/keycloak/services/models/picketlink/relationships/SocialLinkRelationship.java
new file mode 100644
index 0000000..c3a9389
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/models/picketlink/relationships/SocialLinkRelationship.java
@@ -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));
+ }
+}
diff --git a/services/src/main/java/org/keycloak/services/models/RealmModel.java b/services/src/main/java/org/keycloak/services/models/RealmModel.java
index 408df1b..9a1f3e5 100755
--- a/services/src/main/java/org/keycloak/services/models/RealmModel.java
+++ b/services/src/main/java/org/keycloak/services/models/RealmModel.java
@@ -127,7 +127,19 @@ public interface RealmModel {
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();
void setSocial(boolean social);
+
+ public boolean isAutomaticRegistrationAfterSocialLogin();
+
+ public void setAutomaticRegistrationAfterSocialLogin(boolean automaticRegistrationAfterSocialLogin);
}
diff --git a/services/src/main/java/org/keycloak/services/models/SocialLinkModel.java b/services/src/main/java/org/keycloak/services/models/SocialLinkModel.java
new file mode 100644
index 0000000..7a92d8d
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/models/SocialLinkModel.java
@@ -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;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/flows/FormFlows.java b/services/src/main/java/org/keycloak/services/resources/flows/FormFlows.java
index 3804b44..d1e3407 100644
--- a/services/src/main/java/org/keycloak/services/resources/flows/FormFlows.java
+++ b/services/src/main/java/org/keycloak/services/resources/flows/FormFlows.java
@@ -38,6 +38,7 @@ public class FormFlows {
public static final String ERROR_MESSAGE = "KEYCLOAK_FORMS_ERROR_MESSAGE";
public static final String REALM = Realm.class.getName();
public static final String USER = UserModel.class.getName();
+ public static final String SOCIAL_REGISTRATION = "socialRegistration";
private String error;
private MultivaluedMap<String, String> formData;
@@ -47,6 +48,8 @@ public class FormFlows {
private HttpRequest request;
private UserModel userModel;
+ private boolean socialRegistration;
+
FormFlows(RealmModel realm, HttpRequest request) {
this.realm = realm;
this.request = request;
@@ -75,6 +78,8 @@ public class FormFlows {
request.setAttribute(USER, userModel);
}
+ request.setAttribute(SOCIAL_REGISTRATION, socialRegistration);
+
request.forward(form);
return null;
}
@@ -113,6 +118,12 @@ public class FormFlows {
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) {
this.formData = formData;
return this;
diff --git a/services/src/main/java/org/keycloak/services/resources/flows/Urls.java b/services/src/main/java/org/keycloak/services/resources/flows/Urls.java
index 57fc5fb..3079602 100644
--- a/services/src/main/java/org/keycloak/services/resources/flows/Urls.java
+++ b/services/src/main/java/org/keycloak/services/resources/flows/Urls.java
@@ -100,7 +100,7 @@ public class Urls {
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);
}
@@ -116,4 +116,8 @@ public class Urls {
private static UriBuilder tokenBase(URI baseUri) {
return realmBase(baseUri).path(RealmsResource.class, "getTokenService");
}
+
+ public static URI socialRegisterAction(URI baseUri, String realmId) {
+ return socialBase(baseUri).path(SocialResource.class, "socialRegistration").build(realmId);
+ }
}
diff --git a/services/src/main/java/org/keycloak/services/resources/SocialResource.java b/services/src/main/java/org/keycloak/services/resources/SocialResource.java
index 73e139c..178d803 100644
--- a/services/src/main/java/org/keycloak/services/resources/SocialResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/SocialResource.java
@@ -27,33 +27,48 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
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.RealmModel;
import org.keycloak.services.models.RoleModel;
+import org.keycloak.services.models.SocialLinkModel;
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.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;
@@ -77,6 +92,12 @@ public class SocialResource {
@Context
private HttpRequest request;
+ @Context
+ private HttpResponse response;
+
+ @Context
+ ResourceContext resourceContext;
+
private SocialRequestManager socialRequestManager;
private TokenManager tokenManager;
@@ -109,10 +130,6 @@ public class SocialResource {
return oauth.forwardToSecurityFailure("Realm not enabled.");
}
- if (!realm.isEnabled()) {
- return oauth.forwardToSecurityFailure("Realm not enabled.");
- }
-
String clientId = requestData.getClientAttributes().get("clientId");
UserModel client = realm.getUser(clientId);
@@ -138,21 +155,54 @@ public class SocialResource {
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
- // user to
- // multiple social logins
- UserModel user = realm.getUser(provider.getId() + "." + socialUser.getId());
+ SocialLinkModel socialLink = new SocialLinkModel(provider.getId(), socialUser.getUsername());
+ UserModel user = realm.getUserBySocialLink(socialLink);
if (user == null) {
if (!realm.isRegistrationAllowed()) {
return oauth.forwardToSecurityFailure("Registration not allowed");
}
- user = realm.addUser(provider.getId() + "." + socialUser.getId());
- user.setAttribute(provider.getId() + ".id", socialUser.getId());
-
- for (RoleModel role : realm.getDefaultRoles()) {
- realm.grantRole(user, role);
+ // Automatically register user into realm with his social username (don't redirect to registration screen)
+ if (realm.isAutomaticRegistrationAfterSocialLogin()) {
+
+ if (realm.getUser(socialUser.getUsername()) != null) {
+ // 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();
}
}
@@ -202,6 +252,70 @@ 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) {
+ return new Transaction<Response>() {
+ protected Response callImpl() {
+ 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);
+ }
+ }.call();
+ }
+
private RequestDetails getRequestDetails(Map<String, String[]> queryParams) {
Iterator<SocialProvider> itr = ServiceRegistry.lookupProviders(SocialProvider.class);
@@ -240,4 +354,25 @@ public class SocialResource {
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;
+ }
+
}
diff --git a/services/src/main/java/org/keycloak/services/resources/TokenService.java b/services/src/main/java/org/keycloak/services/resources/TokenService.java
index cb63744..41c01a9 100755
--- a/services/src/main/java/org/keycloak/services/resources/TokenService.java
+++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java
@@ -249,84 +249,98 @@ public class TokenService {
return new Transaction<Response>() {
@Override
protected Response callImpl() {
- OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager);
+ Response registrationResponse = processRegisterImpl(clientId, scopeParam, state, redirect, formData, false);
- if (!realm.isEnabled()) {
- return oauth.forwardToSecurityFailure("Realm not enabled");
- }
- UserModel client = realm.getUser(clientId);
- if (client == null) {
- return oauth.forwardToSecurityFailure("Unknown login requester.");
+ // 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);
}
+ }
+ }.call();
+ }
- if (!client.isEnabled()) {
- return oauth.forwardToSecurityFailure("Login requester not enabled.");
- }
+ 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);
- if (!realm.isRegistrationAllowed()) {
- return oauth.forwardToSecurityFailure("Registration not allowed");
- }
+ if (!realm.isEnabled()) {
+ return oauth.forwardToSecurityFailure("Realm not enabled");
+ }
+ UserModel client = realm.getUser(clientId);
+ if (client == null) {
+ return oauth.forwardToSecurityFailure("Unknown login requester.");
+ }
- List<String> requiredCredentialTypes = new LinkedList<String>();
- for (RequiredCredentialModel m : realm.getRequiredCredentials()) {
- requiredCredentialTypes.add(m.getType());
- }
+ if (!client.isEnabled()) {
+ return oauth.forwardToSecurityFailure("Login requester not enabled.");
+ }
- String error = Validation.validateRegistrationForm(formData, requiredCredentialTypes);
- if (error != null) {
- return Flows.forms(realm, request).setError(error).setFormData(formData).forwardToRegistration();
- }
+ if (!realm.isRegistrationAllowed()) {
+ return oauth.forwardToSecurityFailure("Registration not allowed");
+ }
- String username = formData.getFirst("username");
+ List<String> requiredCredentialTypes = new LinkedList<String>();
+ for (RequiredCredentialModel m : realm.getRequiredCredentials()) {
+ requiredCredentialTypes.add(m.getType());
+ }
- UserModel user = realm.getUser(username);
- if (user != null) {
- return Flows.forms(realm, request).setError(Messages.USERNAME_EXISTS).setFormData(formData)
- .forwardToRegistration();
- }
-
- user = realm.addUser(username);
-
- String fullname = formData.getFirst("name");
- if (fullname != null) {
- StringTokenizer tokenizer = new StringTokenizer(fullname, " ");
- StringBuffer first = null;
- String last = "";
- while (tokenizer.hasMoreTokens()) {
- String token = tokenizer.nextToken();
- if (tokenizer.hasMoreTokens()) {
- if (first == null) {
- first = new StringBuffer();
- } else {
- first.append(" ");
- }
- first.append(token);
- } else {
- last = token;
- }
- }
- if (first == null)
+ String error = Validation.validateRegistrationForm(formData, requiredCredentialTypes);
+ if (error != null) {
+ return Flows.forms(realm, request).setError(error).setFormData(formData)
+ .setSocialRegistration(isSocialRegistration).forwardToRegistration();
+ }
+
+ String username = formData.getFirst("username");
+
+ UserModel user = realm.getUser(username);
+ if (user != null) {
+ return Flows.forms(realm, request).setError(Messages.USERNAME_EXISTS).setFormData(formData)
+ .setSocialRegistration(isSocialRegistration).forwardToRegistration();
+ }
+
+ user = realm.addUser(username);
+
+ String fullname = formData.getFirst("name");
+ if (fullname != null) {
+ StringTokenizer tokenizer = new StringTokenizer(fullname, " ");
+ StringBuffer first = null;
+ String last = "";
+ while (tokenizer.hasMoreTokens()) {
+ String token = tokenizer.nextToken();
+ if (tokenizer.hasMoreTokens()) {
+ if (first == null) {
first = new StringBuffer();
- user.setFirstName(first.toString());
- user.setLastName(last);
+ } else {
+ first.append(" ");
+ }
+ first.append(token);
+ } else {
+ last = token;
}
+ }
+ if (first == null)
+ first = new StringBuffer();
+ user.setFirstName(first.toString());
+ user.setLastName(last);
+ }
- user.setEmail(formData.getFirst("email"));
+ user.setEmail(formData.getFirst("email"));
- if (requiredCredentialTypes.contains(CredentialRepresentation.PASSWORD)) {
- UserCredentialModel credentials = new UserCredentialModel();
- credentials.setType(CredentialRepresentation.PASSWORD);
- credentials.setValue(formData.getFirst("password"));
- realm.updateCredential(user, credentials);
- }
+ if (requiredCredentialTypes.contains(CredentialRepresentation.PASSWORD)) {
+ UserCredentialModel credentials = new UserCredentialModel();
+ credentials.setType(CredentialRepresentation.PASSWORD);
+ credentials.setValue(formData.getFirst("password"));
+ realm.updateCredential(user, credentials);
+ }
- for (RoleModel role : realm.getDefaultRoles()) {
- realm.grantRole(user, role);
- }
+ for (RoleModel role : realm.getDefaultRoles()) {
+ realm.grantRole(user, role);
+ }
- return processLogin(clientId, scopeParam, state, redirect, formData);
- }
- }.call();
+ return null;
}
@Path("access/codes")
diff --git a/services/src/test/java/org/keycloak/test/AdapterTest.java b/services/src/test/java/org/keycloak/test/AdapterTest.java
index 2012264..aad9785 100755
--- a/services/src/test/java/org/keycloak/test/AdapterTest.java
+++ b/services/src/test/java/org/keycloak/test/AdapterTest.java
@@ -75,6 +75,7 @@ public class AdapterTest {
realmModel.setPrivateKeyPem("0234234");
realmModel.setPublicKeyPem("0234234");
realmModel.setTokenLifespan(1000);
+ realmModel.setAutomaticRegistrationAfterSocialLogin(true);
realmModel.addDefaultRole("foo");
System.out.println(realmModel.getId());
@@ -86,6 +87,7 @@ public class AdapterTest {
Assert.assertEquals(realmModel.getName(), "JUGGLER");
Assert.assertEquals(realmModel.getPrivateKeyPem(), "0234234");
Assert.assertEquals(realmModel.getPublicKeyPem(), "0234234");
+ Assert.assertEquals(realmModel.isAutomaticRegistrationAfterSocialLogin(), true);
Assert.assertEquals(1, realmModel.getDefaultRoles().size());
Assert.assertEquals("foo", realmModel.getDefaultRoles().get(0).getName());
}
diff --git a/services/src/test/java/org/keycloak/test/ImportTest.java b/services/src/test/java/org/keycloak/test/ImportTest.java
index 68cd9ed..e237c67 100755
--- a/services/src/test/java/org/keycloak/test/ImportTest.java
+++ b/services/src/test/java/org/keycloak/test/ImportTest.java
@@ -15,6 +15,7 @@ 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.UserModel;
import org.keycloak.services.resources.KeycloakApplication;
import org.keycloak.services.resources.SaasService;
@@ -58,6 +59,7 @@ public class ImportTest {
defaultRealm.setSslNotRequired(false);
defaultRealm.setCookieLoginAllowed(true);
defaultRealm.setRegistrationAllowed(true);
+ defaultRealm.setAutomaticRegistrationAfterSocialLogin(false);
manager.generateRealmKeys(defaultRealm);
defaultRealm.addRequiredCredential(CredentialRepresentation.PASSWORD);
RoleModel role = defaultRealm.addRole(SaasService.REALM_CREATOR_ROLE);
@@ -68,6 +70,8 @@ public class ImportTest {
RealmModel realm = manager.createRealm("demo", rep.getRealm());
manager.importRealm(rep, realm);
realm.addRealmAdmin(admin);
+
+ Assert.assertFalse(realm.isAutomaticRegistrationAfterSocialLogin());
List<RequiredCredentialModel> creds = realm.getRequiredCredentials();
Assert.assertEquals(1, creds.size());
RequiredCredentialModel cred = creds.get(0);
@@ -82,6 +86,8 @@ public class ImportTest {
Set<String> scopes = realm.getScope(user);
System.out.println("Scopes size: " + scopes.size());
Assert.assertTrue(scopes.contains("*"));
+ Assert.assertEquals(0, realm.getSocialLinks(user).size());
+
List<ApplicationModel> resources = realm.getApplications();
Assert.assertEquals(2, resources.size());
List<RealmModel> realms = identitySession.getRealms(admin);
@@ -94,6 +100,28 @@ public class ImportTest {
Assert.assertNotNull(oauthClient);
Set<String> appScopes = application.getScope(oauthClient);
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
@@ -106,6 +134,7 @@ public class ImportTest {
defaultRealm.setSslNotRequired(false);
defaultRealm.setCookieLoginAllowed(true);
defaultRealm.setRegistrationAllowed(true);
+ defaultRealm.setAutomaticRegistrationAfterSocialLogin(false);
manager.generateRealmKeys(defaultRealm);
defaultRealm.addRequiredCredential(CredentialRepresentation.PASSWORD);
RoleModel role = defaultRealm.addRole(SaasService.REALM_CREATOR_ROLE);
@@ -117,6 +146,7 @@ public class ImportTest {
manager.importRealm(rep, realm);
realm.addRealmAdmin(admin);
+ Assert.assertTrue(realm.isAutomaticRegistrationAfterSocialLogin());
verifyRequiredCredentials(realm.getRequiredCredentials(), "password");
verifyRequiredCredentials(realm.getRequiredApplicationCredentials(), "totp");
verifyRequiredCredentials(realm.getRequiredOAuthClientCredentials(), "cert");
services/src/test/resources/testrealm.json 23(+23 -0)
diff --git a/services/src/test/resources/testrealm.json b/services/src/test/resources/testrealm.json
index b1e3e39..b2c4454 100755
--- a/services/src/test/resources/testrealm.json
+++ b/services/src/test/resources/testrealm.json
@@ -50,6 +50,10 @@
"value": "clientpassword"
}
]
+ },
+ {
+ "username": "mySocialUser",
+ "enabled": true
}
],
"roleMappings": [
@@ -64,6 +68,25 @@
"roles": ["*"]
}
],
+ "socialMappings": [
+ {
+ "username": "mySocialUser",
+ "socialLinks": [
+ {
+ "socialProvider": "facebook",
+ "socialUsername": "fbuser1"
+ },
+ {
+ "socialProvider": "facebook",
+ "socialUsername": "fbuser2"
+ },
+ {
+ "socialProvider": "google",
+ "socialUsername": "mySocialUser@gmail.com"
+ }
+ ]
+ }
+ ],
"applications": [
{
"name": "Application",
diff --git a/services/src/test/resources/testrealm-demo.json b/services/src/test/resources/testrealm-demo.json
index 75007a3..ffb5f98 100755
--- a/services/src/test/resources/testrealm-demo.json
+++ b/services/src/test/resources/testrealm-demo.json
@@ -5,6 +5,7 @@
"accessCodeLifespan": 10,
"sslNotRequired": true,
"cookieLoginAllowed": true,
+ "automaticRegistrationAfterSocialLogin": 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" ],
diff --git a/social/core/src/main/java/org/keycloak/social/AuthCallback.java b/social/core/src/main/java/org/keycloak/social/AuthCallback.java
index 16fc089..8e21a21 100644
--- a/social/core/src/main/java/org/keycloak/social/AuthCallback.java
+++ b/social/core/src/main/java/org/keycloak/social/AuthCallback.java
@@ -28,16 +28,16 @@ import java.util.Map;
*/
public class AuthCallback {
- private Map<String, String> attributes;
+ private Map<String, Object> attributes;
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.queryParams = queryParams;
}
- public String getAttribute(String name) {
+ public Object getAttribute(String name) {
return attributes.get(name);
}
diff --git a/social/core/src/main/java/org/keycloak/social/AuthRequest.java b/social/core/src/main/java/org/keycloak/social/AuthRequest.java
index a88d805..69731e9 100644
--- a/social/core/src/main/java/org/keycloak/social/AuthRequest.java
+++ b/social/core/src/main/java/org/keycloak/social/AuthRequest.java
@@ -33,9 +33,9 @@ public class AuthRequest {
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.authUri = authUri;
this.attributes = attributes;
@@ -49,7 +49,7 @@ public class AuthRequest {
return authUri;
}
- public Map<String, String> getAttributes() {
+ public Map<String, Object> getAttributes() {
return attributes;
}
diff --git a/social/core/src/main/java/org/keycloak/social/AuthRequestBuilder.java b/social/core/src/main/java/org/keycloak/social/AuthRequestBuilder.java
index 600783d..c5dd66a 100644
--- a/social/core/src/main/java/org/keycloak/social/AuthRequestBuilder.java
+++ b/social/core/src/main/java/org/keycloak/social/AuthRequestBuilder.java
@@ -33,7 +33,7 @@ public class AuthRequestBuilder {
private UriBuilder b;
- private Map<String, String> attributes;
+ private Map<String, Object> attributes;
private String id;
@@ -44,7 +44,7 @@ public class AuthRequestBuilder {
AuthRequestBuilder req = new AuthRequestBuilder();
req.id = id;
req.b = UriBuilder.fromUri(path);
- req.attributes = new HashMap<String, String>();
+ req.attributes = new HashMap<String, Object>();
return req;
}
@@ -53,7 +53,7 @@ public class AuthRequestBuilder {
return this;
}
- public AuthRequestBuilder setAttribute(String name, String value) {
+ public AuthRequestBuilder setAttribute(String name, Object value) {
attributes.put(name, value);
return this;
}
diff --git a/social/core/src/main/java/org/keycloak/social/RequestDetails.java b/social/core/src/main/java/org/keycloak/social/RequestDetails.java
index 0476a64..f77f59b 100644
--- a/social/core/src/main/java/org/keycloak/social/RequestDetails.java
+++ b/social/core/src/main/java/org/keycloak/social/RequestDetails.java
@@ -32,9 +32,9 @@ public class RequestDetails {
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.clientAttributes = clientAttributes;
this.socialAttributes = socialAttributes;
@@ -52,11 +52,11 @@ public class RequestDetails {
return clientAttributes;
}
- public String getSocialAttribute(String name) {
+ public Object getSocialAttribute(String name) {
return socialAttributes.get(name);
}
- public Map<String, String> getSocialAttributes() {
+ public Map<String, Object> getSocialAttributes() {
return socialAttributes;
}
diff --git a/social/core/src/main/java/org/keycloak/social/RequestDetailsBuilder.java b/social/core/src/main/java/org/keycloak/social/RequestDetailsBuilder.java
index a8d8286..aa86fbe 100644
--- a/social/core/src/main/java/org/keycloak/social/RequestDetailsBuilder.java
+++ b/social/core/src/main/java/org/keycloak/social/RequestDetailsBuilder.java
@@ -33,7 +33,7 @@ public class RequestDetailsBuilder {
private Map<String, String> clientAttributes;
- private Map<String, String> socialAttributes;
+ private Map<String, Object> socialAttributes;
private RequestDetailsBuilder() {
}
@@ -42,7 +42,17 @@ public class RequestDetailsBuilder {
RequestDetailsBuilder req = new RequestDetailsBuilder();
req.providerId = providerId;
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;
}
@@ -56,12 +66,12 @@ public class RequestDetailsBuilder {
return this;
}
- public RequestDetailsBuilder putSocialAttribute(String name, String value) {
+ public RequestDetailsBuilder putSocialAttribute(String name, Object value) {
socialAttributes.put(name, value);
return this;
}
- public RequestDetailsBuilder putSocialAttributes(Map<String, String> attributes) {
+ public RequestDetailsBuilder putSocialAttributes(Map<String, Object> attributes) {
socialAttributes.putAll(attributes);
return this;
}
diff --git a/social/core/src/main/java/org/keycloak/social/SocialConstants.java b/social/core/src/main/java/org/keycloak/social/SocialConstants.java
new file mode 100644
index 0000000..f1d5cd7
--- /dev/null
+++ b/social/core/src/main/java/org/keycloak/social/SocialConstants.java
@@ -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";
+}
diff --git a/social/core/src/main/java/org/keycloak/social/SocialRequestManager.java b/social/core/src/main/java/org/keycloak/social/SocialRequestManager.java
index 2993a9a..735ea6b 100644
--- a/social/core/src/main/java/org/keycloak/social/SocialRequestManager.java
+++ b/social/core/src/main/java/org/keycloak/social/SocialRequestManager.java
@@ -57,6 +57,11 @@ public class SocialRequestManager {
return details;
}
+
+ // Just obtain data without expiring it
+ public synchronized RequestDetails getData(String requestId) {
+ return map.get(requestId);
+ }
private void pruneExpired() {
long currentTime = System.currentTimeMillis();
diff --git a/social/core/src/main/java/org/keycloak/social/SocialUser.java b/social/core/src/main/java/org/keycloak/social/SocialUser.java
index fc0b7f2..f9485d6 100644
--- a/social/core/src/main/java/org/keycloak/social/SocialUser.java
+++ b/social/core/src/main/java/org/keycloak/social/SocialUser.java
@@ -3,6 +3,7 @@ package org.keycloak.social;
public class SocialUser {
private String id;
+ private String username;
private String firstName;
private String lastName;
private String email;
@@ -19,6 +20,14 @@ public class SocialUser {
this.id = id;
}
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
public String getFirstName() {
return firstName;
}
diff --git a/social/facebook/src/main/java/org/keycloak/social/facebook/FacebookProvider.java b/social/facebook/src/main/java/org/keycloak/social/facebook/FacebookProvider.java
index dbe4254..94410f1 100644
--- a/social/facebook/src/main/java/org/keycloak/social/facebook/FacebookProvider.java
+++ b/social/facebook/src/main/java/org/keycloak/social/facebook/FacebookProvider.java
@@ -82,6 +82,13 @@ public class FacebookProvider implements SocialProvider {
FacebookUser facebookUser = loadUser(accessToken, client);
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.setLastName(facebookUser.getLastName());
socialUser.setFirstName(facebookUser.getFirstName());
diff --git a/social/google/src/main/java/org/keycloak/social/google/GoogleProvider.java b/social/google/src/main/java/org/keycloak/social/google/GoogleProvider.java
index 9054c45..3a0febb 100644
--- a/social/google/src/main/java/org/keycloak/social/google/GoogleProvider.java
+++ b/social/google/src/main/java/org/keycloak/social/google/GoogleProvider.java
@@ -106,6 +106,10 @@ public class GoogleProvider implements SocialProvider {
Userinfo userInfo = oauth2.userinfo().get().execute();
SocialUser user = new SocialUser(userInfo.getId());
+
+ // Use email as username for Google
+ user.setUsername(userInfo.getEmail());
+
user.setFirstName(userInfo.getGivenName());
user.setLastName(userInfo.getFamilyName());
user.setEmail(userInfo.getEmail());
diff --git a/social/twitter/src/main/java/org/keycloak/social/twitter/TwitterProvider.java b/social/twitter/src/main/java/org/keycloak/social/twitter/TwitterProvider.java
index d014e81..54c4cc2 100644
--- a/social/twitter/src/main/java/org/keycloak/social/twitter/TwitterProvider.java
+++ b/social/twitter/src/main/java/org/keycloak/social/twitter/TwitterProvider.java
@@ -71,13 +71,24 @@ public class TwitterProvider implements SocialProvider {
twitter.setOAuthConsumer(config.getKey(), config.getSecret());
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);
twitter4j.User twitterUser = twitter.verifyCredentials();
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;
} catch (Exception e) {