Skip to content
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

Add custom parameters to authorize and logout endpoints #480

Open
wants to merge 3 commits into
base: master
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
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,88 @@
package org.jenkinsci.plugins.oic;

Check warning on line 1 in src/main/java/org/jenkinsci/plugins/oic/AbstractKeyValueDescribable.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.model.AbstractDescribableImpl;
import hudson.model.Descriptor;
import hudson.util.FormValidation;
import hudson.util.FormValidation.Kind;
import org.apache.commons.lang3.StringUtils;
import org.kohsuke.stapler.QueryParameter;

public abstract class AbstractKeyValueDescribable<T extends AbstractKeyValueDescribable<T>>
extends AbstractDescribableImpl<T> {

private final String key;
private final String value;

/**
* Create a new instance with the provided key/value combination.
* @param key non-blank String to use as the key, will be {@code trim}ed before persisting
* @param value non-blank string for the value, will be {@code trim}ed before persisting
* @throws Descriptor.FormException if either key/value are {@code null} or are not valid values
*/
public AbstractKeyValueDescribable(String key, String value) throws Descriptor.FormException {
this(key, value, false);
}

/**
* Create a new instance with the provided key/value combination.
* @param key non-blank String to use as the key, will be {@code trim}ed before persisting
* @param value possibly blank string for the value, will be {@code trim}ed before persisting
* @param allowBlankValue {@code true} it {@code value} may be blank (but not null)
* @throws Descriptor.FormException if either key/value are {@code null} or are not valid values
*/
public AbstractKeyValueDescribable(String key, String value, boolean allowBlankValue)
throws Descriptor.FormException {
// formValidation should not error for blank entries so we need to explicitly check them
if (StringUtils.isBlank(key)) {
throw new Descriptor.FormException("key must not be blank", "key");
}
if (!allowBlankValue && StringUtils.isBlank(value)) {
throw new Descriptor.FormException("value must not be blank", "value");
}

FormValidation keyValidation = getDescriptor().doCheckKey(key);
if (keyValidation.kind == Kind.ERROR) {
throw new Descriptor.FormException(keyValidation.getMessage(), "key");
}
FormValidation valueValidation = getDescriptor().doCheckValue(value);
if (valueValidation.kind == Kind.ERROR) {

Check warning on line 48 in src/main/java/org/jenkinsci/plugins/oic/AbstractKeyValueDescribable.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 48 is only partially covered, one branch is missing
throw new Descriptor.FormException(valueValidation.getMessage(), "value");

Check warning on line 49 in src/main/java/org/jenkinsci/plugins/oic/AbstractKeyValueDescribable.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 49 is not covered by tests

Check warning on line 49 in src/main/java/org/jenkinsci/plugins/oic/AbstractKeyValueDescribable.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/jenkinsci/plugins/oic/AbstractKeyValueDescribable.java#L49

Added line #L49 was not covered by tests
}
this.key = key.trim();
this.value = value == null ? "" : value.trim();
Copy link
Contributor Author

@eva-mueller-coremedia eva-mueller-coremedia Feb 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @jtnord - I added some more test cases and encountered for LogoutQueryParameters a NPE when the value is null.

Is this solution OK for you?

In case the this.value should then be null, then all getValue() needs to be covered as well.

}

public String getKey() {
return key;
}

public String getValue() {
return value;
}

@Override
public DescriptorImpl<T> getDescriptor() {
return (DescriptorImpl<T>) super.getDescriptor();
}

public static class DescriptorImpl<T extends AbstractKeyValueDescribable<T>> extends Descriptor<T> {

/**
* Check the key for validity.
* In addition to being used by the UI, any FormValidation of {@code Kind.ERROR} will cause a fail to create the describable.
* By default, this method returns {@link FormValidation#ok()}, subclasses should override this in order to provide any required checking.
*/
public FormValidation doCheckKey(@SuppressWarnings("unused") @QueryParameter String key) {
return FormValidation.ok();

Check warning on line 76 in src/main/java/org/jenkinsci/plugins/oic/AbstractKeyValueDescribable.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 76 is not covered by tests

Check warning on line 76 in src/main/java/org/jenkinsci/plugins/oic/AbstractKeyValueDescribable.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/jenkinsci/plugins/oic/AbstractKeyValueDescribable.java#L76

Added line #L76 was not covered by tests
}

/**
* Check the key for validity.
* In addition to being used by the UI, any FormValidation of {@code Kind.ERROR} will cause a fail to create the describable.
* By default this method returns {@link FormValidation#ok()}, subclasses should override this in order to provide any required checking.
*/
public FormValidation doCheckValue(@SuppressWarnings("unused") @QueryParameter String value) {
return FormValidation.ok();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.jenkinsci.plugins.oic;

Check warning on line 1 in src/main/java/org/jenkinsci/plugins/oic/AbstractQueryParameter.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.model.Descriptor;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

public abstract class AbstractQueryParameter<T extends AbstractQueryParameter<T>>
extends AbstractKeyValueDescribable<T> {

public AbstractQueryParameter(String key, String value) throws Descriptor.FormException {
super(key, value);
}

public AbstractQueryParameter(String key, String value, boolean allowEmptyValue) throws Descriptor.FormException {
super(key, value, allowEmptyValue);
}

/**
* Return {@link #getKey()} encoded with {@code application/x-www-form-urlencoded} in {@code UTF-8}
*/
public String getURLEncodedKey() {
return URLEncoder.encode(getKey(), StandardCharsets.UTF_8);
}

/**
* Return {@link #getValue()} encoded with {@code application/x-www-form-urlencoded} in {@code UTF-8}
*/
public String getURLEncodedValue() {
return URLEncoder.encode(getValue(), StandardCharsets.UTF_8);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.jenkinsci.plugins.oic;

Check warning on line 1 in src/main/java/org/jenkinsci/plugins/oic/LoginQueryParameter.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.model.Descriptor.FormException;
import hudson.util.FormValidation;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import org.pac4j.oidc.config.OidcConfiguration;

public class LoginQueryParameter extends AbstractQueryParameter<LoginQueryParameter> {

@DataBoundConstructor
public LoginQueryParameter(String key, String value) throws FormException {
super(key, value);
}

@Extension
public static class DescriptorImpl extends AbstractKeyValueDescribable.DescriptorImpl<LoginQueryParameter> {

@Override
public FormValidation doCheckKey(@QueryParameter String key) {

Check warning

Code scanning / Jenkins Security Scan

Stapler: Missing permission check Warning

Potential missing permission check in DescriptorImpl#doCheckKey
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is nothing sensitive here.
Whilst we could add a permission check we are only doing string comparisons

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jtnord Unfortunately, I do not know how to resolve all the problems found by the github-advanced-security. Can you help me here or step in? That would be great.

Also there is already one dismissed problem. Dunno who or how this got dismissed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can take a look.
I dismissed one as a false positive. (key was not sensitive in that context)

Check warning

Code scanning / Jenkins Security Scan

Stapler: Missing POST/RequirePOST annotation Warning

Potential CSRF vulnerability: If DescriptorImpl#doCheckKey connects to user-specified URLs, modifies state, or is expensive to run, it should be annotated with @POST or @RequirePOST
return switch (key.trim()) {
case 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 -> FormValidation.error(key + " is a reserved word");
default -> FormValidation.ok();
};
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.jenkinsci.plugins.oic;

Check warning on line 1 in src/main/java/org/jenkinsci/plugins/oic/LogoutQueryParameter.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.model.Descriptor.FormException;
import hudson.util.FormValidation;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;

public class LogoutQueryParameter extends AbstractQueryParameter<LogoutQueryParameter> {

@DataBoundConstructor
public LogoutQueryParameter(String key, String value) throws FormException {
super(key, value, true);
}

@Extension
public static class DescriptorImpl extends AbstractKeyValueDescribable.DescriptorImpl<LogoutQueryParameter> {

@Override
public FormValidation doCheckKey(@QueryParameter String key) {

Check warning

Code scanning / Jenkins Security Scan

Stapler: Missing permission check Warning

Potential missing permission check in DescriptorImpl#doCheckKey

Check warning

Code scanning / Jenkins Security Scan

Stapler: Missing POST/RequirePOST annotation Warning

Potential CSRF vulnerability: If DescriptorImpl#doCheckKey connects to user-specified URLs, modifies state, or is expensive to run, it should be annotated with @POST or @RequirePOST
return switch (key.trim()) {
case "id_token_hint", "state", "post_logout_redirect_uri" -> FormValidation.error(
key + " is a reserved word");
default -> FormValidation.ok();
};
}
}
}
77 changes: 66 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 @@ -317,6 +321,12 @@
*/
private transient ProxyAwareResourceRetriever proxyAwareResourceRetriever;

@SuppressFBWarnings(value = "SE_BAD_FIELD", justification = "we are not using standard serialization")
private List<LoginQueryParameter> loginQueryParameters;

@SuppressFBWarnings(value = "SE_BAD_FIELD", justification = "we are not using standard serialization")
private List<LogoutQueryParameter> logoutQueryParameters;

@DataBoundConstructor
public OicSecurityRealm(
String clientId,
Expand Down Expand Up @@ -417,6 +427,24 @@
return this;
}

@DataBoundSetter
public void setLoginQueryParameters(List<LoginQueryParameter> values) {
this.loginQueryParameters = values;
}

public List<LoginQueryParameter> getLoginQueryParameters() {
return loginQueryParameters;
}

@DataBoundSetter
public void setLogoutQueryParameters(List<LogoutQueryParameter> values) {
this.logoutQueryParameters = values;
}

public List<LogoutQueryParameter> getLogoutQueryParameters() {
return logoutQueryParameters;
}

public String getClientId() {
return clientId;
}
Expand Down Expand Up @@ -588,6 +616,11 @@
conf.setDisablePkce(true);
}
opMetadataResolver.init();
if (loginQueryParameters != null && !loginQueryParameters.isEmpty()) {

Check warning on line 619 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 619 is only partially covered, one branch is missing
for (LoginQueryParameter lqp : loginQueryParameters) {
conf.addCustomParam(lqp.getURLEncodedKey(), lqp.getURLEncodedValue());
}
}
return conf;
}

Expand Down Expand Up @@ -1273,22 +1306,44 @@
}

@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));
}
if (logoutQueryParameters != null && !logoutQueryParameters.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
logoutQueryParameters.forEach(lqp -> {
String key = lqp.getURLEncodedKey();
String value = lqp.getURLEncodedValue();
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
@@ -0,0 +1,9 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<f:entry title="Key" field="key">
<f:textbox />
</f:entry>
<f:entry title="Value" field="value">
<f:textbox />
</f:entry>
</j:jelly>
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
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}" field="loginQueryParameters">
<f:repeatableProperty field="loginQueryParameters" header="${%LoginQueryParameter.Title}" hasHeader="true" add="${%Add login parameter}">
<f:block>
<div align="right">
<f:repeatableDeleteButton />
</div>
</f:block>
</f:repeatableProperty>
</f:entry>
<f:entry title="${%LogoutQueryParametersTitle}" field="logoutQueryParameters">
<f:repeatableProperty field="logoutQueryParameters" header="${%LogoutQueryParameter.Title}" hasHeader="true" add="${%Add logout parameter}">
<f:block>
<div align="right">
<f:repeatableDeleteButton />
</div>
</f:block>
</f:repeatableProperty>
</f:entry>
</f:advanced>

<f:entry title="${%LogoutFromOpenIDProvider}" field="logoutFromOpenidProvider">
<f:checkbox id="logoutFromIDP"/>
</f:entry>
Expand Down
Loading
Loading