Skip to content

Commit

Permalink
Add custom parameters to authorize and logout endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
eva-mueller-coremedia committed Dec 24, 2024
1 parent c62804f commit dce7676
Show file tree
Hide file tree
Showing 16 changed files with 420 additions and 22 deletions.
16 changes: 16 additions & 0 deletions docs/configuration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,16 @@ They are called claims in OpenID Connect terminology.
| emailFieldName | jmes path | claim to use for populating user email |
| groupsFieldName | jmes path | groups the user belongs to |

## Custom Query Parameters For Login and Logout Endpoints

Optional list of key / value query parameter pairs which will be appended
when calling the login resp. the logout endpoint.

| field | format | description |
|-----------------|--------|--------------------------------------------------------------------|
| queryParamKey | string | Key of the query parameter. |
| queryParamValue | string | Value of the query parameter. If empty, only the key will be sent. |


## JCasC configuration reference

Expand Down Expand Up @@ -142,6 +152,12 @@ jenkins:
rootURLFromRequest: <boolean>
sendScopesInTokenRequest: <boolean>
postLogoutRedirectUrl: <url>
loginQueryParamKeyValuePairs:
- queryParamKey: <string>
queryParamValue: <string>
logoutQueryParamKeyValuePairs:
- queryParamKey: <string>
queryParamValue: <string>
# Security
allowTokenAccessWithoutOicSession: <boolean>
allowedTokenExpirationClockSkewSeconds: <integer>
Expand Down
Binary file modified docs/images/global-config.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package org.jenkinsci.plugins.oic;

Check warning on line 1 in src/main/java/org/jenkinsci/plugins/oic/OicQueryParameterConfiguration.java

View check run for this annotation

ci.jenkins.io / Java Compiler

checkstyle:check

ERROR: (misc) NewlineAtEndOfFile: Expected line ending for file is LF(\n), but CRLF(\r\n) is detected.

import hudson.Extension;
import hudson.Util;
import hudson.model.AbstractDescribableImpl;
import hudson.model.Descriptor;
import hudson.util.FormValidation;
import java.io.Serializable;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import jenkins.model.Jenkins;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.verb.POST;
import org.springframework.lang.NonNull;

