KEYCLOAK-17202 Restrict Issuance of Refresh tokens to specific clients
This commit is contained in:
parent
8b0b657a8f
commit
d9ebbe4958
17 changed files with 295 additions and 17 deletions
|
@ -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())) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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">
|
||||
|
|
Loading…
Reference in a new issue