[KEYCLOAK-16837] - Authz client still relying on refresh tokens when doing client credentials

This commit is contained in:
Pedro Igor 2021-01-18 10:40:38 -03:00
parent 99a70267d9
commit 0c501f8302
4 changed files with 95 additions and 45 deletions

View file

@ -76,7 +76,7 @@ public final class Throwables {
.response().json(TokenIntrospectionResponse.class).execute();
if (!response.getActive()) {
token.clearToken();
token.clearTokens();
try {
return callable.call();
} catch (Exception e) {

View file

@ -36,7 +36,7 @@ public class TokenCallable implements Callable<String> {
private final Http http;
private final Configuration configuration;
private final ServerConfiguration serverConfiguration;
private AccessTokenResponse clientToken;
private AccessTokenResponse tokenResponse;
public TokenCallable(String userName, String password, Http http, Configuration configuration, ServerConfiguration serverConfiguration) {
this.userName = userName;
@ -52,55 +52,49 @@ public class TokenCallable implements Callable<String> {
@Override
public String call() {
if (clientToken == null || clientToken.getRefreshToken() == null) {
if (userName == null || password == null) {
clientToken = obtainAccessToken();
} else {
clientToken = obtainAccessToken(userName, password);
}
} else {
String refreshTokenValue = clientToken.getRefreshToken();
try {
RefreshToken refreshToken = JsonSerialization.readValue(new JWSInput(refreshTokenValue).getContent(), RefreshToken.class);
if (!refreshToken.isActive() || !isTokenTimeToLiveSufficient(refreshToken)) {
log.debug("Refresh token is expired.");
if (userName == null || password == null) {
clientToken = obtainAccessToken();
} else {
clientToken = obtainAccessToken(userName, password);
}
}
} catch (Exception e) {
clientToken = null;
throw new RuntimeException(e);
}
if (tokenResponse == null) {
tokenResponse = obtainTokens();
}
String token = clientToken.getToken();
try {
AccessToken accessToken = JsonSerialization.readValue(new JWSInput(token).getContent(), AccessToken.class);
String rawAccessToken = tokenResponse.getToken();
AccessToken accessToken = JsonSerialization.readValue(new JWSInput(rawAccessToken).getContent(), AccessToken.class);
if (accessToken.isActive() && this.isTokenTimeToLiveSufficient(accessToken)) {
return token;
return rawAccessToken;
} else {
log.debug("Access token is expired.");
}
clientToken = http.<AccessTokenResponse>post(serverConfiguration.getTokenEndpoint())
.authentication().client()
.form()
.param("grant_type", "refresh_token")
.param("refresh_token", clientToken.getRefreshToken())
.response()
.json(AccessTokenResponse.class)
.execute();
} catch (Exception e) {
clientToken = null;
throw new RuntimeException(e);
} catch (Exception cause) {
clearTokens();
throw new RuntimeException("Failed to parse access token", cause);
}
return clientToken.getToken();
tokenResponse = tryRefreshToken();
return tokenResponse.getToken();
}
private AccessTokenResponse tryRefreshToken() {
String rawRefreshToken = tokenResponse.getRefreshToken();
if (rawRefreshToken == null) {
log.debug("Refresh token not found, obtaining new tokens");
return obtainTokens();
}
try {
RefreshToken refreshToken = JsonSerialization.readValue(new JWSInput(rawRefreshToken).getContent(), RefreshToken.class);
if (!refreshToken.isActive() || !isTokenTimeToLiveSufficient(refreshToken)) {
log.debug("Refresh token is expired.");
return obtainTokens();
}
} catch (Exception cause) {
clearTokens();
throw new RuntimeException("Failed to parse refresh token", cause);
}
return refreshToken(rawRefreshToken);
}
public boolean isTokenTimeToLiveSufficient(AccessToken token) {
@ -112,7 +106,7 @@ public class TokenCallable implements Callable<String> {
*
* @return an {@link AccessTokenResponse}
*/
AccessTokenResponse obtainAccessToken() {
AccessTokenResponse clientCredentialsGrant() {
return this.http.<AccessTokenResponse>post(this.serverConfiguration.getTokenEndpoint())
.authentication()
.client()
@ -126,7 +120,7 @@ public class TokenCallable implements Callable<String> {
*
* @return an {@link AccessTokenResponse}
*/
AccessTokenResponse obtainAccessToken(String userName, String password) {
AccessTokenResponse resourceOwnerPasswordGrant(String userName, String password) {
return this.http.<AccessTokenResponse>post(this.serverConfiguration.getTokenEndpoint())
.authentication()
.oauth2ResourceOwnerPassword(userName, password)
@ -135,6 +129,26 @@ public class TokenCallable implements Callable<String> {
.execute();
}
private AccessTokenResponse refreshToken(String rawRefreshToken) {
log.debug("Refreshing tokens");
return http.<AccessTokenResponse>post(serverConfiguration.getTokenEndpoint())
.authentication().client()
.form()
.param("grant_type", "refresh_token")
.param("refresh_token", rawRefreshToken)
.response()
.json(AccessTokenResponse.class)
.execute();
}
private AccessTokenResponse obtainTokens() {
if (userName == null || password == null) {
return clientCredentialsGrant();
} else {
return resourceOwnerPasswordGrant(userName, password);
}
}
Http getHttp() {
return http;
}
@ -151,7 +165,7 @@ public class TokenCallable implements Callable<String> {
return serverConfiguration;
}
void clearToken() {
clientToken = null;
void clearTokens() {
tokenResponse = null;
}
}

View file

@ -47,6 +47,7 @@ import org.keycloak.authorization.client.ClientAuthenticator;
import org.keycloak.authorization.client.Configuration;
import org.keycloak.authorization.client.resource.ProtectionResource;
import org.keycloak.authorization.client.util.HttpResponseException;
import org.keycloak.common.util.Time;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.representations.AccessToken;
@ -84,6 +85,7 @@ public class AuthzClientCredentialsTest extends AbstractAuthzTest {
.build());
testRealms.add(configureRealm(RealmBuilder.create().name("authz-test"), ClientBuilder.create().secret("secret")).build());
testRealms.add(configureRealm(RealmBuilder.create().name("authz-test-session").accessTokenLifespan(1), ClientBuilder.create().secret("secret")).build());
testRealms.add(configureRealm(RealmBuilder.create().name("authz-test-no-rt").accessTokenLifespan(1), ClientBuilder.create().secret("secret").attribute(OIDCConfigAttributes.USE_REFRESH_TOKEN_FOR_CLIENT_CREDENTIALS_GRANT, "false")).build());
}
@Before
@ -253,6 +255,32 @@ public class AuthzClientCredentialsTest extends AbstractAuthzTest {
assertEquals(1, userSessions.size());
}
@Test
public void testNoRefreshToken() throws Exception {
ClientsResource clients = getAdminClient().realm("authz-test-no-rt").clients();
AuthzClient authzClient = getAuthzClient("default-session-keycloak-no-rt.json");
org.keycloak.authorization.client.resource.AuthorizationResource authorization = authzClient.authorization();
AuthorizationResponse response = authorization.authorize();
AccessToken accessToken = toAccessToken(response.getToken());
assertEquals(1, accessToken.getAuthorization().getPermissions().size());
assertEquals("Default Resource", accessToken.getAuthorization().getPermissions().iterator().next().getResourceName());
ProtectionResource protection = authzClient.protection();
assertEquals(1, protection.resource().findAll().length);
try {
// force token expiration on the client side
Time.setOffset(1000);
// should refresh tokens by doing client credentials again
assertEquals(1, protection.resource().findAll().length);
} finally {
Time.setOffset(0);
}
}
@Test
public void testFindByName() {
AuthzClient authzClient = getAuthzClient("default-session-keycloak.json");

View file

@ -0,0 +1,8 @@
{
"realm": "authz-test-no-rt",
"auth-server-url" : "http://localhost:8180/auth",
"resource" : "resource-server-test",
"credentials": {
"secret": "secret"
}
}