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 Jan 31, 2025
1 parent c99a5ae commit 3e7c8cd
Show file tree
Hide file tree
Showing 18 changed files with 555 additions and 14 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 |
|-----------------|--------|--------------------------------------------------------------------|
| queryParamName | string | Name 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>
loginQueryParamNameValuePairs:
- queryParamName: <string>
queryParamValue: <string>
logoutQueryParamNameValuePairs:
- queryParamName: <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,84 @@
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 paramName;
private String paramValue;

@DataBoundConstructor
public OicQueryParameterConfiguration() {}

public OicQueryParameterConfiguration(@NonNull String paramName, @NonNull String paramValue) {
if (Util.fixEmptyAndTrim(paramName) == null) {
throw new IllegalStateException("Parameter name '" + paramName + "' must not be null or empty.");
}
setQueryParamName(paramName.trim());
setQueryParamValue(paramValue.trim());
}

@DataBoundSetter
public void setQueryParamName(String paramName) {
this.paramName = paramName;
}

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

public String getQueryParamName() {
return paramName;
}

public String getQueryParamValue() {
return paramValue;
}

public String getQueryParamNameEncoded() {
return paramName != null
? URLEncoder.encode(paramName, StandardCharsets.UTF_8).trim()
: null;
}

public String getQueryParamValueEncoded() {
return paramValue != null
? URLEncoder.encode(paramValue, StandardCharsets.UTF_8).trim()
: null;
}

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

@POST
public FormValidation doCheckQueryParamName(@QueryParameter String queryParamName) {
Jenkins.get().checkPermission(Jenkins.ADMINISTER);
if (Util.fixEmptyAndTrim(queryParamName) == null) {
return FormValidation.error(Messages.OicQueryParameterConfiguration_QueryParameterNameRequired());
}
return FormValidation.ok();
}
}
}
105 changes: 94 additions & 11 deletions src/main/java/org/jenkinsci/plugins/oic/OicSecurityRealm.java
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,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 jenkins.model.IdStrategy;
import jenkins.model.IdStrategyDescriptor;
Expand Down Expand Up @@ -316,6 +320,9 @@ ClientAuthenticationMethod toClientAuthenticationMethod() {
*/
private transient ProxyAwareResourceRetriever proxyAwareResourceRetriever;

private List<OicQueryParameterConfiguration> loginQueryParamNameValuePairs;
private List<OicQueryParameterConfiguration> logoutQueryParamNameValuePairs;

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

this.setLoginQueryParamNameValuePairs(this.loginQueryParamNameValuePairs);
this.setLogoutQueryParamNameValuePairs(this.logoutQueryParamNameValuePairs);

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

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

public List<OicQueryParameterConfiguration> getLoginQueryParamNameValuePairs() {
return loginQueryParamNameValuePairs;
}

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

public List<OicQueryParameterConfiguration> getLogoutQueryParamNameValuePairs() {
return logoutQueryParamNameValuePairs;
}

public String getClientId() {
return clientId;
}
Expand Down Expand Up @@ -583,9 +611,37 @@ protected TokenValidator createTokenValidator() {
conf.setDisablePkce(true);
}
opMetadataResolver.init();
if (loginQueryParamNameValuePairs != null && !loginQueryParamNameValuePairs.isEmpty()) {

Check warning on line 614 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 614 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(loginQueryParamNameValuePairs, 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> queryParamNameValuePairs, Set<String> forbiddenKeys) {
return queryParamNameValuePairs.stream()
.filter(c -> Util.fixEmptyAndTrim(c.getQueryParamName()) != null)
.filter(c -> !forbiddenKeys.contains(c.getQueryParamName()))
.collect(Collectors.toMap(
OicQueryParameterConfiguration::getQueryParamNameEncoded,
OicQueryParameterConfiguration::getQueryParamValueEncoded));
}

// Visible for testing
@Restricted(NoExternalUse.class)
protected void filterNonFIPS140CompliantAlgorithms(@NonNull OIDCProviderMetadata oidcProviderMetadata) {
Expand Down Expand Up @@ -1249,22 +1305,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 (logoutQueryParamNameValuePairs != null && !logoutQueryParamNameValuePairs.isEmpty()) {

Check warning on line 1324 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 1324 is only partially covered, one branch is missing
Map<String, String> customParameterMap =
getCustomParametersMap(logoutQueryParamNameValuePairs, 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
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
OicLogoutAction.OicLogout = Oic Logout

OicQueryParameterConfiguration.QueryParameterNameRequired = Query parameter name 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="${%QueryParameterName}" field="queryParamName">
<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 @@
QueryParameterName=Query Parameter Name
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 @@ -32,6 +32,28 @@
<f:dropdownDescriptorSelector title="${%GroupIdStrategy}" field="groupIdStrategy" default="${descriptor.defaultGroupIdStrategy()}" descriptors="${descriptor.idStrategyDescriptors}"/>
</f:entry>
</f:advanced>
<f:advanced title="${%LoginLogoutQueryParametersTitle}">
<f:entry title="${%LoginQueryParametersTitle}">
<f:repeatable field="loginQueryParamNameValuePairs"
header="${%LoginLogoutQueryParamNameValuePairs.header}"
minimum="0"
add="${%LoginQueryParamNameValuePairs.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="logoutQueryParamNameValuePairs"
header="${%LoginLogoutQueryParamNameValuePairs.header}"
minimum="0"
add="${%LogoutQueryParamNameValuePairs.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
LoginLogoutQueryParamNameValuePairs.header=Query Parameter
LoginQueryParametersTitle=Query Parameters for Login Endpoint
LoginQueryParamNameValuePairs.add=Add Login Query Parameter
LogoutQueryParametersTitle=Query Parameters for Logout Endpoint
LogoutQueryParamNameValuePairs.add=Add Logout Query Parameter
LogoutFromOpenIDProvider=Logout from OpenID Provider
PostLogoutRedirectUrl=Post logout redirect URL
Secret=Secret
Expand Down
Loading

0 comments on commit 3e7c8cd

Please sign in to comment.