Skip to content

Feature: configure keycloak context path or not #16

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 14 additions & 14 deletions .github/workflows/pr_workflow.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
# This workflow will build a Java project with Gradle
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle

#name: Testing For PRs
name: Testing For PRs

#on: [ pull_request ]
on: [ pull_request ]

#jobs:
# test:
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v3
# - name: Set up JDK
# uses: actions/setup-java@v3
# with:
# java-version: 17
# distribution: temurin
# - name: Build with Gradle
#
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK
uses: actions/setup-java@v3
with:
java-version: 11
distribution: temurin
- name: Build with Gradle
run: ./gradlew assemble check
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ apply from: "https://raw.githubusercontent.com/gocd/gocd-plugin-gradle-task-help
gocdPlugin {
id = 'cd.go.authorization.keycloak'
pluginVersion = '2.0.0'
goCdVersion = '19.2.0'
goCdVersion = '23.3.0'
name = 'Keycloak oauth authorization plugin'
description = 'Keycloak oauth authorization plugin for GoCD'
vendorName = 'klinux'
Expand Down
1 change: 1 addition & 0 deletions docs/AUTHORIZATION_CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ The `Authorization Configuration` is used to configure a connection to an Keyclo
3. Provide a unique identifier for this authorization configuration and select `Keycloak oauth authorization plugin` as the **Plugin**.

4. **Keycloak Endpoint (`Mandatory`):** Specify your Keycloak Endpoint.
> If you have the context /auth in you Keycloak, set the context path in Endpoint config.

![Keycloak Endpoint](images/keycloak_endpoint.png?raw=true "Keycloak Endpoint")

Expand Down
2 changes: 1 addition & 1 deletion docs/INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Requirements

* GoCD server version v17.5.0 or above
* GoCD server version v17.5.0 for version 1.0.x and 23.3.0 for 2.0.x.
* Keycloak [API documentation](https://www.keycloak.org/docs-api/11.0/rest-api/index.html)

## Installation
Expand Down
2 changes: 1 addition & 1 deletion gocd/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ services:
volumes:
- data:/opt/jboss/keycloak/standalone/data
gocd:
image: gocd/gocd-server:v22.3.0
image: gocd/gocd-server:v23.3.0
volumes:
- data:/godata
- ./plugins:/godata/plugins
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ public String authorizationServerUrl(String callbackUrl) throws Exception {

return HttpUrl.parse(keycloakConfiguration.keycloakEndpoint())
.newBuilder()
.addPathSegments("auth")
.addPathSegments("realms")
.addPathSegments(realm)
.addPathSegments("protocol")
Expand All @@ -73,24 +72,24 @@ public String authorizationServerUrl(String callbackUrl) throws Exception {
.addQueryParameter("client_id", keycloakConfiguration.clientId())
.addQueryParameter("redirect_uri", callbackUrl)
.addQueryParameter("response_type", "code")
.addQueryParameter("scope", "openid profile email groups roles")
.addQueryParameter("scope", keycloakConfiguration.keycloakScopes())
.addQueryParameter("state", UUID.randomUUID().toString())
.addQueryParameter("nonce", UUID.randomUUID().toString())
.build().toString();
}

public TokenInfo fetchAccessToken(Map<String, String> params) throws Exception {
String realm = keycloakConfiguration.keycloakRealm();
final String code = params.get("code");

if (isBlank(code)) {
throw new RuntimeException("[KeycloakApiClient] Authorization code must not be null.");
}

LOG.debug("[KeycloakApiClient] Fetching access token using authorization code.");
String realm = keycloakConfiguration.keycloakRealm();

final String accessTokenUrl = HttpUrl.parse(keycloakConfiguration.keycloakEndpoint())
.newBuilder()
.addPathSegments("auth")
.addPathSegments("realms")
.addPathSegments(realm)
.addPathSegments("protocol")
Expand All @@ -117,6 +116,7 @@ public TokenInfo fetchAccessToken(Map<String, String> params) throws Exception {
public KeycloakUser userProfile(TokenInfo tokenInfo) throws Exception {
validateTokenInfo(tokenInfo);
String accessToken = tokenInfo.accessToken();
String realm = keycloakConfiguration.keycloakRealm();

// Check status of token
LOG.debug("[KeycloakApiClient] Token Before: " + tokenInfo.accessToken());
Expand All @@ -129,11 +129,9 @@ public KeycloakUser userProfile(TokenInfo tokenInfo) throws Exception {
}

LOG.debug("[KeycloakApiClient] Fetching user profile using access token.");
String realm = keycloakConfiguration.keycloakRealm();

final String userProfileUrl = HttpUrl.parse(keycloakConfiguration.keycloakEndpoint())
.newBuilder()
.addPathSegments("auth")
.addPathSegments("realms")
.addPathSegments(realm)
.addPathSegments("protocol")
Expand Down Expand Up @@ -182,7 +180,6 @@ public Boolean introspectToken(String token) throws Exception {

final String introspectUrl = HttpUrl.parse(keycloakConfiguration.keycloakEndpoint())
.newBuilder()
.addPathSegments("auth")
.addPathSegments("realms")
.addPathSegments(realm)
.addPathSegments("protocol")
Expand Down Expand Up @@ -218,10 +215,9 @@ public GoPluginApiResponse fetchRefreshToken(String refresh_token) throws Except
String client = keycloakConfiguration.clientId();
String secret = keycloakConfiguration.clientSecret();
String basicEncode = Base64.getEncoder().encodeToString((client + ":" + secret).getBytes());

final String refreshTokenUrl = HttpUrl.parse(keycloakConfiguration.keycloakEndpoint())
.newBuilder()
.addPathSegments("auth")
.addPathSegments("realms")
.addPathSegments(realm)
.addPathSegments("protocol")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ public class KeycloakConfiguration implements Validatable {
@ProfileField(key = "ClientSecret", required = true, secure = true)
private String clientSecret;

@Expose
@SerializedName("KeycloakScopes")
@ProfileField(key = "KeycloakScopes", required = true, secure = false)
private String keycloakScopes;

private KeycloakApiClient keycloakApiClient;

public KeycloakConfiguration() {
Expand Down Expand Up @@ -75,6 +80,10 @@ public String clientSecret() {
return clientSecret;
}

public String keycloakScopes() {
return keycloakScopes;
}

public String toJSON() {
return GSON.toJson(this);
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/resource-templates/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
~ limitations under the License.
-->

<go-plugin id="${id}" version="1">
<go-plugin id="${id}" version="2">
<about>
<name>${name}</name>
<version>${version}</version>
Expand Down
4 changes: 2 additions & 2 deletions src/main/resources-generated/plugin.properties
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@

id=cd.go.authorization.keycloak
name=Keycloak oauth authorization plugin
version=2.0.0-19
goCdVersion=19.2.0
version=2.0.0-23
goCdVersion=23.3.0
description=Keycloak oauth authorization plugin for GoCD
vendorName=klinux
vendorUrl=https://github.com/klinux/gocd-keycloak-oauth-authorization-plugin
4 changes: 2 additions & 2 deletions src/main/resources-generated/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
<go-plugin id="cd.go.authorization.keycloak" version="1">
<about>
<name>Keycloak oauth authorization plugin</name>
<version>2.0.0-19</version>
<target-go-version>19.2.0</target-go-version>
<version>2.0.0-23</version>
<target-go-version>23.3.0</target-go-version>
<description>Keycloak oauth authorization plugin for GoCD</description>
<vendor>
<name>klinux</name>
Expand Down
15 changes: 14 additions & 1 deletion src/main/resources/auth-config.template.html
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
<label ng-class="{'is-invalid-label': GOINPUTNAME[KeycloakEndpoint].$error.server}">Keycloak Endpoint:<span class='asterix'>*</span>
<div class="tooltip-info">
<span class="tooltip-content">
Your Keycloak authentication endpoint.
Your Keycloak authentication endpoint. If you have the context path /auth in you Keycloak, put the context here.
</span>
</div>
</label>
Expand Down Expand Up @@ -132,4 +132,17 @@
<input ng-class="{'is-invalid-input': GOINPUTNAME[ClientSecret].$error.server}" type="password" ng-model="ClientSecret" ng-required="true"/>
<span class="form_error form-error" ng-class="{'is-visible': GOINPUTNAME[ClientSecret].$error.server}" ng-show="GOINPUTNAME[ClientSecret].$error.server">{{GOINPUTNAME[ClientSecret].$error.server}}</span>
</div>

<div class="form_item_block">
<label ng-class="{'is-invalid-label': GOINPUTNAME[KeycloakScopes].$error.server}">Keycloak Scopes:<span class='asterix'>*</span>
<div class="tooltip-info">
<span class="tooltip-content">
Set the scopes that you be requested to Keycloak. Default: openid profile email groups roles
</span>
</div>
</label>
<input ng-class="{'is-invalid-input': GOINPUTNAME[KeycloakScopes].$error.server}" type="text" ng-model="KeycloakScopes" value="openid profile email groups roles" ng-required="true"/>
<span class="form_error form-error" ng-class="{'is-visible': GOINPUTNAME[KeycloakScopes].$error.server}" ng-show="GOINPUTNAME[KeycloakScopes].$error.server">{{GOINPUTNAME[KeycloakScopes].$error.server}}</span>
</div>

</div>
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,14 @@
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.Mock;

import java.util.Collections;

import static org.hamcrest.CoreMatchers.startsWith;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;

Expand All @@ -44,9 +43,6 @@ public class KeycloakApiClientTest {
private MockWebServer server;
private KeycloakApiClient KeycloakApiClient;

@Rule
public ExpectedException thrown = ExpectedException.none();

@Before
public void setUp() throws Exception {
initMocks(this);
Expand All @@ -58,6 +54,7 @@ public void setUp() throws Exception {
when(KeycloakConfiguration.keycloakRealm()).thenReturn("master");
when(KeycloakConfiguration.clientId()).thenReturn("client-id");
when(KeycloakConfiguration.clientSecret()).thenReturn("client-secret");
when(KeycloakConfiguration.keycloakScopes()).thenReturn("openid profile email groups roles");

CallbackURL.instance().updateRedirectURL("callback-url");

Expand All @@ -73,7 +70,7 @@ public void tearDown() throws Exception {
public void shouldReturnAuthorizationServerUrl() throws Exception {
final String authorizationServerUrl = KeycloakApiClient.authorizationServerUrl("call-back-url");

assertThat(authorizationServerUrl, startsWith("https://example.com/auth/realms/master/protocol/openid-connect/auth?client_id=client-id&redirect_uri=call-back-url&response_type=code&scope=openid%20profile%20email%20groups%20roles&state="));
assertThat(authorizationServerUrl, startsWith("https://example.com/realms/master/protocol/openid-connect/auth?client_id=client-id&redirect_uri=call-back-url&response_type=code&scope=openid%20profile%20email%20groups%20roles&state="));
}

@Test
Expand All @@ -89,7 +86,7 @@ public void shouldFetchTokenInfoUsingAuthorizationCode() throws Exception {
assertThat(tokenInfo.accessToken(), is("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9"));

RecordedRequest request = server.takeRequest();
assertEquals("POST /auth/realms/master/protocol/openid-connect/token HTTP/1.1", request.getRequestLine());
assertEquals("POST /realms/master/protocol/openid-connect/token HTTP/1.1", request.getRequestLine());
assertEquals("application/x-www-form-urlencoded", request.getHeader("Content-Type"));
assertEquals("client_id=client-id&client_secret=client-secret&code=some-code&grant_type=authorization_code&redirect_uri=callback-url", request.getBody().readUtf8());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
import static java.util.Collections.singletonList;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertThat;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.Mockito.*;

public class KeycloakAuthorizerTest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import org.junit.Test;

import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.hamcrest.MatcherAssert.assertThat;

public class KeycloakUserTest {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ public void shouldValidateMandatoryKeys() throws Exception {
" {\n" +
" \"message\": \"ClientSecret must not be blank.\",\n" +
" \"key\": \"ClientSecret\"\n" +
" },\n" +
" {\n" +
" \"message\": \"KeycloakScopes must not be blank.\",\n" +
" \"key\": \"KeycloakScopes\"\n" +
" }\n" +
"]";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,12 @@
import java.util.Collections;

import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;

public class FetchAccessTokenRequestExecutorTest {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Mock
private FetchAccessTokenRequest request;
@Mock
Expand All @@ -64,10 +63,8 @@ public void setUp() throws Exception {
public void shouldErrorOutIfAuthConfigIsNotProvided() throws Exception {
when(request.authConfigs()).thenReturn(Collections.emptyList());

thrown.expect(NoAuthorizationConfigurationException.class);
thrown.expectMessage("[Get Access Token] No authorization configuration found.");

executor.execute();
Throwable thrown = assertThrows(NoAuthorizationConfigurationException.class, () -> executor.execute());
assertThat(thrown.getMessage(), is("[Get Access Token] No authorization configuration found."));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.hamcrest.MatcherAssert.assertThat;

public class GetAuthConfigMetadataRequestExecutorTest {

Expand Down Expand Up @@ -71,6 +71,13 @@ public void assertJsonStructure() throws Exception {
" \"required\": true,\n" +
" \"secure\": true\n" +
" }\n" +
" },\n" +
" {\n" +
" \"key\": \"KeycloakScopes\",\n" +
" \"metadata\": {\n" +
" \"required\": true,\n" +
" \"secure\": false\n" +
" }\n" +
" }\n" +
"]";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import java.util.Map;

import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertThat;
import static org.hamcrest.MatcherAssert.assertThat;

public class GetAuthConfigViewRequestExecutorTest {

Expand Down
Loading