[KEYCLOAK-16837] - Authz client still relying on refresh tokens when doing client credentials
This commit is contained in:
parent
99a70267d9
commit
0c501f8302
4 changed files with 95 additions and 45 deletions
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"realm": "authz-test-no-rt",
|
||||
"auth-server-url" : "http://localhost:8180/auth",
|
||||
"resource" : "resource-server-test",
|
||||
"credentials": {
|
||||
"secret": "secret"
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue