KEYCLOAK-2604 Proof Key for Code Exchange by OAuth Public Clients - RFC
7636 - Arquillian Test Cases
This commit is contained in:
parent
fe5fe4c968
commit
6bab704bba
2 changed files with 584 additions and 0 deletions
|
@ -111,6 +111,11 @@ public class OAuthClient {
|
|||
|
||||
private Map<String, PublicKey> 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;
|
||||
|
|
|
@ -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 <a href="takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
|
||||
*/
|
||||
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<RealmRepresentation> 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);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue