From 1a374a8d1bddaca045831c6f74ca604a2b60928e Mon Sep 17 00:00:00 2001 From: mposolda Date: Mon, 19 Aug 2013 14:36:06 +0200 Subject: [PATCH 1/6] Added SocialLink, which represents binding between User and his social username for particular socialProvider --- .../idm/RealmRepresentation.java | 13 +++++ .../idm/SocialLinkRepresentation.java | 26 +++++++++ .../idm/SocialMappingRepresentation.java | 43 ++++++++++++++ .../services/managers/RealmManager.java | 10 ++++ .../keycloak/services/models/RealmModel.java | 8 +++ .../services/models/SocialLinkModel.java | 31 ++++++++++ .../models/picketlink/RealmAdapter.java | 52 ++++++++++++++++- .../relationships/SocialLinkRelationship.java | 57 +++++++++++++++++++ .../services/resources/SocialResource.java | 4 -- .../java/org/keycloak/test/ImportTest.java | 25 ++++++++ services/src/test/resources/testrealm.json | 23 ++++++++ 11 files changed, 287 insertions(+), 5 deletions(-) create mode 100644 core/src/main/java/org/keycloak/representations/idm/SocialLinkRepresentation.java create mode 100644 core/src/main/java/org/keycloak/representations/idm/SocialMappingRepresentation.java create mode 100644 services/src/main/java/org/keycloak/services/models/SocialLinkModel.java create mode 100644 services/src/main/java/org/keycloak/services/models/picketlink/relationships/SocialLinkRelationship.java 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 e365a6147b..525ed7488f 100755 --- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java @@ -29,6 +29,7 @@ public class RealmRepresentation { protected List users; protected List roleMappings; protected List scopeMappings; + protected List socialMappings; protected List applications; @@ -144,6 +145,18 @@ public class RealmRepresentation { return mapping; } + public List getSocialMappings() { + return socialMappings; + } + + public SocialMappingRepresentation socialMapping(String username) { + SocialMappingRepresentation mapping = new SocialMappingRepresentation(); + mapping.setUsername(username); + if (socialMappings == null) socialMappings = new ArrayList(); + socialMappings.add(mapping); + return mapping; + } + public Set getRequiredCredentials() { return requiredCredentials; } 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 0000000000..a6e183890a --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/SocialLinkRepresentation.java @@ -0,0 +1,26 @@ +package org.keycloak.representations.idm; + +/** + * @author Marek Posolda + */ +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 0000000000..57dd874a8c --- /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 Marek Posolda + */ +public class SocialMappingRepresentation { + + protected String self; // link + protected String username; + protected List 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 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(); + socialLinks.add(link); + return link; + } +} 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 afd24063df..506503e03a 100755 --- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java +++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java @@ -182,6 +182,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) { 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 408df1b80b..8ccc942f7a 100755 --- a/services/src/main/java/org/keycloak/services/models/RealmModel.java +++ b/services/src/main/java/org/keycloak/services/models/RealmModel.java @@ -127,6 +127,14 @@ public interface RealmModel { void updateRequiredApplicationCredentials(Set creds); + UserModel getUserBySocialLink(SocialLinkModel socialLink); + + Set getSocialLinks(UserModel user); + + void addSocialLink(UserModel user, SocialLinkModel socialLink); + + void removeSocialLink(UserModel user, SocialLinkModel socialLink); + boolean isSocial(); void setSocial(boolean social); 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 0000000000..7a92d8d958 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/models/SocialLinkModel.java @@ -0,0 +1,31 @@ +package org.keycloak.services.models; + +/** + * @author Marek Posolda + */ +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/models/picketlink/RealmAdapter.java b/services/src/main/java/org/keycloak/services/models/picketlink/RealmAdapter.java index dc8d6da92d..816aab4a63 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; @@ -694,4 +694,54 @@ public class RealmAdapter implements RealmModel { realm.setDefaultRoles(defaultRoles); updateRealm(); } + + @Override + public UserModel getUserBySocialLink(SocialLinkModel socialLink) { + RelationshipQuery query = getRelationshipManager().createRelationshipQuery(SocialLinkRelationship.class); + query.setParameter(SocialLinkRelationship.SOCIAL_PROVIDER, socialLink.getSocialProvider()); + query.setParameter(SocialLinkRelationship.SOCIAL_USERNAME, socialLink.getSocialUsername()); + List 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 getSocialLinks(UserModel user) { + RelationshipQuery query = getRelationshipManager().createRelationshipQuery(SocialLinkRelationship.class); + query.setParameter(SocialLinkRelationship.USER, ((UserAdapter)user).getUser()); + List plSocialLinks = query.getResultList(); + + Set results = new HashSet(); + 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 0000000000..c3a93895db --- /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 Marek Posolda + */ +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("socialProvider", socialProvider)); + } + + public String getSocialUsername() { + return (String)getAttribute("socialUsername").getValue(); + } + + public void setSocialUsername(String socialProviderUserId) { + setAttribute(new Attribute("socialUsername", socialProviderUserId)); + } +} 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 73e139cf8e..91a36cac0c 100644 --- a/services/src/main/java/org/keycloak/services/resources/SocialResource.java +++ b/services/src/main/java/org/keycloak/services/resources/SocialResource.java @@ -109,10 +109,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); diff --git a/services/src/test/java/org/keycloak/test/ImportTest.java b/services/src/test/java/org/keycloak/test/ImportTest.java index 68cd9edb9a..95590f7e6c 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; @@ -82,6 +83,8 @@ public class ImportTest { Set scopes = realm.getScope(user); System.out.println("Scopes size: " + scopes.size()); Assert.assertTrue(scopes.contains("*")); + Assert.assertEquals(0, realm.getSocialLinks(user).size()); + List resources = realm.getApplications(); Assert.assertEquals(2, resources.size()); List realms = identitySession.getRealms(admin); @@ -94,6 +97,28 @@ public class ImportTest { Assert.assertNotNull(oauthClient); Set appScopes = application.getScope(oauthClient); Assert.assertTrue(appScopes.contains("user")); + + // Test social linking + UserModel socialUser = realm.getUser("mySocialUser"); + Set 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 diff --git a/services/src/test/resources/testrealm.json b/services/src/test/resources/testrealm.json index b1e3e39d05..b2c4454505 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", From b2544dbe8f067891b609f304841ddb65d7ac0e19 Mon Sep 17 00:00:00 2001 From: mposolda Date: Mon, 19 Aug 2013 15:03:42 +0200 Subject: [PATCH 2/6] Added realm option automaticRegistrationAfterSocialLogin to specify whether user is automatically registered after social login or whether he needs to confirm registration data --- .../representations/idm/RealmRepresentation.java | 9 +++++++++ .../org/keycloak/services/managers/RealmManager.java | 3 +++ .../java/org/keycloak/services/models/RealmModel.java | 4 ++++ .../services/models/picketlink/RealmAdapter.java | 11 +++++++++++ .../models/picketlink/mappings/RealmData.java | 10 ++++++++++ .../src/test/java/org/keycloak/test/ImportTest.java | 5 +++++ services/src/test/resources/testrealm-demo.json | 1 + 7 files changed, 43 insertions(+) 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 525ed7488f..92c3f106de 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 roles; @@ -236,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/services/src/main/java/org/keycloak/services/managers/RealmManager.java b/services/src/main/java/org/keycloak/services/managers/RealmManager.java index 506503e03a..603fc56786 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 { @@ -252,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/RealmModel.java b/services/src/main/java/org/keycloak/services/models/RealmModel.java index 8ccc942f7a..9a1f3e5c8e 100755 --- a/services/src/main/java/org/keycloak/services/models/RealmModel.java +++ b/services/src/main/java/org/keycloak/services/models/RealmModel.java @@ -138,4 +138,8 @@ public interface RealmModel { 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/picketlink/RealmAdapter.java b/services/src/main/java/org/keycloak/services/models/picketlink/RealmAdapter.java index 816aab4a63..4641f0923b 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 @@ -122,6 +122,17 @@ public class RealmAdapter implements RealmModel { realm.setSocial(social); } + @Override + public boolean isAutomaticRegistrationAfterSocialLogin() { + return realm.isAutomaticRegistrationAfterSocialLogin(); + } + + @Override + public void setAutomaticRegistrationAfterSocialLogin(boolean automaticRegistrationAfterSocialLogin) { + realm.setAutomaticRegistrationAfterSocialLogin(automaticRegistrationAfterSocialLogin); + updateRealm(); + } + @Override public boolean isSslNotRequired() { return realm.isSslNotRequired(); 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 1fc6b7f54a..aeb0ad75a2 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; @@ -55,6 +56,15 @@ public class RealmData extends AbstractPartition { this.social = social; } + @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/test/java/org/keycloak/test/ImportTest.java b/services/src/test/java/org/keycloak/test/ImportTest.java index 95590f7e6c..e237c67b8d 100755 --- a/services/src/test/java/org/keycloak/test/ImportTest.java +++ b/services/src/test/java/org/keycloak/test/ImportTest.java @@ -59,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); @@ -69,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 creds = realm.getRequiredCredentials(); Assert.assertEquals(1, creds.size()); RequiredCredentialModel cred = creds.get(0); @@ -131,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); @@ -142,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"); diff --git a/services/src/test/resources/testrealm-demo.json b/services/src/test/resources/testrealm-demo.json index 75007a3ca1..ffb5f98394 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" ], From 932ed10c178940b154faf19a20fb96e8e5d3e027 Mon Sep 17 00:00:00 2001 From: mposolda Date: Mon, 19 Aug 2013 18:14:40 +0200 Subject: [PATCH 3/6] KEYCLOAK-27 Basic social flow with automatic registration and non-duplicated username --- .../main/resources/META-INF/testrealm.json | 1 + .../picketlink/mappings/RealmEntity.java | 10 ++++++ .../services/resources/SocialResource.java | 31 ++++++++++++++----- .../java/org/keycloak/test/AdapterTest.java | 2 ++ .../java/org/keycloak/social/SocialUser.java | 9 ++++++ .../social/facebook/FacebookProvider.java | 7 +++++ .../social/google/GoogleProvider.java | 4 +++ .../social/twitter/TwitterProvider.java | 13 +++++++- 8 files changed, 68 insertions(+), 9 deletions(-) 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 f92e34c199..f69d0bd7ae 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,7 @@ "sslNotRequired": true, "cookieLoginAllowed": true, "registrationAllowed": 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/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 e7e0883388..0d64e86e43 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/resources/SocialResource.java b/services/src/main/java/org/keycloak/services/resources/SocialResource.java index 91a36cac0c..c5596f42bf 100644 --- a/services/src/main/java/org/keycloak/services/resources/SocialResource.java +++ b/services/src/main/java/org/keycloak/services/resources/SocialResource.java @@ -46,6 +46,7 @@ import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.TokenManager; 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; @@ -134,21 +135,35 @@ 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()); + // Automatically register user into realm with his social username (don't redirect to registration screen) + if (realm.isAutomaticRegistrationAfterSocialLogin()) { - for (RoleModel role : realm.getDefaultRoles()) { - realm.grantRole(user, role); + if (realm.getUser(socialUser.getUsername()) != null) { + // TODO: Username is already in realm. Show message and let user to bind accounts + 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()); + } + + realm.addSocialLink(user, socialLink); + + for (RoleModel role : realm.getDefaultRoles()) { + realm.grantRole(user, role); + } + } else { + // TODO: redirect to registration screen with pre-filled info } } diff --git a/services/src/test/java/org/keycloak/test/AdapterTest.java b/services/src/test/java/org/keycloak/test/AdapterTest.java index 2012264c6e..aad9785d21 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/social/core/src/main/java/org/keycloak/social/SocialUser.java b/social/core/src/main/java/org/keycloak/social/SocialUser.java index fc0b7f24a8..f9485d6da6 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 dbe42547c2..94410f18b4 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 9054c45a8e..3a0febb782 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 d014e81137..49b6e6ebe1 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 @@ -77,7 +77,18 @@ public class TwitterProvider implements SocialProvider { 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) { From dd3c438e009051672f6078c10f4534536b17abca Mon Sep 17 00:00:00 2001 From: mposolda Date: Tue, 20 Aug 2013 12:42:19 +0200 Subject: [PATCH 4/6] KEYCLOAK-25 Added flow with user registration with prefilled attributes from social --- .../main/resources/META-INF/testrealm.json | 3 +- .../java/org/keycloak/forms/RegisterBean.java | 10 ++ .../java/org/keycloak/forms/SocialBean.java | 31 ++++ .../main/java/org/keycloak/forms/UrlBean.java | 14 ++ .../forms/theme/default/register.xhtml | 4 +- .../forms/theme/default/template-login.xhtml | 4 +- .../services/resources/SocialResource.java | 127 +++++++++++++- .../services/resources/TokenService.java | 158 +++++++++--------- .../services/resources/flows/FormFlows.java | 11 ++ .../services/resources/flows/Urls.java | 6 +- .../org/keycloak/social/AuthCallback.java | 6 +- .../java/org/keycloak/social/AuthRequest.java | 6 +- .../keycloak/social/AuthRequestBuilder.java | 6 +- .../org/keycloak/social/RequestDetails.java | 8 +- .../social/RequestDetailsBuilder.java | 18 +- .../org/keycloak/social/SocialConstants.java | 11 ++ .../keycloak/social/SocialRequestManager.java | 5 + .../social/twitter/TwitterProvider.java | 2 +- 18 files changed, 329 insertions(+), 101 deletions(-) create mode 100644 social/core/src/main/java/org/keycloak/social/SocialConstants.java 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 f69d0bd7ae..bb918efc31 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,7 +6,8 @@ "sslNotRequired": true, "cookieLoginAllowed": true, "registrationAllowed": true, - "automaticRegistrationAfterSocialLogin": 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 60d247acd1..77d6a8cbe6 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 formData; + private boolean socialRegistration; + @PostConstruct public void init() { FacesContext ctx = FacesContext.getCurrentInstance(); @@ -49,6 +52,9 @@ public class RegisterBean { this.formData = new HashMap(); + Boolean socialRegistrationAttr = (Boolean)request.getAttribute(FormFlows.SOCIAL_REGISTRATION); + this.socialRegistration = socialRegistrationAttr != null && socialRegistrationAttr; + @SuppressWarnings("unchecked") MultivaluedMap formData = (MultivaluedMap) 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 b007083044..fab91b165e 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 ba006096d1..62017d00bb 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 0078271801..ca9d8b6745 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 @@ -

#{messages.alreadyHaveAccount} #{messages.logIn}.

+ +

#{messages.alreadyHaveAccount} #{messages.logIn}.

+
\ 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 05ee4fb803..e99a442c26 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 { - +