KEYCLOAK-27 Basic social flow with automatic registration and non-duplicated username
This commit is contained in:
parent
b2544dbe8f
commit
932ed10c17
8 changed files with 68 additions and 9 deletions
|
@ -6,6 +6,7 @@
|
||||||
"sslNotRequired": true,
|
"sslNotRequired": true,
|
||||||
"cookieLoginAllowed": true,
|
"cookieLoginAllowed": true,
|
||||||
"registrationAllowed": true,
|
"registrationAllowed": true,
|
||||||
|
"automaticRegistrationAfterSocialLogin": true,
|
||||||
"privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
|
"privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
|
||||||
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
|
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
|
||||||
"requiredCredentials": [ "password" ],
|
"requiredCredentials": [ "password" ],
|
||||||
|
|
|
@ -37,6 +37,8 @@ public class RealmEntity implements Serializable {
|
||||||
@AttributeValue
|
@AttributeValue
|
||||||
private boolean social;
|
private boolean social;
|
||||||
@AttributeValue
|
@AttributeValue
|
||||||
|
private boolean automaticRegistrationAfterSocialLogin;
|
||||||
|
@AttributeValue
|
||||||
private int tokenLifespan;
|
private int tokenLifespan;
|
||||||
@AttributeValue
|
@AttributeValue
|
||||||
private int accessCodeLifespan;
|
private int accessCodeLifespan;
|
||||||
|
@ -106,6 +108,14 @@ public class RealmEntity implements Serializable {
|
||||||
this.social = social;
|
this.social = social;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isAutomaticRegistrationAfterSocialLogin() {
|
||||||
|
return automaticRegistrationAfterSocialLogin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAutomaticRegistrationAfterSocialLogin(boolean automaticRegistrationAfterSocialLogin) {
|
||||||
|
this.automaticRegistrationAfterSocialLogin = automaticRegistrationAfterSocialLogin;
|
||||||
|
}
|
||||||
|
|
||||||
public int getTokenLifespan() {
|
public int getTokenLifespan() {
|
||||||
return tokenLifespan;
|
return tokenLifespan;
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,7 @@ import org.keycloak.services.managers.RealmManager;
|
||||||
import org.keycloak.services.managers.TokenManager;
|
import org.keycloak.services.managers.TokenManager;
|
||||||
import org.keycloak.services.models.RealmModel;
|
import org.keycloak.services.models.RealmModel;
|
||||||
import org.keycloak.services.models.RoleModel;
|
import org.keycloak.services.models.RoleModel;
|
||||||
|
import org.keycloak.services.models.SocialLinkModel;
|
||||||
import org.keycloak.services.models.UserModel;
|
import org.keycloak.services.models.UserModel;
|
||||||
import org.keycloak.services.resources.flows.Flows;
|
import org.keycloak.services.resources.flows.Flows;
|
||||||
import org.keycloak.services.resources.flows.OAuthFlows;
|
import org.keycloak.services.resources.flows.OAuthFlows;
|
||||||
|
@ -134,21 +135,35 @@ public class SocialResource {
|
||||||
return oauth.forwardToSecurityFailure("Failed to process social callback");
|
return oauth.forwardToSecurityFailure("Failed to process social callback");
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Lookup user based on attribute for provider id - this is so a user can have a friendly username + link a
|
SocialLinkModel socialLink = new SocialLinkModel(provider.getId(), socialUser.getUsername());
|
||||||
// user to
|
UserModel user = realm.getUserBySocialLink(socialLink);
|
||||||
// multiple social logins
|
|
||||||
UserModel user = realm.getUser(provider.getId() + "." + socialUser.getId());
|
|
||||||
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
if (!realm.isRegistrationAllowed()) {
|
if (!realm.isRegistrationAllowed()) {
|
||||||
return oauth.forwardToSecurityFailure("Registration not allowed");
|
return oauth.forwardToSecurityFailure("Registration not allowed");
|
||||||
}
|
}
|
||||||
|
|
||||||
user = realm.addUser(provider.getId() + "." + socialUser.getId());
|
// Automatically register user into realm with his social username (don't redirect to registration screen)
|
||||||
user.setAttribute(provider.getId() + ".id", socialUser.getId());
|
if (realm.isAutomaticRegistrationAfterSocialLogin()) {
|
||||||
|
|
||||||
for (RoleModel role : realm.getDefaultRoles()) {
|
if (realm.getUser(socialUser.getUsername()) != null) {
|
||||||
realm.grantRole(user, role);
|
// TODO: Username is already in realm. Show message and let user to bind accounts
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -75,6 +75,7 @@ public class AdapterTest {
|
||||||
realmModel.setPrivateKeyPem("0234234");
|
realmModel.setPrivateKeyPem("0234234");
|
||||||
realmModel.setPublicKeyPem("0234234");
|
realmModel.setPublicKeyPem("0234234");
|
||||||
realmModel.setTokenLifespan(1000);
|
realmModel.setTokenLifespan(1000);
|
||||||
|
realmModel.setAutomaticRegistrationAfterSocialLogin(true);
|
||||||
realmModel.addDefaultRole("foo");
|
realmModel.addDefaultRole("foo");
|
||||||
|
|
||||||
System.out.println(realmModel.getId());
|
System.out.println(realmModel.getId());
|
||||||
|
@ -86,6 +87,7 @@ public class AdapterTest {
|
||||||
Assert.assertEquals(realmModel.getName(), "JUGGLER");
|
Assert.assertEquals(realmModel.getName(), "JUGGLER");
|
||||||
Assert.assertEquals(realmModel.getPrivateKeyPem(), "0234234");
|
Assert.assertEquals(realmModel.getPrivateKeyPem(), "0234234");
|
||||||
Assert.assertEquals(realmModel.getPublicKeyPem(), "0234234");
|
Assert.assertEquals(realmModel.getPublicKeyPem(), "0234234");
|
||||||
|
Assert.assertEquals(realmModel.isAutomaticRegistrationAfterSocialLogin(), true);
|
||||||
Assert.assertEquals(1, realmModel.getDefaultRoles().size());
|
Assert.assertEquals(1, realmModel.getDefaultRoles().size());
|
||||||
Assert.assertEquals("foo", realmModel.getDefaultRoles().get(0).getName());
|
Assert.assertEquals("foo", realmModel.getDefaultRoles().get(0).getName());
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package org.keycloak.social;
|
||||||
public class SocialUser {
|
public class SocialUser {
|
||||||
|
|
||||||
private String id;
|
private String id;
|
||||||
|
private String username;
|
||||||
private String firstName;
|
private String firstName;
|
||||||
private String lastName;
|
private String lastName;
|
||||||
private String email;
|
private String email;
|
||||||
|
@ -19,6 +20,14 @@ public class SocialUser {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUsername(String username) {
|
||||||
|
this.username = username;
|
||||||
|
}
|
||||||
|
|
||||||
public String getFirstName() {
|
public String getFirstName() {
|
||||||
return firstName;
|
return firstName;
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,6 +82,13 @@ public class FacebookProvider implements SocialProvider {
|
||||||
FacebookUser facebookUser = loadUser(accessToken, client);
|
FacebookUser facebookUser = loadUser(accessToken, client);
|
||||||
|
|
||||||
SocialUser socialUser = new SocialUser(facebookUser.getId());
|
SocialUser socialUser = new SocialUser(facebookUser.getId());
|
||||||
|
socialUser.setUsername(facebookUser.getUsername());
|
||||||
|
|
||||||
|
// This could happen with Facebook testing users
|
||||||
|
if (facebookUser.getUsername() == null || facebookUser.getUsername().length() == 0) {
|
||||||
|
socialUser.setUsername(facebookUser.getId());
|
||||||
|
}
|
||||||
|
|
||||||
socialUser.setEmail(facebookUser.getEmail());
|
socialUser.setEmail(facebookUser.getEmail());
|
||||||
socialUser.setLastName(facebookUser.getLastName());
|
socialUser.setLastName(facebookUser.getLastName());
|
||||||
socialUser.setFirstName(facebookUser.getFirstName());
|
socialUser.setFirstName(facebookUser.getFirstName());
|
||||||
|
|
|
@ -106,6 +106,10 @@ public class GoogleProvider implements SocialProvider {
|
||||||
Userinfo userInfo = oauth2.userinfo().get().execute();
|
Userinfo userInfo = oauth2.userinfo().get().execute();
|
||||||
|
|
||||||
SocialUser user = new SocialUser(userInfo.getId());
|
SocialUser user = new SocialUser(userInfo.getId());
|
||||||
|
|
||||||
|
// Use email as username for Google
|
||||||
|
user.setUsername(userInfo.getEmail());
|
||||||
|
|
||||||
user.setFirstName(userInfo.getGivenName());
|
user.setFirstName(userInfo.getGivenName());
|
||||||
user.setLastName(userInfo.getFamilyName());
|
user.setLastName(userInfo.getFamilyName());
|
||||||
user.setEmail(userInfo.getEmail());
|
user.setEmail(userInfo.getEmail());
|
||||||
|
|
|
@ -77,7 +77,18 @@ public class TwitterProvider implements SocialProvider {
|
||||||
twitter4j.User twitterUser = twitter.verifyCredentials();
|
twitter4j.User twitterUser = twitter.verifyCredentials();
|
||||||
|
|
||||||
SocialUser user = new SocialUser(Long.toString(twitterUser.getId()));
|
SocialUser user = new SocialUser(Long.toString(twitterUser.getId()));
|
||||||
user.setFirstName(twitterUser.getName());
|
|
||||||
|
// Use screenName as username for Twitter
|
||||||
|
user.setUsername(twitterUser.getScreenName());
|
||||||
|
|
||||||
|
String twitterName = twitterUser.getName();
|
||||||
|
int spaceIndex = twitterName.lastIndexOf(' ');
|
||||||
|
if (spaceIndex != -1) {
|
||||||
|
user.setFirstName(twitterName.substring(0, spaceIndex));
|
||||||
|
user.setLastName(twitterName.substring(spaceIndex + 1));
|
||||||
|
} else {
|
||||||
|
user.setFirstName(twitterName);
|
||||||
|
}
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|
Loading…
Reference in a new issue