/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.oauth;
import org.apache.http.HttpResponse;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
import org.keycloak.common.constants.ServiceAccountConstants;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.RefreshToken;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.ClientManager;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.UserBuilder;
import java.util.List;
import static org.junit.Assert.assertEquals;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ServiceAccountTest extends AbstractKeycloakTest {
private static String userId;
@Rule
public AssertEvents events = new AssertEvents(this);
@Override
public void beforeAbstractKeycloakTest() throws Exception {
super.beforeAbstractKeycloakTest();
}
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
RealmBuilder realm = RealmBuilder.create().name("test")
.privateKey("MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=")
.publicKey("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB")
.testEventListener();
ClientRepresentation enabledApp = ClientBuilder.create()
.id(KeycloakModelUtils.generateId())
.clientId("service-account-cl")
.secret("secret1")
.serviceAccountsEnabled(true)
.build();
realm.client(enabledApp);
ClientRepresentation disabledApp = ClientBuilder.create()
.id(KeycloakModelUtils.generateId())
.clientId("service-account-disabled")
.secret("secret1")
.build();
realm.client(disabledApp);
UserBuilder defaultUser = UserBuilder.create()
.id(KeycloakModelUtils.generateId())
.username("test-user@localhost");
realm.user(defaultUser);
userId = KeycloakModelUtils.generateId();
UserBuilder serviceAccountUser = UserBuilder.create()
.id(userId)
.username(ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + enabledApp.getClientId())
.serviceAccountId(enabledApp.getClientId());
realm.user(serviceAccountUser);
testRealms.add(realm.build());
}
@Test
public void clientCredentialsAuthSuccess() throws Exception {
oauth.clientId("service-account-cl");
OAuthClient.AccessTokenResponse response = oauth.doClientCredentialsGrantAccessTokenRequest("secret1");
assertEquals(200, response.getStatusCode());
AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
RefreshToken refreshToken = oauth.verifyRefreshToken(response.getRefreshToken());
events.expectClientLogin()
.client("service-account-cl")
.user(userId)
.session(accessToken.getSessionState())
.detail(Details.TOKEN_ID, accessToken.getId())
.detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
.detail(Details.USERNAME, ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + "service-account-cl")
.assertEvent();
assertEquals(accessToken.getSessionState(), refreshToken.getSessionState());
System.out.println("Access token other claims: " + accessToken.getOtherClaims());
Assert.assertEquals("service-account-cl", accessToken.getOtherClaims().get(ServiceAccountConstants.CLIENT_ID));
Assert.assertTrue(accessToken.getOtherClaims().containsKey(ServiceAccountConstants.CLIENT_ADDRESS));
Assert.assertTrue(accessToken.getOtherClaims().containsKey(ServiceAccountConstants.CLIENT_HOST));
OAuthClient.AccessTokenResponse refreshedResponse = oauth.doRefreshTokenRequest(response.getRefreshToken(), "secret1");
AccessToken refreshedAccessToken = oauth.verifyToken(refreshedResponse.getAccessToken());
RefreshToken refreshedRefreshToken = oauth.verifyRefreshToken(refreshedResponse.getRefreshToken());
assertEquals(accessToken.getSessionState(), refreshedAccessToken.getSessionState());
assertEquals(accessToken.getSessionState(), refreshedRefreshToken.getSessionState());
events.expectRefresh(refreshToken.getId(), refreshToken.getSessionState()).user(userId).client("service-account-cl").assertEvent();
}
@Test
public void clientCredentialsLogout() throws Exception {
oauth.clientId("service-account-cl");
OAuthClient.AccessTokenResponse response = oauth.doClientCredentialsGrantAccessTokenRequest("secret1");
assertEquals(200, response.getStatusCode());
AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
RefreshToken refreshToken = oauth.verifyRefreshToken(response.getRefreshToken());
events.expectClientLogin()
.client("service-account-cl")
.user(userId)
.session(accessToken.getSessionState())
.detail(Details.TOKEN_ID, accessToken.getId())
.detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
.detail(Details.USERNAME, ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + "service-account-cl")
.detail(Details.CLIENT_AUTH_METHOD, ClientIdAndSecretAuthenticator.PROVIDER_ID)
.assertEvent();
HttpResponse logoutResponse = oauth.doLogout(response.getRefreshToken(), "secret1");
assertEquals(204, logoutResponse.getStatusLine().getStatusCode());
events.expectLogout(accessToken.getSessionState())
.client("service-account-cl")
.user(userId)
.removeDetail(Details.REDIRECT_URI)
.assertEvent();
response = oauth.doRefreshTokenRequest(response.getRefreshToken(), "secret1");
assertEquals(400, response.getStatusCode());
assertEquals("invalid_grant", response.getError());
events.expectRefresh(refreshToken.getId(), refreshToken.getSessionState())
.client("service-account-cl")
.user(userId)
.removeDetail(Details.TOKEN_ID)
.removeDetail(Details.UPDATED_REFRESH_TOKEN_ID)
.error(Errors.INVALID_TOKEN).assertEvent();
}
@Test
public void clientCredentialsInvalidClientCredentials() throws Exception {
oauth.clientId("service-account-cl");
OAuthClient.AccessTokenResponse response = oauth.doClientCredentialsGrantAccessTokenRequest("secret2");
assertEquals(400, response.getStatusCode());
assertEquals("unauthorized_client", response.getError());
events.expectClientLogin()
.client("service-account-cl")
.session((String) null)
.clearDetails()
.error(Errors.INVALID_CLIENT_CREDENTIALS)
.user((String) null)
.assertEvent();
}
@Test
public void clientCredentialsDisabledServiceAccount() throws Exception {
oauth.clientId("service-account-disabled");
OAuthClient.AccessTokenResponse response = oauth.doClientCredentialsGrantAccessTokenRequest("secret1");
assertEquals(401, response.getStatusCode());
assertEquals("unauthorized_client", response.getError());
events.expectClientLogin()
.client("service-account-disabled")
.user((String) null)
.session((String) null)
.removeDetail(Details.USERNAME)
.removeDetail(Details.RESPONSE_TYPE)
.error(Errors.INVALID_CLIENT)
.assertEvent();
}
@Test
public void changeClientIdTest() throws Exception {
ClientManager.realm(adminClient.realm("test")).clientId("service-account-cl").renameTo("updated-client");
oauth.clientId("updated-client");
OAuthClient.AccessTokenResponse response = oauth.doClientCredentialsGrantAccessTokenRequest("secret1");
assertEquals(200, response.getStatusCode());
AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
RefreshToken refreshToken = oauth.verifyRefreshToken(response.getRefreshToken());
Assert.assertEquals("updated-client", accessToken.getOtherClaims().get(ServiceAccountConstants.CLIENT_ID));
// Username updated after client ID changed
events.expectClientLogin()
.client("updated-client")
.user(userId)
.session(accessToken.getSessionState())
.detail(Details.TOKEN_ID, accessToken.getId())
.detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
.detail(Details.USERNAME, ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + "updated-client")
.assertEvent();
ClientManager.realm(adminClient.realm("test")).clientId("updated-client").renameTo("service-account-cl");
}
}