From 6bab704bbaecfb2d6da490d7d4e4f73dc5cec702 Mon Sep 17 00:00:00 2001 From: Takashi Norimatsu Date: Fri, 3 Feb 2017 14:41:36 +0900 Subject: [PATCH] KEYCLOAK-2604 Proof Key for Code Exchange by OAuth Public Clients - RFC 7636 - Arquillian Test Cases --- .../keycloak/testsuite/util/OAuthClient.java | 35 ++ .../OAuthProofKeyForCodeExchangeTest.java | 549 ++++++++++++++++++ 2 files changed, 584 insertions(+) create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthProofKeyForCodeExchangeTest.java diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java index d7509eddd5..001e30e684 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java @@ -111,6 +111,11 @@ public class OAuthClient { private Map publicKeys = new HashMap<>(); + // https://tools.ietf.org/html/rfc7636#section-4 + private String codeVerifier; + private String codeChallenge; + private String codeChallengeMethod; + public class LogoutUrlBuilder { private final UriBuilder b = OIDCLoginProtocolService.logoutUrl(UriBuilder.fromUri(baseUrl)); @@ -164,6 +169,10 @@ public class OAuthClient { nonce = null; request = null; requestUri = null; + // https://tools.ietf.org/html/rfc7636#section-4 + codeVerifier = null; + codeChallenge = null; + codeChallengeMethod = null; } public AuthorizationEndpointResponse doLogin(String username, String password) { @@ -219,6 +228,11 @@ public class OAuthClient { parameters.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_HOST, clientSessionHost)); } + // https://tools.ietf.org/html/rfc7636#section-4.5 + if (codeVerifier != null) { + parameters.add(new BasicNameValuePair(OAuth2Constants.CODE_VERIFIER, codeVerifier)); + } + UrlEncodedFormEntity formEntity = null; try { formEntity = new UrlEncodedFormEntity(parameters, "UTF-8"); @@ -581,6 +595,13 @@ public class OAuthClient { if (requestUri != null) { b.queryParam(OIDCLoginProtocol.REQUEST_URI_PARAM, requestUri); } + // https://tools.ietf.org/html/rfc7636#section-4.3 + if (codeChallenge != null) { + b.queryParam(OAuth2Constants.CODE_CHALLENGE, codeChallenge); + } + if (codeChallengeMethod != null) { + b.queryParam(OAuth2Constants.CODE_CHALLENGE_METHOD, codeChallengeMethod); + } return b.build(realm).toString(); } @@ -696,6 +717,20 @@ public class OAuthClient { return realm; } + // https://tools.ietf.org/html/rfc7636#section-4 + public OAuthClient codeVerifier(String codeVerifier) { + this.codeVerifier = codeVerifier; + return this; + } + public OAuthClient codeChallenge(String codeChallenge) { + this.codeChallenge = codeChallenge; + return this; + } + public OAuthClient codeChallengeMethod(String codeChallengeMethod) { + this.codeChallengeMethod = codeChallengeMethod; + return this; + } + public static class AuthorizationEndpointResponse { private boolean isRedirected; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthProofKeyForCodeExchangeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthProofKeyForCodeExchangeTest.java new file mode 100644 index 0000000000..a72aa3a8d0 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthProofKeyForCodeExchangeTest.java @@ -0,0 +1,549 @@ +package org.keycloak.testsuite.oauth; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.http.NameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.message.BasicNameValuePair; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.OAuth2Constants; +import org.keycloak.OAuthErrorException; +import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.admin.client.resource.ClientTemplateResource; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.admin.client.resource.UserResource; +import org.keycloak.common.enums.SslRequired; +import org.keycloak.common.util.Base64Url; +import org.keycloak.events.Details; +import org.keycloak.events.Errors; +import org.keycloak.jose.jws.JWSHeader; +import org.keycloak.jose.jws.JWSInput; +import org.keycloak.jose.jws.JWSInputException; +import org.keycloak.models.Constants; +import org.keycloak.models.ProtocolMapperModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.models.utils.ModelToRepresentation; +import org.keycloak.protocol.oidc.OIDCLoginProtocolService; +import org.keycloak.protocol.oidc.mappers.HardcodedClaim; +import org.keycloak.representations.AccessToken; +import org.keycloak.representations.IDToken; +import org.keycloak.representations.RefreshToken; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.ClientTemplateRepresentation; +import org.keycloak.representations.idm.EventRepresentation; +import org.keycloak.representations.idm.ProtocolMapperRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.AbstractKeycloakTest; +import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.arquillian.AuthServerTestEnricher; +import org.keycloak.testsuite.util.ClientBuilder; +import org.keycloak.testsuite.util.ClientManager; +import org.keycloak.testsuite.util.OAuthClient; +import org.keycloak.testsuite.util.RealmManager; +import org.keycloak.testsuite.util.RoleBuilder; +import org.keycloak.testsuite.util.UserBuilder; +import org.keycloak.testsuite.util.UserInfoClientUtil; +import org.keycloak.testsuite.util.UserManager; +import org.keycloak.util.BasicAuthHelper; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Form; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import java.io.IOException; +import java.net.URI; +import java.security.MessageDigest; +import java.util.LinkedList; +import java.util.List; + +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.lessThanOrEqualTo; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson; +import static org.keycloak.testsuite.admin.ApiUtil.findClientByClientId; +import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsername; +import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsernameId; +import static org.keycloak.testsuite.util.OAuthClient.AUTH_SERVER_ROOT; +import static org.keycloak.testsuite.util.ProtocolMapperUtil.createRoleNameMapper; + +//https://tools.ietf.org/html/rfc7636 + +/** + * @author Takashi Norimatsu + */ +public class OAuthProofKeyForCodeExchangeTest extends AbstractKeycloakTest { + + @Rule + public AssertEvents events = new AssertEvents(this); + + + @Override + public void beforeAbstractKeycloakTest() throws Exception { + super.beforeAbstractKeycloakTest(); + } + + @Before + public void clientConfiguration() { + ClientManager.realm(adminClient.realm("test")).clientId("test-app").directAccessGrant(true); + /* + * Configure the default client ID. Seems like OAuthClient is keeping the state of clientID + * For example: If some test case configure oauth.clientId("sample-public-client"), other tests + * will faile and the clientID will always be "sample-public-client + * @see AccessTokenTest#testAuthorizationNegotiateHeaderIgnored() + */ + oauth.clientId("test-app"); + } + + @Override + public void addTestRealms(List testRealms) { + + RealmRepresentation realm = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class); + + UserBuilder user = UserBuilder.create() + .id(KeycloakModelUtils.generateId()) + .username("no-permissions") + .addRoles("user") + .password("password"); + realm.getUsers().add(user.build()); + + testRealms.add(realm); + + } + + @Test + public void accessTokenRequestWithoutPKCE() throws Exception { + // test case : success : A-1-1 + oauth.doLogin("test-user@localhost", "password"); + + EventRepresentation loginEvent = events.expectLogin().assertEvent(); + + String sessionId = loginEvent.getSessionId(); + String codeId = loginEvent.getDetails().get(Details.CODE_ID); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + + expectSuccessfulResponseFromTokenEndpoint(codeId, sessionId, code); + } + + @Test + public void accessTokenRequestInPKCEValidS256CodeChallengeMethod() throws Exception { + // test case : success : A-1-2 + String codeVerifier = "1234567890123456789012345678901234567890123"; // 43 + String codeChallenge = generateS256CodeChallenge(codeVerifier); + oauth.codeChallenge(codeChallenge); + oauth.codeChallengeMethod(OAuth2Constants.PKCE_METHOD_S256); + + oauth.doLogin("test-user@localhost", "password"); + + EventRepresentation loginEvent = events.expectLogin().assertEvent(); + + String sessionId = loginEvent.getSessionId(); + String codeId = loginEvent.getDetails().get(Details.CODE_ID); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + + oauth.codeVerifier(codeVerifier); + + expectSuccessfulResponseFromTokenEndpoint(codeId, sessionId, code); + } + + @Test + public void accessTokenRequestInPKCEUnmatchedCodeVerifierWithS256CodeChallengeMethod() throws Exception { + // test case : failure : A-1-5 + String codeVerifier = "1234567890123456789012345678901234567890123"; + String codeChallenge = codeVerifier; + oauth.codeChallenge(codeChallenge); + oauth.codeChallengeMethod(OAuth2Constants.PKCE_METHOD_S256); + + oauth.doLogin("test-user@localhost", "password"); + + EventRepresentation loginEvent = events.expectLogin().assertEvent(); + + String sessionId = loginEvent.getSessionId(); + String codeId = loginEvent.getDetails().get(Details.CODE_ID); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + + oauth.codeVerifier(codeVerifier); + + OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password"); + + assertEquals(400, response.getStatusCode()); + assertEquals(OAuthErrorException.INVALID_GRANT, response.getError()); + assertEquals("PKCE verification failed", response.getErrorDescription()); + + events.expectCodeToToken(codeId, sessionId).error(Errors.PKCE_VERIFICATION_FAILED).clearDetails().assertEvent(); + } + + @Test + public void accessTokenRequestInPKCEValidPlainCodeChallengeMethod() throws Exception { + // test case : success : A-1-3 + oauth.codeChallenge(".234567890-234567890~234567890_234567890123"); + oauth.codeChallengeMethod(OAuth2Constants.PKCE_METHOD_PLAIN); + + oauth.doLogin("test-user@localhost", "password"); + + EventRepresentation loginEvent = events.expectLogin().assertEvent(); + + String sessionId = loginEvent.getSessionId(); + String codeId = loginEvent.getDetails().get(Details.CODE_ID); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + + oauth.codeVerifier(".234567890-234567890~234567890_234567890123"); + + expectSuccessfulResponseFromTokenEndpoint(codeId, sessionId, code); + } + + @Test + public void accessTokenRequestInPKCEUnmachedCodeVerifierWithPlainCodeChallengeMethod() throws Exception { + // test case : failure : A-1-6 + oauth.codeChallenge("1234567890123456789012345678901234567890123"); + oauth.codeChallengeMethod(OAuth2Constants.PKCE_METHOD_PLAIN); + + oauth.doLogin("test-user@localhost", "password"); + + EventRepresentation loginEvent = events.expectLogin().assertEvent(); + + String sessionId = loginEvent.getSessionId(); + String codeId = loginEvent.getDetails().get(Details.CODE_ID); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + + oauth.codeVerifier("aZ_-.~1234567890123456789012345678901234567890123Za"); + + OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password"); + + assertEquals(400, response.getStatusCode()); + assertEquals(OAuthErrorException.INVALID_GRANT, response.getError()); + assertEquals("PKCE verification failed", response.getErrorDescription()); + + events.expectCodeToToken(codeId, sessionId).error(Errors.PKCE_VERIFICATION_FAILED).clearDetails().assertEvent(); + } + + @Test + public void accessTokenRequestInPKCEValidDefaultCodeChallengeMethod() throws Exception { + // test case : success : A-1-4 + oauth.codeChallenge("1234567890123456789012345678901234567890123"); + + oauth.doLogin("test-user@localhost", "password"); + + EventRepresentation loginEvent = events.expectLogin().assertEvent(); + + String sessionId = loginEvent.getSessionId(); + String codeId = loginEvent.getDetails().get(Details.CODE_ID); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + + oauth.codeVerifier("1234567890123456789012345678901234567890123"); + + expectSuccessfulResponseFromTokenEndpoint(codeId, sessionId, code); + } + + @Test + public void accessTokenRequestInPKCEWithoutCodeChallengeWithValidCodeChallengeMethod() throws Exception { + // test case : failure : A-1-7 + oauth.codeChallengeMethod(OAuth2Constants.PKCE_METHOD_PLAIN); + UriBuilder b = UriBuilder.fromUri(oauth.getLoginFormUrl()); + + driver.navigate().to(b.build().toURL()); + + OAuthClient.AuthorizationEndpointResponse errorResponse = new OAuthClient.AuthorizationEndpointResponse(oauth); + + Assert.assertTrue(errorResponse.isRedirected()); + Assert.assertEquals(errorResponse.getError(), OAuthErrorException.INVALID_REQUEST); + Assert.assertEquals(errorResponse.getErrorDescription(), "Missing parameter: code_challenge"); + + events.expectLogin().error(Errors.INVALID_REQUEST).user((String) null).session((String) null).clearDetails().assertEvent(); + } + + @Test + public void accessTokenRequestInPKCEInvalidUnderCodeChallengeWithS256CodeChallengeMethod() throws Exception { + // test case : failure : A-1-8 + oauth.codeChallengeMethod(OAuth2Constants.PKCE_METHOD_S256); + oauth.codeChallenge("ABCDEFGabcdefg1234567ABCDEFGabcdefg1234567"); // 42 + UriBuilder b = UriBuilder.fromUri(oauth.getLoginFormUrl()); + + driver.navigate().to(b.build().toURL()); + + OAuthClient.AuthorizationEndpointResponse errorResponse = new OAuthClient.AuthorizationEndpointResponse(oauth); + + Assert.assertTrue(errorResponse.isRedirected()); + Assert.assertEquals(errorResponse.getError(), OAuthErrorException.INVALID_REQUEST); + Assert.assertEquals(errorResponse.getErrorDescription(), "Invalid parameter: code_challenge"); + + events.expectLogin().error(Errors.INVALID_REQUEST).user((String) null).session((String) null).clearDetails().assertEvent(); + } + + @Test + public void accessTokenRequestInPKCEInvalidOverCodeChallengeWithPlainCodeChallengeMethod() throws Exception { + // test case : failure : A-1-9 + oauth.codeChallengeMethod(OAuth2Constants.PKCE_METHOD_PLAIN); + oauth.codeChallenge("3fRc92kac_keic8c7al-3ncbdoaie.DDeizlck3~3fRc92kac_keic8c7al-3ncbdoaie.DDeizlck3~3fRc92kac_keic8c7al-3ncbdoaie.DDeizlck3~123456789"); // 129 + + UriBuilder b = UriBuilder.fromUri(oauth.getLoginFormUrl()); + + driver.navigate().to(b.build().toURL()); + + OAuthClient.AuthorizationEndpointResponse errorResponse = new OAuthClient.AuthorizationEndpointResponse(oauth); + + Assert.assertTrue(errorResponse.isRedirected()); + Assert.assertEquals(errorResponse.getError(), OAuthErrorException.INVALID_REQUEST); + Assert.assertEquals(errorResponse.getErrorDescription(), "Invalid parameter: code_challenge"); + + events.expectLogin().error(Errors.INVALID_REQUEST).user((String) null).session((String) null).clearDetails().assertEvent(); + } + + @Test + public void accessTokenRequestInPKCEInvalidUnderCodeVerifierWithS256CodeChallengeMethod() throws Exception { + // test case : success : A-1-10 + String codeVerifier = "ABCDEFGabcdefg1234567ABCDEFGabcdefg1234567"; // 42 + String codeChallenge = generateS256CodeChallenge(codeVerifier); + + oauth.codeChallenge(codeChallenge); + oauth.codeChallengeMethod(OAuth2Constants.PKCE_METHOD_S256); + + oauth.doLogin("test-user@localhost", "password"); + + EventRepresentation loginEvent = events.expectLogin().assertEvent(); + + String sessionId = loginEvent.getSessionId(); + String codeId = loginEvent.getDetails().get(Details.CODE_ID); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + + oauth.codeVerifier(codeVerifier); + + OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password"); + + assertEquals(400, response.getStatusCode()); + assertEquals(OAuthErrorException.INVALID_GRANT, response.getError()); + assertEquals("PKCE invalid code verifier", response.getErrorDescription()); + + events.expectCodeToToken(codeId, sessionId).error(Errors.INVALID_CODE_VERIFIER).clearDetails().assertEvent(); + } + + @Test + public void accessTokenRequestInPKCEInvalidOverCodeVerifierWithS256CodeChallengeMethod() throws Exception { + // test case : success : A-1-11 + String codeVerifier = "3fRc92kac_keic8c7al-3ncbdoaie.DDeizlck3~3fRc92kac_keic8c7al-3ncbdoaie.DDeizlck3~3fRc92kac_keic8c7al-3ncbdoaie.DDeizlck3~123456789"; // 129 + String codeChallenge = generateS256CodeChallenge(codeVerifier); + oauth.codeChallenge(codeChallenge); + oauth.codeChallengeMethod(OAuth2Constants.PKCE_METHOD_S256); + + oauth.doLogin("test-user@localhost", "password"); + + EventRepresentation loginEvent = events.expectLogin().assertEvent(); + + String sessionId = loginEvent.getSessionId(); + String codeId = loginEvent.getDetails().get(Details.CODE_ID); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + + oauth.codeVerifier(codeVerifier); + + OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password"); + + assertEquals(400, response.getStatusCode()); + assertEquals(OAuthErrorException.INVALID_GRANT, response.getError()); + assertEquals("PKCE invalid code verifier", response.getErrorDescription()); + + events.expectCodeToToken(codeId, sessionId).error(Errors.INVALID_CODE_VERIFIER).clearDetails().assertEvent(); + } + + @Test + public void accessTokenRequestInPKCEWIthoutCodeVerifierWithS256CodeChallengeMethod() throws Exception { + // test case : failure : A-1-12 + String codeVerifier = "1234567890123456789012345678901234567890123"; + String codeChallenge = codeVerifier; + oauth.codeChallenge(codeChallenge); + oauth.codeChallengeMethod(OAuth2Constants.PKCE_METHOD_S256); + + oauth.doLogin("test-user@localhost", "password"); + + EventRepresentation loginEvent = events.expectLogin().assertEvent(); + + String sessionId = loginEvent.getSessionId(); + String codeId = loginEvent.getDetails().get(Details.CODE_ID); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + + OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password"); + + assertEquals(400, response.getStatusCode()); + assertEquals(OAuthErrorException.INVALID_GRANT, response.getError()); + assertEquals("PKCE code verifier not specified", response.getErrorDescription()); + + events.expectCodeToToken(codeId, sessionId).error(Errors.CODE_VERIFIER_MISSING).clearDetails().assertEvent(); + } + + @Test + public void accessTokenRequestInPKCEInvalidCodeChallengeWithS256CodeChallengeMethod() throws Exception { + // test case : failure : A-1-13 + String codeVerifier = "1234567890123456789=12345678901234567890123"; + String codeChallenge = codeVerifier; + oauth.codeChallenge(codeChallenge); + oauth.codeChallengeMethod(OAuth2Constants.PKCE_METHOD_S256); + + UriBuilder b = UriBuilder.fromUri(oauth.getLoginFormUrl()); + + driver.navigate().to(b.build().toURL()); + + OAuthClient.AuthorizationEndpointResponse errorResponse = new OAuthClient.AuthorizationEndpointResponse(oauth); + + Assert.assertTrue(errorResponse.isRedirected()); + Assert.assertEquals(errorResponse.getError(), OAuthErrorException.INVALID_REQUEST); + Assert.assertEquals(errorResponse.getErrorDescription(), "Invalid parameter: code_challenge"); + + events.expectLogin().error(Errors.INVALID_REQUEST).user((String) null).session((String) null).clearDetails().assertEvent(); + } + + @Test + public void accessTokenRequestInPKCEInvalidCodeVerifierWithS256CodeChallengeMethod() throws Exception { + // test case : failure : A-1-14 + String codeVerifier = "123456789.123456789-123456789~1234$6789_123"; + String codeChallenge = generateS256CodeChallenge(codeVerifier); + oauth.codeChallenge(codeChallenge); + oauth.codeChallengeMethod(OAuth2Constants.PKCE_METHOD_S256); + + oauth.doLogin("test-user@localhost", "password"); + + EventRepresentation loginEvent = events.expectLogin().assertEvent(); + + String sessionId = loginEvent.getSessionId(); + String codeId = loginEvent.getDetails().get(Details.CODE_ID); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + + oauth.codeVerifier(codeVerifier); + + OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password"); + + assertEquals(400, response.getStatusCode()); + assertEquals(OAuthErrorException.INVALID_GRANT, response.getError()); + assertEquals("PKCE invalid code verifier", response.getErrorDescription()); + + events.expectCodeToToken(codeId, sessionId).error(Errors.INVALID_CODE_VERIFIER).clearDetails().assertEvent(); + } + + private String generateS256CodeChallenge(String codeVerifier) throws Exception { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + md.update(codeVerifier.getBytes()); + StringBuilder sb = new StringBuilder(); + for (byte b : md.digest()) { + String hex = String.format("%02x", b); + sb.append(hex); + } + String codeChallenge = Base64Url.encode(sb.toString().getBytes()); + return codeChallenge; + } + + private void expectSuccessfulResponseFromTokenEndpoint(String codeId, String sessionId, String code) throws Exception { + OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password"); + + assertEquals(200, response.getStatusCode()); + Assert.assertThat(response.getExpiresIn(), allOf(greaterThanOrEqualTo(250), lessThanOrEqualTo(300))); + Assert.assertThat(response.getRefreshExpiresIn(), allOf(greaterThanOrEqualTo(1750), lessThanOrEqualTo(1800))); + assertEquals("bearer", response.getTokenType()); + + String expectedKid = oauth.doCertsRequest("test").getKeys()[0].getKeyId(); + + JWSHeader header = new JWSInput(response.getAccessToken()).getHeader(); + assertEquals("RS256", header.getAlgorithm().name()); + assertEquals("JWT", header.getType()); + assertEquals(expectedKid, header.getKeyId()); + assertNull(header.getContentType()); + + header = new JWSInput(response.getIdToken()).getHeader(); + assertEquals("RS256", header.getAlgorithm().name()); + assertEquals("JWT", header.getType()); + assertEquals(expectedKid, header.getKeyId()); + assertNull(header.getContentType()); + + header = new JWSInput(response.getRefreshToken()).getHeader(); + assertEquals("RS256", header.getAlgorithm().name()); + assertEquals("JWT", header.getType()); + assertEquals(expectedKid, header.getKeyId()); + assertNull(header.getContentType()); + + AccessToken token = oauth.verifyToken(response.getAccessToken()); + + assertEquals(findUserByUsername(adminClient.realm("test"), "test-user@localhost").getId(), token.getSubject()); + Assert.assertNotEquals("test-user@localhost", token.getSubject()); + assertEquals(sessionId, token.getSessionState()); + assertEquals(1, token.getRealmAccess().getRoles().size()); + assertTrue(token.getRealmAccess().isUserInRole("user")); + assertEquals(1, token.getResourceAccess(oauth.getClientId()).getRoles().size()); + assertTrue(token.getResourceAccess(oauth.getClientId()).isUserInRole("customer-user")); + + EventRepresentation event = events.expectCodeToToken(codeId, sessionId).assertEvent(); + + assertEquals(token.getId(), event.getDetails().get(Details.TOKEN_ID)); + assertEquals(oauth.verifyRefreshToken(response.getRefreshToken()).getId(), event.getDetails().get(Details.REFRESH_TOKEN_ID)); + assertEquals(sessionId, token.getSessionState()); + + // make sure PKCE does not affect token refresh on Token Endpoint + + String refreshTokenString = response.getRefreshToken(); + RefreshToken refreshToken = oauth.verifyRefreshToken(refreshTokenString); + + Assert.assertNotNull(refreshTokenString); + Assert.assertThat(token.getExpiration() - getCurrentTime(), allOf(greaterThanOrEqualTo(200), lessThanOrEqualTo(350))); + int actual = refreshToken.getExpiration() - getCurrentTime(); + Assert.assertThat(actual, allOf(greaterThanOrEqualTo(1799), lessThanOrEqualTo(1800))); + assertEquals(sessionId, refreshToken.getSessionState()); + + setTimeOffset(2); + + OAuthClient.AccessTokenResponse refreshResponse = oauth.doRefreshTokenRequest(refreshTokenString, "password"); + + AccessToken refreshedToken = oauth.verifyToken(refreshResponse.getAccessToken()); + RefreshToken refreshedRefreshToken = oauth.verifyRefreshToken(refreshResponse.getRefreshToken()); + + assertEquals(200, refreshResponse.getStatusCode()); + assertEquals(sessionId, refreshedToken.getSessionState()); + assertEquals(sessionId, refreshedRefreshToken.getSessionState()); + + Assert.assertThat(refreshResponse.getExpiresIn(), allOf(greaterThanOrEqualTo(250), lessThanOrEqualTo(300))); + Assert.assertThat(refreshedToken.getExpiration() - getCurrentTime(), allOf(greaterThanOrEqualTo(250), lessThanOrEqualTo(300))); + + Assert.assertThat(refreshedToken.getExpiration() - token.getExpiration(), allOf(greaterThanOrEqualTo(1), lessThanOrEqualTo(10))); + Assert.assertThat(refreshedRefreshToken.getExpiration() - refreshToken.getExpiration(), allOf(greaterThanOrEqualTo(1), lessThanOrEqualTo(10))); + + Assert.assertNotEquals(token.getId(), refreshedToken.getId()); + Assert.assertNotEquals(refreshToken.getId(), refreshedRefreshToken.getId()); + + assertEquals("bearer", refreshResponse.getTokenType()); + + assertEquals(findUserByUsername(adminClient.realm("test"), "test-user@localhost").getId(), refreshedToken.getSubject()); + Assert.assertNotEquals("test-user@localhost", refreshedToken.getSubject()); + + assertEquals(1, refreshedToken.getRealmAccess().getRoles().size()); + Assert.assertTrue(refreshedToken.getRealmAccess().isUserInRole("user")); + + assertEquals(1, refreshedToken.getResourceAccess(oauth.getClientId()).getRoles().size()); + Assert.assertTrue(refreshedToken.getResourceAccess(oauth.getClientId()).isUserInRole("customer-user")); + + EventRepresentation refreshEvent = events.expectRefresh(event.getDetails().get(Details.REFRESH_TOKEN_ID), sessionId).assertEvent(); + Assert.assertNotEquals(event.getDetails().get(Details.TOKEN_ID), refreshEvent.getDetails().get(Details.TOKEN_ID)); + Assert.assertNotEquals(event.getDetails().get(Details.REFRESH_TOKEN_ID), refreshEvent.getDetails().get(Details.UPDATED_REFRESH_TOKEN_ID)); + + setTimeOffset(0); + } +}