diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/service/ClientConverter.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/service/ClientConverter.java index 3a23f6ade..cb251e9c4 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/service/ClientConverter.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/service/ClientConverter.java @@ -105,7 +105,7 @@ public ClientDetailsEntity entityFromClientManagementRequest(RegisteredClientDTO public RegisteredClientDTO registeredClientDtoFromEntity(ClientDetailsEntity entity) { RegisteredClientDTO clientDTO = new RegisteredClientDTO(); - + clientDTO.setClientId(entity.getClientId()); clientDTO.setClientSecret(entity.getClientSecret()); clientDTO.setClientName(entity.getClientName()); @@ -127,6 +127,9 @@ public RegisteredClientDTO registeredClientDtoFromEntity(ClientDetailsEntity ent clientDTO.setTosUri(entity.getTosUri()); clientDTO.setCreatedAt(entity.getCreatedAt()); + if (entity.getClientLastUsed() != null) { + clientDTO.setLastUsed(entity.getClientLastUsed().getLastUsed()); + } clientDTO.setAccessTokenValiditySeconds(entity.getAccessTokenValiditySeconds()); clientDTO.setAllowIntrospection(entity.isAllowIntrospection()); clientDTO.setClearAccessTokensOnRefresh(entity.isClearAccessTokensOnRefresh()); diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/service/ClientService.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/service/ClientService.java index 8194742e5..421439303 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/client/service/ClientService.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/client/service/ClientService.java @@ -46,5 +46,4 @@ Optional findClientByClientIdAndAccount(String clientId, ClientDetailsEntity updateClient(ClientDetailsEntity client); void deleteClient(ClientDetailsEntity client); - } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/common/client/RegisteredClientDTO.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/common/client/RegisteredClientDTO.java index f483b27e6..2125fa4d5 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/common/client/RegisteredClientDTO.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/common/client/RegisteredClientDTO.java @@ -15,6 +15,7 @@ */ package it.infn.mw.iam.api.common.client; +import java.time.LocalDate; import java.util.Date; import java.util.Set; @@ -29,6 +30,7 @@ import org.hibernate.validator.constraints.URL; +import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonView; import com.fasterxml.jackson.databind.PropertyNamingStrategies; @@ -234,6 +236,11 @@ public class RegisteredClientDTO { ClientViews.DynamicRegistration.class}) private Date createdAt; + @JsonView({ClientViews.Limited.class, ClientViews.Full.class, ClientViews.ClientManagement.class, + ClientViews.DynamicRegistration.class}) + @JsonFormat(shape = JsonFormat.Shape.STRING) + private LocalDate lastUsed; + @JsonView({ClientViews.Full.class, ClientViews.ClientManagement.class, ClientViews.DynamicRegistration.class}) @Size(max = 2048, groups = {OnClientCreation.class, OnClientUpdate.class}) @@ -472,6 +479,14 @@ public void setCreatedAt(Date createdAt) { this.createdAt = createdAt; } + public LocalDate getLastUsed() { + return lastUsed; + } + + public void setLastUsed(LocalDate lastUsed) { + this.lastUsed = lastUsed; + } + public String getJwk() { return jwk; } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/IamAuditApplicationEvent.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/IamAuditApplicationEvent.java index 35827d72c..6118f9bb2 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/IamAuditApplicationEvent.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/IamAuditApplicationEvent.java @@ -42,7 +42,8 @@ public enum IamEventCategory { SCOPE_POLICY, AUP, MEMBERSHIP, - CLIENT + CLIENT, + TOKEN } private static final long serialVersionUID = -6276169409979227109L; diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/tokens/AccessTokenIssuedEvent.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/tokens/AccessTokenIssuedEvent.java new file mode 100644 index 000000000..5b74c545e --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/tokens/AccessTokenIssuedEvent.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare (INFN). 2016-2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package it.infn.mw.iam.audit.events.tokens; + +import org.mitre.oauth2.model.OAuth2AccessTokenEntity; + + +public class AccessTokenIssuedEvent extends TokenEvent { + + private static final long serialVersionUID = 1L; + + public AccessTokenIssuedEvent(Object source, OAuth2AccessTokenEntity token) { + super(source, token, "Access token issued"); + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/tokens/AccessTokenRefreshedEvent.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/tokens/AccessTokenRefreshedEvent.java new file mode 100644 index 000000000..4f7fd0326 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/tokens/AccessTokenRefreshedEvent.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare (INFN). 2016-2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package it.infn.mw.iam.audit.events.tokens; + +import org.mitre.oauth2.model.OAuth2AccessTokenEntity; + + +public class AccessTokenRefreshedEvent extends TokenEvent { + + private static final long serialVersionUID = 1L; + + public AccessTokenRefreshedEvent(Object source, OAuth2AccessTokenEntity token) { + super(source, token, "Access token refreshed"); + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/tokens/TokenEvent.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/tokens/TokenEvent.java new file mode 100644 index 000000000..210969e3f --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/tokens/TokenEvent.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare (INFN). 2016-2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package it.infn.mw.iam.audit.events.tokens; + +import java.util.Date; +import java.util.Set; + +import org.mitre.oauth2.model.OAuth2AccessTokenEntity; + +import it.infn.mw.iam.audit.events.IamAuditApplicationEvent; + +public abstract class TokenEvent extends IamAuditApplicationEvent { + private static final long serialVersionUID = 1L; + private final Date expiration; + private final String clientId; + private final Set scopes; + + public TokenEvent(Object source, OAuth2AccessTokenEntity token, String message) { + super(IamEventCategory.TOKEN, source, message); + this.expiration = token.getExpiration(); + this.clientId = token.getClient().getClientId(); + this.scopes = token.getScope(); + } + + public Date getExpiration() { + return expiration; + } + + public String getClientId() { + return clientId; + } + + public Set getScopes() { + return scopes; + } + + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/config/IamProperties.java b/iam-login-service/src/main/java/it/infn/mw/iam/config/IamProperties.java index b36a061e7..8e40eb190 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/config/IamProperties.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/config/IamProperties.java @@ -92,7 +92,6 @@ public void setPassword(String password) { } - public static class ExternalConnectivityProbeProperties { private boolean enabled = true; @@ -100,7 +99,6 @@ public static class ExternalConnectivityProbeProperties { private String endpoint = "https://www.google.com"; private int timeoutInSecs = 10; - public boolean isEnabled() { return enabled; } @@ -292,7 +290,6 @@ public void setAllowCompleteVerificationUri(Boolean allowCompleteVerificationUri this.allowCompleteVerificationUri = allowCompleteVerificationUri; } - } public static class JWKProperties { @@ -540,6 +537,18 @@ public void setLocation(String location) { } } + public static class ClientProperties { + private boolean trackLastUsed; + + public boolean isTrackLastUsed() { + return trackLastUsed; + } + + public void setTrackLastUsed(boolean trackLastUsed) { + this.trackLastUsed = trackLastUsed; + } + } + private String host; private String issuer; @@ -588,14 +597,14 @@ public void setLocation(String location) { private CustomizationProperties customization = new CustomizationProperties(); - private VersionedStaticResourcesProperties versionedStaticResources = - new VersionedStaticResourcesProperties(); + private VersionedStaticResourcesProperties versionedStaticResources = new VersionedStaticResourcesProperties(); - private ExternalConnectivityProbeProperties externalConnectivityProbe = - new ExternalConnectivityProbeProperties(); + private ExternalConnectivityProbeProperties externalConnectivityProbe = new ExternalConnectivityProbeProperties(); private AccountLinkingProperties accountLinking = new AccountLinkingProperties(); + private ClientProperties client = new ClientProperties(); + public String getBaseUrl() { return baseUrl; } @@ -814,4 +823,12 @@ public void setAccountLinking(AccountLinkingProperties accountLinking) { this.accountLinking = accountLinking; } + public void setClient(ClientProperties client) { + this.client = client; + } + + public ClientProperties getClient(){ + return client; + } + } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/IamTokenService.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/IamTokenService.java index bf5b56935..6ee8d2bc6 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/core/IamTokenService.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/IamTokenService.java @@ -18,17 +18,26 @@ import java.util.Date; import java.util.Set; +import java.time.LocalDate; +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.model.ClientLastUsedEntity; import org.mitre.oauth2.model.OAuth2AccessTokenEntity; import org.mitre.oauth2.model.OAuth2RefreshTokenEntity; import org.mitre.oauth2.service.impl.DefaultOAuth2ProviderTokenService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Primary; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.TokenRequest; import org.springframework.stereotype.Service; import com.google.common.collect.Sets; +import it.infn.mw.iam.audit.events.tokens.AccessTokenIssuedEvent; +import it.infn.mw.iam.audit.events.tokens.AccessTokenRefreshedEvent; +import it.infn.mw.iam.config.IamProperties; import it.infn.mw.iam.persistence.repository.IamOAuthAccessTokenRepository; import it.infn.mw.iam.persistence.repository.IamOAuthRefreshTokenRepository; @@ -40,14 +49,19 @@ public class IamTokenService extends DefaultOAuth2ProviderTokenService { private final IamOAuthAccessTokenRepository accessTokenRepo; private final IamOAuthRefreshTokenRepository refreshTokenRepo; + private final ApplicationEventPublisher eventPublisher; + private final IamProperties iamProperties; @Autowired public IamTokenService(IamOAuthAccessTokenRepository atRepo, - IamOAuthRefreshTokenRepository rtRepo) { + IamOAuthRefreshTokenRepository rtRepo, ApplicationEventPublisher publisher, + IamProperties iamProperties) { this.accessTokenRepo = atRepo; this.refreshTokenRepo = rtRepo; + this.eventPublisher = publisher; + this.iamProperties = iamProperties; } @Override @@ -76,4 +90,48 @@ public void revokeRefreshToken(OAuth2RefreshTokenEntity refreshToken) { refreshTokenRepo.delete(refreshToken); } + @Override + @SuppressWarnings("deprecation") + public OAuth2AccessTokenEntity createAccessToken(OAuth2Authentication authentication) { + + OAuth2AccessTokenEntity token = super.createAccessToken(authentication); + + if (iamProperties.getClient().isTrackLastUsed()) { + updateClientLastUsed(token); + } + + eventPublisher.publishEvent(new AccessTokenIssuedEvent(this, token)); + return token; + } + + @Override + @SuppressWarnings("deprecation") + public OAuth2AccessTokenEntity refreshAccessToken(String refreshTokenValue, + TokenRequest authRequest) { + + OAuth2AccessTokenEntity token = super.refreshAccessToken(refreshTokenValue, authRequest); + + if (iamProperties.getClient().isTrackLastUsed()) { + updateClientLastUsed(token); + } + + eventPublisher.publishEvent(new AccessTokenRefreshedEvent(this, token)); + return token; + } + + private void updateClientLastUsed(OAuth2AccessTokenEntity token) { + ClientDetailsEntity client = token.getClient(); + ClientLastUsedEntity clientLastUsed = client.getClientLastUsed(); + LocalDate now = LocalDate.now(); + + if (clientLastUsed == null) { + clientLastUsed = new ClientLastUsedEntity(client, now); + client.setClientLastUsed(clientLastUsed); + } else { + LocalDate lastUsed = clientLastUsed.getLastUsed(); + if (lastUsed.isBefore(now)) { + clientLastUsed.setLastUsed(now); + } + } + } } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/web/util/IamViewInfoInterceptor.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/web/util/IamViewInfoInterceptor.java index 5a7cdc865..06d4e458c 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/core/web/util/IamViewInfoInterceptor.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/web/util/IamViewInfoInterceptor.java @@ -42,6 +42,7 @@ public class IamViewInfoInterceptor implements HandlerInterceptor { public static final String RCAUTH_ENABLED_KEY = "iamRcauthEnabled"; public static final String RESOURCES_PATH_KEY = "resourcesPrefix"; public static final String CLIENT_DEFAULTS_PROPERTIES_KEY = "clientDefaultsProperties"; + public static final String CLIENT_TRACK_LAST_USED_KEY = "clientTrackLastUsed"; @Value("${iam.version}") String iamVersion; @@ -83,6 +84,8 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons request.setAttribute(RCAUTH_ENABLED_KEY, rcAuthProperties.isEnabled()); request.setAttribute(CLIENT_DEFAULTS_PROPERTIES_KEY, clientRegistrationProperties.getClientDefaults()); + + request.setAttribute(CLIENT_TRACK_LAST_USED_KEY, iamProperties.getClient().isTrackLastUsed()); if (iamProperties.getVersionedStaticResources().isEnableVersioning()) { request.setAttribute(RESOURCES_PATH_KEY, String.format("/resources/%s", gitCommitId)); diff --git a/iam-login-service/src/main/resources/application.yml b/iam-login-service/src/main/resources/application.yml index cbd2ac908..6d552a918 100644 --- a/iam-login-service/src/main/resources/application.yml +++ b/iam-login-service/src/main/resources/application.yml @@ -225,6 +225,9 @@ client-registration: default-id-token-validity-seconds: ${DEFAULT_ID_TOKEN_VALIDITY_SECONDS:600} default-refresh-token-validity-seconds: ${DEFAULT_REFRESH_TOKEN_VALIDITY_SECONDS:2592000} +client: + track-last-used: ${IAM_CLIENT_TRACK_LAST_USED:true} + management: health: redis: diff --git a/iam-login-service/src/main/webapp/WEB-INF/tags/iamHeader.tag b/iam-login-service/src/main/webapp/WEB-INF/tags/iamHeader.tag index e62cadd8f..61b29fce3 100644 --- a/iam-login-service/src/main/webapp/WEB-INF/tags/iamHeader.tag +++ b/iam-login-service/src/main/webapp/WEB-INF/tags/iamHeader.tag @@ -111,4 +111,8 @@ function getAccessTokenValiditySeconds() { function getRefreshTokenValiditySeconds() { return ${clientDefaultsProperties.defaultRefreshTokenValiditySeconds}; } + +function getClientTrackLastUsed() { + return ${clientTrackLastUsed}; +} diff --git a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/clientslist/clientslist.component.html b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/clientslist/clientslist.component.html index a2ab9cc5f..83cdb6218 100644 --- a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/clientslist/clientslist.component.html +++ b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/clientslist/clientslist.component.html @@ -77,6 +77,7 @@ Client name & id Created Dyn. registered + Last Used Information Actions @@ -105,6 +106,15 @@ {{ c.dynamically_registered }} + + +
+ {{c.last_used | date }} +
+
+ N/A +
+ diff --git a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/clientslist/clientslist.component.js b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/clientslist/clientslist.component.js index 58cc337dd..bf893ccb2 100644 --- a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/clientslist/clientslist.component.js +++ b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/clients/clientslist/clientslist.component.js @@ -26,6 +26,7 @@ self.resetFilter = resetFilter; self.onChangePage = onChangePage; self.deleteClient = deleteClient; + self.clientTrackLastUsed = getClientTrackLastUsed(); self.$onInit = function () { console.debug('ClientsListController.self', self); diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/client/last_used/ClientLastUsedTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/client/last_used/ClientLastUsedTests.java new file mode 100644 index 000000000..a70029ba8 --- /dev/null +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/client/last_used/ClientLastUsedTests.java @@ -0,0 +1,149 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare (INFN). 2016-2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package it.infn.mw.iam.test.client.last_used; + +import static java.util.Collections.emptyMap; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.time.LocalDate; +import java.util.Collections; +import java.util.Set; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.model.OAuth2AccessTokenEntity; +import org.mitre.oauth2.model.OAuth2RefreshTokenEntity; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.oauth2.provider.TokenRequest; +import org.springframework.test.context.junit4.SpringRunner; + +import it.infn.mw.iam.config.IamProperties; +import it.infn.mw.iam.test.api.tokens.TestTokensUtils; +import it.infn.mw.iam.test.util.annotation.IamMockMvcIntegrationTest; + +@SuppressWarnings("deprecation") +@RunWith(SpringRunner.class) +@IamMockMvcIntegrationTest +public class ClientLastUsedTests extends TestTokensUtils { + + public static final String POST_CLIENT = "post-client"; + public static final String TOKEN_LOOKUP_CLIENT = "token-lookup-client"; + public static final String TEST_347_USER = "test_347"; + public static final String[] SCOPES = { "offline_access" }; + + @Autowired + IamProperties iamProperties; + + @Test + public void testClientLastUsedCreationOnTokenCreation() { + // Initially, the last used is null + ClientDetailsEntity client = loadTestClient(TOKEN_LOOKUP_CLIENT); + assertNull(client.getClientLastUsed()); + + // When the last used date is not tracked, it is not updated and remains null + iamProperties.getClient().setTrackLastUsed(false); + buildAccessToken(client, TEST_347_USER, SCOPES); + assertNull(client.getClientLastUsed()); + + // When the last used date is tracked, it is created with the current date + iamProperties.getClient().setTrackLastUsed(true); + buildAccessToken(client, TEST_347_USER, SCOPES); + assertNotNull(client.getClientLastUsed()); + LocalDate lastUsed = client.getClientLastUsed().getLastUsed(); + LocalDate today = LocalDate.now(); + assertEquals(today, lastUsed); + } + + @Test + public void testLastUsedUpdateOnTokenCreation() { + iamProperties.getClient().setTrackLastUsed(true); + + // Initially, the last used date is set to the default value + ClientDetailsEntity client = loadTestClient(POST_CLIENT); + assertNotNull(client.getClientLastUsed()); + LocalDate lastUsed = client.getClientLastUsed().getLastUsed(); + LocalDate defaultDate = LocalDate.of(1994, 3, 19); + assertEquals(defaultDate, lastUsed); + + // After creating a token, the last used date is updated + buildAccessToken(client, TEST_347_USER, SCOPES); + assertNotNull(client.getClientLastUsed()); + lastUsed = client.getClientLastUsed().getLastUsed(); + LocalDate today = LocalDate.now(); + assertEquals(today, lastUsed); + } + + @Test + public void testClientLastUsedCreationOnTokenRefresh() { + iamProperties.getClient().setTrackLastUsed(false); + + ClientDetailsEntity client = loadTestClient(TOKEN_LOOKUP_CLIENT); + assertTrue(client.isAllowRefresh()); + + // Initially, the last used date is null + OAuth2AccessTokenEntity accessToken = buildAccessToken(client, TEST_347_USER, SCOPES); + assertNull(client.getClientLastUsed()); + + // After refreshing the access token, the last used date is created with the + // current date + iamProperties.getClient().setTrackLastUsed(true); + OAuth2RefreshTokenEntity refreshToken = accessToken.getRefreshToken(); + TokenRequest tokenRequest = new TokenRequest(emptyMap(), TOKEN_LOOKUP_CLIENT, Collections.emptySet(), ""); + tokenService.refreshAccessToken(refreshToken.getValue(), tokenRequest); + assertNotNull(client.getClientLastUsed()); + LocalDate lastUsed = client.getClientLastUsed().getLastUsed(); + LocalDate today = LocalDate.now(); + assertEquals(today, lastUsed); + } + + @Test + public void testClientLastUsedUpdateOnTokenRefresh() { + iamProperties.getClient().setTrackLastUsed(false); + + // Get a client with a default last used date and able to generate refresh + // tokens + ClientDetailsEntity client = loadTestClient(POST_CLIENT); + client.setGrantTypes(Set.of("refresh_token")); + assertTrue(client.isAllowRefresh()); + + // Initially, the last used date is set to the default value + assertNotNull(client.getClientLastUsed()); + LocalDate lastUsed = client.getClientLastUsed().getLastUsed(); + LocalDate defaultDate = LocalDate.of(1994, 3, 19); + assertEquals(defaultDate, lastUsed); + + // After creating an access token, the last used date is not updated + OAuth2AccessTokenEntity accessToken = buildAccessToken(client, TEST_347_USER, SCOPES); + assertNotNull(client.getClientLastUsed()); + lastUsed = client.getClientLastUsed().getLastUsed(); + assertEquals(defaultDate, lastUsed); + + // After refreshing the access token, the last used date is updated + iamProperties.getClient().setTrackLastUsed(true); + OAuth2RefreshTokenEntity refreshToken = accessToken.getRefreshToken(); + TokenRequest tokenRequest = new TokenRequest(emptyMap(), POST_CLIENT, Collections.emptySet(), ""); + tokenService.refreshAccessToken(refreshToken.getValue(), tokenRequest); + assertNotNull(client.getClientLastUsed()); + lastUsed = client.getClientLastUsed().getLastUsed(); + LocalDate today = LocalDate.now(); + assertEquals(today, lastUsed); + } + +} diff --git a/iam-persistence/src/main/resources/db/migration/h2/V102__client_last_used.sql b/iam-persistence/src/main/resources/db/migration/h2/V102__client_last_used.sql new file mode 100644 index 000000000..7ee2679d6 --- /dev/null +++ b/iam-persistence/src/main/resources/db/migration/h2/V102__client_last_used.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS client_last_used ( + client_details_id BIGINT PRIMARY KEY, + last_used TIMESTAMP NOT NULL); + +ALTER TABLE client_last_used ADD CONSTRAINT fk_client_last_used FOREIGN KEY (client_details_id) REFERENCES client_details(id); diff --git a/iam-persistence/src/main/resources/db/migration/mysql/V102__client_last_used.sql b/iam-persistence/src/main/resources/db/migration/mysql/V102__client_last_used.sql new file mode 100644 index 000000000..1a5ae0518 --- /dev/null +++ b/iam-persistence/src/main/resources/db/migration/mysql/V102__client_last_used.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS client_last_used ( + client_details_id BIGINT PRIMARY KEY, + last_used TIMESTAMP NOT NULL); + +ALTER TABLE client_last_used ADD CONSTRAINT fk_client_last_used FOREIGN KEY (client_details_id) REFERENCES client_details(id); \ No newline at end of file diff --git a/iam-persistence/src/main/resources/db/migration/test/V100000___test_data.sql b/iam-persistence/src/main/resources/db/migration/test/V100000___test_data.sql index 3651230d2..0a2303505 100644 --- a/iam-persistence/src/main/resources/db/migration/test/V100000___test_data.sql +++ b/iam-persistence/src/main/resources/db/migration/test/V100000___test_data.sql @@ -1528,3 +1528,10 @@ insert into iam_account_client(id, account_id, client_id, creation_time) VALUES -- TOTP multi-factor secrets insert into iam_totp_mfa(active, secret, creation_time, last_update_time, account_id) VALUES (true, 'secret', CURRENT_TIMESTAMP(), CURRENT_TIMESTAMP(), 1000); + +-- Client last used dates +insert into client_last_used(client_details_id, last_used) VALUES +(1, '1994-03-21'), +(2, '1994-03-20'), +(3, '1994-03-19'), +(4, '1994-03-23'); diff --git a/pom.xml b/pom.xml index 90a3c11ac..79fcb5a7d 100644 --- a/pom.xml +++ b/pom.xml @@ -50,7 +50,7 @@ 1.16.2 - 1.3.6.cnaf-20240119 + 1.3.6.cnaf-20240207 2.5.2.RELEASE 3.3.2