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 14, 2024
1 parent a97bfb7 commit 58e2662
Show file tree
Hide file tree
Showing 19 changed files with 273 additions and 35 deletions.
30 changes: 19 additions & 11 deletions docs/configuration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ but this can be overriden by the `scopesOverride` config parameter.
|---------------------------------|--------|------------------------------------------------------------------|
| wellKnownOpenIDConfigurationUrl | url | Providers' well-known configuration endpoint |
| scopesOverride | string | Space separated list of scopes to request (default: request all) |
| loginQueryParameters | string | Ampersand separated separated key=value pairs |
| logoutQueryParameters | string | Ampersand separated separated key=value pairs |

When configuring from the interface, the automatic mode will fill in the
fields expected in manual mode. This can be useful for prefilling the
Expand All @@ -52,17 +54,19 @@ The scopes can be configured but default to `openid email`.
If the JWKS endpoint is configured, JWS' signatures will be verified
(unless disabled).

| field | format | description |
|------------------------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| authorizationServerUrl | url | URL the user is redirected to at login |
| tokenServerUrl | url | URL used by jenkins to request the tokens |
| endSessionUrl | url | URL to logout from provider (used if activated) |
| jwksServerUrl | url | URL of provider's jws certificates (unused if disabled) |
| scopes | string | Space separated list of scopes to request (default: `openid email`) |
| tokenAuthMethod | enum | Method used for authenticating when requesting token(s)<br />- `client_secret_basic`: for client id/secret as basic authentication user/pass<br />- `client_secret_post`: for client id/secret sent in post request |
| userInfoServerUrl | url | URL to get user's details |
| useRefreshTokens | boolean | If server supports refresh tokens, make sure to specify any additional scopes required for refresh token support. |
| issuer | string | The expected received ID Token's issuer |
| field | format | description |
|------------------------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| authorizationServerUrl | url | URL the user is redirected to at login |
| tokenServerUrl | url | URL used by jenkins to request the tokens |
| endSessionUrl | url | URL to logout from provider (used if activated) |
| jwksServerUrl | url | URL of provider's jws certificates (unused if disabled) |
| scopes | string | Space separated list of scopes to request (default: `openid email`) |
| tokenAuthMethod | enum | Method used for authenticating when requesting token(s)<br />- `client_secret_basic`: for client id/secret as basic authentication user/pass<br />- `client_secret_post`: for client id/secret sent in post request |
| userInfoServerUrl | url | URL to get user's details |
| useRefreshTokens | boolean | If server supports refresh tokens, make sure to specify any additional scopes required for refresh token support. |
| issuer | string | The expected received ID Token's issuer |
| loginQueryParameters | string | Ampersand separated separated key=value pairs |
| logoutQueryParameters | string | Ampersand separated separated key=value pairs |

### Advanced configuration

Expand Down Expand Up @@ -116,6 +120,8 @@ jenkins:
# use only one of wellKnown or manual
# Automatic config of endpoint
wellKnown:
loginQueryParameters: <string:ampersand separated key=value>
logoutQueryParameters: <string:ampersand separated key=value>
wellKnownOpenIDConfigurationUrl: <url>
scopesOverride: <string:space separated words>
# Manual config of endpoint
Expand All @@ -124,6 +130,8 @@ jenkins:
endSessionUrl: <url>
issuer: <string>
jwksServerUrl: <url>
loginQueryParameters: <string:ampersand separated key=value>
logoutQueryParameters: <string:ampersand separated key=value>
tokenAuthMethod: <string:enum>
tokenServerUrl: <url>
scopes: <string:space separated words>
Expand Down
100 changes: 82 additions & 18 deletions src/main/java/org/jenkinsci/plugins/oic/OicSecurityRealm.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,20 @@
import java.text.ParseException;
import java.time.Clock;
import java.util.ArrayList;
import java.util.Arrays;
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 @@ -505,7 +510,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 514 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 +539,46 @@ private OidcConfiguration buildOidcConfiguration() {
if (this.isPkceEnabled()) {
conf.setPkceMethod(CodeChallengeMethod.S256);
}
if (addCustomLoginParams && this.serverConfiguration.getLoginQueryParameters() != null) {
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(this.serverConfiguration.getLoginQueryParameters(), forbiddenKeys);
LOGGER.info("Append the following custom parameters to the authorize endpoint: " + customParameterMap);
customParameterMap.forEach(conf::addCustomParam);
}
return conf;
}

Map<String, String> getCustomParametersMap(String queryParameters, Set<String> forbiddenKeys) {
return Arrays.stream(queryParameters.split("&"))
.filter(a -> a.contains("="))
.map(s -> s.split("="))
.filter(a -> Util.fixEmptyAndTrim(a[0]) != null && !forbiddenKeys.contains(a[0]))
.collect(Collectors.toMap(a -> Util.fixEmptyAndTrim(a[0]), a -> (a.length > 1 ? a[1].trim() : "")))
.entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey, entry -> encodeIfUrl(entry.getValue())));
}

private String encodeIfUrl(String value) {
if (value.startsWith("https:") || value.startsWith("http:")) {

Check warning on line 575 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 575 is only partially covered, 2 branches are missing
return URLEncoder.encode(value, StandardCharsets.UTF_8);

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

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 576 is not covered by tests

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

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/jenkinsci/plugins/oic/OicSecurityRealm.java#L576

Added line #L576 was not covered by tests
} else {
return value;
}
}

