Skip to content

Commit

Permalink
feat(jans-auth-server): added online_access scope to issue session bo…
Browse files Browse the repository at this point in the history
…und refresh token #3012 (#4106)

* feat(jans-auth-server): added online_access scope to issue session bound refresh token #3012

* feat(jans-auth-server): added test for online_access scope implementation #3012

* feat(jans-auth-server): added doc about online_access scope #3012
  • Loading branch information
yuriyz authored Mar 10, 2023
1 parent c5b89ce commit 635f611
Show file tree
Hide file tree
Showing 11 changed files with 177 additions and 19 deletions.
4 changes: 4 additions & 0 deletions docs/admin/auth-server/tokens/oauth-refresh-tokens.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).


Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public class TokenAttributes implements Serializable {

@JsonProperty("x5cs256")
private String x5cs256;
@JsonProperty("online_access")
private boolean onlineAccess;
@JsonProperty("attributes")
private Map<String, String> attributes;

Expand All @@ -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 + '\'' +
'}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -196,19 +196,26 @@ public List<TokenEntity> getGrantsBySessionDn(String sessionDn) {

public void logout(String sessionDn) {
final List<TokenEntity> tokens = getGrantsBySessionDn(sessionDn);
if (BooleanUtils.isFalse(appConfiguration.getRemoveRefreshTokensForClientOnLogout())) {
List<TokenEntity> 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<TokenEntity> tokens) {
if (BooleanUtils.isTrue(appConfiguration.getRemoveRefreshTokensForClientOnLogout())) {
return;
}

List<TokenEntity> 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<TokenEntity> 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<TokenEntity> 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<TokenEntity> tokens = new ArrayList<>();
tokens.add(token);
tokens.add(another);

grantService.filterOutRefreshTokenFromDeletion(tokens);
assertEquals(tokens.size(), 2);
}
}
1 change: 1 addition & 0 deletions jans-auth-server/server/src/test/resources/testng.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<class name="io.jans.as.server.service.external.ExternalAuthenticationServiceTest" />
<class name="io.jans.as.server.servlet.OpenIdConfigurationTest" />
<class name="io.jans.as.server.service.net.UriServiceTest" />
<class name="io.jans.as.server.service.GrantServiceTest" />

<class name="io.jans.as.server.token.ws.rs.TokenExchangeServiceTest" />
<class name="io.jans.as.server.token.ws.rs.TokenRestWebServiceValidatorTest" />
Expand Down
11 changes: 11 additions & 0 deletions jans-linux-setup/jans_setup/openbanking/templates/scopes.ldif
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions jans-linux-setup/jans_setup/templates/scopes.ldif
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 635f611

Please sign in to comment.