KEYCLOAK-17202 Restrict Issuance of Refresh tokens to specific clients

This commit is contained in:
Michito Okai 2021-03-05 14:12:38 +09:00 committed by Marek Posolda
parent 8b0b657a8f
commit d9ebbe4958
17 changed files with 295 additions and 17 deletions

View file

@ -71,6 +71,7 @@ import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.oidc.TokenManager.AccessTokenResponseBuilder;
@ -361,11 +362,13 @@ public class AuthorizationTokenService {
// Skip generating refresh token for accessToken without sessionState claim. This is "stateless" accessToken not pointing to any real persistent userSession
rpt.setSessionState(null);
} else {
responseBuilder.generateRefreshToken();
RefreshToken refreshToken = responseBuilder.getRefreshToken();
if (OIDCAdvancedConfigWrapper.fromClientModel(client).isUseRefreshToken()) {
responseBuilder.generateRefreshToken();
RefreshToken refreshToken = responseBuilder.getRefreshToken();
refreshToken.issuedFor(client.getClientId());
refreshToken.setAuthorization(authorization);
refreshToken.issuedFor(client.getClientId());
refreshToken.setAuthorization(authorization);
}
}
if (!rpt.hasAudience(targetClient.getClientId())) {

View file

@ -131,6 +131,16 @@ public class OIDCAdvancedConfigWrapper {
setAttribute(OIDCConfigAttributes.USE_MTLS_HOK_TOKEN, val);
}
public boolean isUseRefreshToken() {
String useRefreshToken = getAttribute(OIDCConfigAttributes.USE_REFRESH_TOKEN, "true");
return Boolean.parseBoolean(useRefreshToken);
}
public void setUseRefreshToken(boolean useRefreshToken) {
String val = String.valueOf(useRefreshToken);
setAttribute(OIDCConfigAttributes.USE_REFRESH_TOKEN, val);
}
/**
* If true, then Client Credentials Grant generates refresh token and creates user session. This is not per specs, so it is false by default
* For the details @see https://tools.ietf.org/html/rfc6749#section-4.4.3

View file

@ -62,6 +62,8 @@ public final class OIDCConfigAttributes {
public static final String USE_REFRESH_TOKEN_FOR_CLIENT_CREDENTIALS_GRANT = "client_credentials.use_refresh_token";
public static final String USE_REFRESH_TOKEN = "use.refresh.tokens";
private OIDCConfigAttributes() {
}

View file

@ -337,11 +337,14 @@ public class TokenManager {
validation.newToken.setAuthorization(refreshToken.getAuthorization());
}
AccessTokenResponseBuilder responseBuilder = responseBuilder(realm, authorizedClient, event, session, validation.userSession, validation.clientSessionCtx)
.accessToken(validation.newToken)
.generateRefreshToken();
AccessTokenResponseBuilder responseBuilder = responseBuilder(realm, authorizedClient, event, session,
validation.userSession, validation.clientSessionCtx).accessToken(validation.newToken);
if (OIDCAdvancedConfigWrapper.fromClientModel(authorizedClient).isUseRefreshToken()) {
responseBuilder.generateRefreshToken();
}
if (validation.newToken.getAuthorization() != null) {
if (validation.newToken.getAuthorization() != null
&& OIDCAdvancedConfigWrapper.fromClientModel(authorizedClient).isUseRefreshToken()) {
responseBuilder.getRefreshToken().setAuthorization(validation.newToken.getAuthorization());
}
@ -351,7 +354,9 @@ public class TokenManager {
AccessToken.CertConf certConf = refreshToken.getCertConf();
if (certConf != null) {
responseBuilder.getAccessToken().setCertConf(certConf);
responseBuilder.getRefreshToken().setCertConf(certConf);
if (OIDCAdvancedConfigWrapper.fromClientModel(authorizedClient).isUseRefreshToken()) {
responseBuilder.getRefreshToken().setCertConf(certConf);
}
}
String scopeParam = clientSession.getNote(OAuth2Constants.SCOPE);

View file

@ -457,8 +457,10 @@ public class TokenEndpoint {
AccessToken token = tokenManager.createClientAccessToken(session, realm, client, user, userSession, clientSessionCtx);
TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager
.responseBuilder(realm, client, event, session, userSession, clientSessionCtx).accessToken(token)
.generateRefreshToken();
.responseBuilder(realm, client, event, session, userSession, clientSessionCtx).accessToken(token);
if (OIDCAdvancedConfigWrapper.fromClientModel(client).isUseRefreshToken()) {
responseBuilder.generateRefreshToken();
}
// KEYCLOAK-6771 Certificate Bound Token
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-3
@ -466,7 +468,9 @@ public class TokenEndpoint {
AccessToken.CertConf certConf = MtlsHoKTokenUtil.bindTokenWithClientCertificate(request, session);
if (certConf != null) {
responseBuilder.getAccessToken().setCertConf(certConf);
responseBuilder.getRefreshToken().setCertConf(certConf);
if (OIDCAdvancedConfigWrapper.fromClientModel(client).isUseRefreshToken()) {
responseBuilder.getRefreshToken().setCertConf(certConf);
}
} else {
event.error(Errors.INVALID_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST,
@ -684,9 +688,11 @@ public class TokenEndpoint {
UserSessionModel userSession = processor.getUserSession();
updateUserSessionFromClientAuth(userSession);
TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager.responseBuilder(realm, client, event, session, userSession, clientSessionCtx)
.generateAccessToken()
.generateRefreshToken();
TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager
.responseBuilder(realm, client, event, session, userSession, clientSessionCtx).generateAccessToken();
if (OIDCAdvancedConfigWrapper.fromClientModel(client).isUseRefreshToken()) {
responseBuilder.generateRefreshToken();
}
String scopeParam = clientSessionCtx.getClientSession().getNote(OAuth2Constants.SCOPE);
if (TokenUtil.isOIDCRequest(scopeParam)) {
@ -1025,7 +1031,8 @@ public class TokenEndpoint {
responseBuilder.getAccessToken().addAudience(audience);
}
if (requestedTokenType.equals(OAuth2Constants.REFRESH_TOKEN_TYPE)) {
if (requestedTokenType.equals(OAuth2Constants.REFRESH_TOKEN_TYPE)
&& OIDCAdvancedConfigWrapper.fromClientModel(client).isUseRefreshToken()) {
responseBuilder.generateRefreshToken();
responseBuilder.getRefreshToken().issuedFor(client.getClientId());
}

View file

@ -321,7 +321,9 @@ public class DescriptionConverter {
if (client.getAuthorizationServicesEnabled() != null && client.getAuthorizationServicesEnabled()) {
grantTypes.add(OAuth2Constants.UMA_GRANT_TYPE);
}
grantTypes.add(OAuth2Constants.REFRESH_TOKEN);
if (OIDCAdvancedConfigWrapper.fromClientRepresentation(client).isUseRefreshToken()) {
grantTypes.add(OAuth2Constants.REFRESH_TOKEN);
}
return grantTypes;
}

View file

@ -24,6 +24,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.mappers.AbstractPairwiseSubMapper;
import org.keycloak.protocol.oidc.mappers.PairwiseSubMapperHelper;
import org.keycloak.protocol.oidc.mappers.SHA256PairwiseSubMapper;
@ -79,6 +80,10 @@ public class OIDCClientRegistrationProvider extends AbstractClientRegistrationPr
client.setAuthorizationServicesEnabled(true);
}
if (!(grantTypes == null || grantTypes.contains(OAuth2Constants.REFRESH_TOKEN))) {
OIDCAdvancedConfigWrapper.fromClientRepresentation(client).setUseRefreshToken(false);
}
OIDCClientRegistrationContext oidcContext = new OIDCClientRegistrationContext(session, client, this, clientOIDC);
client = create(oidcContext);

View file

@ -21,6 +21,7 @@ import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@ -55,9 +56,11 @@ import org.keycloak.authorization.client.AuthorizationDeniedException;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.representation.TokenIntrospectionResponse;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.authorization.AuthorizationResponse;
import org.keycloak.representations.idm.authorization.JSPolicyRepresentation;
@ -551,6 +554,25 @@ public class UmaGrantTypeTest extends AbstractResourceServerTest {
assertThat((Collection<String>) claim.get("scopes"), containsInAnyOrder("ScopeA", "ScopeB"));
}
@Test
public void testNoRefreshToken() {
ClientResource client = getClient(getRealm());
ClientRepresentation clientRepresentation = client.toRepresentation();
clientRepresentation.getAttributes().put(OIDCConfigAttributes.USE_REFRESH_TOKEN, "false");
client.update(clientRepresentation);
AccessTokenResponse accessTokenResponse = getAuthzClient().obtainAccessToken("marta", "password");
AuthorizationResponse response = authorize(null, null, null, null, accessTokenResponse.getToken(), null, null,
new PermissionRequest("Resource A", "ScopeA", "ScopeB"));
String rpt = response.getToken();
String refreshToken = response.getRefreshToken();
assertNotNull(rpt);
assertNull(refreshToken);
clientRepresentation.getAttributes().put(OIDCConfigAttributes.USE_REFRESH_TOKEN, "true");
client.update(clientRepresentation);
}
private String getIdToken(String username, String password) {
oauth.realm("authz-test");
oauth.clientId("test-app");

View file

@ -562,4 +562,49 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
OIDCAdvancedConfigWrapper config = OIDCAdvancedConfigWrapper.fromClientRepresentation(kcClient);
Assert.assertNames(config.getRequestUris(), "http://host/foo", "https://host2/bar");
}
@Test
public void testClientWithoutRefreshToken() throws Exception {
OIDCClientRepresentation clientRep = null;
OIDCClientRepresentation response = null;
clientRep = createRep();
clientRep.setGrantTypes(Arrays.asList(OAuth2Constants.AUTHORIZATION_CODE));
response = reg.oidc().create(clientRep);
// Test Keycloak representation
ClientRepresentation kcClient = getClient(response.getClientId());
OIDCAdvancedConfigWrapper config = OIDCAdvancedConfigWrapper.fromClientRepresentation(kcClient);
Assert.assertFalse(config.isUseRefreshToken());
}
@Test
public void testClientWithRefreshToken() throws Exception {
OIDCClientRepresentation clientRep = null;
OIDCClientRepresentation response = null;
clientRep = createRep();
clientRep.setGrantTypes(Arrays.asList(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.REFRESH_TOKEN));
response = reg.oidc().create(clientRep);
// Test Keycloak representation
ClientRepresentation kcClient = getClient(response.getClientId());
OIDCAdvancedConfigWrapper config = OIDCAdvancedConfigWrapper.fromClientRepresentation(kcClient);
Assert.assertTrue(config.isUseRefreshToken());
}
@Test
public void testClientWithoutGrantTypes() throws Exception {
OIDCClientRepresentation response = create();
assertTrue(CollectionUtil.collectionEquals(
Arrays.asList(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.REFRESH_TOKEN), response.getGrantTypes()));
// Test Keycloak representation
ClientRepresentation kcClient = getClient(response.getClientId());
OIDCAdvancedConfigWrapper config = OIDCAdvancedConfigWrapper.fromClientRepresentation(kcClient);
Assert.assertTrue(config.isUseRefreshToken());
}
}

View file

@ -1211,6 +1211,27 @@ public class AccessTokenTest extends AbstractKeycloakTest {
}
}
@Test
public void accessTokenRequestNoRefreshToken() {
ClientResource client = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app");
ClientRepresentation clientRepresentation = client.toRepresentation();
clientRepresentation.getAttributes().put(OIDCConfigAttributes.USE_REFRESH_TOKEN, "false");
client.update(clientRepresentation);
oauth.doLogin("test-user@localhost", "password");
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
assertEquals(200, response.getStatusCode());
assertNotNull(response.getAccessToken());
assertNull(response.getRefreshToken());
clientRepresentation.getAttributes().put(OIDCConfigAttributes.USE_REFRESH_TOKEN, "true");
client.update(clientRepresentation);
}
@Test
public void accessTokenRequest_ClientPS384_RealmRS256() throws Exception {
conductAccessTokenRequest(Algorithm.HS256, Algorithm.PS384, Algorithm.RS256);

View file

@ -21,6 +21,7 @@ import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.TokenVerifier;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.common.Profile;
@ -31,10 +32,12 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation;
import org.keycloak.representations.idm.authorization.DecisionStrategy;
@ -43,6 +46,7 @@ import org.keycloak.services.resources.admin.permissions.AdminPermissions;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.DisableFeature;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
@ -61,6 +65,7 @@ import java.util.Map;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_ID;
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_USERNAME;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
@ -171,12 +176,23 @@ public class ClientTokenExchangeTest extends AbstractKeycloakTest {
directNoSecret.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
directNoSecret.setFullScopeAllowed(false);
ClientModel noRefreshToken = realm.addClient("no-refresh-token");
noRefreshToken.setClientId("no-refresh-token");
noRefreshToken.setPublicClient(false);
noRefreshToken.setDirectAccessGrantsEnabled(true);
noRefreshToken.setEnabled(true);
noRefreshToken.setSecret("secret");
noRefreshToken.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
noRefreshToken.setFullScopeAllowed(false);
noRefreshToken.getAttributes().put(OIDCConfigAttributes.USE_REFRESH_TOKEN, "false");
// permission for client to client exchange to "target" client
ClientPolicyRepresentation clientRep = new ClientPolicyRepresentation();
clientRep.setName("to");
clientRep.addClient(clientExchanger.getId());
clientRep.addClient(legal.getId());
clientRep.addClient(directLegal.getId());
clientRep.addClient(noRefreshToken.getId());
ResourceServer server = management.realmResourceServer();
Policy clientPolicy = management.authz().getStoreFactory().getPolicyStore().create(clientRep, server);
@ -467,6 +483,41 @@ public class ClientTokenExchangeTest extends AbstractKeycloakTest {
}
}
@Test
@UncaughtServerErrorExpected
public void testExchangeNoRefreshToken() throws Exception {
testingClient.server().run(ClientTokenExchangeTest::setupRealm);
oauth.realm(TEST);
oauth.clientId("client-exchanger");
ClientResource client = ApiUtil.findClientByClientId(adminClient.realm(TEST), "no-refresh-token");
ClientRepresentation clientRepresentation = client.toRepresentation();
clientRepresentation.getAttributes().put(OIDCConfigAttributes.USE_REFRESH_TOKEN, "false");
client.update(clientRepresentation);
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "user", "password");
String accessToken = response.getAccessToken();
{
response = oauth.doTokenExchange(TEST, accessToken, "target", "client-exchanger", "secret");
String exchangedTokenString = response.getAccessToken();
String refreshTokenString = response.getRefreshToken();
assertNotNull(exchangedTokenString);
assertNotNull(refreshTokenString);
}
{
response = oauth.doTokenExchange(TEST, accessToken, "target", "no-refresh-token", "secret");
String exchangedTokenString = response.getAccessToken();
String refreshTokenString = response.getRefreshToken();
assertNotNull(exchangedTokenString);
assertNull(refreshTokenString);
}
clientRepresentation.getAttributes().put(OIDCConfigAttributes.USE_REFRESH_TOKEN, "true");
client.update(clientRepresentation);
}
private static void addDirectExchanger(KeycloakSession session) {
RealmModel realm = session.realms().getRealmByName(TEST);
RoleModel exampleRole = realm.addRole("example");

View file

@ -17,6 +17,7 @@
package org.keycloak.testsuite.oauth;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.keycloak.models.OAuth2DeviceConfig.DEFAULT_OAUTH2_DEVICE_CODE_LIFESPAN;
import static org.keycloak.models.OAuth2DeviceConfig.DEFAULT_OAUTH2_DEVICE_POLLING_INTERVAL;
@ -29,6 +30,7 @@ import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.models.OAuth2DeviceConfig;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
@ -150,6 +152,40 @@ public class OAuth2DeviceAuthorizationGrantTest extends AbstractKeycloakTest {
assertNotNull(token);
}
@Test
public void testNoRefreshToken() throws Exception {
ClientResource client = ApiUtil.findClientByClientId(adminClient.realm(REALM_NAME), DEVICE_APP);
ClientRepresentation clientRepresentation = client.toRepresentation();
clientRepresentation.getAttributes().put(OIDCConfigAttributes.USE_REFRESH_TOKEN, "false");
client.update(clientRepresentation);
// Device Authorization Request from device
oauth.realm(REALM_NAME);
oauth.clientId(DEVICE_APP);
OAuthClient.DeviceAuthorizationResponse response = oauth.doDeviceAuthorizationRequest(DEVICE_APP, "secret");
// Verify user code from verification page using browser
openVerificationPage(response.getVerificationUri());
verificationPage.submit(response.getUserCode());
// Do Login
oauth.fillLoginForm("device-login", "password");
// Consent
grantPage.accept();
// Token request from device
OAuthClient.AccessTokenResponse tokenResponse = oauth.doDeviceTokenRequest(DEVICE_APP, "secret",
response.getDeviceCode());
Assert.assertEquals(200, tokenResponse.getStatusCode());
assertNotNull(tokenResponse.getAccessToken());
assertNull(tokenResponse.getRefreshToken());
clientRepresentation.getAttributes().put(OIDCConfigAttributes.USE_REFRESH_TOKEN, "true");
client.update(clientRepresentation);
}
@Test
public void testConsentCancel() throws Exception {
// Device Authorization Request from device

View file

@ -1140,6 +1140,32 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
}
}
@Test
public void refreshTokenRequestNoRefreshToken() {
ClientResource client = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app");
ClientRepresentation clientRepresentation = client.toRepresentation();
oauth.doLogin("test-user@localhost", "password");
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
String refreshTokenString = tokenResponse.getRefreshToken();
setTimeOffset(2);
clientRepresentation.getAttributes().put(OIDCConfigAttributes.USE_REFRESH_TOKEN, "false");
client.update(clientRepresentation);
OAuthClient.AccessTokenResponse response = oauth.doRefreshTokenRequest(refreshTokenString, "password");
assertNotNull(response.getAccessToken());
assertNull(response.getRefreshToken());
clientRepresentation.getAttributes().put(OIDCConfigAttributes.USE_REFRESH_TOKEN, "true");
client.update(clientRepresentation);
}
@Test
public void tokenRefreshRequest_ClientRS384_RealmRS384() throws Exception {
conductTokenRefreshRequest(Algorithm.HS256, Algorithm.RS384, Algorithm.RS384);

View file

@ -40,6 +40,7 @@ import org.keycloak.jose.jws.JWSInput;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.RefreshToken;
import org.keycloak.representations.idm.ClientRepresentation;
@ -64,6 +65,7 @@ import java.util.LinkedList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import javax.ws.rs.core.HttpHeaders;
@ -115,6 +117,11 @@ public class ResourceOwnerPasswordCredentialsGrantTest extends AbstractKeycloakT
.build();
realm.client(app2);
ClientRepresentation app3 = ClientBuilder.create().id(KeycloakModelUtils.generateId())
.clientId("resource-owner-refresh").directAccessGrants().secret("secret").build();
OIDCAdvancedConfigWrapper.fromClientRepresentation(app3).setUseRefreshToken(false);
realm.client(app3);
UserBuilder defaultUser = UserBuilder.create()
.id(KeycloakModelUtils.generateId())
.username("test-user@localhost")
@ -661,6 +668,17 @@ public class ResourceOwnerPasswordCredentialsGrantTest extends AbstractKeycloakT
}
}
@Test
public void grantAccessTokenNoRefreshToken() throws Exception {
oauth.clientId("resource-owner-refresh");
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "direct-login", "password", null);
assertEquals(200, response.getStatusCode());
assertNotNull(response.getAccessToken());
assertNull(response.getRefreshToken());
}
private int getAuthenticationSessionsCount() {
return testingClient.testing().cache(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME).size();
}

View file

@ -413,6 +413,8 @@ oidc-compatibility-modes=OpenID Connect Compatibility Modes
oidc-compatibility-modes.tooltip=Expand this section to configure settings for backwards compatibility with older OpenID Connect / OAuth2 adapters. It is useful especially if your client uses older version of Keycloak / RH-SSO adapter.
exclude-session-state-from-auth-response=Exclude Session State From Authentication Response
exclude-session-state-from-auth-response.tooltip=If this is on, the parameter 'session_state' will not be included in OpenID Connect Authentication Response. It is useful if your client uses older OIDC / OAuth2 adapter, which does not support 'session_state' parameter.
use-refresh-tokens=Use Refresh Tokens
use-refresh-tokens.tooltip=If this is on, a refresh_token will be created and added to the token response. If this is off then no refresh_token will be generated.
use-refresh-token-for-client-credentials-grant=Use Refresh Tokens For Client Credentials Grant
use-refresh-token-for-client-credentials-grant.tooltip=If this is on, a refresh_token will be created and added to the token response if the client_credentials grant is used. The OAuth 2.0 RFC6749 Section 4.4.3 states that a refresh_token should not be generated when client_credentials grant is used. If this is off then no refresh_token will be generated and the associated user session will be removed.

View file

@ -1112,6 +1112,7 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
// KEYCLOAK-6771 Certificate Bound Token
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-3
$scope.tlsClientCertificateBoundAccessTokens = false;
$scope.useRefreshTokens = true;
$scope.accessTokenLifespan = TimeUnit2.asUnit(client.attributes['access.token.lifespan']);
$scope.samlAssertionLifespan = TimeUnit2.asUnit(client.attributes['saml.assertion.lifespan']);
@ -1290,6 +1291,14 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
}
}
if ($scope.client.attributes["use.refresh.tokens"]) {
if ($scope.client.attributes["use.refresh.tokens"] == "true") {
$scope.useRefreshTokens = true;
} else {
$scope.useRefreshTokens = false;
}
}
// KEYCLOAK-6771 Certificate Bound Token
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-3
if ($scope.client.attributes["tls.client.certificate.bound.access.tokens"]) {
@ -1697,6 +1706,12 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
$scope.clientEdit.attributes["oauth2.device.authorization.grant.enabled"] = "false";
}
if ($scope.useRefreshTokens == true) {
$scope.clientEdit.attributes["use.refresh.tokens"] = "true";
} else {
$scope.clientEdit.attributes["use.refresh.tokens"] = "false";
}
// KEYCLOAK-6771 Certificate Bound Token
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-3
if ($scope.tlsClientCertificateBoundAccessTokens == true) {

View file

@ -559,6 +559,14 @@
</div>
<kc-tooltip>{{:: 'exclude-session-state-from-auth-response.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix block" data-ng-show="protocol == 'openid-connect'">
<label class="col-md-2 control-label" for="useRefreshTokens">{{:: 'use-refresh-tokens' | translate}}</label>
<div class="col-sm-6">
<input ng-model="useRefreshTokens" ng-click="switchChange()" name="useRefreshTokens" id="useRefreshTokens"
onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
</div>
<kc-tooltip>{{:: 'use-refresh-tokens.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix block" data-ng-show="protocol == 'openid-connect'">
<label class="col-md-2 control-label" for="useRefreshTokenForClientCredentialsGrant">{{:: 'use-refresh-token-for-client-credentials-grant' | translate}}</label>
<div class="col-md-6">