diff --git a/forms/account-api/src/main/java/org/keycloak/account/AccountPages.java b/forms/account-api/src/main/java/org/keycloak/account/AccountPages.java index 2fc9a29107..a3e60d2460 100644 --- a/forms/account-api/src/main/java/org/keycloak/account/AccountPages.java +++ b/forms/account-api/src/main/java/org/keycloak/account/AccountPages.java @@ -5,6 +5,6 @@ package org.keycloak.account; */ public enum AccountPages { - ACCOUNT, PASSWORD, TOTP; + ACCOUNT, PASSWORD, TOTP, SOCIAL; } diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccount.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccount.java index 64f41354ad..1a5cc6720b 100644 --- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccount.java +++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccount.java @@ -4,6 +4,7 @@ import org.jboss.resteasy.logging.Logger; import org.keycloak.account.Account; import org.keycloak.account.AccountPages; import org.keycloak.account.freemarker.model.AccountBean; +import org.keycloak.account.freemarker.model.AccountSocialBean; import org.keycloak.account.freemarker.model.MessageBean; import org.keycloak.account.freemarker.model.ReferrerBean; import org.keycloak.account.freemarker.model.TotpBean; @@ -88,6 +89,10 @@ public class FreeMarkerAccount implements Account { attributes.put("url", new UrlBean(realm, theme, baseUri)); + if (realm.isSocial()) { + attributes.put("isSocialRealm", true); + } + switch (page) { case ACCOUNT: attributes.put("account", new AccountBean(user)); @@ -95,6 +100,9 @@ public class FreeMarkerAccount implements Account { case TOTP: attributes.put("totp", new TotpBean(user, baseUri)); break; + case SOCIAL: + attributes.put("social", new AccountSocialBean(realm, user, uriInfo.getBaseUri())); + break; } try { diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/Templates.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/Templates.java index 4e701a3e31..5a63ef5949 100644 --- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/Templates.java +++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/Templates.java @@ -15,6 +15,8 @@ public class Templates { return "password.ftl"; case TOTP: return "totp.ftl"; + case SOCIAL: + return "social.ftl"; default: throw new IllegalArgumentException(); } diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccountSocialBean.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccountSocialBean.java new file mode 100644 index 0000000000..ed94d6980b --- /dev/null +++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccountSocialBean.java @@ -0,0 +1,95 @@ +package org.keycloak.account.freemarker.model; + +import java.net.URI; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.ws.rs.core.UriBuilder; + +import org.keycloak.models.RealmModel; +import org.keycloak.models.SocialLinkModel; +import org.keycloak.models.UserModel; +import org.keycloak.services.resources.flows.Urls; +import org.keycloak.social.SocialLoader; +import org.keycloak.social.SocialProvider; + +/** + * @author Marek Posolda + */ +public class AccountSocialBean { + + private final List socialLinks; + + public AccountSocialBean(RealmModel realm, UserModel user, URI baseUri) { + URI accountSocialUpdateUri = Urls.accountSocialUpdate(baseUri, realm.getName()); + this.socialLinks = new LinkedList(); + + Map socialConfig = realm.getSocialConfig(); + Set userSocialLinks = realm.getSocialLinks(user); + + if (socialConfig != null && !socialConfig.isEmpty()) { + for (SocialProvider provider : SocialLoader.load()) { + String socialProviderId = provider.getId(); + if (socialConfig.containsKey(socialProviderId + ".key")) { + String socialUsername = getSocialUsername(userSocialLinks, socialProviderId); + + String action = socialUsername!=null ? "remove" : "add"; + String actionUrl = UriBuilder.fromUri(accountSocialUpdateUri).queryParam("action", action).queryParam("provider_id", socialProviderId).build().toString(); + + SocialLinkEntry entry = new SocialLinkEntry(socialProviderId, provider.getName(), socialUsername, actionUrl); + this.socialLinks.add(entry); + } + } + } + } + + private String getSocialUsername(Set userSocialLinks, String socialProviderId) { + for (SocialLinkModel link : userSocialLinks) { + if (socialProviderId.equals(link.getSocialProvider())) { + return link.getSocialUsername(); + } + } + return null; + } + + public List getLinks() { + return socialLinks; + } + + public class SocialLinkEntry { + + private final String providerId; + private final String providerName; + private final String socialUsername; + private final String actionUrl; + + public SocialLinkEntry(String providerId, String providerName, String socialUsername, String actionUrl) { + this.providerId = providerId; + this.providerName = providerName; + this.socialUsername = socialUsername!=null ? socialUsername : ""; + this.actionUrl = actionUrl; + } + + public String getProviderId() { + return providerId; + } + + public String getProviderName() { + return providerName; + } + + public String getSocialUsername() { + return socialUsername; + } + + public boolean isConnected() { + return !socialUsername.isEmpty(); + } + + public String getActionUrl() { + return actionUrl; + } + } +} diff --git a/forms/common-themes/src/main/resources/theme/account/base/draft.social.ftl b/forms/common-themes/src/main/resources/theme/account/base/draft.social.ftl deleted file mode 100755 index b2b3358abd..0000000000 --- a/forms/common-themes/src/main/resources/theme/account/base/draft.social.ftl +++ /dev/null @@ -1,38 +0,0 @@ -<#-- TODO: Only a placeholder, implementation needed --> -<#import "template.ftl" as layout> -<@layout.mainLayout active='social' bodyClass='social'; section> - - <#if section = "header"> - -

Social Accounts

- - <#elseif section = "content"> - -
-
-

You have the following social accounts associated to your Keycloak account:

- - - - - - - - - - - - - - - - - - - -
Table of social accounts
Connected as john@google.comRemove Google
Add Twitter
Add Facebook
-
-
- - - \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/account/base/messages/messages.properties b/forms/common-themes/src/main/resources/theme/account/base/messages/messages.properties index 831a189056..b68a25ebfc 100644 --- a/forms/common-themes/src/main/resources/theme/account/base/messages/messages.properties +++ b/forms/common-themes/src/main/resources/theme/account/base/messages/messages.properties @@ -24,4 +24,13 @@ successTotp=Google authenticator configured. successTotpRemoved=Google authenticator removed. accountUpdated=Your account has been updated -accountPasswordUpdated=Your password has been updated \ No newline at end of file +accountPasswordUpdated=Your password has been updated + +missingSocialProvider=Social provider not specified +invalidSocialAction=Invalid or missing action +socialProviderNotFound=Specified social provider not found +socialLinkNotActive=This social link is not active anymore +socialRedirectError=Failed to redirect to social provider +socialProviderRemoved=Social provider removed successfully + +accountDisabled=Account is disabled, contact admin \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/account/base/social.ftl b/forms/common-themes/src/main/resources/theme/account/base/social.ftl new file mode 100644 index 0000000000..a1941de571 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/account/base/social.ftl @@ -0,0 +1,30 @@ +<#import "template.ftl" as layout> +<@layout.mainLayout active='social' bodyClass='social'; section> + +
+
+

Social Accounts

+
+
+ +
+ <#list social.links as socialLink> +
+
+ +
+
+ +
+
+ <#if socialLink.connected> + Remove ${socialLink.providerName} + <#else> + Add ${socialLink.providerName} + +
+
+ +
+ + \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/account/base/template.ftl b/forms/common-themes/src/main/resources/theme/account/base/template.ftl index 1ff34c4246..56941ff03b 100644 --- a/forms/common-themes/src/main/resources/theme/account/base/template.ftl +++ b/forms/common-themes/src/main/resources/theme/account/base/template.ftl @@ -28,7 +28,7 @@ @@ -42,6 +42,7 @@
  • Account
  • Password
  • Authenticator
  • + <#if isSocialRealm?has_content>
  • Social
  • diff --git a/model/api/src/main/java/org/keycloak/models/RealmModel.java b/model/api/src/main/java/org/keycloak/models/RealmModel.java index 89592e8b2e..1c02a9081c 100755 --- a/model/api/src/main/java/org/keycloak/models/RealmModel.java +++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java @@ -128,9 +128,11 @@ public interface RealmModel extends RoleContainerModel, RoleMapperModel, ScopeMa Set getSocialLinks(UserModel user); + SocialLinkModel getSocialLink(UserModel user, String socialProvider); + void addSocialLink(UserModel user, SocialLinkModel socialLink); - void removeSocialLink(UserModel user, SocialLinkModel socialLink); + boolean removeSocialLink(UserModel user, String socialProvider); boolean isSocial(); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java index f7e17ce17b..986e5657ce 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java @@ -568,6 +568,12 @@ public class RealmAdapter implements RealmModel { return set; } + @Override + public SocialLinkModel getSocialLink(UserModel user, String socialProvider) { + SocialLinkEntity entity = findSocialLink(user, socialProvider); + return (entity != null) ? new SocialLinkModel(entity.getSocialProvider(), entity.getSocialUsername()) : null; + } + @Override public void addSocialLink(UserModel user, SocialLinkModel socialLink) { SocialLinkEntity entity = new SocialLinkEntity(); @@ -580,15 +586,23 @@ public class RealmAdapter implements RealmModel { } @Override - public void removeSocialLink(UserModel user, SocialLinkModel socialLink) { - TypedQuery query = em.createNamedQuery("findSocialLinkByAll", SocialLinkEntity.class); - query.setParameter("realm", realm); + public boolean removeSocialLink(UserModel user, String socialProvider) { + SocialLinkEntity entity = findSocialLink(user, socialProvider); + if (entity != null) { + em.remove(entity); + em.flush(); + return true; + } else { + return false; + } + } + + private SocialLinkEntity findSocialLink(UserModel user, String socialProvider) { + TypedQuery query = em.createNamedQuery("findSocialLinkByUserAndProvider", SocialLinkEntity.class); query.setParameter("user", ((UserAdapter) user).getUser()); - query.setParameter("socialProvider", socialLink.getSocialProvider()); - query.setParameter("socialUsername", socialLink.getSocialUsername()); + query.setParameter("socialProvider", socialProvider); List results = query.getResultList(); - for (SocialLinkEntity entity : results) em.remove(entity); - em.flush(); + return results.size() > 0 ? results.get(0) : null; } @Override diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/SocialLinkEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/SocialLinkEntity.java index ab85191d33..8d53f14535 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/SocialLinkEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/SocialLinkEntity.java @@ -15,8 +15,8 @@ import org.hibernate.annotations.GenericGenerator; */ @NamedQueries({ @NamedQuery(name="findSocialLinkByUser", query="select link from SocialLinkEntity link where link.user = :user"), - @NamedQuery(name="findUserByLinkAndRealm", query="select link.user from SocialLinkEntity link where link.realm = :realm and link.socialProvider = :socialProvider and link.socialUsername = :socialUsername"), - @NamedQuery(name="findSocialLinkByAll", query="select link.user from SocialLinkEntity link where link.realm = :realm and link.socialProvider = :socialProvider and link.socialUsername = :socialUsername and link.user = :user") + @NamedQuery(name="findSocialLinkByUserAndProvider", query="select link from SocialLinkEntity link where link.user = :user and link.socialProvider = :socialProvider"), + @NamedQuery(name="findUserByLinkAndRealm", query="select link.user from SocialLinkEntity link where link.realm = :realm and link.socialProvider = :socialProvider and link.socialUsername = :socialUsername") }) @Entity public class SocialLinkEntity { diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java index 7672c3ed8f..d2b5a33dd4 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java @@ -877,6 +877,12 @@ public class RealmAdapter extends AbstractMongoAdapter implements R return result; } + @Override + public SocialLinkModel getSocialLink(UserModel user, String socialProvider) { + SocialLinkEntity socialLinkEntity = findSocialLink(user, socialProvider); + return socialLinkEntity!=null ? new SocialLinkModel(socialLinkEntity.getSocialProvider(), socialLinkEntity.getSocialUsername()) : null; + } + @Override public void addSocialLink(UserModel user, SocialLinkModel socialLink) { UserEntity userEntity = ((UserAdapter)user).getUser(); @@ -888,13 +894,29 @@ public class RealmAdapter extends AbstractMongoAdapter implements R } @Override - public void removeSocialLink(UserModel user, SocialLinkModel socialLink) { - SocialLinkEntity socialLinkEntity = new SocialLinkEntity(); - socialLinkEntity.setSocialProvider(socialLink.getSocialProvider()); - socialLinkEntity.setSocialUsername(socialLink.getSocialUsername()); - + public boolean removeSocialLink(UserModel user,String socialProvider) { + SocialLinkEntity socialLinkEntity = findSocialLink(user, socialProvider); + if (socialLinkEntity == null) { + return false; + } UserEntity userEntity = ((UserAdapter)user).getUser(); - getMongoStore().pullItemFromList(userEntity, "socialLinks", socialLinkEntity, invocationContext); + + return getMongoStore().pullItemFromList(userEntity, "socialLinks", socialLinkEntity, invocationContext); + } + + private SocialLinkEntity findSocialLink(UserModel user, String socialProvider) { + UserEntity userEntity = ((UserAdapter)user).getUser(); + List linkEntities = userEntity.getSocialLinks(); + if (linkEntities == null) { + return null; + } + + for (SocialLinkEntity socialLinkEntity : linkEntities) { + if (socialLinkEntity.getSocialProvider().equals(socialProvider)) { + return socialLinkEntity; + } + } + return null; } protected void updateRealm() { diff --git a/model/tests/src/test/java/org/keycloak/model/test/ImportTest.java b/model/tests/src/test/java/org/keycloak/model/test/ImportTest.java index 16a736985a..3c41a5d35e 100755 --- a/model/tests/src/test/java/org/keycloak/model/test/ImportTest.java +++ b/model/tests/src/test/java/org/keycloak/model/test/ImportTest.java @@ -127,25 +127,35 @@ public class ImportTest extends AbstractModelTest { UserModel socialUser = realm.getUser("mySocialUser"); Set socialLinks = realm.getSocialLinks(socialUser); Assert.assertEquals(3, socialLinks.size()); - int facebookCount = 0; - int googleCount = 0; + boolean facebookFound = false; + boolean googleFound = false; + boolean twitterFound = false; for (SocialLinkModel socialLinkModel : socialLinks) { if ("facebook".equals(socialLinkModel.getSocialProvider())) { - facebookCount++; + facebookFound = true; + Assert.assertEquals(socialLinkModel.getSocialUsername(), "fbuser1"); } else if ("google".equals(socialLinkModel.getSocialProvider())) { - googleCount++; + googleFound = true; Assert.assertEquals(socialLinkModel.getSocialUsername(), "mySocialUser@gmail.com"); + } else if ("twitter".equals(socialLinkModel.getSocialProvider())) { + twitterFound = true; + Assert.assertEquals(socialLinkModel.getSocialUsername(), "twuser1"); } } - Assert.assertEquals(2, facebookCount); - Assert.assertEquals(1, googleCount); + Assert.assertTrue(facebookFound && twitterFound && googleFound); UserModel foundSocialUser = realm.getUserBySocialLink(new SocialLinkModel("facebook", "fbuser1")); Assert.assertEquals(foundSocialUser.getLoginName(), socialUser.getLoginName()); Assert.assertNull(realm.getUserBySocialLink(new SocialLinkModel("facebook", "not-existing"))); + SocialLinkModel foundSocialLink = realm.getSocialLink(socialUser, "facebook"); + Assert.assertEquals("fbuser1", foundSocialLink.getSocialUsername()); + Assert.assertEquals("facebook", foundSocialLink.getSocialProvider()); - + // Test removing social link + Assert.assertTrue(realm.removeSocialLink(socialUser, "facebook")); + Assert.assertNull(realm.getSocialLink(socialUser, "facebook")); + Assert.assertFalse(realm.removeSocialLink(socialUser, "facebook")); } @Test diff --git a/model/tests/src/test/resources/testrealm.json b/model/tests/src/test/resources/testrealm.json index 44709b4910..6f573d465a 100755 --- a/model/tests/src/test/resources/testrealm.json +++ b/model/tests/src/test/resources/testrealm.json @@ -55,8 +55,8 @@ "socialUsername": "fbuser1" }, { - "socialProvider": "facebook", - "socialUsername": "fbuser2" + "socialProvider": "twitter", + "socialUsername": "twuser1" }, { "socialProvider": "google", diff --git a/services/src/main/java/org/keycloak/services/messages/Messages.java b/services/src/main/java/org/keycloak/services/messages/Messages.java index 2c659fbd57..e5a9e596c8 100644 --- a/services/src/main/java/org/keycloak/services/messages/Messages.java +++ b/services/src/main/java/org/keycloak/services/messages/Messages.java @@ -62,6 +62,18 @@ public class Messages { public static final String ACTION_WARN_EMAIL = "actionEmailWarning"; + public static final String MISSING_SOCIAL_PROVIDER = "missingSocialProvider"; + + public static final String INVALID_SOCIAL_ACTION = "invalidSocialAction"; + + public static final String SOCIAL_PROVIDER_NOT_FOUND = "socialProviderNotFound"; + + public static final String SOCIAL_LINK_NOT_ACTIVE = "socialLinkNotActive"; + + public static final String SOCIAL_REDIRECT_ERROR = "socialRedirectError"; + + public static final String SOCIAL_PROVIDER_REMOVED = "socialProviderRemoved"; + public static final String ERROR = "error"; } diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java index 5b654b79e0..43c309f705 100755 --- a/services/src/main/java/org/keycloak/services/resources/AccountService.java +++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java @@ -33,16 +33,22 @@ import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.services.managers.AppAuthManager; import org.keycloak.services.managers.Auth; import org.keycloak.services.managers.ModelToRepresentation; +import org.keycloak.services.managers.SocialRequestManager; import org.keycloak.services.managers.TokenManager; import org.keycloak.services.messages.Messages; import org.keycloak.services.resources.flows.Flows; import org.keycloak.services.resources.flows.Urls; import org.keycloak.services.validation.Validation; +import org.keycloak.social.SocialLoader; +import org.keycloak.social.SocialProvider; +import org.keycloak.social.SocialProviderConfig; +import org.keycloak.social.SocialProviderException; import javax.ws.rs.*; import javax.ws.rs.core.*; import java.net.URI; import java.util.List; +import java.util.UUID; /** * @author Stian Thorgersen @@ -51,6 +57,8 @@ public class AccountService { private static final Logger logger = Logger.getLogger(AccountService.class); + public static final String KEYCLOAK_ACCOUNT_IDENTITY_COOKIE = "KEYCLOAK_ACCOUNT_IDENTITY"; + private RealmModel realm; @Context @@ -62,14 +70,15 @@ public class AccountService { @Context private UriInfo uriInfo; - private AppAuthManager authManager; + private final AppAuthManager authManager; + private final ApplicationModel application; + private final SocialRequestManager socialRequestManager; - private ApplicationModel application; - - public AccountService(RealmModel realm, ApplicationModel application, TokenManager tokenManager) { + public AccountService(RealmModel realm, ApplicationModel application, TokenManager tokenManager, SocialRequestManager socialRequestManager) { this.realm = realm; this.application = application; - this.authManager = new AppAuthManager("KEYCLOAK_ACCOUNT_IDENTITY", tokenManager); + this.authManager = new AppAuthManager(KEYCLOAK_ACCOUNT_IDENTITY_COOKIE, tokenManager); + this.socialRequestManager = socialRequestManager; } public static UriBuilder accountServiceBaseUrl(UriInfo uriInfo) { @@ -134,6 +143,12 @@ public class AccountService { return forwardToPage("password", AccountPages.PASSWORD); } + @Path("social") + @GET + public Response socialPage() { + return forwardToPage("social", AccountPages.SOCIAL); + } + @Path("/") @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @@ -241,6 +256,61 @@ public class AccountService { return account.setSuccess("accountPasswordUpdated").createResponse(AccountPages.PASSWORD); } + @Path("social-update") + @GET + public Response processSocialUpdate(@QueryParam("action") String action, + @QueryParam("provider_id") String providerId) { + Auth auth = getAuth(true); + require(auth, AccountRoles.MANAGE_ACCOUNT); + UserModel user = auth.getUser(); + + Account account = AccountLoader.load().createAccount(uriInfo).setRealm(realm).setUser(auth.getUser()); + + if (Validation.isEmpty(providerId)) { + return account.setError(Messages.MISSING_SOCIAL_PROVIDER).createResponse(AccountPages.SOCIAL); + } + AccountSocialAction accountSocialAction = AccountSocialAction.getAction(action); + if (accountSocialAction == null) { + return account.setError(Messages.INVALID_SOCIAL_ACTION).createResponse(AccountPages.SOCIAL); + } + + SocialProvider provider = SocialLoader.load(providerId); + if (provider == null) { + return account.setError(Messages.SOCIAL_PROVIDER_NOT_FOUND).createResponse(AccountPages.SOCIAL); + } + + if (!user.isEnabled()) { + return account.setError(Messages.ACCOUNT_DISABLED).createResponse(AccountPages.SOCIAL); + } + + switch (accountSocialAction) { + case ADD: + String redirectUri = UriBuilder.fromUri(Urls.accountSocialPage(uriInfo.getBaseUri(), realm.getName())).build().toString(); + + try { + return Flows.social(socialRequestManager, realm, uriInfo, provider) + .putClientAttribute("realm", realm.getName()) + .putClientAttribute("clientId", Constants.ACCOUNT_MANAGEMENT_APP) + .putClientAttribute("state", UUID.randomUUID().toString()).putClientAttribute("redirectUri", redirectUri) + .putClientAttribute("userId", user.getId()) + .redirectToSocialProvider(); + } catch (SocialProviderException spe) { + return account.setError(Messages.SOCIAL_REDIRECT_ERROR).createResponse(AccountPages.SOCIAL); + } + case REMOVE: + if (realm.removeSocialLink(user, providerId)) { + logger.debug("Social provider " + providerId + " removed successfully from user " + user.getLoginName()); + return account.setSuccess(Messages.SOCIAL_PROVIDER_REMOVED).createResponse(AccountPages.SOCIAL); + } else { + return account.setError(Messages.SOCIAL_LINK_NOT_ACTIVE).createResponse(AccountPages.SOCIAL); + } + default: + // Shouldn't happen + logger.warn("Action is null!"); + return null; + } + } + @Path("login-redirect") @GET public Response loginRedirect(@QueryParam("code") String code, @@ -357,4 +427,19 @@ public class AccountService { } } + public enum AccountSocialAction { + ADD, + REMOVE; + + public static AccountSocialAction getAction(String action) { + if ("add".equalsIgnoreCase(action)) { + return ADD; + } else if ("remove".equalsIgnoreCase(action)) { + return REMOVE; + } else { + return null; + } + } + } + } diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java index 83bdaca8dd..6cccf25097 100755 --- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java +++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java @@ -39,10 +39,11 @@ public class KeycloakApplication extends Application { //classes.add(KeycloakSessionCleanupFilter.class); TokenManager tokenManager = new TokenManager(); + SocialRequestManager socialRequestManager = new SocialRequestManager(); - singletons.add(new RealmsResource(tokenManager)); + singletons.add(new RealmsResource(tokenManager, socialRequestManager)); singletons.add(new AdminService(tokenManager)); - singletons.add(new SocialResource(tokenManager, new SocialRequestManager())); + singletons.add(new SocialResource(tokenManager, socialRequestManager)); classes.add(SkeletonKeyContextResolver.class); classes.add(QRCodeResource.class); classes.add(ThemeResource.class); diff --git a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java index 3a2ea920f1..96d2c3f0e2 100755 --- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java @@ -6,6 +6,7 @@ import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.services.managers.RealmManager; +import org.keycloak.services.managers.SocialRequestManager; import org.keycloak.services.managers.TokenManager; import javax.ws.rs.NotFoundException; @@ -38,9 +39,11 @@ public class RealmsResource { protected KeycloakSession session; protected TokenManager tokenManager; + protected SocialRequestManager socialRequestManager; - public RealmsResource(TokenManager tokenManager) { + public RealmsResource(TokenManager tokenManager, SocialRequestManager socialRequestManager) { this.tokenManager = tokenManager; + this.socialRequestManager = socialRequestManager; } public static UriBuilder realmBaseUrl(UriInfo uriInfo) { @@ -75,7 +78,7 @@ public class RealmsResource { throw new NotFoundException(); } - AccountService accountService = new AccountService(realm, application, tokenManager); + AccountService accountService = new AccountService(realm, application, tokenManager, socialRequestManager); resourceContext.initResource(accountService); return accountService; } 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 eae0ba4614..55cb9a3ddb 100755 --- a/services/src/main/java/org/keycloak/services/resources/SocialResource.java +++ b/services/src/main/java/org/keycloak/services/resources/SocialResource.java @@ -24,12 +24,16 @@ package org.keycloak.services.resources; import org.jboss.resteasy.logging.Logger; import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpResponse; +import org.keycloak.models.AccountRoles; import org.keycloak.models.ClientModel; +import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.SocialLinkModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.services.managers.AppAuthManager; +import org.keycloak.services.managers.Auth; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.TokenManager; @@ -55,6 +59,7 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; +import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; import java.net.URISyntaxException; import java.util.HashMap; @@ -144,6 +149,33 @@ public class SocialResource { SocialLinkModel socialLink = new SocialLinkModel(provider.getId(), socialUser.getId()); UserModel user = realm.getUserBySocialLink(socialLink); + // Check if user is already authenticated (this means linking social into existing user account) + String userId = requestData.getClientAttribute("userId"); + if (userId != null) { + UserModel authenticatedUser = realm.getUserById(userId); + + if (user != null) { + return oauth.forwardToSecurityFailure("This social account is already linked to other user"); + } + + if (!authenticatedUser.isEnabled()) { + return oauth.forwardToSecurityFailure("User is disabled"); + } + if (!realm.hasRole(authenticatedUser, realm.getApplicationByName(Constants.ACCOUNT_MANAGEMENT_APP).getRole(AccountRoles.MANAGE_ACCOUNT))) { + return oauth.forwardToSecurityFailure("Insufficient permissions to link social account"); + } + + realm.addSocialLink(authenticatedUser, socialLink); + logger.debug("Social provider " + provider.getId() + " linked with user " + authenticatedUser.getLoginName()); + + String redirectUri = requestData.getClientAttributes().get("redirectUri"); + if (redirectUri == null) { + return oauth.forwardToSecurityFailure("Unknown redirectUri"); + } + + return Response.status(Status.FOUND).location(UriBuilder.fromUri(redirectUri).build()).build(); + } + if (user == null) { if (!realm.isRegistrationAllowed()) { return oauth.forwardToSecurityFailure("Registration not allowed"); @@ -187,12 +219,6 @@ public class SocialResource { return Flows.forms(realm, request, uriInfo).setError("Social provider not found").createErrorPage(); } - String key = realm.getSocialConfig().get(providerId + ".key"); - String secret = realm.getSocialConfig().get(providerId + ".secret"); - String callbackUri = Urls.socialCallback(uriInfo.getBaseUri()).toString(); - - SocialProviderConfig config = new SocialProviderConfig(key, secret, callbackUri); - ClientModel client = realm.findClient(clientId); if (client == null) { logger.warn("Unknown login requester: " + clientId); @@ -209,16 +235,11 @@ public class SocialResource { } try { - AuthRequest authRequest = provider.getAuthUrl(config); - - RequestDetails socialRequest = RequestDetails.create(providerId) - .putSocialAttributes(authRequest.getAttributes()).putClientAttribute("realm", realmName) + return Flows.social(socialRequestManager, realm, uriInfo, provider) + .putClientAttribute("realm", realmName) .putClientAttribute("clientId", clientId).putClientAttribute("scope", scope) - .putClientAttribute("state", state).putClientAttribute("redirectUri", redirectUri).build(); - - socialRequestManager.addRequest(authRequest.getId(), socialRequest); - - return Response.status(Status.FOUND).location(authRequest.getAuthUri()).build(); + .putClientAttribute("state", state).putClientAttribute("redirectUri", redirectUri) + .redirectToSocialProvider(); } catch (Throwable t) { return Flows.forms(realm, request, uriInfo).setError("Failed to redirect to social auth").createErrorPage(); } diff --git a/services/src/main/java/org/keycloak/services/resources/flows/Flows.java b/services/src/main/java/org/keycloak/services/resources/flows/Flows.java index 5df9493629..f61699ce29 100755 --- a/services/src/main/java/org/keycloak/services/resources/flows/Flows.java +++ b/services/src/main/java/org/keycloak/services/resources/flows/Flows.java @@ -26,7 +26,9 @@ import org.keycloak.login.LoginForms; import org.keycloak.login.LoginFormsLoader; import org.keycloak.models.RealmModel; import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.services.managers.SocialRequestManager; import org.keycloak.services.managers.TokenManager; +import org.keycloak.social.SocialProvider; import javax.ws.rs.core.UriInfo; @@ -47,6 +49,10 @@ public class Flows { return new OAuthFlows(realm, request, uriInfo, authManager, tokenManager); } + public static SocialRedirectFlows social(SocialRequestManager socialRequestManager, RealmModel realm, UriInfo uriInfo, SocialProvider provider) { + return new SocialRedirectFlows(socialRequestManager, realm, uriInfo, provider); + } + public static ErrorFlows errors() { return new ErrorFlows(); } diff --git a/services/src/main/java/org/keycloak/services/resources/flows/SocialRedirectFlows.java b/services/src/main/java/org/keycloak/services/resources/flows/SocialRedirectFlows.java new file mode 100644 index 0000000000..f30a92336a --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/flows/SocialRedirectFlows.java @@ -0,0 +1,51 @@ +package org.keycloak.services.resources.flows; + +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; + +import org.keycloak.models.RealmModel; +import org.keycloak.services.managers.SocialRequestManager; +import org.keycloak.social.AuthRequest; +import org.keycloak.social.RequestDetails; +import org.keycloak.social.SocialProvider; +import org.keycloak.social.SocialProviderConfig; +import org.keycloak.social.SocialProviderException; + +/** + * @author Marek Posolda + */ +public class SocialRedirectFlows { + + private final SocialRequestManager socialRequestManager; + private final RealmModel realm; + private final UriInfo uriInfo; + private final SocialProvider socialProvider; + private final RequestDetails.RequestDetailsBuilder socialRequestBuilder; + + SocialRedirectFlows(SocialRequestManager socialRequestManager, RealmModel realm, UriInfo uriInfo, SocialProvider provider) { + this.socialRequestManager = socialRequestManager; + this.realm = realm; + this.uriInfo = uriInfo; + this.socialRequestBuilder = RequestDetails.create(provider.getId()); + this.socialProvider = provider; + } + + public SocialRedirectFlows putClientAttribute(String name, String value) { + socialRequestBuilder.putClientAttribute(name, value); + return this; + } + + public Response redirectToSocialProvider() throws SocialProviderException { + String socialProviderId = socialProvider.getId(); + + String key = realm.getSocialConfig().get(socialProviderId + ".key"); + String secret = realm.getSocialConfig().get(socialProviderId + ".secret"); + String callbackUri = Urls.socialCallback(uriInfo.getBaseUri()).toString(); + SocialProviderConfig config = new SocialProviderConfig(key, secret, callbackUri); + + AuthRequest authRequest = socialProvider.getAuthUrl(config); + RequestDetails socialRequest = socialRequestBuilder.putSocialAttributes(authRequest.getAttributes()).build(); + socialRequestManager.addRequest(authRequest.getId(), socialRequest); + return Response.status(Response.Status.FOUND).location(authRequest.getAuthUri()).build(); + } +} 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 23fa68e5a9..4147adafc6 100755 --- a/services/src/main/java/org/keycloak/services/resources/flows/Urls.java +++ b/services/src/main/java/org/keycloak/services/resources/flows/Urls.java @@ -62,6 +62,10 @@ public class Urls { return accountBase(baseUri).path(AccountService.class, "socialPage").build(realmId); } + public static URI accountSocialUpdate(URI baseUri, String realmName) { + return accountBase(baseUri).path(AccountService.class, "processSocialUpdate").build(realmName); + } + public static URI accountTotpPage(URI baseUri, String realmId) { return accountBase(baseUri).path(AccountService.class, "totpPage").build(realmId); }