From 5f6687ec6272b1f54acc2e667c5d37d64c52aad0 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Wed, 4 Jun 2014 12:18:33 +0100 Subject: [PATCH] KEYCLOAK-244 Social should be stateless --- .../managers/SocialRequestManager.java | 82 ----------- .../services/resources/AccountService.java | 15 +- .../resources/KeycloakApplication.java | 6 +- .../services/resources/RealmsResource.java | 8 +- .../services/resources/SocialResource.java | 136 +++++++++++------- .../services/resources/flows/Flows.java | 5 +- .../resources/flows/SocialRedirectFlows.java | 53 +++++-- .../social/AbstractOAuth2Provider.java | 20 ++- .../org/keycloak/social/AuthCallback.java | 15 +- .../java/org/keycloak/social/AuthRequest.java | 20 +-- .../org/keycloak/social/RequestDetails.java | 120 ---------------- .../org/keycloak/social/SocialProvider.java | 2 +- .../java/org/keycloak/social/UriBuilder.java | 72 ++++++++++ .../social/facebook/FacebookProvider.java | 7 - .../social/twitter/TwitterProvider.java | 12 +- .../social/twitter/TwitterProvider.java.orig | 102 +++++++++++++ .../org/keycloak/testsuite/DummySocial.java | 17 +-- 17 files changed, 348 insertions(+), 344 deletions(-) delete mode 100644 services/src/main/java/org/keycloak/services/managers/SocialRequestManager.java delete mode 100644 social/core/src/main/java/org/keycloak/social/RequestDetails.java create mode 100644 social/core/src/main/java/org/keycloak/social/UriBuilder.java create mode 100755 social/twitter/src/main/java/org/keycloak/social/twitter/TwitterProvider.java.orig diff --git a/services/src/main/java/org/keycloak/services/managers/SocialRequestManager.java b/services/src/main/java/org/keycloak/services/managers/SocialRequestManager.java deleted file mode 100644 index 472ae7b4d6..0000000000 --- a/services/src/main/java/org/keycloak/services/managers/SocialRequestManager.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * JBoss, Home of Professional Open Source. - * Copyright 2012, Red Hat, Inc., and individual contributors - * as indicated by the @author tags. See the copyright.txt file in the - * distribution for a full listing of individual contributors. - * - * This is free software; you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation; either version 2.1 of - * the License, or (at your option) any later version. - * - * This software is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this software; if not, write to the Free - * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA - * 02110-1301 USA, or see the FSF site: http://www.fsf.org. - */ -package org.keycloak.services.managers; - -import org.keycloak.social.RequestDetails; - -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Map.Entry; - -/** - * @author Stian Thorgersen - */ -public class SocialRequestManager { - - public static final long TIMEOUT = 10 * 60 * 1000; - - private Map map = new HashMap(); - - private LinkedHashMap expires = new LinkedHashMap(); - - public synchronized void addRequest(String requestId, RequestDetails request) { - pruneExpired(); - - map.put(requestId, request); - expires.put(requestId, System.currentTimeMillis() + TIMEOUT); - } - - public synchronized boolean isRequestId(String requestId) { - return map.containsKey(requestId); - } - - public synchronized RequestDetails retrieveData(String requestId) { - expires.remove(requestId); - RequestDetails details = map.remove(requestId); - - pruneExpired(); - - return details; - } - - // Just obtain data without expiring it - public synchronized RequestDetails getData(String requestId) { - return map.get(requestId); - } - - private void pruneExpired() { - long currentTime = System.currentTimeMillis(); - Iterator> itr = expires.entrySet().iterator(); - while (itr.hasNext()) { - Entry e = itr.next(); - if (e.getValue() < currentTime) { - itr.remove(); - map.remove(e.getKey()); - } else { - return; - } - } - } - -} 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 e26a111c80..a9ee7748f3 100755 --- a/services/src/main/java/org/keycloak/services/resources/AccountService.java +++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java @@ -53,7 +53,6 @@ import org.keycloak.services.managers.AppAuthManager; import org.keycloak.services.managers.Auth; import org.keycloak.services.managers.AuthenticationManager; 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; @@ -126,17 +125,15 @@ public class AccountService { private final AppAuthManager authManager; private final ApplicationModel application; private Audit audit; - private final SocialRequestManager socialRequestManager; private AccountProvider account; private Auth auth; private AuditProvider auditProvider; - public AccountService(RealmModel realm, ApplicationModel application, TokenManager tokenManager, SocialRequestManager socialRequestManager, Audit audit) { + public AccountService(RealmModel realm, ApplicationModel application, Audit audit) { this.realm = realm; this.application = application; this.audit = audit; this.authManager = new AppAuthManager(providers); - this.socialRequestManager = socialRequestManager; } public void init() { @@ -502,11 +499,11 @@ public class AccountService { 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(OAuth2Constants.STATE, UUID.randomUUID().toString()).putClientAttribute("redirectUri", redirectUri) - .putClientAttribute("userId", user.getId()) + return Flows.social(realm, uriInfo, provider) + .user(user) + .putClientAttribute(OAuth2Constants.CLIENT_ID, Constants.ACCOUNT_MANAGEMENT_APP) + .putClientAttribute(OAuth2Constants.STATE, UUID.randomUUID().toString()) + .putClientAttribute(OAuth2Constants.REDIRECT_URI, redirectUri) .redirectToSocialProvider(); } catch (SocialProviderException spe) { return account.setError(Messages.SOCIAL_REDIRECT_ERROR).createResponse(AccountPages.SOCIAL); 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 2571824611..644dfbfe90 100755 --- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java +++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java @@ -17,7 +17,6 @@ import org.keycloak.services.DefaultProviderSessionFactory; import org.keycloak.services.managers.ApplianceBootstrap; import org.keycloak.services.managers.BruteForceProtector; import org.keycloak.services.managers.RealmManager; -import org.keycloak.services.managers.SocialRequestManager; import org.keycloak.services.managers.TokenManager; import org.keycloak.services.resources.admin.AdminRoot; import org.keycloak.services.scheduled.ClearExpiredAuditEvents; @@ -73,10 +72,9 @@ public class KeycloakApplication extends Application { context.setAttribute(ProviderSessionFactory.class.getName(), this.providerSessionFactory); TokenManager tokenManager = new TokenManager(); - SocialRequestManager socialRequestManager = new SocialRequestManager(); - singletons.add(new RealmsResource(tokenManager, socialRequestManager)); - singletons.add(new SocialResource(tokenManager, socialRequestManager)); + singletons.add(new RealmsResource(tokenManager)); + singletons.add(new SocialResource(tokenManager)); singletons.add(new AdminRoot(tokenManager)); classes.add(SkeletonKeyContextResolver.class); classes.add(QRCodeResource.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 1db8230e9d..ee875573f2 100755 --- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java @@ -18,7 +18,6 @@ import org.keycloak.services.managers.AuditManager; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.BruteForceProtector; import org.keycloak.services.managers.RealmManager; -import org.keycloak.services.managers.SocialRequestManager; import org.keycloak.services.managers.TokenManager; import org.keycloak.util.StreamUtil; @@ -34,7 +33,6 @@ import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; import java.io.IOException; import java.io.InputStream; -import java.util.Set; /** * @author Bill Burke @@ -68,11 +66,9 @@ public class RealmsResource { protected BruteForceProtector protector; protected TokenManager tokenManager; - protected SocialRequestManager socialRequestManager; - public RealmsResource(TokenManager tokenManager, SocialRequestManager socialRequestManager) { + public RealmsResource(TokenManager tokenManager) { this.tokenManager = tokenManager; - this.socialRequestManager = socialRequestManager; } public static UriBuilder realmBaseUrl(UriInfo uriInfo) { @@ -183,7 +179,7 @@ public class RealmsResource { } Audit audit = new AuditManager(realm, providers, clientConnection).createAudit(); - AccountService accountService = new AccountService(realm, application, tokenManager, socialRequestManager, audit); + AccountService accountService = new AccountService(realm, application, audit); ResteasyProviderFactory.getInstance().injectProperties(accountService); //resourceContext.initResource(accountService); accountService.init(); 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 8129ff19f1..2c7a4f4a18 100755 --- a/services/src/main/java/org/keycloak/services/resources/SocialResource.java +++ b/services/src/main/java/org/keycloak/services/resources/SocialResource.java @@ -29,6 +29,8 @@ import org.keycloak.audit.Audit; import org.keycloak.audit.Details; import org.keycloak.audit.Errors; import org.keycloak.audit.EventType; +import org.keycloak.jose.jws.JWSBuilder; +import org.keycloak.jose.jws.JWSInput; import org.keycloak.models.AccountRoles; import org.keycloak.models.ClientModel; import org.keycloak.models.Constants; @@ -38,18 +40,17 @@ import org.keycloak.models.SocialLinkModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.KeycloakModelUtils; -import org.keycloak.services.ClientConnection; import org.keycloak.provider.ProviderSession; +import org.keycloak.services.ClientConnection; import org.keycloak.services.managers.AuditManager; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.RealmManager; -import org.keycloak.services.managers.SocialRequestManager; import org.keycloak.services.managers.TokenManager; import org.keycloak.services.resources.flows.Flows; import org.keycloak.services.resources.flows.OAuthFlows; import org.keycloak.services.resources.flows.Urls; +import org.keycloak.services.util.CookieHelper; import org.keycloak.social.AuthCallback; -import org.keycloak.social.RequestDetails; import org.keycloak.social.SocialAccessDeniedException; import org.keycloak.social.SocialLoader; import org.keycloak.social.SocialProvider; @@ -62,11 +63,13 @@ import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; +import javax.ws.rs.core.Cookie; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; +import java.io.IOException; import java.net.URISyntaxException; import java.util.HashMap; import java.util.List; @@ -90,11 +93,6 @@ public class SocialResource { @Context private HttpRequest request; - /* - @Context - ResourceContext resourceContext; - */ - @Context protected ProviderSession providerSession; @@ -107,35 +105,33 @@ public class SocialResource { @Context protected ClientConnection clientConnection; - private SocialRequestManager socialRequestManager; - private TokenManager tokenManager; - public SocialResource(TokenManager tokenManager, SocialRequestManager socialRequestManager) { + public SocialResource(TokenManager tokenManager) { this.tokenManager = tokenManager; - this.socialRequestManager = socialRequestManager; } @GET @Path("callback") - public Response callback() throws URISyntaxException { - Map queryParams = getQueryParams(); - - RequestDetails requestData = getRequestDetails(queryParams); - if (requestData == null) { - Flows.forms(providerSession, null, uriInfo).setError("Unexpected callback").createErrorPage(); + public Response callback(@QueryParam("state") String encodedState) throws URISyntaxException, IOException { + State initialRequest; + try { + initialRequest = new JWSInput(encodedState).readJsonContent(State.class); + } catch (Throwable t) { + logger.warn("Invalid social callback", t); + return Flows.forms(providerSession, null, uriInfo).setError("Unexpected callback").createErrorPage(); } - SocialProvider provider = SocialLoader.load(requestData.getProviderId()); + SocialProvider provider = SocialLoader.load(initialRequest.getProvider()); - String realmName = requestData.getClientAttribute("realm"); + String realmName = initialRequest.getRealm(); RealmManager realmManager = new RealmManager(session); RealmModel realm = realmManager.getRealmByName(realmName); Audit audit = new AuditManager(realm, providers, clientConnection).createAudit() .event(EventType.LOGIN) - .detail(Details.RESPONSE_TYPE, "code") + .detail(Details.RESPONSE_TYPE, initialRequest.get(OAuth2Constants.RESPONSE_TYPE)) .detail(Details.AUTH_METHOD, "social@" + provider.getId()); AuthenticationManager authManager = new AuthenticationManager(providers); @@ -146,11 +142,11 @@ public class SocialResource { return oauth.forwardToSecurityFailure("Realm not enabled."); } - String clientId = requestData.getClientAttributes().get("clientId"); - String redirectUri = requestData.getClientAttribute("redirectUri"); - String scope = requestData.getClientAttributes().get(OAuth2Constants.SCOPE); - String state = requestData.getClientAttributes().get(OAuth2Constants.STATE); - String responseType = requestData.getClientAttribute("responseType"); + String clientId = initialRequest.get(OAuth2Constants.CLIENT_ID); + String redirectUri = initialRequest.get(OAuth2Constants.REDIRECT_URI); + String scope = initialRequest.get(OAuth2Constants.SCOPE); + String state = initialRequest.get(OAuth2Constants.STATE); + String responseType = initialRequest.get(OAuth2Constants.RESPONSE_TYPE); audit.client(clientId).detail(Details.REDIRECT_URI, redirectUri); @@ -164,12 +160,15 @@ public class SocialResource { return oauth.forwardToSecurityFailure("Login requester not enabled."); } - String key = realm.getSocialConfig().get(requestData.getProviderId() + ".key"); - String secret = realm.getSocialConfig().get(requestData.getProviderId() + ".secret"); + String key = realm.getSocialConfig().get(provider.getId() + ".key"); + String secret = realm.getSocialConfig().get(provider.getId() + ".secret"); String callbackUri = Urls.socialCallback(uriInfo.getBaseUri()).toString(); SocialProviderConfig config = new SocialProviderConfig(key, secret, callbackUri); - AuthCallback callback = new AuthCallback(requestData.getSocialAttributes(), queryParams); + Map queryParams = getQueryParams(); + Map attributes = getAttributes(); + + AuthCallback callback = new AuthCallback(queryParams, attributes); SocialUser socialUser; try { @@ -195,7 +194,7 @@ public class SocialResource { UserModel user = realm.getUserBySocialLink(socialLink); // Check if user is already authenticated (this means linking social into existing user account) - String userId = requestData.getClientAttribute("userId"); + String userId = initialRequest.getUser(); if (userId != null) { UserModel authenticatedUser = realm.getUserById(userId); @@ -307,34 +306,19 @@ public class SocialResource { } try { - return Flows.social(socialRequestManager, realm, uriInfo, provider) - .putClientAttribute("realm", realmName) - .putClientAttribute("clientId", clientId).putClientAttribute(OAuth2Constants.SCOPE, scope) - .putClientAttribute(OAuth2Constants.STATE, state).putClientAttribute("redirectUri", redirectUri) - .putClientAttribute("responseType", responseType).redirectToSocialProvider(); + return Flows.social(realm, uriInfo, provider) + .putClientAttribute(OAuth2Constants.CLIENT_ID, clientId) + .putClientAttribute(OAuth2Constants.SCOPE, scope) + .putClientAttribute(OAuth2Constants.STATE, state) + .putClientAttribute(OAuth2Constants.REDIRECT_URI, redirectUri) + .putClientAttribute(OAuth2Constants.RESPONSE_TYPE, responseType) + .redirectToSocialProvider(); } catch (Throwable t) { logger.error("Failed to redirect to social auth", t); return Flows.forms(providerSession, realm, uriInfo).setError("Failed to redirect to social auth").createErrorPage(); } } - private RequestDetails getRequestDetails(Map queryParams) { - String requestId = null; - if (queryParams.containsKey(OAuth2Constants.STATE)) { - requestId = queryParams.get(OAuth2Constants.STATE)[0]; - } else if (queryParams.containsKey("oauth_token")) { - requestId = queryParams.get("oauth_token")[0]; - } else if (queryParams.containsKey("denied")) { - requestId = queryParams.get("denied")[0]; - } - - if (requestId != null && socialRequestManager.isRequestId(requestId)) { - return socialRequestManager.retrieveData(requestId); - } - - return null; - } - private Map getQueryParams() { Map queryParams = new HashMap(); for (Entry> e : uriInfo.getQueryParameters().entrySet()) { @@ -343,4 +327,52 @@ public class SocialResource { return queryParams; } + private Map getAttributes() throws IOException { + Cookie cookie = headers.getCookies().get("KEYCLOAK_SOCIAL"); + return cookie != null ? new JWSInput(cookie.getValue()).readJsonContent(HashMap.class) : null; + } + + public static class State { + private String realm; + private String provider; + private String user; + private Map attributes = new HashMap(); + + public String getRealm() { + return realm; + } + + public void setRealm(String realm) { + this.realm = realm; + } + + public String getProvider() { + return provider; + } + + public void setProvider(String provider) { + this.provider = provider; + } + + public String getUser() { + return user; + } + + public void setUser(String user) { + this.user = user; + } + + public Map getAttributes() { + return attributes; + } + + public String get(String key) { + return attributes.get(key); + } + + public void set(String key, String value) { + attributes.put(key, value); + } + } + } 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 388ff82167..c83d6f94d8 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,6 @@ import org.keycloak.login.LoginFormsProvider; import org.keycloak.models.RealmModel; import org.keycloak.provider.ProviderSession; import org.keycloak.services.managers.AuthenticationManager; -import org.keycloak.services.managers.SocialRequestManager; import org.keycloak.services.managers.TokenManager; import org.keycloak.social.SocialProvider; @@ -49,8 +48,8 @@ public class Flows { return new OAuthFlows(session, 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 SocialRedirectFlows social(RealmModel realm, UriInfo uriInfo, SocialProvider provider) { + return new SocialRedirectFlows(realm, uriInfo, provider); } public static ErrorFlows errors() { 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 index c9c5f44f10..37c0922d50 100755 --- a/services/src/main/java/org/keycloak/services/resources/flows/SocialRedirectFlows.java +++ b/services/src/main/java/org/keycloak/services/resources/flows/SocialRedirectFlows.java @@ -1,37 +1,48 @@ package org.keycloak.services.resources.flows; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriInfo; - +import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.models.RealmModel; -import org.keycloak.services.managers.SocialRequestManager; +import org.keycloak.models.UserModel; +import org.keycloak.services.resources.RealmsResource; +import org.keycloak.services.resources.SocialResource; +import org.keycloak.services.util.CookieHelper; import org.keycloak.social.AuthRequest; -import org.keycloak.social.RequestDetails; +import org.keycloak.social.UriBuilder; import org.keycloak.social.SocialProvider; import org.keycloak.social.SocialProviderConfig; import org.keycloak.social.SocialProviderException; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; +import java.net.URI; + /** * @author Marek Posolda */ public class SocialRedirectFlows { - private final SocialRequestManager socialRequestManager; private final RealmModel realm; private final UriInfo uriInfo; private final SocialProvider socialProvider; - private final RequestDetails.RequestDetailsBuilder socialRequestBuilder; + private final SocialResource.State state; - SocialRedirectFlows(SocialRequestManager socialRequestManager, RealmModel realm, UriInfo uriInfo, SocialProvider provider) { - this.socialRequestManager = socialRequestManager; + SocialRedirectFlows(RealmModel realm, UriInfo uriInfo, SocialProvider provider) { this.realm = realm; this.uriInfo = uriInfo; - this.socialRequestBuilder = RequestDetails.create(provider.getId()); this.socialProvider = provider; + + state = new SocialResource.State(); + state.setRealm(realm.getName()); + state.setProvider(provider.getId()); } - public SocialRedirectFlows putClientAttribute(String name, String value) { - socialRequestBuilder.putClientAttribute(name, value); + public SocialRedirectFlows putClientAttribute(String key, String value) { + state.set(key, value); + return this; + } + + public SocialRedirectFlows user(UserModel user) { + state.setUser(user.getId()); return this; } @@ -43,9 +54,21 @@ public class SocialRedirectFlows { 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); + String encodedState = new JWSBuilder().jsonContent(state).rsa256(realm.getPrivateKey()); + + AuthRequest authRequest = socialProvider.getAuthUrl(config, encodedState); + + if (authRequest.getAttributes() != null) { + String cookiePath = Urls.socialBase(uriInfo.getBaseUri()).build().getRawPath().toString(); + + String encoded = new JWSBuilder() + .jsonContent(authRequest.getAttributes()) + .rsa256(realm.getPrivateKey()); + + CookieHelper.addCookie("KEYCLOAK_SOCIAL", encoded, cookiePath, null, null, -1, !realm.isSslNotRequired(), true); + } + return Response.status(302).location(authRequest.getAuthUri()).build(); } + } diff --git a/social/core/src/main/java/org/keycloak/social/AbstractOAuth2Provider.java b/social/core/src/main/java/org/keycloak/social/AbstractOAuth2Provider.java index f3fdea728b..5e81ecdfe4 100644 --- a/social/core/src/main/java/org/keycloak/social/AbstractOAuth2Provider.java +++ b/social/core/src/main/java/org/keycloak/social/AbstractOAuth2Provider.java @@ -5,7 +5,7 @@ import org.keycloak.OAuth2Constants; import org.keycloak.social.utils.SimpleHttp; import java.io.IOException; -import java.util.UUID; +import java.net.URI; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -44,12 +44,14 @@ public abstract class AbstractOAuth2Provider implements SocialProvider { protected abstract SocialUser getProfile(String accessToken) throws SocialProviderException; @Override - public AuthRequest getAuthUrl(SocialProviderConfig config) throws SocialProviderException { - String state = UUID.randomUUID().toString(); - - return AuthRequest.create(state, getAuthUrl()).setQueryParam(CLIENT_ID, config.getKey()) - .setQueryParam(RESPONSE_TYPE, CODE).setQueryParam(SCOPE, getScope()) - .setQueryParam(REDIRECT_URI, config.getCallbackUrl()).setQueryParam(STATE, state).setAttribute(STATE, state).build(); + public AuthRequest getAuthUrl(SocialProviderConfig config, String state) throws SocialProviderException { + return AuthRequest.create(getAuthUrl()) + .setQueryParam(CLIENT_ID, config.getKey()) + .setQueryParam(RESPONSE_TYPE, CODE) + .setQueryParam(SCOPE, getScope()) + .setQueryParam(REDIRECT_URI, config.getCallbackUrl()) + .setQueryParam(STATE, state) + .build(); } @Override @@ -66,10 +68,6 @@ public abstract class AbstractOAuth2Provider implements SocialProvider { try { String code = callback.getQueryParam(CODE); - if (!callback.getQueryParam(STATE).equals(callback.getAttribute(STATE))) { - throw new SocialProviderException("Invalid state"); - } - String response = SimpleHttp.doPost(getTokenUrl()).param(CODE, code).param(CLIENT_ID, config.getKey()) .param(CLIENT_SECRET, config.getSecret()) .param(REDIRECT_URI, config.getCallbackUrl()) diff --git a/social/core/src/main/java/org/keycloak/social/AuthCallback.java b/social/core/src/main/java/org/keycloak/social/AuthCallback.java index 3ad0774f1a..ebb166060b 100644 --- a/social/core/src/main/java/org/keycloak/social/AuthCallback.java +++ b/social/core/src/main/java/org/keycloak/social/AuthCallback.java @@ -27,18 +27,13 @@ import java.util.Map; * @author Stian Thorgersen */ public class AuthCallback { - - private Map attributes; private Map queryParams; + private Map attributes; - public AuthCallback(Map attributes, Map queryParams) { - this.attributes = attributes; + public AuthCallback(Map queryParams, Map attributes) { this.queryParams = queryParams; - } - - public Object getAttribute(String name) { - return attributes.get(name); + this.attributes = attributes; } public String getQueryParam(String name) { @@ -49,4 +44,8 @@ public class AuthCallback { return null; } + public String getAttribute(String name) { + return attributes != null ? attributes.get(name) : null; + } + } diff --git a/social/core/src/main/java/org/keycloak/social/AuthRequest.java b/social/core/src/main/java/org/keycloak/social/AuthRequest.java index 9b213b0f05..ee89fbe473 100644 --- a/social/core/src/main/java/org/keycloak/social/AuthRequest.java +++ b/social/core/src/main/java/org/keycloak/social/AuthRequest.java @@ -33,33 +33,24 @@ import java.util.Map; */ public class AuthRequest { - private String id; - private URI authUri; private Map attributes; - public static AuthRequestBuilder create(String id, String path) { + public static AuthRequestBuilder create(String url) { AuthRequestBuilder req = new AuthRequestBuilder(); - req.id = id; req.b = new StringBuilder(); - req.b.append(path); + req.b.append(url); - req.attributes = new HashMap(); return req; } - private AuthRequest(String id, URI authUri, Map attributes) { - this.id = id; + private AuthRequest(URI authUri, Map attributes) { this.authUri = authUri; this.attributes = attributes; } - public String getId() { - return id; - } - public URI getAuthUri() { return authUri; } @@ -100,13 +91,16 @@ public class AuthRequest { } public AuthRequestBuilder setAttribute(String name, String value) { + if (attributes == null) { + attributes = new HashMap(); + } attributes.put(name, value); return this; } public AuthRequest build() { try { - return new AuthRequest(id, new URI(b.toString()), attributes); + return new AuthRequest(new URI(b.toString()), attributes); } catch (URISyntaxException e) { throw new IllegalArgumentException(e); } diff --git a/social/core/src/main/java/org/keycloak/social/RequestDetails.java b/social/core/src/main/java/org/keycloak/social/RequestDetails.java deleted file mode 100644 index 0d878126fb..0000000000 --- a/social/core/src/main/java/org/keycloak/social/RequestDetails.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * JBoss, Home of Professional Open Source. - * Copyright 2012, Red Hat, Inc., and individual contributors - * as indicated by the @author tags. See the copyright.txt file in the - * distribution for a full listing of individual contributors. - * - * This is free software; you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation; either version 2.1 of - * the License, or (at your option) any later version. - * - * This software is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this software; if not, write to the Free - * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA - * 02110-1301 USA, or see the FSF site: http://www.fsf.org. - */ -package org.keycloak.social; - -import java.util.HashMap; -import java.util.Map; - -/** - * @author Stian Thorgersen - */ -public class RequestDetails { - - private String providerId; - - private Map clientAttributes; - - private Map socialAttributes; - - public static RequestDetailsBuilder create(String providerId) { - RequestDetailsBuilder req = new RequestDetailsBuilder(); - req.providerId = providerId; - req.clientAttributes = new HashMap(); - req.socialAttributes = new HashMap(); - return req; - } - - public static RequestDetailsBuilder create(RequestDetails from) { - RequestDetailsBuilder req = new RequestDetailsBuilder(); - req.providerId = from.getProviderId(); - req.clientAttributes = new HashMap(); - req.clientAttributes.putAll(from.getClientAttributes()); - req.socialAttributes = new HashMap(); - req.socialAttributes.putAll(from.getSocialAttributes()); - return req; - } - - private RequestDetails(String providerId, Map clientAttributes, Map socialAttributes) { - this.providerId = providerId; - this.clientAttributes = clientAttributes; - this.socialAttributes = socialAttributes; - } - - public String getProviderId() { - return providerId; - } - - public String getClientAttribute(String name) { - return clientAttributes.get(name); - } - - public Map getClientAttributes() { - return clientAttributes; - } - - public String getSocialAttribute(String name) { - return socialAttributes.get(name); - } - - public Map getSocialAttributes() { - return socialAttributes; - } - - - public static class RequestDetailsBuilder { - - private String providerId; - - private Map clientAttributes; - - private Map socialAttributes; - - private RequestDetailsBuilder() { - } - - public RequestDetailsBuilder putClientAttribute(String name, String value) { - clientAttributes.put(name, value); - return this; - } - - public RequestDetailsBuilder putClientAttributes(Map attributes) { - clientAttributes.putAll(attributes); - return this; - } - - public RequestDetailsBuilder putSocialAttribute(String name, String value) { - socialAttributes.put(name, value); - return this; - } - - public RequestDetailsBuilder putSocialAttributes(Map attributes) { - socialAttributes.putAll(attributes); - return this; - } - - public RequestDetails build() { - return new RequestDetails(providerId, clientAttributes, socialAttributes); - } - - } - -} diff --git a/social/core/src/main/java/org/keycloak/social/SocialProvider.java b/social/core/src/main/java/org/keycloak/social/SocialProvider.java index 73a8f21d93..b0980776c2 100644 --- a/social/core/src/main/java/org/keycloak/social/SocialProvider.java +++ b/social/core/src/main/java/org/keycloak/social/SocialProvider.java @@ -29,7 +29,7 @@ public interface SocialProvider { String getId(); - AuthRequest getAuthUrl(SocialProviderConfig config) throws SocialProviderException; + AuthRequest getAuthUrl(SocialProviderConfig config, String state) throws SocialProviderException; String getName(); diff --git a/social/core/src/main/java/org/keycloak/social/UriBuilder.java b/social/core/src/main/java/org/keycloak/social/UriBuilder.java new file mode 100644 index 0000000000..fb107fe278 --- /dev/null +++ b/social/core/src/main/java/org/keycloak/social/UriBuilder.java @@ -0,0 +1,72 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2012, Red Hat, Inc., and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.keycloak.social; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLEncoder; + +/** + * @author Stian Thorgersen + */ +public class UriBuilder { + + private StringBuilder sb; + + private char sep; + + public static UriBuilder create(String path) { + StringBuilder sb = new StringBuilder(); + sb.append(path); + + UriBuilder builder = new UriBuilder(); + builder.sb = sb; + return builder; + } + + public UriBuilder setQueryParam(String name, String value) { + try { + if (sep == '?') { + sb.append(sep); + sep = '&'; + } else { + sb.append(sep); + } + sb.append(URLEncoder.encode(name, "UTF-8")); + sb.append("="); + sb.append(URLEncoder.encode(value, "UTF-8")); + return this; + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException(e); + } + } + + public URI build() { + try { + return new URI(sb.toString()); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + } + +} diff --git a/social/facebook/src/main/java/org/keycloak/social/facebook/FacebookProvider.java b/social/facebook/src/main/java/org/keycloak/social/facebook/FacebookProvider.java index 830abff642..4f15fbbe2b 100755 --- a/social/facebook/src/main/java/org/keycloak/social/facebook/FacebookProvider.java +++ b/social/facebook/src/main/java/org/keycloak/social/facebook/FacebookProvider.java @@ -2,8 +2,6 @@ package org.keycloak.social.facebook; import org.codehaus.jackson.JsonNode; import org.keycloak.social.AbstractOAuth2Provider; -import org.keycloak.social.AuthRequest; -import org.keycloak.social.SocialProviderConfig; import org.keycloak.social.SocialProviderException; import org.keycloak.social.SocialUser; import org.keycloak.social.utils.SimpleHttp; @@ -63,9 +61,4 @@ public class FacebookProvider extends AbstractOAuth2Provider { } } - @Override - public AuthRequest getAuthUrl(SocialProviderConfig config) throws SocialProviderException { - return super.getAuthUrl(config); - } - } diff --git a/social/twitter/src/main/java/org/keycloak/social/twitter/TwitterProvider.java b/social/twitter/src/main/java/org/keycloak/social/twitter/TwitterProvider.java index f07eb7df59..a7824d6dd3 100755 --- a/social/twitter/src/main/java/org/keycloak/social/twitter/TwitterProvider.java +++ b/social/twitter/src/main/java/org/keycloak/social/twitter/TwitterProvider.java @@ -28,10 +28,13 @@ import org.keycloak.social.SocialProvider; import org.keycloak.social.SocialProviderConfig; import org.keycloak.social.SocialProviderException; import org.keycloak.social.SocialUser; +import org.keycloak.social.UriBuilder; import twitter4j.Twitter; import twitter4j.TwitterFactory; import twitter4j.auth.RequestToken; +import java.net.URI; + /** * @author Stian Thorgersen */ @@ -43,14 +46,15 @@ public class TwitterProvider implements SocialProvider { } @Override - public AuthRequest getAuthUrl(SocialProviderConfig config) throws SocialProviderException { + public AuthRequest getAuthUrl(SocialProviderConfig config, String state) throws SocialProviderException { try { Twitter twitter = new TwitterFactory().getInstance(); twitter.setOAuthConsumer(config.getKey(), config.getSecret()); - RequestToken requestToken = twitter.getOAuthRequestToken(config.getCallbackUrl()); + URI uri = new URI(config.getCallbackUrl() + "?state=" + state); - return AuthRequest.create(requestToken.getToken(), requestToken.getAuthenticationURL()) + RequestToken requestToken = twitter.getOAuthRequestToken(uri.toString()); + return AuthRequest.create(requestToken.getAuthenticationURL()) .setAttribute("token", requestToken.getToken()).setAttribute("tokenSecret", requestToken.getTokenSecret()) .build(); } catch (Exception e) { @@ -73,7 +77,9 @@ public class TwitterProvider implements SocialProvider { Twitter twitter = new TwitterFactory().getInstance(); twitter.setOAuthConsumer(config.getKey(), config.getSecret()); + String token = callback.getQueryParam("oauth_token"); String verifier = callback.getQueryParam("oauth_verifier"); + RequestToken requestToken = new RequestToken((String)callback.getAttribute("token"), (String)callback.getAttribute("tokenSecret")); twitter.getOAuthAccessToken(requestToken, verifier); diff --git a/social/twitter/src/main/java/org/keycloak/social/twitter/TwitterProvider.java.orig b/social/twitter/src/main/java/org/keycloak/social/twitter/TwitterProvider.java.orig new file mode 100755 index 0000000000..e92da8867b --- /dev/null +++ b/social/twitter/src/main/java/org/keycloak/social/twitter/TwitterProvider.java.orig @@ -0,0 +1,102 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2012, Red Hat, Inc., and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.keycloak.social.twitter; + +import org.keycloak.social.AuthCallback; +import org.keycloak.social.SocialAccessDeniedException; +import org.keycloak.social.SocialProvider; +import org.keycloak.social.SocialProviderConfig; +import org.keycloak.social.SocialProviderException; +import org.keycloak.social.SocialUser; +import org.keycloak.social.UriBuilder; +import twitter4j.Twitter; +import twitter4j.TwitterFactory; +import twitter4j.auth.RequestToken; + +import java.net.URI; + +/** + * @author Stian Thorgersen + */ +public class TwitterProvider implements SocialProvider { + + @Override + public String getId() { + return "twitter"; + } + + @Override + public URI getAuthUrl(SocialProviderConfig config, String state) throws SocialProviderException { + try { + Twitter twitter = new TwitterFactory().getInstance(); + twitter.setOAuthConsumer(config.getKey(), config.getSecret()); + + URI uri = new URI(config.getCallbackUrl() + "?state=" + state); + + RequestToken requestToken = twitter.getOAuthRequestToken(uri.toString()); + + return UriBuilder.create(requestToken.getAuthenticationURL()) + //.setAttribute("token", requestToken.getToken()).setAttribute("tokenSecret", requestToken.getTokenSecret()) + .build(); + } catch (Exception e) { + throw new SocialProviderException(e); + } + } + + @Override + public String getName() { + return "Twitter"; + } + + @Override + public SocialUser processCallback(SocialProviderConfig config, AuthCallback callback) throws SocialProviderException { + if (callback.getQueryParam("denied") != null) { + throw new SocialAccessDeniedException(); + } + + try { + Twitter twitter = new TwitterFactory().getInstance(); + twitter.setOAuthConsumer(config.getKey(), config.getSecret()); + + String verifier = callback.getQueryParam("oauth_verifier"); +<<<<<<< Updated upstream + + // TODO Get token and tokenSecret + //RequestToken requestToken = new RequestToken((String)callback.getAttribute("token"), (String)callback.getAttribute("tokenSecret")); + RequestToken requestToken = new RequestToken(null, null); +======= + //RequestToken requestToken = new RequestToken((String)callback.getAttribute("token"), (String)callback.getAttribute("tokenSecret")); +>>>>>>> Stashed changes + + twitter.getOAuthAccessToken(verifier); + twitter4j.User twitterUser = twitter.verifyCredentials(); + + SocialUser user = new SocialUser(Long.toString(twitterUser.getId()), twitterUser.getScreenName()); + user.setName(twitterUser.getName()); + + return user; + } catch (Exception e) { + throw new SocialProviderException(e); + } + } + +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/DummySocial.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/DummySocial.java index b028e57b02..f3d9a3f516 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/DummySocial.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/DummySocial.java @@ -3,13 +3,14 @@ package org.keycloak.testsuite; import org.keycloak.OAuth2Constants; import org.keycloak.social.AuthCallback; import org.keycloak.social.AuthRequest; +import org.keycloak.social.UriBuilder; import org.keycloak.social.SocialAccessDeniedException; import org.keycloak.social.SocialProvider; import org.keycloak.social.SocialProviderConfig; import org.keycloak.social.SocialProviderException; import org.keycloak.social.SocialUser; -import java.util.UUID; +import java.net.URI; public class DummySocial implements SocialProvider { @@ -21,11 +22,11 @@ public class DummySocial implements SocialProvider { } @Override - public AuthRequest getAuthUrl(SocialProviderConfig config) throws SocialProviderException { - String state = UUID.randomUUID().toString(); - - return AuthRequest.create(state, AUTH_PATH).setQueryParam(OAuth2Constants.RESPONSE_TYPE, "token") - .setQueryParam(OAuth2Constants.REDIRECT_URI, config.getCallbackUrl()).setQueryParam(OAuth2Constants.STATE, state).setAttribute(OAuth2Constants.STATE, state).build(); + public AuthRequest getAuthUrl(SocialProviderConfig config, String state) throws SocialProviderException { + return AuthRequest.create(AUTH_PATH) + .setQueryParam(OAuth2Constants.RESPONSE_TYPE, "token") + .setQueryParam(OAuth2Constants.REDIRECT_URI, config.getCallbackUrl()) + .setQueryParam(OAuth2Constants.STATE, state).build(); } @Override @@ -40,10 +41,6 @@ public class DummySocial implements SocialProvider { throw new SocialAccessDeniedException(); } - if (!callback.getQueryParam(OAuth2Constants.STATE).equals(callback.getAttribute(OAuth2Constants.STATE))) { - throw new SocialProviderException("Invalid state"); - } - String id = callback.getQueryParam("id"); String username = callback.getQueryParam("username"); SocialUser user = new SocialUser(id, username);