Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 3 commits into from
Mar 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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