KEYCLOAK-26 Linking social providers to existing account
This commit is contained in:
parent
8c3225914c
commit
3d0d130622
22 changed files with 428 additions and 90 deletions
|
@ -5,6 +5,6 @@ package org.keycloak.account;
|
|||
*/
|
||||
public enum AccountPages {
|
||||
|
||||
ACCOUNT, PASSWORD, TOTP;
|
||||
ACCOUNT, PASSWORD, TOTP, SOCIAL;
|
||||
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -15,6 +15,8 @@ public class Templates {
|
|||
return "password.ftl";
|
||||
case TOTP:
|
||||
return "totp.ftl";
|
||||
case SOCIAL:
|
||||
return "social.ftl";
|
||||
default:
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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">
|
||||
|
||||
<h2>Social Accounts</h2>
|
||||
|
||||
<#elseif section = "content">
|
||||
|
||||
<form>
|
||||
<fieldset>
|
||||
<p class="info">You have the following social accounts associated to your Keycloak account:</p>
|
||||
<table class="list">
|
||||
<caption>Table of social accounts</caption>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="provider"><span class="social google">Google</span></td>
|
||||
<td class="soft">Connected as john@google.com</td>
|
||||
<td class="action"><a href="#" class="button">Remove Google</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="provider"><span class="social twitter">Twitter</span></td>
|
||||
<td class="soft"></td>
|
||||
<td class="action"><a href="#" class="button">Add Twitter</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="provider"><span class="social facebook">Facebook</span></td>
|
||||
<td class="soft"></td>
|
||||
<td class="action"><a href="#" class="button">Add Facebook</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
</#if>
|
||||
</@layout.mainLayout>
|
|
@ -24,4 +24,13 @@ successTotp=Google authenticator configured.
|
|||
successTotpRemoved=Google authenticator removed.
|
||||
|
||||
accountUpdated=Your account has been updated
|
||||
accountPasswordUpdated=Your password has been updated
|
||||
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
|
|
@ -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>
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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<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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -877,6 +877,12 @@ public class RealmAdapter extends AbstractMongoAdapter<RealmEntity> 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<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();
|
||||
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<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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -55,8 +55,8 @@
|
|||
"socialUsername": "fbuser1"
|
||||
},
|
||||
{
|
||||
"socialProvider": "facebook",
|
||||
"socialUsername": "fbuser2"
|
||||
"socialProvider": "twitter",
|
||||
"socialUsername": "twuser1"
|
||||
},
|
||||
{
|
||||
"socialProvider": "google",
|
||||
|
|
|
@ -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";
|
||||
|
||||
}
|
||||
|
|
|
@ -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 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue