KEYCLOAK-26 Linking social providers to existing account

This commit is contained in:
mposolda 2014-02-25 19:18:22 +01:00
parent 8c3225914c
commit 3d0d130622
22 changed files with 428 additions and 90 deletions

View file

@ -5,6 +5,6 @@ package org.keycloak.account;
*/
public enum AccountPages {
ACCOUNT, PASSWORD, TOTP;
ACCOUNT, PASSWORD, TOTP, SOCIAL;
}

View file

@ -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 {

View file

@ -15,6 +15,8 @@ public class Templates {
return "password.ftl";
case TOTP:
return "totp.ftl";
case SOCIAL:
return "social.ftl";
default:
throw new IllegalArgumentException();
}

View file

@ -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;
}
}
}

View file

@ -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>

View file

@ -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

View file

@ -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>

View file

@ -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>

View file

@ -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();

View file

@ -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

View file

@ -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 {

View file

@ -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() {

View file

@ -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

View file

@ -55,8 +55,8 @@
"socialUsername": "fbuser1"
},
{
"socialProvider": "facebook",
"socialUsername": "fbuser2"
"socialProvider": "twitter",
"socialUsername": "twuser1"
},
{
"socialProvider": "google",

View file

@ -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";
}

View file

@ -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;
}
}
}
}

View file

@ -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);

View file

@ -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;
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}
}

View file

@ -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);
}