Skip to content

Commit

Permalink
[ELY-2574] Add the ability to configure additional scope for authenti…
Browse files Browse the repository at this point in the history
…cation request
  • Loading branch information
PrarthonaPaul committed Jul 20, 2023
1 parent 15d2ded commit c9a589b
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 15 deletions.
Expand Up @@ -100,6 +100,9 @@ protected OidcClientConfiguration internalBuild(final OidcJsonConfiguration oidc
if (oidcJsonConfiguration.getTokenCookiePath() != null) {
oidcClientConfiguration.setOidcStateCookiePath(oidcJsonConfiguration.getTokenCookiePath());
}
if (oidcJsonConfiguration.getScope() != null) {
oidcClientConfiguration.setScope(oidcJsonConfiguration.getScope());
}
if (oidcJsonConfiguration.getPrincipalAttribute() != null) oidcClientConfiguration.setPrincipalAttribute(oidcJsonConfiguration.getPrincipalAttribute());

oidcClientConfiguration.setResourceCredentials(oidcJsonConfiguration.getCredentials());
Expand Down
Expand Up @@ -46,7 +46,7 @@
"register-node-at-startup", "register-node-period", "token-store", "adapter-state-cookie-path", "principal-attribute",
"proxy-url", "turn-off-change-session-id-on-login", "token-minimum-time-to-live",
"min-time-between-jwks-requests", "public-key-cache-ttl",
"ignore-oauth-query-parameter", "verify-token-audience", "token-signature-algorithm"
"ignore-oauth-query-parameter", "verify-token-audience", "token-signature-algorithm", "scope"
})
public class OidcJsonConfiguration {

Expand Down Expand Up @@ -140,6 +140,9 @@ public class OidcJsonConfiguration {
@JsonProperty("token-signature-algorithm")
protected String tokenSignatureAlgorithm = DEFAULT_TOKEN_SIGNATURE_ALGORITHM;

@JsonProperty("scope")
protected String scope;

/**
* The Proxy url to use for requests to the auth-server, configurable via the adapter config property {@code proxy-url}.
*/
Expand Down Expand Up @@ -511,5 +514,13 @@ public void setTokenSignatureAlgorithm(String tokenSignatureAlgorithm) {
this.tokenSignatureAlgorithm = tokenSignatureAlgorithm;
}

public String getScope(){
return scope;
}

public void setScope(String scope) {
this.scope = scope;
}

}

Expand Up @@ -43,10 +43,13 @@
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
//import java.util.stream.Collectors;

import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
Expand Down Expand Up @@ -169,7 +172,7 @@ protected String getRedirectUri(String state) {
for (String paramName : forwardableQueryParams) {
String paramValue = getQueryParamValue(facade, paramName);
if (SCOPE.equals(paramName)) {
paramValue = addOidcScopeIfNeeded(paramValue);
paramValue = addDeploymentAndQueryScopes(paramValue);
}
if (paramValue != null && !paramValue.isEmpty()) {
forwardedQueryParams.add(new BasicNameValuePair(paramName, paramValue));
Expand All @@ -180,6 +183,7 @@ protected String getRedirectUri(String state) {
if (deployment.getAuthUrl() == null) {
return null;
}

URIBuilder redirectUriBuilder = new URIBuilder(deployment.getAuthUrl())
.addParameter(RESPONSE_TYPE, CODE)
.addParameter(CLIENT_ID, deployment.getResourceName())
Expand Down Expand Up @@ -416,4 +420,29 @@ private static boolean hasScope(String scopeParam, String targetScope) {
}
return false;
}

private String addDeploymentAndQueryScopes(String paramValue){
String combinedScopes = null;
Set<String> allScopes = new HashSet<String>();
if (paramValue != null && !paramValue.isEmpty()) {
Set<String> queryScopes = new HashSet<String>(Arrays.asList(paramValue.split("\\s+")));
allScopes.addAll(queryScopes);
}
if (deployment.getScope() != null && !deployment.getScope().isEmpty()){
Set<String> deploymentScopes = new HashSet<String>(Arrays.asList(deployment.getScope().split("\\s+")));
allScopes.addAll(deploymentScopes);
}

for (String scope : allScopes) {
if (scope != null && !scope.isEmpty()){
if (combinedScopes == null){
combinedScopes = scope;
}
else if (!hasScope(scope, OIDC_SCOPE)) {//some OpenID providers require openid scope to be added in the beginning
combinedScopes = combinedScopes + " " + scope;
}
}
}
return addOidcScopeIfNeeded(combinedScopes);
}
}
Expand Up @@ -181,6 +181,7 @@ protected HtmlInput loginToKeycloak(String username, String password, URI reques
webClient.addCookie(getCookieString(cookie), requestUri.toURL(), null);
}
}

HtmlPage keycloakLoginPage = webClient.getPage(location);
HtmlForm loginForm = keycloakLoginPage.getForms().get(0);
loginForm.getInputByName(KEYCLOAK_USERNAME).setValueAttribute(username);
Expand Down
Expand Up @@ -22,14 +22,17 @@
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
import static org.wildfly.security.http.oidc.Oidc.OIDC_NAME;
import static org.wildfly.security.http.oidc.Oidc.OIDC_SCOPE;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.gargoylesoftware.htmlunit.WebClient;
import org.apache.http.HttpStatus;
import org.junit.AfterClass;
import org.junit.BeforeClass;
Expand All @@ -42,6 +45,7 @@
import io.restassured.RestAssured;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.QueueDispatcher;
import org.wildfly.security.http.HttpServerCookie;

/**
* Tests for the OpenID Connect authentication mechanism.
Expand Down Expand Up @@ -107,13 +111,13 @@ public void testWrongPassword() throws Exception {
@Test
public void testWrongAuthServerUrl() throws Exception {
performAuthentication(getOidcConfigurationInputStream(CLIENT_SECRET, "http://fakeauthserver/auth"), KeycloakConfiguration.ALICE,
KeycloakConfiguration.ALICE_PASSWORD, false, -1, null, null);
KeycloakConfiguration.ALICE_PASSWORD, false, -1, null, null, null, false);
}

@Test
public void testWrongClientSecret() throws Exception {
performAuthentication(getOidcConfigurationInputStream("WRONG_CLIENT_SECRET"), KeycloakConfiguration.ALICE,
KeycloakConfiguration.ALICE_PASSWORD, true, HttpStatus.SC_FORBIDDEN, null,"Forbidden");
KeycloakConfiguration.ALICE_PASSWORD, true, HttpStatus.SC_FORBIDDEN, null,"Forbidden", null, false);
}

@Test(expected = RuntimeException.class)
Expand All @@ -124,19 +128,19 @@ public void testMissingRequiredConfigurationOption() {
@Test
public void testSucessfulAuthenticationWithAuthServerUrl() throws Exception {
performAuthentication(getOidcConfigurationInputStream(), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD,
true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT);
true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT, null, false);
}

@Test
public void testSucessfulAuthenticationWithProviderUrl() throws Exception {
performAuthentication(getOidcConfigurationInputStreamWithProviderUrl(), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD,
true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT);
true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT, null, false);
}

@Test
public void testSucessfulAuthenticationWithProviderUrlTrailingSlash() throws Exception {
performAuthentication(getOidcConfigurationInputStreamWithProviderUrlTrailingSlash(), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD,
true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT);
true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT, null, false);
}

@Test
Expand All @@ -146,24 +150,63 @@ public void testSucessfulAuthenticationWithEnvironmentVariableExpression() throw
assertEquals(oidcProviderUrl, providerUrlEnv);

performAuthentication(getOidcConfigurationInputStreamWithEnvironmentVariableExpression(), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD,
true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT);
true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT, null, false);
}

@Test
public void testSucessfulAuthenticationWithSystemPropertyExpression() throws Exception {
performAuthentication(getOidcConfigurationInputStreamWithSystemPropertyExpression(), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD,
true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT);
true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT, null, false);
}

@Test
public void testTokenSignatureAlgorithm() throws Exception {
// keycloak uses RS256
performAuthentication(getOidcConfigurationInputStreamWithTokenSignatureAlgorithm(), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD,
true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT);
true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT, null, false);
}
@Test
public void testInvalidScope() throws Exception {
String expectedScope = OIDC_SCOPE + "+INVALID_SCOPE";
performAuthentication(getOidcConfigurationInputStreamWithScope(CLIENT_SECRET, "INVALID_SCOPE"), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD,
true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), "error=invalid_scope", expectedScope, true);
}

@Test
public void testEmptyScope() throws Exception {
performAuthentication(getOidcConfigurationInputStreamWithScope(CLIENT_SECRET, ""), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD,
true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT, OIDC_SCOPE, false);
}

@Test
public void testSingleScopeValue() throws Exception {
String expectedScope = OIDC_SCOPE + "+profile";
performAuthentication(getOidcConfigurationInputStreamWithScope(CLIENT_SECRET, "profile"), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD,
true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT, expectedScope, false);
}

@Test
public void testMultipleScopeValue() throws Exception {
String expectedScope = OIDC_SCOPE + "+phone+profile+email";
performAuthentication(getOidcConfigurationInputStreamWithScope(CLIENT_SECRET, "email phone profile"), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD,
true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT, expectedScope, false);
}

@Test
public void testOpenIDScopeValue() throws Exception {
String expectedScope = OIDC_SCOPE;
performAuthentication(getOidcConfigurationInputStreamWithScope(CLIENT_SECRET, OIDC_SCOPE), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD,
true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT, expectedScope, false);
}

@Test
public void testOpenIDWithMultipleScopeValue() throws Exception {
String expectedScope = OIDC_SCOPE + "+phone+profile+email";//order gets changed when combining with query parameters
performAuthentication(getOidcConfigurationInputStreamWithScope(CLIENT_SECRET, "email phone profile " + OIDC_SCOPE), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD,
true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT, expectedScope, false);
}
private void performAuthentication(InputStream oidcConfig, String username, String password, boolean loginToKeycloak,
int expectedDispatcherStatusCode, String expectedLocation, String clientPageText) throws Exception {
int expectedDispatcherStatusCode, String expectedLocation, String clientPageText, String expectedScope, boolean checkInvalidScopeError) throws Exception {
try {
Map<String, Object> props = new HashMap<>();
OidcClientConfiguration oidcClientConfiguration = OidcClientConfigurationBuilder.build(oidcConfig);
Expand All @@ -179,12 +222,30 @@ private void performAuthentication(InputStream oidcConfig, String username, Stri
TestingHttpServerResponse response = request.getResponse();
assertEquals(loginToKeycloak ? HttpStatus.SC_MOVED_TEMPORARILY : HttpStatus.SC_FORBIDDEN, response.getStatusCode());
assertEquals(Status.NO_AUTH, request.getResult());
if (expectedScope != null){
assertTrue(response.getFirstResponseHeaderValue("Location").contains("scope=" + expectedScope));
}

if (loginToKeycloak) {
client.setDispatcher(createAppResponse(mechanism, expectedDispatcherStatusCode, expectedLocation, clientPageText));
TextPage page = loginToKeycloak(username, password, requestUri, response.getLocation(),
response.getCookies()).click();
assertTrue(page.getContent().contains(clientPageText));

if (checkInvalidScopeError){
WebClient webClient = getWebClient();
List<HttpServerCookie> cookies = response.getCookies();

if (cookies != null) {
for (HttpServerCookie cookie : cookies) {
webClient.addCookie(getCookieString(cookie), requestUri.toURL(), null);
}
}
TextPage keycloakLoginPage = webClient.getPage(response.getLocation());
assertTrue(keycloakLoginPage.getWebResponse().getWebRequest().toString().contains("error_description=Invalid+scopes"));
}
else {
TextPage page = loginToKeycloak(username, password, requestUri, response.getLocation(),
response.getCookies()).click();
assertTrue(page.getContent().contains(clientPageText));
}
}
} finally {
client.setDispatcher(new QueueDispatcher());
Expand Down Expand Up @@ -290,4 +351,18 @@ private InputStream getOidcConfigurationInputStreamWithTokenSignatureAlgorithm()
"}";
return new ByteArrayInputStream(oidcConfig.getBytes(StandardCharsets.UTF_8));
}

private InputStream getOidcConfigurationInputStreamWithScope(String clientSecret, String scopeValue){
String oidcConfig = "{\n" +
" \"client-id\" : \"" + CLIENT_ID + "\",\n" +
" \"provider-url\" : \"" + KEYCLOAK_CONTAINER.getAuthServerUrl() + "/realms/" + TEST_REALM + "/" + "\",\n" +
" \"public-client\" : \"false\",\n" +
" \"scope\" : \"" + scopeValue + "\",\n" +
" \"ssl-required\" : \"EXTERNAL\",\n" +
" \"credentials\" : {\n" +
" \"secret\" : \"" + CLIENT_SECRET + "\"\n" +
" }\n" +
"}";
return new ByteArrayInputStream(oidcConfig.getBytes(StandardCharsets.UTF_8));
}
}

0 comments on commit c9a589b

Please sign in to comment.