keycloak-uncached
Changes
forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccount.java 8(+8 -0)
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();
}