mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-10 15:32:05 -03:30
Fix expires_in in internal to external token exchange
Closes #35704 Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
parent
b633d352db
commit
e7e6185175
@ -55,6 +55,8 @@ public class IdentityProviderModel implements Serializable {
|
||||
public static final String POST_BROKER_LOGIN_FLOW_ID = "postBrokerLoginFlowId";
|
||||
public static final String SEARCH = "search";
|
||||
public static final String SYNC_MODE = "syncMode";
|
||||
public static final String MIN_VALIDITY_TOKEN = "minValidityToken";
|
||||
public static final int DEFAULT_MIN_VALIDITY_TOKEN = 5;
|
||||
|
||||
private String internalId;
|
||||
|
||||
@ -344,6 +346,25 @@ public class IdentityProviderModel implements Serializable {
|
||||
getConfig().put(CASE_SENSITIVE_ORIGINAL_USERNAME, Boolean.valueOf(caseSensitive).toString());
|
||||
}
|
||||
|
||||
public void setMinValidityToken(int minValidityToken) {
|
||||
getConfig().put(MIN_VALIDITY_TOKEN, Integer.toString(minValidityToken));
|
||||
}
|
||||
|
||||
public int getMinValidityToken() {
|
||||
String minValidityTokenString = getConfig().get(MIN_VALIDITY_TOKEN);
|
||||
if (minValidityTokenString != null) {
|
||||
try {
|
||||
int minValidityToken = Integer.parseInt(minValidityTokenString);
|
||||
if (minValidityToken > 0) {
|
||||
return minValidityToken;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
// no-op return default
|
||||
}
|
||||
}
|
||||
return DEFAULT_MIN_VALIDITY_TOKEN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hash = 5;
|
||||
|
||||
@ -203,7 +203,8 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||
String modelTokenString = model.getToken();
|
||||
AccessTokenResponse tokenResponse = JsonSerialization.readValue(modelTokenString, AccessTokenResponse.class);
|
||||
Integer exp = (Integer) tokenResponse.getOtherClaims().get(ACCESS_TOKEN_EXPIRATION);
|
||||
if (exp != null && exp < Time.currentTime()) {
|
||||
final int currentTime = Time.currentTime();
|
||||
if (exp != null && exp < currentTime + getConfig().getMinValidityToken()) {
|
||||
if (tokenResponse.getRefreshToken() == null) {
|
||||
return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
|
||||
}
|
||||
@ -219,7 +220,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||
}
|
||||
AccessTokenResponse newResponse = JsonSerialization.readValue(response, AccessTokenResponse.class);
|
||||
if (newResponse.getExpiresIn() > 0) {
|
||||
int accessTokenExpiration = Time.currentTime() + (int) newResponse.getExpiresIn();
|
||||
int accessTokenExpiration = currentTime + (int) newResponse.getExpiresIn();
|
||||
newResponse.getOtherClaims().put(ACCESS_TOKEN_EXPIRATION, accessTokenExpiration);
|
||||
}
|
||||
|
||||
@ -231,7 +232,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||
|
||||
String oldToken = tokenUserSession.getNote(FEDERATED_ACCESS_TOKEN);
|
||||
if (oldToken != null && oldToken.equals(tokenResponse.getToken())) {
|
||||
int accessTokenExpiration = newResponse.getExpiresIn() > 0 ? Time.currentTime() + (int) newResponse.getExpiresIn() : 0;
|
||||
int accessTokenExpiration = newResponse.getExpiresIn() > 0 ? currentTime + (int) newResponse.getExpiresIn() : 0;
|
||||
tokenUserSession.setNote(FEDERATED_TOKEN_EXPIRATION, Long.toString(accessTokenExpiration));
|
||||
tokenUserSession.setNote(FEDERATED_REFRESH_TOKEN, newResponse.getRefreshToken());
|
||||
tokenUserSession.setNote(FEDERATED_ACCESS_TOKEN, newResponse.getToken());
|
||||
@ -241,7 +242,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||
model.setToken(response);
|
||||
tokenResponse = newResponse;
|
||||
} else if (exp != null) {
|
||||
tokenResponse.setExpiresIn(exp - Time.currentTime());
|
||||
tokenResponse.setExpiresIn(exp - currentTime);
|
||||
}
|
||||
tokenResponse.setIdToken(null);
|
||||
tokenResponse.setRefreshToken(null);
|
||||
@ -275,7 +276,6 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||
protected Response exchangeSessionToken(UriInfo uriInfo, EventBuilder event, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject) {
|
||||
String refreshToken = tokenUserSession.getNote(FEDERATED_REFRESH_TOKEN);
|
||||
String accessToken = tokenUserSession.getNote(FEDERATED_ACCESS_TOKEN);
|
||||
String idToken = tokenUserSession.getNote(FEDERATED_ID_TOKEN);
|
||||
|
||||
if (accessToken == null) {
|
||||
event.detail(Details.REASON, "requested_issuer is not linked");
|
||||
@ -284,9 +284,10 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||
}
|
||||
try (VaultStringSecret vaultStringSecret = session.vault().getStringSecret(getConfig().getClientSecret())) {
|
||||
long expiration = Long.parseLong(tokenUserSession.getNote(FEDERATED_TOKEN_EXPIRATION));
|
||||
if (expiration == 0 || expiration > Time.currentTime()) {
|
||||
final int currentTime = Time.currentTime();
|
||||
if (expiration == 0 || expiration > currentTime + getConfig().getMinValidityToken()) {
|
||||
AccessTokenResponse tokenResponse = new AccessTokenResponse();
|
||||
tokenResponse.setExpiresIn(expiration);
|
||||
tokenResponse.setExpiresIn(expiration > 0? expiration - currentTime : 0);
|
||||
tokenResponse.setToken(accessToken);
|
||||
tokenResponse.setIdToken(null);
|
||||
tokenResponse.setRefreshToken(null);
|
||||
@ -304,7 +305,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||
return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
|
||||
}
|
||||
AccessTokenResponse newResponse = JsonSerialization.readValue(response, AccessTokenResponse.class);
|
||||
long accessTokenExpiration = newResponse.getExpiresIn() > 0 ? Time.currentTime() + newResponse.getExpiresIn() : 0;
|
||||
long accessTokenExpiration = newResponse.getExpiresIn() > 0 ? currentTime + newResponse.getExpiresIn() : 0;
|
||||
tokenUserSession.setNote(FEDERATED_TOKEN_EXPIRATION, Long.toString(accessTokenExpiration));
|
||||
tokenUserSession.setNote(FEDERATED_REFRESH_TOKEN, newResponse.getRefreshToken());
|
||||
tokenUserSession.setNote(FEDERATED_ACCESS_TOKEN, newResponse.getToken());
|
||||
|
||||
@ -44,6 +44,11 @@ public class IdentityProviderAttributeUpdater {
|
||||
return this;
|
||||
}
|
||||
|
||||
public IdentityProviderAttributeUpdater setStoreToken(boolean storeToken) {
|
||||
rep.setStoreToken(storeToken);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Closeable update() {
|
||||
identityProviderResource.update(rep);
|
||||
|
||||
|
||||
@ -19,12 +19,14 @@ package org.keycloak.testsuite.broker;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.greaterThan;
|
||||
import static org.hamcrest.Matchers.lessThanOrEqualTo;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.keycloak.testsuite.broker.BrokerTestConstants.IDP_OIDC_ALIAS;
|
||||
import static org.keycloak.testsuite.util.ProtocolMapperUtil.createHardcodedClaim;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import jakarta.ws.rs.client.Client;
|
||||
import jakarta.ws.rs.client.Entity;
|
||||
import jakarta.ws.rs.client.WebTarget;
|
||||
@ -32,6 +34,8 @@ import jakarta.ws.rs.core.Form;
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.Response.Status;
|
||||
import java.io.Closeable;
|
||||
import java.util.Map;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.admin.client.resource.ClientResource;
|
||||
@ -50,6 +54,7 @@ import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.representations.AccessTokenResponse;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.IdentityProviderMapperRepresentation;
|
||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||
@ -58,12 +63,13 @@ import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation
|
||||
import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement;
|
||||
import org.keycloak.services.resources.admin.permissions.AdminPermissions;
|
||||
import org.keycloak.testsuite.Assert;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeatures;
|
||||
import org.keycloak.testsuite.updaters.IdentityProviderAttributeUpdater;
|
||||
import org.keycloak.testsuite.util.AdminClientUtil;
|
||||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
import org.keycloak.util.BasicAuthHelper;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
||||
@EnableFeatures({@EnableFeature(Profile.Feature.TOKEN_EXCHANGE), @EnableFeature(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ)})
|
||||
public final class KcOidcBrokerTokenExchangeTest extends AbstractInitializedBaseBrokerTest {
|
||||
@ -87,11 +93,10 @@ public final class KcOidcBrokerTokenExchangeTest extends AbstractInitializedBase
|
||||
hardCodedSessionNoteMapper.setName("hard-coded");
|
||||
hardCodedSessionNoteMapper.setIdentityProviderAlias(bc.getIDPAlias());
|
||||
hardCodedSessionNoteMapper.setIdentityProviderMapper(UserAttributeMapper.PROVIDER_ID);
|
||||
hardCodedSessionNoteMapper.setConfig(ImmutableMap.<String, String>builder()
|
||||
.put(IdentityProviderMapperModel.SYNC_MODE, IdentityProviderMapperSyncMode.INHERIT.toString())
|
||||
.put(UserAttributeMapper.USER_ATTRIBUTE, "mapped-from-claim")
|
||||
.put(UserAttributeMapper.CLAIM, "hard-coded")
|
||||
.build());
|
||||
hardCodedSessionNoteMapper.setConfig(Map.of(
|
||||
IdentityProviderMapperModel.SYNC_MODE, IdentityProviderMapperSyncMode.INHERIT.toString(),
|
||||
UserAttributeMapper.USER_ATTRIBUTE, "mapped-from-claim",
|
||||
UserAttributeMapper.CLAIM, "hard-coded"));
|
||||
|
||||
RealmResource consumerRealm = realmsResouce().realm(bc.consumerRealmName());
|
||||
IdentityProviderResource identityProviderResource = consumerRealm.identityProviders().get(bc.getIDPAlias());
|
||||
@ -188,6 +193,30 @@ public final class KcOidcBrokerTokenExchangeTest extends AbstractInitializedBase
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInternalExternalTokenExchangeSessionToken() throws Exception {
|
||||
testingClient.server(bc.consumerRealmName()).run(KcOidcBrokerTokenExchangeTest::setupRealm);
|
||||
|
||||
try (Closeable idpUpdater = new IdentityProviderAttributeUpdater(
|
||||
realmsResouce().realm(bc.consumerRealmName()).identityProviders().get(bc.getIDPAlias()))
|
||||
.setStoreToken(false)
|
||||
.update()) {
|
||||
testInternalExternalTokenExchange();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInternalExternalTokenExchangeStoredToken() throws Exception {
|
||||
testingClient.server(bc.consumerRealmName()).run(KcOidcBrokerTokenExchangeTest::setupRealm);
|
||||
|
||||
try (Closeable idpUpdater = new IdentityProviderAttributeUpdater(
|
||||
realmsResouce().realm(bc.consumerRealmName()).identityProviders().get(bc.getIDPAlias()))
|
||||
.setStoreToken(true)
|
||||
.update()) {
|
||||
testInternalExternalTokenExchange();
|
||||
}
|
||||
}
|
||||
|
||||
private static void setupRealm(KeycloakSession session) {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
IdentityProviderModel idp = session.identityProviders().getByAlias(IDP_OIDC_ALIAS);
|
||||
@ -201,12 +230,13 @@ public final class KcOidcBrokerTokenExchangeTest extends AbstractInitializedBase
|
||||
client.setSecret("secret");
|
||||
client.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||
client.setFullScopeAllowed(false);
|
||||
ClientModel brokerApp = realm.getClientByClientId("broker-app");
|
||||
|
||||
AdminPermissionManagement management = AdminPermissions.management(session, realm);
|
||||
management.idps().setPermissionsEnabled(idp, true);
|
||||
ClientPolicyRepresentation clientRep = new ClientPolicyRepresentation();
|
||||
clientRep.setName("toIdp");
|
||||
clientRep.addClient(client.getId());
|
||||
clientRep.addClient(client.getId(), brokerApp.getId());
|
||||
ResourceServer server = management.realmResourceServer();
|
||||
Policy clientPolicy = management.authz().getStoreFactory().getPolicyStore().create(server, clientRep);
|
||||
management.idps().exchangeToPermission(idp).addAssociatedPolicy(clientPolicy);
|
||||
@ -216,4 +246,48 @@ public final class KcOidcBrokerTokenExchangeTest extends AbstractInitializedBase
|
||||
client.addRedirectUri(OAuthClient.APP_ROOT + "/auth");
|
||||
client.setAttribute(OIDCConfigAttributes.BACKCHANNEL_LOGOUT_URL, OAuthClient.APP_ROOT + "/admin/backchannelLogout");
|
||||
}
|
||||
|
||||
private void testInternalExternalTokenExchange() throws Exception {
|
||||
final RealmResource consumerRealm = realmsResouce().realm(bc.consumerRealmName());
|
||||
final int expires = realmsResouce().realm(bc.providerRealmName()).toRepresentation().getAccessTokenLifespan();
|
||||
final ClientRepresentation brokerApp = ApiUtil.findClientByClientId(consumerRealm, "broker-app").toRepresentation();
|
||||
|
||||
logInAsUserInIDPForFirstTimeAndAssertSuccess();
|
||||
final String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, brokerApp.getSecret());
|
||||
assertThat(tokenResponse.getError(), nullValue());
|
||||
assertThat(tokenResponse.getAccessToken(), notNullValue());
|
||||
|
||||
exchangeToIdP(brokerApp, tokenResponse.getAccessToken(), expires);
|
||||
|
||||
setTimeOffset(expires - IdentityProviderModel.DEFAULT_MIN_VALIDITY_TOKEN);
|
||||
|
||||
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), brokerApp.getSecret());
|
||||
assertThat(tokenResponse.getError(), nullValue());
|
||||
assertThat(tokenResponse.getAccessToken(), notNullValue());
|
||||
|
||||
exchangeToIdP(brokerApp, tokenResponse.getAccessToken(), expires);
|
||||
}
|
||||
|
||||
private void exchangeToIdP(ClientRepresentation brokerApp, String subjectToken, long expires) {
|
||||
try (Client httpClient = AdminClientUtil.createResteasyClient();
|
||||
Response response = httpClient.target(OAuthClient.AUTH_SERVER_ROOT)
|
||||
.path("/realms").path(bc.consumerRealmName()).path("protocol/openid-connect/token")
|
||||
.request()
|
||||
.header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(brokerApp.getClientId(), brokerApp.getSecret()))
|
||||
.post(Entity.form(new Form()
|
||||
.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
|
||||
.param(OAuth2Constants.SUBJECT_TOKEN, subjectToken)
|
||||
.param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE)
|
||||
.param(OAuth2Constants.REQUESTED_ISSUER, bc.getIDPAlias()))
|
||||
)) {
|
||||
assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode()));
|
||||
|
||||
AccessTokenResponse exchangeResponse = response.readEntity(AccessTokenResponse.class);
|
||||
assertThat(exchangeResponse.getError(), nullValue());
|
||||
assertThat(exchangeResponse.getToken(), notNullValue());
|
||||
assertThat(exchangeResponse.getExpiresIn(), greaterThan((long) IdentityProviderModel.DEFAULT_MIN_VALIDITY_TOKEN));
|
||||
assertThat(exchangeResponse.getExpiresIn(), lessThanOrEqualTo(expires));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user