public class OicQueryParameterConfiguration extends AbstractDescribableImpl<OicQueryParameterConfiguration>
implements Serializable {

private static final long serialVersionUID = 1L;

private String key;

Check warning

Code scanning / Jenkins Security Scan

Jenkins: Plaintext password storage Warning

Field should be reviewed whether it stores a password and is serialized to disk: key
private String value;

@DataBoundConstructor
public OicQueryParameterConfiguration() {}

public OicQueryParameterConfiguration(@NonNull String key, @NonNull String value) {
if (Util.fixEmptyAndTrim(key) == null) {

Check warning on line 30 in src/main/java/org/jenkinsci/plugins/oic/OicQueryParameterConfiguration.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 30 is only partially covered, one branch is missing
throw new IllegalStateException("Key '" + key + "' must not be null or empty.");

Check warning on line 31 in src/main/java/org/jenkinsci/plugins/oic/OicQueryParameterConfiguration.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 31 is not covered by tests

Check warning on line 31 in src/main/java/org/jenkinsci/plugins/oic/OicQueryParameterConfiguration.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/jenkinsci/plugins/oic/OicQueryParameterConfiguration.java#L31

Added line #L31 was not covered by tests
}
setQueryParamKey(key);
setQueryParamValue(value.trim());
}

@DataBoundSetter
public void setQueryParamKey(String key) {
this.key = key;
}

@DataBoundSetter
public void setQueryParamValue(String value) {
this.value = value;
}

public String getQueryParamKey() {
return key;
}

public String getQueryParamValue() {
return value;
}

public String getQueryParamKeyDecoded() {
return key != null ? URLEncoder.encode(key, StandardCharsets.UTF_8) : null;

Check warning on line 56 in src/main/java/org/jenkinsci/plugins/oic/OicQueryParameterConfiguration.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 56 is only partially covered, one branch is missing
}

public String getQueryParamValueDecoded() {
return value != null ? URLEncoder.encode(value, StandardCharsets.UTF_8) : null;

Check warning on line 60 in src/main/java/org/jenkinsci/plugins/oic/OicQueryParameterConfiguration.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 60 is only partially covered, one branch is missing
}

@Extension
public static final class DescriptorImpl extends Descriptor<OicQueryParameterConfiguration> {
@NonNull
@Override
public String getDisplayName() {
return "Query Parameter Configuration";
}

@POST
public FormValidation doCheckQueryParamKey(@QueryParameter String value) {
Jenkins.get().checkPermission(Jenkins.ADMINISTER);

Check warning on line 73 in src/main/java/org/jenkinsci/plugins/oic/OicQueryParameterConfiguration.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/jenkinsci/plugins/oic/OicQueryParameterConfiguration.java#L73

Added line #L73 was not covered by tests
if (Util.fixEmptyAndTrim(value) == null) {
return FormValidation.error(Messages.OicQueryParameterConfiguration_QueryParameterKeyRequired());

Check warning on line 75 in src/main/java/org/jenkinsci/plugins/oic/OicQueryParameterConfiguration.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/jenkinsci/plugins/oic/OicQueryParameterConfiguration.java#L75

Added line #L75 was not covered by tests
}
return FormValidation.ok();

Check warning on line 77 in src/main/java/org/jenkinsci/plugins/oic/OicQueryParameterConfiguration.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 73-77 are not covered by tests

Check warning on line 77 in src/main/java/org/jenkinsci/plugins/oic/OicQueryParameterConfiguration.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/jenkinsci/plugins/oic/OicQueryParameterConfiguration.java#L77

Added line #L77 was not covered by tests
}
}
}
118 changes: 100 additions & 18 deletions src/main/java/org/jenkinsci/plugins/oic/OicSecurityRealm.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,17 @@
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.annotation.PostConstruct;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
Expand Down Expand Up @@ -305,6 +309,9 @@ ClientAuthenticationMethod toClientAuthenticationMethod() {
*/
private transient ProxyAwareResourceRetriever proxyAwareResourceRetriever;

private List<OicQueryParameterConfiguration> loginQueryParamKeyValuePairs;
private List<OicQueryParameterConfiguration> logoutQueryParamKeyValuePairs;

@DataBoundConstructor
public OicSecurityRealm(
String clientId,
Expand Down Expand Up @@ -357,6 +364,9 @@ protected Object readResolve() throws ObjectStreamException {
// ensure escapeHatchSecret is encrypted
this.setEscapeHatchSecret(this.escapeHatchSecret);

this.setLoginQueryParamKeyValuePairs(this.loginQueryParamKeyValuePairs);
this.setLogoutQueryParamKeyValuePairs(this.logoutQueryParamKeyValuePairs);

// validate this option in FIPS env or not
try {
this.setEscapeHatchEnabled(this.escapeHatchEnabled);
Expand Down Expand Up @@ -397,6 +407,24 @@ protected Object readResolve() throws ObjectStreamException {
return this;
}

@DataBoundSetter
public void setLoginQueryParamKeyValuePairs(List<OicQueryParameterConfiguration> values) {
this.loginQueryParamKeyValuePairs = values;
}

public List<OicQueryParameterConfiguration> getLoginQueryParamKeyValuePairs() {
return loginQueryParamKeyValuePairs;
}

@DataBoundSetter
public void setLogoutQueryParamKeyValuePairs(List<OicQueryParameterConfiguration> values) {
this.logoutQueryParamKeyValuePairs = values;
}

public List<OicQueryParameterConfiguration> getLogoutQueryParamKeyValuePairs() {
return logoutQueryParamKeyValuePairs;
}

public String getClientId() {
return clientId;
}
Expand Down Expand Up @@ -505,7 +533,7 @@ ProxyAwareResourceRetriever getResourceRetriever() {
return proxyAwareResourceRetriever;
}

private OidcConfiguration buildOidcConfiguration() {
private OidcConfiguration buildOidcConfiguration(boolean addCustomLoginParams) {
// TODO cache this and use the well known if available.

Check warning on line 537 in src/main/java/org/jenkinsci/plugins/oic/OicSecurityRealm.java

View check run for this annotation

ci.jenkins.io / Open Tasks Scanner

TODO

NORMAL: cache this and use the well known if available.
OidcConfiguration conf = new CustomOidcConfiguration(this.isDisableSslVerification());
conf.setClientId(clientId);
Expand Down Expand Up @@ -534,9 +562,36 @@ private OidcConfiguration buildOidcConfiguration() {
if (this.isPkceEnabled()) {
conf.setPkceMethod(CodeChallengeMethod.S256);
}
if (addCustomLoginParams && loginQueryParamKeyValuePairs != null && !loginQueryParamKeyValuePairs.isEmpty()) {

Check warning on line 565 in src/main/java/org/jenkinsci/plugins/oic/OicSecurityRealm.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 565 is only partially covered, one branch is missing
Set<String> forbiddenKeys = Set.of(
OidcConfiguration.SCOPE,
OidcConfiguration.RESPONSE_TYPE,
OidcConfiguration.RESPONSE_MODE,
OidcConfiguration.REDIRECT_URI,
OidcConfiguration.CLIENT_ID,
OidcConfiguration.STATE,
OidcConfiguration.MAX_AGE,
OidcConfiguration.PROMPT,
OidcConfiguration.NONCE,
OidcConfiguration.CODE_CHALLENGE,
OidcConfiguration.CODE_CHALLENGE_METHOD);
Map<String, String> customParameterMap =
getCustomParametersMap(loginQueryParamKeyValuePairs, forbiddenKeys);
LOGGER.info("Append the following custom parameters to the authorize endpoint: " + customParameterMap);
customParameterMap.forEach(conf::addCustomParam);
}
return conf;
}

Map<String, String> getCustomParametersMap(
List<OicQueryParameterConfiguration> queryParamKeyValuePairs, Set<String> forbiddenKeys) {
return queryParamKeyValuePairs.stream()
.filter(c -> !forbiddenKeys.contains(c.getQueryParamKeyDecoded()))
.collect(Collectors.toMap(
OicQueryParameterConfiguration::getQueryParamKeyDecoded,
OicQueryParameterConfiguration::getQueryParamValueDecoded));
}

// Visible for testing
@Restricted(NoExternalUse.class)
protected void filterNonFIPS140CompliantAlgorithms(@NonNull OIDCProviderMetadata oidcProviderMetadata) {
Expand Down Expand Up @@ -670,8 +725,8 @@ private void filterJwsAlgorithms(@NonNull OIDCProviderMetadata oidcProviderMetad
}

@Restricted(NoExternalUse.class) // exposed for testing only
protected OidcClient buildOidcClient() {
OidcConfiguration oidcConfiguration = buildOidcConfiguration();
protected OidcClient buildOidcClient(boolean addCustomLoginParams) {
OidcConfiguration oidcConfiguration = buildOidcConfiguration(addCustomLoginParams);
OidcClient client = new OidcClient(oidcConfiguration);
// add the extra settings for the client...
client.setCallbackUrl(buildOAuthRedirectUrl());
Expand Down Expand Up @@ -932,7 +987,7 @@ protected String getValidRedirectUrl(String url) {
public void doCommenceLogin(@QueryParameter String from, @Header("Referer") final String referer)
throws URISyntaxException {

OidcClient client = buildOidcClient();
OidcClient client = buildOidcClient(true);
// add the extra params for the client...
final String redirectOnFinish = getValidRedirectUrl(from != null ? from : referer);

Expand Down Expand Up @@ -1172,7 +1227,7 @@ public String getPostLogOutUrl2(StaplerRequest req, Authentication auth) {
@VisibleForTesting
Object getStateAttribute(HttpSession session) {
// return null;
OidcClient client = buildOidcClient();
OidcClient client = buildOidcClient(false);
WebContext webContext =
JEEContextFactory.INSTANCE.newContext(Stapler.getCurrentRequest(), Stapler.getCurrentResponse());
SessionStore sessionStore = JEESessionStoreFactory.INSTANCE.newSessionStore();
Expand All @@ -1183,22 +1238,49 @@ Object getStateAttribute(HttpSession session) {
}

@CheckForNull
private String maybeOpenIdLogoutEndpoint(String idToken, String state, String postLogoutRedirectUrl) {
String maybeOpenIdLogoutEndpoint(String idToken, String state, String postLogoutRedirectUrl) {
final URI url = serverConfiguration.toProviderMetadata().getEndSessionEndpointURI();
if (this.logoutFromOpenidProvider && url != null) {
StringBuilder openidLogoutEndpoint = new StringBuilder(url.toString());

Map<String, String> segmentsMap = new HashMap<>();
Set<String> segmentsSet = new HashSet<>();
if (!Strings.isNullOrEmpty(idToken)) {
openidLogoutEndpoint.append("?id_token_hint=").append(idToken).append("&");
} else {
openidLogoutEndpoint.append("?");
segmentsMap.put("id_token_hint", idToken);
}
if (!Strings.isNullOrEmpty(state) && !"null".equals(state)) {
segmentsMap.put("state", state);
}
openidLogoutEndpoint.append("state=").append(state);

if (postLogoutRedirectUrl != null) {
openidLogoutEndpoint
.append("&post_logout_redirect_uri=")
.append(URLEncoder.encode(postLogoutRedirectUrl, StandardCharsets.UTF_8));
segmentsMap.put(
"post_logout_redirect_uri", URLEncoder.encode(postLogoutRedirectUrl, StandardCharsets.UTF_8));
}
Set<String> forbiddenKeys = Set.of("id_token_hint", "state", "post_logout_redirect_uri");
if (logoutQueryParamKeyValuePairs != null && !logoutQueryParamKeyValuePairs.isEmpty()) {

Check warning on line 1257 in src/main/java/org/jenkinsci/plugins/oic/OicSecurityRealm.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 1257 is only partially covered, one branch is missing
Map<String, String> customParameterMap =
getCustomParametersMap(logoutQueryParamKeyValuePairs, forbiddenKeys);
LOGGER.info("Append the following custom parameters to the logout endpoint: " + customParameterMap);

customParameterMap.forEach((k, v) -> {
String key = k.trim();
String value = v.trim();
if (value.isEmpty()) {
segmentsSet.add(key);
} else {
segmentsMap.put(key, value);
}
});
}

StringBuilder openidLogoutEndpoint = new StringBuilder(url.toString());
String concatChar = openidLogoutEndpoint.toString().contains("?") ? "&" : "?";
if (!segmentsMap.isEmpty()) {
String joinedString = segmentsMap.entrySet().stream()
.map(entry -> entry.getKey() + "=" + entry.getValue())
.collect(Collectors.joining("&"));
openidLogoutEndpoint.append(concatChar).append(joinedString);
concatChar = "&";
}
if (!segmentsSet.isEmpty()) {
openidLogoutEndpoint.append(concatChar).append(String.join("&", segmentsSet));
}
return openidLogoutEndpoint.toString();
}
Expand Down Expand Up @@ -1243,7 +1325,7 @@ private String buildOAuthRedirectUrl() throws NullPointerException {
* @throws ParseException if the JWT (or other response) could not be parsed.
*/
public void doFinishLogin(StaplerRequest request, StaplerResponse response) throws IOException, ParseException {
OidcClient client = buildOidcClient();
OidcClient client = buildOidcClient(false);

WebContext webContext = JEEContextFactory.INSTANCE.newContext(request, response);
SessionStore sessionStore = JEESessionStoreFactory.INSTANCE.newSessionStore();
Expand Down Expand Up @@ -1386,7 +1468,7 @@ private boolean refreshExpiredToken(

WebContext webContext = JEEContextFactory.INSTANCE.newContext(httpRequest, httpResponse);
SessionStore sessionStore = JEESessionStoreFactory.INSTANCE.newSessionStore();
OidcClient client = buildOidcClient();
OidcClient client = buildOidcClient(false);
// PAC4J maintains the nonce even though servers should not respond with an id token containing the nonce
// https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse
// it SHOULD NOT have a nonce Claim, even when the ID Token issued at the time of the original authentication
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
OicLogoutAction.OicLogout = Oic Logout

OicQueryParameterConfiguration.QueryParameterKeyRequired = Query parameter key is required.
OicQueryParameterConfiguration.QueryParameterValueRequired = Query parameter value is required.

OicSecurityRealm.DisplayName = Login with Openid Connect
OicSecurityRealm.CouldNotRefreshToken = Unable to refresh access token
OicSecurityRealm.ClientIdRequired = Client id is required.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<f:entry title="${%QueryParameterKey}" field="queryParamKey">
<f:textbox />
</f:entry>
<f:entry title="${%QueryParameterValue}" field="queryParamValue">
<f:textbox />
</f:entry>
</j:jelly>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
QueryParameterKey=Query Parameter Key
QueryParameterValue=Query Parameter Value
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div>
Additional custom query parameters added to a URL.
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,28 @@
<f:textbox/>
</f:entry>
</f:advanced>
<f:advanced title="${%LoginLogoutQueryParametersTitle}">
<f:entry title="${%LoginQueryParametersTitle}">
<f:repeatable field="loginQueryParamKeyValuePairs"
header="${%LoginLogoutQueryParamKeyValuePairs.header}"
minimum="0"
add="${%LoginQueryParamKeyValuePairs.add}">
<st:include page="config.jelly"
class="org.jenkinsci.plugins.oic.OicQueryParameterConfiguration"/>
<div align="right"><f:repeatableDeleteButton/></div>
</f:repeatable>
</f:entry>
<f:entry title="${%LogoutQueryParametersTitle}">
<f:repeatable field="logoutQueryParamKeyValuePairs"
header="${%LoginLogoutQueryParamKeyValuePairs.header}"
minimum="0"
add="${%LogoutQueryParamKeyValuePairs.add}">
<st:include page="config.jelly"
class="org.jenkinsci.plugins.oic.OicQueryParameterConfiguration"/>
<div align="right"><f:repeatableDeleteButton/></div>
</f:repeatable>
</f:entry>
</f:advanced>
<f:entry title="${%LogoutFromOpenIDProvider}" field="logoutFromOpenidProvider">
<f:checkbox id="logoutFromIDP"/>
</f:entry>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ EnablePKCE=Enable Proof Key for Code Exchange (PKCE)
FullnameFieldName=Full name field name
Group=Group
GroupsFieldName=Groups field name
LoginLogoutQueryParametersTitle=Query Parameters for Login and Logout Endpoints
LoginLogoutQueryParamKeyValuePairs.header=Query Parameter
LoginQueryParametersTitle=Query Parameters for Login Endpoint
LoginQueryParamKeyValuePairs.add=Add Login Query Parameter
LogoutQueryParametersTitle=Query Parameters for Logout Endpoint
LogoutQueryParamKeyValuePairs.add=Add Logout Query Parameter
LogoutFromOpenIDProvider=Logout from OpenID Provider
PostLogoutRedirectUrl=Post logout redirect URL
Secret=Secret
Expand Down
Loading

0 comments on commit dce7676

Please sign in to comment.