Skip to content

Commit

Permalink
Fix JCasC configuration reference
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 418804d
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;

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) {
throw new IllegalStateException("Key '" + key + "' must not be null or empty.");
}
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;
}

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

@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);
if (Util.fixEmptyAndTrim(value) == null) {
return FormValidation.error(Messages.OicQueryParameterConfiguration_QueryParameterKeyRequired());
}
return FormValidation.ok();
}
}
}
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.
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()) {
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()) {
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 418804d

Please sign in to comment.