// Visible for testing
@Restricted(NoExternalUse.class)
protected void filterNonFIPS140CompliantAlgorithms(@NonNull OIDCProviderMetadata oidcProviderMetadata) {
Expand Down Expand Up @@ -670,8 +712,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 +974,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 +1214,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 +1225,44 @@ 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 (this.serverConfiguration.getLogoutQueryParameters() != null) {
String logoutQueryParameters = this.serverConfiguration.getLogoutQueryParameters();
Map<String, String> customParameterMap = getCustomParametersMap(logoutQueryParameters, forbiddenKeys);
LOGGER.info("Append the following custom parameters to the logout endpoint: " + customParameterMap);
segmentsMap.putAll(customParameterMap);
segmentsSet.addAll(Arrays.stream(logoutQueryParameters.split("&"))
.filter(a -> !a.contains("=") && Util.fixEmptyAndTrim(a) != null && !forbiddenKeys.contains(a))

Check warning on line 1250 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 1250 is only partially covered, 2 branches are missing
.map(Util::fixEmptyAndTrim)
.collect(Collectors.toSet()));
}

StringBuilder openidLogoutEndpoint = new StringBuilder(url.toString());
String concatChar = openidLogoutEndpoint.toString().contains("?") ? "&" : "?";

Check warning on line 1256 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 1256 is only partially covered, one branch is missing
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 +1307,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 @@ -1384,7 +1448,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
Expand Up @@ -14,4 +14,8 @@ public abstract class OicServerConfiguration extends AbstractDescribableImpl<Oic
* Convert the OicServerConfiguration to {@link OIDCProviderMetadata} for use by the client.
*/
public abstract OIDCProviderMetadata toProviderMetadata();

abstract String getLoginQueryParameters();

abstract String getLogoutQueryParameters();
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,16 @@ public class OicServerManualConfiguration extends OicServerConfiguration {

private final String authorizationServerUrl;
private final String tokenServerUrl;
private final String issuer;

private TokenAuthMethod tokenAuthMethod = TokenAuthMethod.client_secret_post;
private String jwksServerUrl;
private String endSessionUrl;
private String scopes = "openid email";
private String userInfoServerUrl;
private boolean useRefreshTokens;
private String issuer;
private String loginQueryParameters;
private String logoutQueryParameters;

@DataBoundConstructor
public OicServerManualConfiguration(String issuer, String tokenServerUrl, String authorizationServerUrl)
Expand Down Expand Up @@ -81,6 +84,16 @@ public void setUserInfoServerUrl(@Nullable String userInfoServerUrl) {
this.userInfoServerUrl = Util.fixEmptyAndTrim(userInfoServerUrl);
}

@DataBoundSetter
public void setLoginQueryParameters(String loginQueryParameters) {
this.loginQueryParameters = Util.fixEmptyAndTrim(loginQueryParameters);
}

@DataBoundSetter
public void setLogoutQueryParameters(String logoutQueryParameters) {
this.logoutQueryParameters = Util.fixEmptyAndTrim(logoutQueryParameters);
}

@DataBoundSetter
public void setUseRefreshTokens(boolean useRefreshTokens) {
this.useRefreshTokens = useRefreshTokens;
Expand All @@ -102,6 +115,14 @@ public boolean isUseRefreshTokens() {
return useRefreshTokens;
}

public String getLoginQueryParameters() {
return loginQueryParameters;
}

public String getLogoutQueryParameters() {
return logoutQueryParameters;
}

public String getJwksServerUrl() {
return jwksServerUrl;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ public class OicServerWellKnownConfiguration extends OicServerConfiguration {

private final String wellKnownOpenIDConfigurationUrl;
private String scopesOverride;
private String loginQueryParameters;
private String logoutQueryParameters;

/**
* Time of the wellknown configuration expiration
Expand All @@ -67,6 +69,24 @@ public String getScopesOverride() {
return scopesOverride;
}

@DataBoundSetter
public void setLoginQueryParameters(String loginQueryParameters) {
this.loginQueryParameters = Util.fixEmptyAndTrim(loginQueryParameters);
}

public String getLoginQueryParameters() {
return loginQueryParameters;
}

@DataBoundSetter
public void setLogoutQueryParameters(String logoutQueryParameters) {
this.logoutQueryParameters = Util.fixEmptyAndTrim(logoutQueryParameters);
}

Check warning on line 84 in src/main/java/org/jenkinsci/plugins/oic/OicServerWellKnownConfiguration.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/jenkinsci/plugins/oic/OicServerWellKnownConfiguration.java#L83-L84

Added lines #L83 - L84 were not covered by tests

public String getLogoutQueryParameters() {
return logoutQueryParameters;

Check warning on line 87 in src/main/java/org/jenkinsci/plugins/oic/OicServerWellKnownConfiguration.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 83-87 are not covered by tests

Check warning on line 87 in src/main/java/org/jenkinsci/plugins/oic/OicServerWellKnownConfiguration.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/org/jenkinsci/plugins/oic/OicServerWellKnownConfiguration.java#L87

Added line #L87 was not covered by tests
}

public String getWellKnownOpenIDConfigurationUrl() {
return wellKnownOpenIDConfigurationUrl;
}
Expand Down
Loading

0 comments on commit 58e2662

Please sign in to comment.