diff --git a/docs/admin/auth-server/tokens/oauth-refresh-tokens.md b/docs/admin/auth-server/tokens/oauth-refresh-tokens.md index e8e70db0e76..07435d51027 100644 --- a/docs/admin/auth-server/tokens/oauth-refresh-tokens.md +++ b/docs/admin/auth-server/tokens/oauth-refresh-tokens.md @@ -49,4 +49,8 @@ By default AS always creates new Refresh Token on refresh call to Token Endpoint To revoke a token, a client can do so via the [revocation endpoint][../endpoints/token-revocation] (including revocation of all tokens by `client_id`). +### online_access scope + +If `online_access` scope is present then refresh token expires when the session ends (for example via front channel logout). + diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/common/ScopeConstants.java b/jans-auth-server/model/src/main/java/io/jans/as/model/common/ScopeConstants.java index 9ad00c48a52..4c825cfb4a5 100644 --- a/jans-auth-server/model/src/main/java/io/jans/as/model/common/ScopeConstants.java +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/common/ScopeConstants.java @@ -13,6 +13,7 @@ public class ScopeConstants { public static final String OPENID = "openid"; public static final String OFFLINE_ACCESS = "offline_access"; + public static final String ONLINE_ACCESS = "online_access"; public static final String DEVICE_SSO = "device_sso"; private ScopeConstants() { diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebServiceImpl.java b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebServiceImpl.java index ca4f43db959..52b58f184a2 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebServiceImpl.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebServiceImpl.java @@ -478,7 +478,7 @@ private ResponseBuilder authorize(AuthzRequest authzRequest) throws AcrChangedEx addCustomHeaders(builder, authzRequest); updateSession(authzRequest, sessionUser); - runCiba(authzRequest.getAuthReqId(), client, authzRequest.getHttpRequest(), authzRequest.getHttpResponse()); + runCiba(authzRequest, client); processDeviceAuthorization(deviceAuthzUserCode, user); return builder; @@ -737,7 +737,8 @@ private String getAcrForGrant(String acrValuesStr, SessionId sessionUser) { return StringUtils.isNotBlank(acr) ? acr : acrValuesStr; } - private void runCiba(String authReqId, Client client, HttpServletRequest httpRequest, HttpServletResponse httpResponse) { + private void runCiba(AuthzRequest authzRequest, Client client) { + String authReqId = authzRequest.getAuthReqId(); if (StringUtils.isBlank(authReqId)) { return; } @@ -752,17 +753,18 @@ private void runCiba(String authReqId, Client client, HttpServletRequest httpReq cibaRequestService.removeCibaRequest(authReqId); CIBAGrant cibaGrant = authorizationGrantList.createCIBAGrant(cibaRequest); - ExecutionContext executionContext = new ExecutionContext(httpRequest, httpResponse); + ExecutionContext executionContext = new ExecutionContext(authzRequest.getHttpRequest(), authzRequest.getHttpResponse()); executionContext.setAppConfiguration(appConfiguration); executionContext.setAttributeService(attributeService); executionContext.setGrant(cibaGrant); executionContext.setClient(client); - executionContext.setCertAsPem(httpRequest.getHeader("X-ClientCert")); + executionContext.setCertAsPem(authzRequest.getHttpRequest().getHeader("X-ClientCert")); + executionContext.setScopes(StringUtils.isNotBlank(authzRequest.getScope()) ? new HashSet<>(Arrays.asList(authzRequest.getScope().split(" "))) : new HashSet<>()); AccessToken accessToken = cibaGrant.createAccessToken(executionContext); log.debug("Issuing access token: {}", accessToken.getCode()); - ExternalUpdateTokenContext context = new ExternalUpdateTokenContext(httpRequest, cibaGrant, client, appConfiguration, attributeService); + ExternalUpdateTokenContext context = new ExternalUpdateTokenContext(authzRequest.getHttpRequest(), cibaGrant, client, appConfiguration, attributeService); final int refreshTokenLifetimeInSeconds = externalUpdateTokenService.getRefreshTokenLifetimeInSeconds(context); diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/AuthorizationGrant.java b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/AuthorizationGrant.java index 57d0048cf2f..6db5c7f607d 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/AuthorizationGrant.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/AuthorizationGrant.java @@ -11,6 +11,7 @@ import io.jans.as.common.model.common.User; import io.jans.as.common.model.registration.Client; import io.jans.as.common.service.AttributeService; +import io.jans.as.model.common.ScopeConstants; import io.jans.as.model.config.WebKeysConfiguration; import io.jans.as.model.crypto.signature.SignatureAlgorithm; import io.jans.as.model.jwt.Jwt; @@ -304,6 +305,10 @@ private RefreshToken saveRefreshToken(RefreshToken refreshToken, ExecutionContex return null; } + if (executionContext.getScopes().contains(ScopeConstants.ONLINE_ACCESS)) { + entity.getAttributes().setOnlineAccess(true); + } + persist(entity); statService.reportRefreshToken(getGrantType()); metricService.incCounter(MetricType.TOKEN_REFRESH_TOKEN_COUNT); diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/model/ldap/TokenAttributes.java b/jans-auth-server/server/src/main/java/io/jans/as/server/model/ldap/TokenAttributes.java index 902dba2b81f..606356195a4 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/model/ldap/TokenAttributes.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/model/ldap/TokenAttributes.java @@ -23,6 +23,8 @@ public class TokenAttributes implements Serializable { @JsonProperty("x5cs256") private String x5cs256; + @JsonProperty("online_access") + private boolean onlineAccess; @JsonProperty("attributes") private Map attributes; @@ -43,11 +45,20 @@ public void setX5cs256(String x5cs256) { this.x5cs256 = x5cs256; } + public boolean isOnlineAccess() { + return onlineAccess; + } + + public void setOnlineAccess(boolean onlineAccess) { + this.onlineAccess = onlineAccess; + } + @Override public String toString() { return "TokenAttributes{" + "attributes='" + attributes + '\'' + "x5cs256='" + x5cs256 + '\'' + + "onlineAccess='" + onlineAccess + '\'' + '}'; } } diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/service/GrantService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/service/GrantService.java index 07ef70a6997..b96ba0484e8 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/service/GrantService.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/service/GrantService.java @@ -18,13 +18,13 @@ import io.jans.orm.search.filter.Filter; import io.jans.service.CacheService; import io.jans.service.cache.CacheConfiguration; +import jakarta.ejb.Stateless; +import jakarta.inject.Inject; +import jakarta.inject.Named; import org.apache.commons.lang.BooleanUtils; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; -import jakarta.ejb.Stateless; -import jakarta.inject.Inject; -import jakarta.inject.Named; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -196,19 +196,26 @@ public List getGrantsBySessionDn(String sessionDn) { public void logout(String sessionDn) { final List tokens = getGrantsBySessionDn(sessionDn); - if (BooleanUtils.isFalse(appConfiguration.getRemoveRefreshTokensForClientOnLogout())) { - List refreshTokens = Lists.newArrayList(); - for (TokenEntity token : tokens) { - if (token.getTokenTypeEnum() == TokenType.REFRESH_TOKEN) { - refreshTokens.add(token); - } - } - if (!refreshTokens.isEmpty()) { - log.trace("Refresh tokens are not removed on logout (because removeRefreshTokensForClientOnLogout configuration property is false)"); - tokens.removeAll(refreshTokens); + filterOutRefreshTokenFromDeletion(tokens); + removeSilently(tokens); + } + + public void filterOutRefreshTokenFromDeletion(List tokens) { + if (BooleanUtils.isTrue(appConfiguration.getRemoveRefreshTokensForClientOnLogout())) { + return; + } + + List refreshTokensForExclusion = Lists.newArrayList(); + + for (TokenEntity token : tokens) { + if (token.getTokenTypeEnum() == TokenType.REFRESH_TOKEN && !token.getAttributes().isOnlineAccess()) { + refreshTokensForExclusion.add(token); } } - removeSilently(tokens); + if (!refreshTokensForExclusion.isEmpty()) { + log.trace("Refresh tokens are not removed on logout (because removeRefreshTokensForClientOnLogout configuration property is false or online_access scope is used)."); + tokens.removeAll(refreshTokensForExclusion); + } } public void removeAllTokensBySession(String sessionDn) { diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenRestWebServiceImpl.java b/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenRestWebServiceImpl.java index 44ad15c5d7a..eeb2452a595 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenRestWebServiceImpl.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenRestWebServiceImpl.java @@ -64,7 +64,9 @@ import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; +import java.util.Arrays; import java.util.Date; +import java.util.HashSet; import java.util.function.Function; import static io.jans.as.model.config.Constants.*; @@ -195,6 +197,7 @@ public Response requestAccessToken(String grantType, String code, executionContext.setAppConfiguration(appConfiguration); executionContext.setAttributeService(attributeService); executionContext.setAuditLog(auditLog); + executionContext.setScopes(StringUtils.isNotBlank(scope) ? new HashSet<>(Arrays.asList(scope.split(" "))) : new HashSet<>()); if (gt == GrantType.AUTHORIZATION_CODE) { return processAuthorizationCode(code, scope, codeVerifier, sessionIdObj, executionContext); diff --git a/jans-auth-server/server/src/test/java/io/jans/as/server/service/GrantServiceTest.java b/jans-auth-server/server/src/test/java/io/jans/as/server/service/GrantServiceTest.java new file mode 100644 index 00000000000..973054276c9 --- /dev/null +++ b/jans-auth-server/server/src/test/java/io/jans/as/server/service/GrantServiceTest.java @@ -0,0 +1,102 @@ +package io.jans.as.server.service; + +import io.jans.as.model.config.StaticConfiguration; +import io.jans.as.model.configuration.AppConfiguration; +import io.jans.as.server.model.ldap.TokenEntity; +import io.jans.as.server.model.ldap.TokenType; +import io.jans.orm.PersistenceEntryManager; +import io.jans.service.CacheService; +import io.jans.service.cache.CacheConfiguration; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.testng.MockitoTestNGListener; +import org.slf4j.Logger; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.testng.Assert.*; + +/** + * @author Yuriy Z + */ +@Listeners(MockitoTestNGListener.class) +public class GrantServiceTest { + + @InjectMocks + private GrantService grantService; + + @Mock + private Logger log; + + @Mock + private PersistenceEntryManager persistenceEntryManager; + + @Mock + private ClientService clientService; + + @Mock + private CacheService cacheService; + + @Mock + private StaticConfiguration staticConfiguration; + + @Mock + private AppConfiguration appConfiguration; + + @Mock + private CacheConfiguration cacheConfiguration; + + @Test + public void filterOutRefreshTokenFromDeletion_forTokenWithoutOnlineAccess_shouldFilterOut() { + Mockito.doReturn(false).when(appConfiguration).getRemoveRefreshTokensForClientOnLogout(); + + TokenEntity token = new TokenEntity(); + token.setTokenTypeEnum(TokenType.REFRESH_TOKEN); + token.getAttributes().setOnlineAccess(false); + + List tokens = new ArrayList<>(); + tokens.add(token); + + grantService.filterOutRefreshTokenFromDeletion(tokens); + assertTrue(tokens.isEmpty()); + } + + @Test + public void filterOutRefreshTokenFromDeletion_forTokenWithOnlineAccess_shouldNotFilterOut() { + Mockito.doReturn(false).when(appConfiguration).getRemoveRefreshTokensForClientOnLogout(); + + TokenEntity token = new TokenEntity(); + token.setTokenTypeEnum(TokenType.REFRESH_TOKEN); + token.getAttributes().setOnlineAccess(true); + + List tokens = new ArrayList<>(); + tokens.add(token); + + grantService.filterOutRefreshTokenFromDeletion(tokens); + assertFalse(tokens.isEmpty()); + } + + @Test + public void filterOutRefreshTokenFromDeletion_whenConfigurationRemoveRefreshTokensForClientOnLogoutIsTrue_shouldNotFilterOut() { + Mockito.doReturn(true).when(appConfiguration).getRemoveRefreshTokensForClientOnLogout(); + + TokenEntity token = new TokenEntity(); + token.setTokenTypeEnum(TokenType.REFRESH_TOKEN); + token.getAttributes().setOnlineAccess(false); + + TokenEntity another = new TokenEntity(); + another.setTokenTypeEnum(TokenType.REFRESH_TOKEN); + another.getAttributes().setOnlineAccess(true); + + List tokens = new ArrayList<>(); + tokens.add(token); + tokens.add(another); + + grantService.filterOutRefreshTokenFromDeletion(tokens); + assertEquals(tokens.size(), 2); + } +} diff --git a/jans-auth-server/server/src/test/resources/testng.xml b/jans-auth-server/server/src/test/resources/testng.xml index dbaf66c846c..a56af38c426 100644 --- a/jans-auth-server/server/src/test/resources/testng.xml +++ b/jans-auth-server/server/src/test/resources/testng.xml @@ -19,6 +19,7 @@ + diff --git a/jans-linux-setup/jans_setup/openbanking/templates/scopes.ldif b/jans-linux-setup/jans_setup/openbanking/templates/scopes.ldif index 578d34b8d37..db590c373a1 100644 --- a/jans-linux-setup/jans_setup/openbanking/templates/scopes.ldif +++ b/jans-linux-setup/jans_setup/openbanking/templates/scopes.ldif @@ -63,6 +63,17 @@ jansScopeTyp: openid objectClass: top objectClass: jansScope +dn: inum=C4F4,ou=scopes,o=jans +description: Expire OAuth 2.0 Refresh Token when session ends. +displayName: online_access +inum: C4F4 +jansAttrs: {"spontaneousClientId":"","spontaneousClientScopes":[],"showInConfigurationEndpoint":true} +jansDefScope: true +jansId: online_access +jansScopeTyp: openid +objectClass: top +objectClass: jansScope + dn: inum=43F1,ou=scopes,o=jans description: View your basic profile info. displayName: view_profile diff --git a/jans-linux-setup/jans_setup/templates/scopes.ldif b/jans-linux-setup/jans_setup/templates/scopes.ldif index 99d68a948f1..8c3ec20b9b6 100644 --- a/jans-linux-setup/jans_setup/templates/scopes.ldif +++ b/jans-linux-setup/jans_setup/templates/scopes.ldif @@ -142,6 +142,17 @@ jansScopeTyp: openid objectClass: top objectClass: jansScope +dn: inum=C4F4,ou=scopes,o=jans +description: Expire OAuth 2.0 Refresh Token when session ends. +displayName: online_access +inum: C4F4 +jansAttrs: {"spontaneousClientId":"","spontaneousClientScopes":[],"showInConfigurationEndpoint":true} +jansDefScope: true +jansId: online_access +jansScopeTyp: openid +objectClass: top +objectClass: jansScope + dn: inum=C4F5,ou=scopes,o=jans description: View your user permission and roles. displayName: view_user_permissions_roles