/* * 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 com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.Rule; import org.junit.Test; import org.keycloak.OAuth2Constants; import org.keycloak.OAuthErrorException; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.OAuth2ErrorRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.oidc.TokenMetadataRepresentation; import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.util.KeycloakModelUtils; import org.keycloak.testsuite.util.OAuthClient.AccessTokenResponse; import org.keycloak.util.JsonSerialization; import java.util.ArrayList; import java.util.List; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; /** * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a> * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. */ public class TokenIntrospectionTest extends AbstractTestRealmKeycloakTest { @Rule public AssertEvents events = new AssertEvents(this); @Override public void configureTestRealm(RealmRepresentation testRealm) { ClientRepresentation confApp = KeycloakModelUtils.createClient(testRealm, "confidential-cli"); confApp.setSecret("secret1"); confApp.setServiceAccountsEnabled(Boolean.TRUE); ClientRepresentation pubApp = KeycloakModelUtils.createClient(testRealm, "public-cli"); pubApp.setPublicClient(Boolean.TRUE); UserRepresentation user = new UserRepresentation(); user.setUsername("no-permissions"); CredentialRepresentation credential = new CredentialRepresentation(); credential.setType("password"); credential.setValue("password"); List<CredentialRepresentation> creds = new ArrayList<>(); creds.add(credential); user.setCredentials(creds); user.setEnabled(Boolean.TRUE); List<String> realmRoles = new ArrayList<>(); realmRoles.add("user"); user.setRealmRoles(realmRoles); testRealm.getUsers().add(user); } @Test public void testConfidentialClientCredentialsBasicAuthentication() throws Exception { oauth.doLogin("test-user@localhost", "password"); String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); AccessTokenResponse accessTokenResponse = oauth.doAccessTokenRequest(code, "password"); String tokenResponse = oauth.introspectAccessTokenWithClientCredential("confidential-cli", "secret1", accessTokenResponse.getAccessToken()); ObjectMapper objectMapper = new ObjectMapper(); JsonNode jsonNode = objectMapper.readTree(tokenResponse); assertTrue(jsonNode.get("active").asBoolean()); assertEquals("test-user@localhost", jsonNode.get("username").asText()); assertEquals("test-app", jsonNode.get("client_id").asText()); assertTrue(jsonNode.has("exp")); assertTrue(jsonNode.has("iat")); assertTrue(jsonNode.has("nbf")); assertTrue(jsonNode.has("sub")); assertTrue(jsonNode.has("aud")); assertTrue(jsonNode.has("iss")); assertTrue(jsonNode.has("jti")); TokenMetadataRepresentation rep = objectMapper.readValue(tokenResponse, TokenMetadataRepresentation.class); assertTrue(rep.isActive()); assertEquals("test-user@localhost", rep.getUserName()); assertEquals("test-app", rep.getClientId()); assertEquals(jsonNode.get("exp").asInt(), rep.getExpiration()); assertEquals(jsonNode.get("iat").asInt(), rep.getIssuedAt()); assertEquals(jsonNode.get("nbf").asInt(), rep.getNotBefore()); assertEquals(jsonNode.get("sub").asText(), rep.getSubject()); assertEquals(jsonNode.get("aud").asText(), rep.getAudience()[0]); assertEquals(jsonNode.get("iss").asText(), rep.getIssuer()); assertEquals(jsonNode.get("jti").asText(), rep.getId()); } @Test public void testInvalidClientCredentials() throws Exception { oauth.doLogin("test-user@localhost", "password"); String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); AccessTokenResponse accessTokenResponse = oauth.doAccessTokenRequest(code, "password"); String tokenResponse = oauth.introspectAccessTokenWithClientCredential("confidential-cli", "bad_credential", accessTokenResponse.getAccessToken()); OAuth2ErrorRepresentation errorRep = JsonSerialization.readValue(tokenResponse, OAuth2ErrorRepresentation.class); Assert.assertEquals("Authentication failed.", errorRep.getErrorDescription()); Assert.assertEquals(OAuthErrorException.INVALID_REQUEST, errorRep.getError()); } @Test public void testIntrospectRefreshToken() throws Exception { oauth.doLogin("test-user@localhost", "password"); String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); EventRepresentation loginEvent = events.expectLogin().assertEvent(); String sessionId = loginEvent.getSessionId(); AccessTokenResponse accessTokenResponse = oauth.doAccessTokenRequest(code, "password"); String tokenResponse = oauth.introspectRefreshTokenWithClientCredential("confidential-cli", "secret1", accessTokenResponse.getAccessToken()); ObjectMapper objectMapper = new ObjectMapper(); JsonNode jsonNode = objectMapper.readTree(tokenResponse); assertTrue(jsonNode.get("active").asBoolean()); assertEquals(sessionId, jsonNode.get("session_state").asText()); assertEquals("test-app", jsonNode.get("client_id").asText()); assertTrue(jsonNode.has("exp")); assertTrue(jsonNode.has("iat")); assertTrue(jsonNode.has("nbf")); assertTrue(jsonNode.has("sub")); assertTrue(jsonNode.has("aud")); assertTrue(jsonNode.has("iss")); assertTrue(jsonNode.has("jti")); TokenMetadataRepresentation rep = objectMapper.readValue(tokenResponse, TokenMetadataRepresentation.class); assertTrue(rep.isActive()); assertEquals("test-app", rep.getClientId()); assertEquals(jsonNode.get("session_state").asText(), rep.getSessionState()); assertEquals(jsonNode.get("exp").asInt(), rep.getExpiration()); assertEquals(jsonNode.get("iat").asInt(), rep.getIssuedAt()); assertEquals(jsonNode.get("nbf").asInt(), rep.getNotBefore()); assertEquals(jsonNode.get("iss").asText(), rep.getIssuer()); assertEquals(jsonNode.get("jti").asText(), rep.getId()); } @Test public void testPublicClientCredentialsNotAllowed() throws Exception { oauth.doLogin("test-user@localhost", "password"); String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); AccessTokenResponse accessTokenResponse = oauth.doAccessTokenRequest(code, "password"); String tokenResponse = oauth.introspectAccessTokenWithClientCredential("public-cli", "it_doesnt_matter", accessTokenResponse.getAccessToken()); OAuth2ErrorRepresentation errorRep = JsonSerialization.readValue(tokenResponse, OAuth2ErrorRepresentation.class); Assert.assertEquals("Client not allowed.", errorRep.getErrorDescription()); Assert.assertEquals(OAuthErrorException.INVALID_REQUEST, errorRep.getError()); } @Test public void testInactiveAccessToken() throws Exception { oauth.doLogin("test-user@localhost", "password"); String inactiveAccessToken = "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJGSjg2R2NGM2pUYk5MT2NvNE52WmtVQ0lVbWZZQ3FvcXRPUWVNZmJoTmxFIn0.eyJqdGkiOiI5NjgxZTRlOC01NzhlLTQ3M2ItOTIwNC0yZWE5OTdhYzMwMTgiLCJleHAiOjE0NzYxMDY4NDksIm5iZiI6MCwiaWF0IjoxNDc2MTA2NTQ5LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgxODAvYXV0aC9yZWFsbXMvdGVzdCIsImF1ZCI6InRlc3QtYXBwIiwic3ViIjoiZWYyYzk0NjAtZDRkYy00OTk5LWJlYmUtZWVmYWVkNmJmMGU3IiwidHlwIjoiQmVhcmVyIiwiYXpwIjoidGVzdC1hcHAiLCJhdXRoX3RpbWUiOjE0NzYxMDY1NDksInNlc3Npb25fc3RhdGUiOiI1OGY4M2MzMi03MDhkLTQzNjktODhhNC05YjI5OGRjMDY5NzgiLCJhY3IiOiIxIiwiY2xpZW50X3Nlc3Npb24iOiI2NTYyOTVkZC1kZWNkLTQyZDAtYWJmYy0zZGJjZjJlMDE3NzIiLCJhbGxvd2VkLW9yaWdpbnMiOlsiaHR0cDovL2xvY2FsaG9zdDo4MTgwIl0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJ1c2VyIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsidGVzdC1hcHAiOnsicm9sZXMiOlsiY3VzdG9tZXItdXNlciJdfSwiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsInZpZXctcHJvZmlsZSJdfX0sIm5hbWUiOiJUb20gQnJhZHkiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ0ZXN0LXVzZXJAbG9jYWxob3N0IiwiZ2l2ZW5fbmFtZSI6IlRvbSIsImZhbWlseV9uYW1lIjoiQnJhZHkiLCJlbWFpbCI6InRlc3QtdXNlckBsb2NhbGhvc3QifQ.LYU7opqZsc9e-ZmdsIhcecjHL3kQkpP13VpwO4MHMqEVNeJsZI1WOkTM5HGVAihcPfQazhaYvcik0gFTF_6ZcKzDqanjx80TGhSIrV5FoCeUrbp7w_66VKDH7ImPc8T2kICQGHh2d521WFBnvXNifw7P6AR1rGg4qrUljHdf_KU"; String tokenResponse = oauth.introspectAccessTokenWithClientCredential("confidential-cli", "secret1", inactiveAccessToken); ObjectMapper objectMapper = new ObjectMapper(); JsonNode jsonNode = objectMapper.readTree(tokenResponse); assertFalse(jsonNode.get("active").asBoolean()); TokenMetadataRepresentation rep = objectMapper.readValue(tokenResponse, TokenMetadataRepresentation.class); assertFalse(rep.isActive()); assertNull(rep.getUserName()); assertNull(rep.getClientId()); assertNull(rep.getSubject()); } @Test public void testUnsupportedToken() throws Exception { oauth.doLogin("test-user@localhost", "password"); String inactiveAccessToken = "unsupported"; String tokenResponse = oauth.introspectAccessTokenWithClientCredential("confidential-cli", "secret1", inactiveAccessToken); ObjectMapper objectMapper = new ObjectMapper(); JsonNode jsonNode = objectMapper.readTree(tokenResponse); assertFalse(jsonNode.get("active").asBoolean()); TokenMetadataRepresentation rep = objectMapper.readValue(tokenResponse, TokenMetadataRepresentation.class); assertFalse(rep.isActive()); assertNull(rep.getUserName()); assertNull(rep.getClientId()); assertNull(rep.getSubject()); } @Test public void testIntrospectAccessToken() throws Exception { oauth.doLogin("test-user@localhost", "password"); String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); EventRepresentation loginEvent = events.expectLogin().assertEvent(); AccessTokenResponse accessTokenResponse = oauth.doAccessTokenRequest(code, "password"); String tokenResponse = oauth.introspectAccessTokenWithClientCredential("confidential-cli", "secret1", accessTokenResponse.getAccessToken()); TokenMetadataRepresentation rep = JsonSerialization.readValue(tokenResponse, TokenMetadataRepresentation.class); assertTrue(rep.isActive()); assertEquals("test-user@localhost", rep.getUserName()); assertEquals("test-app", rep.getClientId()); assertEquals(loginEvent.getUserId(), rep.getSubject()); } @Test public void testIntrospectAccessTokenSessionInvalid() throws Exception { oauth.doLogin("test-user@localhost", "password"); String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); AccessTokenResponse accessTokenResponse = oauth.doAccessTokenRequest(code, "password"); oauth.doLogout(accessTokenResponse.getRefreshToken(), "password"); String tokenResponse = oauth.introspectAccessTokenWithClientCredential("confidential-cli", "secret1", accessTokenResponse.getAccessToken()); TokenMetadataRepresentation rep = JsonSerialization.readValue(tokenResponse, TokenMetadataRepresentation.class); assertFalse(rep.isActive()); assertNull(rep.getUserName()); assertNull(rep.getClientId()); assertNull(rep.getSubject()); } // KEYCLOAK-4829 @Test public void testIntrospectAccessTokenOfflineAccess() throws Exception { oauth.scope(OAuth2Constants.OFFLINE_ACCESS); oauth.doLogin("test-user@localhost", "password"); String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); AccessTokenResponse accessTokenResponse = oauth.doAccessTokenRequest(code, "password"); setTimeOffset(86400); // "Online" session still exists, but is invalid accessTokenResponse = oauth.doRefreshTokenRequest(accessTokenResponse.getRefreshToken(), "password"); String tokenResponse = oauth.introspectAccessTokenWithClientCredential("confidential-cli", "secret1", accessTokenResponse.getAccessToken()); TokenMetadataRepresentation rep = JsonSerialization.readValue(tokenResponse, TokenMetadataRepresentation.class); assertTrue(rep.isActive()); assertEquals("test-user@localhost", rep.getUserName()); assertEquals("test-app", rep.getClientId()); // "Online" session doesn't even exists testingClient.testing().removeExpired("test"); accessTokenResponse = oauth.doRefreshTokenRequest(accessTokenResponse.getRefreshToken(), "password"); tokenResponse = oauth.introspectAccessTokenWithClientCredential("confidential-cli", "secret1", accessTokenResponse.getAccessToken()); rep = JsonSerialization.readValue(tokenResponse, TokenMetadataRepresentation.class); assertTrue(rep.isActive()); assertEquals("test-user@localhost", rep.getUserName()); assertEquals("test-app", rep.getClientId()); } @Test public void testIntrospectAccessTokenUserDisabled() throws Exception { oauth.doLogin("test-user@localhost", "password"); String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); AccessTokenResponse accessTokenResponse = oauth.doAccessTokenRequest(code, "password"); EventRepresentation loginEvent = events.expectLogin().assertEvent(); UserRepresentation userRep = new UserRepresentation(); try { userRep.setEnabled(false); adminClient.realm(oauth.getRealm()).users().get(loginEvent.getUserId()).update(userRep); String tokenResponse = oauth.introspectAccessTokenWithClientCredential("confidential-cli", "secret1", accessTokenResponse.getAccessToken()); TokenMetadataRepresentation rep = JsonSerialization.readValue(tokenResponse, TokenMetadataRepresentation.class); assertFalse(rep.isActive()); assertNull(rep.getUserName()); assertNull(rep.getClientId()); assertNull(rep.getSubject()); } finally { userRep.setEnabled(true); adminClient.realm(oauth.getRealm()).users().get(loginEvent.getUserId()).update(userRep); } } @Test public void testIntrospectAccessTokenExpired() throws Exception { oauth.doLogin("test-user@localhost", "password"); String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); AccessTokenResponse accessTokenResponse = oauth.doAccessTokenRequest(code, "password"); setTimeOffset(adminClient.realm(oauth.getRealm()).toRepresentation().getAccessTokenLifespan() + 1); String tokenResponse = oauth.introspectAccessTokenWithClientCredential("confidential-cli", "secret1", accessTokenResponse.getAccessToken()); TokenMetadataRepresentation rep = JsonSerialization.readValue(tokenResponse, TokenMetadataRepresentation.class); assertFalse(rep.isActive()); assertNull(rep.getUserName()); assertNull(rep.getClientId()); assertNull(rep.getSubject()); } }