KEYCLOAK-2691
This commit is contained in:
parent
1dd4bdf0b7
commit
12bf4b498e
13 changed files with 116 additions and 22 deletions
|
@ -63,12 +63,12 @@ public abstract class AbstractIdentityProvider<C extends IdentityProviderModel>
|
|||
}
|
||||
|
||||
@Override
|
||||
public Response keycloakInitiatedBrowserLogout(UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) {
|
||||
public Response keycloakInitiatedBrowserLogout(KeycloakSession session, UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void backchannelLogout(UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) {
|
||||
public void backchannelLogout(KeycloakSession session, UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) {
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -79,9 +79,9 @@ public interface IdentityProvider<C extends IdentityProviderModel> extends Provi
|
|||
* @param identity
|
||||
* @return
|
||||
*/
|
||||
Response retrieveToken(FederatedIdentityModel identity);
|
||||
Response retrieveToken(KeycloakSession session, FederatedIdentityModel identity);
|
||||
|
||||
void backchannelLogout(UserSessionModel userSession, UriInfo uriInfo, RealmModel realm);
|
||||
void backchannelLogout(KeycloakSession session, UserSessionModel userSession, UriInfo uriInfo, RealmModel realm);
|
||||
|
||||
/**
|
||||
* Called when a Keycloak application initiates a logout through the browser. This is expected to do a logout
|
||||
|
@ -92,7 +92,7 @@ public interface IdentityProvider<C extends IdentityProviderModel> extends Provi
|
|||
* @param realm
|
||||
* @return null if this is not supported by this provider
|
||||
*/
|
||||
Response keycloakInitiatedBrowserLogout(UserSessionModel userSession, UriInfo uriInfo, RealmModel realm);
|
||||
Response keycloakInitiatedBrowserLogout(KeycloakSession session, UserSessionModel userSession, UriInfo uriInfo, RealmModel realm);
|
||||
|
||||
/**
|
||||
* Export a representation of the IdentityProvider in a specific format. For example, a SAML EntityDescriptor
|
||||
|
|
|
@ -47,7 +47,6 @@ public interface IdentityProviderFactory<T extends IdentityProvider> extends Pro
|
|||
* <p>Creates an {@link IdentityProvider} based on the configuration from
|
||||
* <code>inputStream</code>.</p>
|
||||
*
|
||||
* @param model The model containing the common abd basic configuration for an identity provider.
|
||||
* @param inputStream The input stream from where configuration will be loaded from..
|
||||
* @return
|
||||
*/
|
||||
|
|
|
@ -54,6 +54,7 @@ import java.util.regex.Pattern;
|
|||
public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityProviderConfig> extends AbstractIdentityProvider<C> {
|
||||
protected static final Logger logger = Logger.getLogger(AbstractOAuth2IdentityProvider.class);
|
||||
|
||||
public static final String OAUTH2_GRANT_TYPE_REFRESH_TOKEN = "refresh_token";
|
||||
public static final String OAUTH2_GRANT_TYPE_AUTHORIZATION_CODE = "authorization_code";
|
||||
public static final String FEDERATED_ACCESS_TOKEN = "FEDERATED_ACCESS_TOKEN";
|
||||
public static final String FEDERATED_REFRESH_TOKEN = "FEDERATED_REFRESH_TOKEN";
|
||||
|
@ -97,7 +98,7 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
|
|||
}
|
||||
|
||||
@Override
|
||||
public Response retrieveToken(FederatedIdentityModel identity) {
|
||||
public Response retrieveToken(KeycloakSession session, FederatedIdentityModel identity) {
|
||||
return Response.ok(identity.getToken()).build();
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import org.keycloak.broker.provider.util.SimpleHttp;
|
|||
import org.keycloak.broker.provider.AuthenticationRequest;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.IdentityBrokerException;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
|
@ -31,6 +32,7 @@ import org.keycloak.jose.jws.JWSInput;
|
|||
import org.keycloak.jose.jws.JWSInputException;
|
||||
import org.keycloak.jose.jws.crypto.RSAProvider;
|
||||
import org.keycloak.models.ClientSessionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.representations.AccessTokenResponse;
|
||||
|
@ -41,6 +43,7 @@ import org.keycloak.services.managers.AuthenticationManager;
|
|||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.resources.IdentityBrokerService;
|
||||
import org.keycloak.services.resources.RealmsResource;
|
||||
import org.keycloak.truststore.JSSETruststoreConfigurator;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.common.util.PemUtils;
|
||||
|
||||
|
@ -130,9 +133,9 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
|||
}
|
||||
|
||||
@Override
|
||||
public void backchannelLogout(UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) {
|
||||
public void backchannelLogout(KeycloakSession session, UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) {
|
||||
if (getConfig().getLogoutUrl() == null || getConfig().getLogoutUrl().trim().equals("") || !getConfig().isBackchannelSupported()) return;
|
||||
String idToken = userSession.getNote(FEDERATED_ID_TOKEN);
|
||||
String idToken = getIDTokenForLogout(session, userSession);
|
||||
if (idToken == null) return;
|
||||
backchannelLogout(userSession, idToken);
|
||||
}
|
||||
|
@ -156,9 +159,9 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
|||
|
||||
|
||||
@Override
|
||||
public Response keycloakInitiatedBrowserLogout(UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) {
|
||||
public Response keycloakInitiatedBrowserLogout(KeycloakSession session, UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) {
|
||||
if (getConfig().getLogoutUrl() == null || getConfig().getLogoutUrl().trim().equals("")) return null;
|
||||
String idToken = userSession.getNote(FEDERATED_ID_TOKEN);
|
||||
String idToken = getIDTokenForLogout(session, userSession);
|
||||
if (idToken != null && getConfig().isBackchannelSupported()) {
|
||||
backchannelLogout(userSession, idToken);
|
||||
return null;
|
||||
|
@ -177,6 +180,47 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns access token response as a string from a refresh token invocation on the remote OIDC broker
|
||||
*
|
||||
* @param session
|
||||
* @param userSession
|
||||
* @return
|
||||
*/
|
||||
public String refreshToken(KeycloakSession session, UserSessionModel userSession) {
|
||||
String refreshToken = userSession.getNote(FEDERATED_REFRESH_TOKEN);
|
||||
JSSETruststoreConfigurator configurator = new JSSETruststoreConfigurator(session);
|
||||
try {
|
||||
return SimpleHttp.doPost(getConfig().getTokenUrl())
|
||||
.param("refresh_token", refreshToken)
|
||||
.param(OAUTH2_PARAMETER_GRANT_TYPE, OAUTH2_GRANT_TYPE_REFRESH_TOKEN)
|
||||
.param(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
|
||||
.param(OAUTH2_PARAMETER_CLIENT_SECRET, getConfig().getClientSecret())
|
||||
.sslFactory(configurator.getSSLSocketFactory())
|
||||
.hostnameVerifier(configurator.getHostnameVerifier()).asString();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private String getIDTokenForLogout(KeycloakSession session, UserSessionModel userSession) {
|
||||
long exp = Long.parseLong(userSession.getNote(FEDERATED_TOKEN_EXPIRATION));
|
||||
int currentTime = Time.currentTime();
|
||||
if (exp > 0 && currentTime > exp) {
|
||||
String response = refreshToken(session, userSession);
|
||||
AccessTokenResponse tokenResponse = null;
|
||||
try {
|
||||
tokenResponse = JsonSerialization.readValue(response, AccessTokenResponse.class);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return tokenResponse.getIdToken();
|
||||
} else {
|
||||
return userSession.getNote(FEDERATED_ID_TOKEN);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected UriBuilder createAuthorizationUrl(AuthenticationRequest request) {
|
||||
UriBuilder authorizationUrl = super.createAuthorizationUrl(request);
|
||||
|
@ -322,6 +366,10 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
|||
@Override
|
||||
public void attachUserSession(UserSessionModel userSession, ClientSessionModel clientSession, BrokeredIdentityContext context) {
|
||||
AccessTokenResponse tokenResponse = (AccessTokenResponse)context.getContextData().get(FEDERATED_ACCESS_TOKEN_RESPONSE);
|
||||
int currentTime = Time.currentTime();
|
||||
long expiration = tokenResponse.getExpiresIn() > 0 ? tokenResponse.getExpiresIn() + currentTime : 0;
|
||||
userSession.setNote(FEDERATED_TOKEN_EXPIRATION, Long.toString(expiration));
|
||||
userSession.setNote(FEDERATED_REFRESH_TOKEN, tokenResponse.getRefreshToken());
|
||||
userSession.setNote(FEDERATED_ACCESS_TOKEN, tokenResponse.getToken());
|
||||
userSession.setNote(FEDERATED_ID_TOKEN, tokenResponse.getIdToken());
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
|||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.models.ClientSessionModel;
|
||||
import org.keycloak.models.FederatedIdentityModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.protocol.saml.JaxrsSAML2BindingBuilder;
|
||||
|
@ -145,12 +146,12 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
|
|||
}
|
||||
|
||||
@Override
|
||||
public Response retrieveToken(FederatedIdentityModel identity) {
|
||||
public Response retrieveToken(KeycloakSession session, FederatedIdentityModel identity) {
|
||||
return Response.ok(identity.getToken()).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void backchannelLogout(UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) {
|
||||
public void backchannelLogout(KeycloakSession session, UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) {
|
||||
String singleLogoutServiceUrl = getConfig().getSingleLogoutServiceUrl();
|
||||
if (singleLogoutServiceUrl == null || singleLogoutServiceUrl.trim().equals("") || !getConfig().isBackchannelSupported()) return;
|
||||
SAML2LogoutRequestBuilder logoutBuilder = buildLogoutRequest(userSession, uriInfo, realm, singleLogoutServiceUrl);
|
||||
|
@ -170,12 +171,12 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
|
|||
}
|
||||
|
||||
@Override
|
||||
public Response keycloakInitiatedBrowserLogout(UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) {
|
||||
public Response keycloakInitiatedBrowserLogout(KeycloakSession session, UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) {
|
||||
String singleLogoutServiceUrl = getConfig().getSingleLogoutServiceUrl();
|
||||
if (singleLogoutServiceUrl == null || singleLogoutServiceUrl.trim().equals("")) return null;
|
||||
|
||||
if (getConfig().isBackchannelSupported()) {
|
||||
backchannelLogout(userSession, uriInfo, realm);
|
||||
backchannelLogout(session, userSession, uriInfo, realm);
|
||||
return null;
|
||||
} else {
|
||||
try {
|
||||
|
|
|
@ -125,7 +125,7 @@ public class AuthenticationManager {
|
|||
if (brokerId != null) {
|
||||
IdentityProvider identityProvider = IdentityBrokerService.getIdentityProvider(session, realm, brokerId);
|
||||
try {
|
||||
identityProvider.backchannelLogout(userSession, uriInfo, realm);
|
||||
identityProvider.backchannelLogout(session, userSession, uriInfo, realm);
|
||||
} catch (Exception e) {
|
||||
}
|
||||
}
|
||||
|
@ -221,7 +221,7 @@ public class AuthenticationManager {
|
|||
String brokerId = userSession.getNote(Details.IDENTITY_PROVIDER);
|
||||
if (brokerId != null) {
|
||||
IdentityProvider identityProvider = IdentityBrokerService.getIdentityProvider(session, realm, brokerId);
|
||||
Response response = identityProvider.keycloakInitiatedBrowserLogout(userSession, uriInfo, realm);
|
||||
Response response = identityProvider.keycloakInitiatedBrowserLogout(session, userSession, uriInfo, realm);
|
||||
if (response != null) return response;
|
||||
}
|
||||
return finishBrowserLogout(session, realm, userSession, uriInfo, connection, headers);
|
||||
|
|
|
@ -227,7 +227,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
|||
|
||||
this.event.success();
|
||||
|
||||
return corsResponse(identityProvider.retrieveToken(identity), clientModel);
|
||||
return corsResponse(identityProvider.retrieveToken(session, identity), clientModel);
|
||||
}
|
||||
|
||||
return corsResponse(badRequest("Identity Provider [" + providerId + "] does not support this operation."), clientModel);
|
||||
|
|
|
@ -185,7 +185,7 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
|
|||
}
|
||||
|
||||
@Override
|
||||
public Response retrieveToken(FederatedIdentityModel identity) {
|
||||
public Response retrieveToken(KeycloakSession session, FederatedIdentityModel identity) {
|
||||
return Response.ok(identity.getToken()).type(MediaType.APPLICATION_JSON).build();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import org.junit.Before;
|
|||
import org.junit.ClassRule;
|
||||
import org.junit.Rule;
|
||||
import org.keycloak.authentication.authenticators.broker.IdpReviewProfileAuthenticatorFactory;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.AuthenticatorConfigModel;
|
||||
import org.keycloak.models.FederatedIdentityModel;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
|
@ -115,6 +116,8 @@ public abstract class AbstractIdentityProviderTest {
|
|||
|
||||
protected KeycloakSession session;
|
||||
|
||||
protected int logoutTimeOffset = 0;
|
||||
|
||||
@Before
|
||||
public void onBefore() {
|
||||
this.session = brokerServerRule.startSession();
|
||||
|
@ -160,11 +163,26 @@ public abstract class AbstractIdentityProviderTest {
|
|||
assertEquals(getProviderId(), federatedIdentityModel.getIdentityProvider());
|
||||
assertEquals(federatedUser.getUsername(), federatedIdentityModel.getUserName());
|
||||
|
||||
driver.navigate().to("http://localhost:8081/test-app/logout");
|
||||
// test access token timeot on logout
|
||||
if (logoutTimeOffset > 0) {
|
||||
Time.setOffset(logoutTimeOffset);
|
||||
}
|
||||
try {
|
||||
driver.navigate().to("http://localhost:8081/test-app/logout");
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
|
||||
String afterLogoutUrl = driver.getCurrentUrl();
|
||||
String afterLogoutPageSource = driver.getPageSource();
|
||||
System.out.println("afterLogoutUrl: " + afterLogoutUrl);
|
||||
//System.out.println("after logout page source: " + afterLogoutPageSource);
|
||||
|
||||
driver.navigate().to("http://localhost:8081/test-app");
|
||||
|
||||
assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth"));
|
||||
return federatedUser;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -19,9 +19,13 @@ package org.keycloak.testsuite.broker;
|
|||
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.admin.client.Keycloak;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.representations.AccessTokenResponse;
|
||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.services.managers.RealmManager;
|
||||
import org.keycloak.testsuite.Constants;
|
||||
|
@ -34,6 +38,7 @@ import org.keycloak.util.JsonSerialization;
|
|||
import org.openqa.selenium.NoSuchElementException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
|
||||
|
@ -117,6 +122,26 @@ public class OIDCKeyCloakServerBrokerBasicTest extends AbstractKeycloakIdentityP
|
|||
super.testSuccessfulAuthentication();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLogoutWorksWithTokenTimeout() {
|
||||
Keycloak keycloak = Keycloak.getInstance("http://localhost:8081/auth", "master", "admin", "admin", org.keycloak.models.Constants.ADMIN_CLI_CLIENT_ID);
|
||||
RealmRepresentation realm = keycloak.realm("realm-with-oidc-identity-provider").toRepresentation();
|
||||
assertNotNull(realm);
|
||||
int oldLifespan = realm.getAccessTokenLifespan();
|
||||
realm.setAccessTokenLifespan(1);
|
||||
keycloak.realm("realm-with-oidc-identity-provider").update(realm);
|
||||
IdentityProviderRepresentation idp = keycloak.realm("realm-with-broker").identityProviders().get("kc-oidc-idp").toRepresentation();
|
||||
idp.getConfig().put("backchannelSupported", "false");
|
||||
keycloak.realm("realm-with-broker").identityProviders().get("kc-oidc-idp").update(idp);
|
||||
logoutTimeOffset = 2;
|
||||
super.testSuccessfulAuthentication();
|
||||
logoutTimeOffset = 0;
|
||||
realm.setAccessTokenLifespan(oldLifespan);
|
||||
keycloak.realm("realm-with-oidc-identity-provider").update(realm);
|
||||
idp.getConfig().put("backchannelSupported", "true");
|
||||
keycloak.realm("realm-with-broker").identityProviders().get("kc-oidc-idp").update(idp);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccessfulAuthenticationWithoutUpdateProfile() {
|
||||
super.testSuccessfulAuthenticationWithoutUpdateProfile();
|
||||
|
|
|
@ -20,6 +20,7 @@ import org.keycloak.broker.provider.AbstractIdentityProvider;
|
|||
import org.keycloak.broker.provider.AuthenticationRequest;
|
||||
import org.keycloak.models.FederatedIdentityModel;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
|
@ -38,7 +39,7 @@ public class CustomIdentityProvider extends AbstractIdentityProvider<IdentityPro
|
|||
}
|
||||
|
||||
@Override
|
||||
public Response retrieveToken(FederatedIdentityModel identity) {
|
||||
public Response retrieveToken(KeycloakSession session, FederatedIdentityModel identity) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import org.keycloak.broker.provider.AuthenticationRequest;
|
|||
import org.keycloak.models.FederatedIdentityModel;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.broker.social.SocialIdentityProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
|
@ -39,7 +40,7 @@ public class CustomSocialProvider extends AbstractIdentityProvider<IdentityProvi
|
|||
}
|
||||
|
||||
@Override
|
||||
public Response retrieveToken(FederatedIdentityModel identity) {
|
||||
public Response retrieveToken(KeycloakSession session, FederatedIdentityModel identity) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue