keycloak-uncached

Merge pull request #276 from mposolda/account-social KEYCLOAK-26

3/10/2014 8:43:01 AM

Changes

forms/common-themes/src/main/resources/theme/account/base/draft.social.ftl 38(+0 -38)

Details

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 2fc9a29..a3e60d2 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 64f4135..1a5cc67 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/model/AccountSocialBean.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccountSocialBean.java
new file mode 100644
index 0000000..ed94d69
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class AccountSocialBean {
+
+    private final List<SocialLinkEntry> socialLinks;
+
+    public AccountSocialBean(RealmModel realm, UserModel user, URI baseUri) {
+        URI accountSocialUpdateUri = Urls.accountSocialUpdate(baseUri, realm.getName());
+        this.socialLinks = new LinkedList<SocialLinkEntry>();
+
+        Map<String, String> socialConfig = realm.getSocialConfig();
+        Set<SocialLinkModel> 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<SocialLinkModel> userSocialLinks, String socialProviderId) {
+        for (SocialLinkModel link : userSocialLinks) {
+            if (socialProviderId.equals(link.getSocialProvider())) {
+                return link.getSocialUsername();
+            }
+        }
+        return null;
+    }
+
+    public List<SocialLinkEntry> 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/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 4e701a3..5a63ef5 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/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 831a189..b68a25e 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 0000000..a1941de
--- /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>
+
+    <div class="row">
+        <div class="col-md-10">
+            <h2>Social Accounts</h2>
+        </div>
+    </div>
+
+    <form action="${url.passwordUrl}" class="form-horizontal" method="post">
+        <#list social.links as socialLink>
+            <div class="form-group">
+                <div class="col-sm-2 col-md-2">
+                    <label for="${socialLink.providerId}" class="control-label">${socialLink.providerName}</label>
+                </div>
+                <div class="col-sm-5 col-md-5">
+                    <input disabled="true" class="form-control" value="${socialLink.socialUsername}">
+                </div>
+                <div class="col-sm-5 col-md-5">
+                    <#if socialLink.connected>
+                        <a href="${socialLink.actionUrl}" type="submit" class="btn btn-primary btn-lg">Remove ${socialLink.providerName}</a>
+                    <#else>
+                        <a href="${socialLink.actionUrl}" type="submit" class="btn btn-primary btn-lg">Add ${socialLink.providerName}</a>
+                    </#if>
+                </div>
+            </div>
+        </#list>
+    </form>
+
+</@layout.mainLayout>
\ 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 1ff34c4..56941ff 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 @@
 
                 <div class="navbar-collapse">
                     <ul class="nav navbar-nav navbar-utility">
-                        <#if referrer?has_content><li><a href="${referrer.baseUrl}">Back to ${referrer.name}</a></li></#if>
+                        <#if referrer?has_content && referrer.baseUrl?has_content><li><a href="${referrer.baseUrl}">Back to ${referrer.name}</a></li></#if>
                         <li><a href="${url.logoutUrl}">Sign Out</a></li>
                     </ul>
                 </div>
@@ -42,6 +42,7 @@
                 <li class="<#if active=='account'>active</#if>"><a href="${url.accountUrl}">Account</a></li>
                 <li class="<#if active=='password'>active</#if>"><a href="${url.passwordUrl}">Password</a></li>
                 <li class="<#if active=='totp'>active</#if>"><a href="${url.totpUrl}">Authenticator</a></li>
+                <#if isSocialRealm?has_content><li class="<#if active=='social'>active</#if>"><a href="${url.socialUrl}">Social</a></li></#if>
             </ul>
         </div>
 
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 89592e8..1c02a90 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<SocialLinkModel> 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/entities/SocialLinkEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/SocialLinkEntity.java
index ab85191..8d53f14 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/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index f7e17ce..986e565 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
@@ -569,6 +569,12 @@ public class RealmAdapter implements RealmModel {
     }
 
     @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();
         entity.setRealm(realm);
@@ -580,15 +586,23 @@ public class RealmAdapter implements RealmModel {
     }
 
     @Override
-    public void removeSocialLink(UserModel user, SocialLinkModel socialLink) {
-        TypedQuery<SocialLinkEntity> 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<SocialLinkEntity> 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<SocialLinkEntity> 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/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 7672c3e..d2b5a33 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
@@ -878,6 +878,12 @@ public class RealmAdapter extends AbstractMongoAdapter<RealmEntity> implements R
     }
 
     @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();
         SocialLinkEntity socialLinkEntity = new SocialLinkEntity();
@@ -888,13 +894,29 @@ public class RealmAdapter extends AbstractMongoAdapter<RealmEntity> 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();
+
+        return getMongoStore().pullItemFromList(userEntity, "socialLinks", socialLinkEntity, invocationContext);
+    }
 
+    private SocialLinkEntity findSocialLink(UserModel user, String socialProvider) {
         UserEntity userEntity = ((UserAdapter)user).getUser();
-        getMongoStore().pullItemFromList(userEntity, "socialLinks", socialLinkEntity, invocationContext);
+        List<SocialLinkEntity> 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 16a7369..3c41a5d 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<SocialLinkModel> 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 44709b4..6f573d4 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 2c659fb..e5a9e59 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 5b654b7..43c309f 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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -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 ApplicationModel application;
+    private final AppAuthManager authManager;
+    private final ApplicationModel application;
+    private final SocialRequestManager socialRequestManager;
 
-    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/flows/Flows.java b/services/src/main/java/org/keycloak/services/resources/flows/Flows.java
index 5df9493..f61699c 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 0000000..f30a923
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+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 23fa68e..4147ada 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);
     }
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 83bdaca..6cccf25 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 3a2ea92..96d2c3f 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 eae0ba4..55cb9a3 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();
         }