diff --git a/events/api/src/main/java/org/keycloak/events/Details.java b/events/api/src/main/java/org/keycloak/events/Details.java index cee7475ec66..1a9c479b355 100755 --- a/events/api/src/main/java/org/keycloak/events/Details.java +++ b/events/api/src/main/java/org/keycloak/events/Details.java @@ -24,5 +24,7 @@ public interface Details { String NODE_HOST = "node_host"; String REASON = "reason"; String REVOKED_CLIENT = "revoked_client"; + String CLIENT_SESSION_STATE = "client_session_state"; + String CLIENT_SESSION_HOST = "client_session_host"; } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java index ffbc6a78e6e..e29d5d4c2c2 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java @@ -37,6 +37,7 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; +import java.util.List; import java.util.Map; /** @@ -227,16 +228,7 @@ public class TokenEndpoint { throw new ErrorResponseException("invalid_grant", "Session not active", Response.Status.BAD_REQUEST); } - String adapterSessionId = formParams.getFirst(AdapterConstants.CLIENT_SESSION_STATE); - if (adapterSessionId != null) { - String adapterSessionHost = formParams.getFirst(AdapterConstants.CLIENT_SESSION_HOST); - logger.debugf("Adapter Session '%s' saved in ClientSession for client '%s'. Host is '%s'", adapterSessionId, client.getClientId(), adapterSessionHost); - - event.detail(AdapterConstants.CLIENT_SESSION_STATE, adapterSessionId); - clientSession.setNote(AdapterConstants.CLIENT_SESSION_STATE, adapterSessionId); - event.detail(AdapterConstants.CLIENT_SESSION_HOST, adapterSessionHost); - clientSession.setNote(AdapterConstants.CLIENT_SESSION_HOST, adapterSessionHost); - } + updateClientSession(clientSession); AccessToken token = tokenManager.createClientAccessToken(session, accessCode.getRequestedRoles(), realm, client, user, userSession, clientSession); @@ -259,6 +251,10 @@ public class TokenEndpoint { AccessTokenResponse res; try { res = tokenManager.refreshAccessToken(session, uriInfo, clientConnection, realm, client, refreshToken, event, headers); + + UserSessionModel userSession = session.sessions().getUserSession(realm, res.getSessionState()); + updateClientSessions(userSession.getClientSessions()); + } catch (OAuthErrorException e) { event.error(Errors.INVALID_TOKEN); throw new ErrorResponseException(e.getError(), e.getDescription(), Response.Status.BAD_REQUEST); @@ -269,6 +265,45 @@ public class TokenEndpoint { return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build(); } + private void updateClientSession(ClientSessionModel clientSession) { + + if(clientSession == null) { + logger.error("client session is null"); + return; + } + + String adapterSessionId = formParams.getFirst(AdapterConstants.CLIENT_SESSION_STATE); + if (adapterSessionId != null) { + String adapterSessionHost = formParams.getFirst(AdapterConstants.CLIENT_SESSION_HOST); + logger.debugf("Adapter Session '%s' saved in ClientSession for client '%s'. Host is '%s'", adapterSessionId, client.getClientId(), adapterSessionHost); + + event.detail(AdapterConstants.CLIENT_SESSION_STATE, adapterSessionId); + clientSession.setNote(AdapterConstants.CLIENT_SESSION_STATE, adapterSessionId); + event.detail(AdapterConstants.CLIENT_SESSION_HOST, adapterSessionHost); + clientSession.setNote(AdapterConstants.CLIENT_SESSION_HOST, adapterSessionHost); + } + } + + private void updateClientSessions(List clientSessions) { + if(clientSessions == null) { + logger.error("client sessions is null"); + return; + } + for (ClientSessionModel clientSession : clientSessions) { + if(clientSession == null) { + logger.error("client session is null"); + continue; + } + if(clientSession.getClient() == null) { + logger.error("client model in client session is null"); + continue; + } + if(client.getId().equals(clientSession.getClient().getId())) { + updateClientSession(clientSession); + } + } + } + public Response buildResourceOwnerPasswordCredentialsGrant() { if (!realm.isPasswordCredentialGrantAllowed()) { throw new ErrorResponseException("not_enabled", "Direct Grant REST API not enabled", Response.Status.FORBIDDEN); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java index 3c81b8aa307..4e4f57a0305 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java @@ -35,6 +35,7 @@ import org.junit.Assert; import org.keycloak.OAuth2Constants; import org.keycloak.RSATokenVerifier; import org.keycloak.VerificationException; +import org.keycloak.constants.AdapterConstants; import org.keycloak.freemarker.LocaleHelper; import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.crypto.RSAProvider; @@ -78,6 +79,10 @@ public class OAuthClient { private PublicKey realmPublicKey; + private String clientSessionState; + + private String clientSessionHost; + public OAuthClient(WebDriver driver) { this.driver = driver; @@ -128,6 +133,14 @@ public class OAuthClient { parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, clientId)); } + if(clientSessionState != null) { + parameters.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_STATE, clientSessionState)); + } + + if(clientSessionHost != null) { + parameters.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_HOST, clientSessionHost)); + } + UrlEncodedFormEntity formEntity = null; try { formEntity = new UrlEncodedFormEntity(parameters, "UTF-8"); @@ -155,6 +168,13 @@ public class OAuthClient { parameters.add(new BasicNameValuePair("username", username)); parameters.add(new BasicNameValuePair("password", password)); + if(clientSessionState != null) { + parameters.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_STATE, clientSessionState)); + } + if(clientSessionHost != null) { + parameters.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_HOST, clientSessionHost)); + } + UrlEncodedFormEntity formEntity; try { formEntity = new UrlEncodedFormEntity(parameters, "UTF-8"); @@ -211,6 +231,13 @@ public class OAuthClient { parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, clientId)); } + if(clientSessionState != null) { + parameters.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_STATE, clientSessionState)); + } + if(clientSessionHost != null) { + parameters.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_HOST, clientSessionHost)); + } + UrlEncodedFormEntity formEntity; try { formEntity = new UrlEncodedFormEntity(parameters, "UTF-8"); @@ -360,6 +387,16 @@ public class OAuthClient { return this; } + public OAuthClient clientSessionState(String client_session_state) { + this.clientSessionState = client_session_state; + return this; + } + + public OAuthClient clientSessionHost(String client_session_host) { + this.clientSessionHost = client_session_host; + return this; + } + public String getRealm() { return realm; } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthDanceClientSessionExtensionTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthDanceClientSessionExtensionTest.java new file mode 100644 index 00000000000..f0978928a30 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthDanceClientSessionExtensionTest.java @@ -0,0 +1,71 @@ +package org.keycloak.testsuite.oauth; + +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.OAuth2Constants; +import org.keycloak.events.Details; +import org.keycloak.events.Event; +import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.OAuthClient; +import org.keycloak.testsuite.rule.KeycloakRule; +import org.keycloak.testsuite.rule.WebResource; +import org.keycloak.testsuite.rule.WebRule; + +/** + * @author Sebastian Rose, AOE on 02.06.15. + */ +public class OAuthDanceClientSessionExtensionTest { + + @ClassRule + public static KeycloakRule keycloakRule = new KeycloakRule(); + + @Rule + public WebRule webRule = new WebRule(this); + + @WebResource + protected OAuthClient oauth; + + @Rule + public AssertEvents events = new AssertEvents(keycloakRule); + + @Test + public void doOauthDanceWithClientSessionStateAndHost() throws Exception { + oauth.doLogin("test-user@localhost", "password"); + + Event loginEvent = events.expectLogin().assertEvent(); + + String sessionId = loginEvent.getSessionId(); + String codeId = loginEvent.getDetails().get(Details.CODE_ID); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + + String clientSessionState = "1234"; + String clientSessionHost = "test-client-host"; + + OAuthClient.AccessTokenResponse tokenResponse = oauth.clientSessionState(clientSessionState) + .clientSessionHost(clientSessionHost) + .doAccessTokenRequest(code, "password"); + + String refreshTokenString = tokenResponse.getRefreshToken(); + + Event tokenEvent = events.expectCodeToToken(codeId, sessionId) + .detail(Details.CLIENT_SESSION_STATE, clientSessionState) + .detail(Details.CLIENT_SESSION_HOST, clientSessionHost) + .assertEvent(); + + + String updatedClientSessionState = "5678"; + + oauth.clientSessionState(updatedClientSessionState) + .clientSessionHost(clientSessionHost) + .doRefreshTokenRequest(refreshTokenString, "password"); + + events.expectRefresh(tokenEvent.getDetails().get(Details.REFRESH_TOKEN_ID), sessionId) + .detail(Details.CLIENT_SESSION_STATE, updatedClientSessionState) + .detail(Details.CLIENT_SESSION_HOST, clientSessionHost) + .assertEvent(); + + } + +}