From c2ef3e63dc51f9bd836ae477db1ce4fb91c2492c Mon Sep 17 00:00:00 2001 From: James Nord Date: Mon, 16 Sep 2024 16:13:23 +0100 Subject: [PATCH] rework configuration of the plugin The server configuration (token URLs, scopes etc) are now in separate describable. This makes the UX cleaner, and the code cleaner in the realm. The config.xml is backwards compatable with previous versions, but the casc format is not. --- pom.xml | 1 + .../plugins/oic/OicSecurityRealm.java | 515 +++++------------- .../plugins/oic/OicServerConfiguration.java | 34 ++ .../oic/OicServerManualConfiguration.java | 204 +++++++ .../oic/OicServerWellKnownConfiguration.java | 272 +++++++++ .../jenkinsci/plugins/oic/Messages.properties | 3 + .../plugins/oic/OicSecurityRealm/config.jelly | 53 +- .../oic/OicSecurityRealm/config.properties | 20 +- .../OicSecurityRealm/help-jwksServerUrl.html | 4 - .../OicSecurityRealm/help-overrideScopes.html | 5 - .../help-serverConfiguration.html | 3 + .../OicServerManualConfiguration/config.jelly | 34 ++ .../config.properties | 10 + .../help-authorizationServerUrl.html | 0 .../help-authorizationServerUrl_fr.html | 0 .../help-endSessionUrl.html | 0 .../help-endSessionUrl_fr.html | 0 .../help-jwksServerUrl.html | 4 + .../help-jwksServerUrl_fr.html | 0 .../help-tokenAuthMethod.html | 0 .../help-tokenAuthMethod_fr.html | 0 .../help-tokenServerUrl.html | 0 .../help-tokenServerUrl_fr.html | 0 .../help-useRefreshTokens.html | 0 .../help-userInfoServerUrl.html | 0 .../help-userInfoServerUrl_fr.html | 0 .../OicServerManualConfiguration/help.html | 4 + .../config.jelly | 15 + .../config.properties | 2 + .../help-scopesOverride.html | 4 + .../help-scopesOverride_fr.html} | 2 +- .../help-wellKnownOpenIDConfigurationUrl.html | 6 +- ...lp-wellKnownOpenIDConfigurationUrl_fr.html | 4 +- .../OicServerWellKnownConfiguration/help.html | 3 + .../plugins/oic/ConfigurationAsCodeTest.java | 65 ++- .../plugins/oic/DescriptorImplTest.java | 21 +- .../org/jenkinsci/plugins/oic/PluginTest.java | 23 +- .../org/jenkinsci/plugins/oic/TestRealm.java | 18 +- .../plugins/oic/ConfigurationAsCode.yml | 12 +- .../plugins/oic/ConfigurationAsCodeExport.yml | 12 +- .../oic/ConfigurationAsCodeMinimal.yml | 6 +- .../ConfigurationAsCodeMinimalWellKnown.yml | 4 +- 42 files changed, 823 insertions(+), 540 deletions(-) create mode 100644 src/main/java/org/jenkinsci/plugins/oic/OicServerConfiguration.java create mode 100644 src/main/java/org/jenkinsci/plugins/oic/OicServerManualConfiguration.java create mode 100644 src/main/java/org/jenkinsci/plugins/oic/OicServerWellKnownConfiguration.java delete mode 100644 src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-jwksServerUrl.html delete mode 100644 src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-overrideScopes.html create mode 100644 src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-serverConfiguration.html create mode 100644 src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/config.jelly create mode 100644 src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/config.properties rename src/main/resources/org/jenkinsci/plugins/oic/{OicSecurityRealm => OicServerManualConfiguration}/help-authorizationServerUrl.html (100%) rename src/main/resources/org/jenkinsci/plugins/oic/{OicSecurityRealm => OicServerManualConfiguration}/help-authorizationServerUrl_fr.html (100%) rename src/main/resources/org/jenkinsci/plugins/oic/{OicSecurityRealm => OicServerManualConfiguration}/help-endSessionUrl.html (100%) rename src/main/resources/org/jenkinsci/plugins/oic/{OicSecurityRealm => OicServerManualConfiguration}/help-endSessionUrl_fr.html (100%) create mode 100644 src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/help-jwksServerUrl.html rename src/main/resources/org/jenkinsci/plugins/oic/{OicSecurityRealm => OicServerManualConfiguration}/help-jwksServerUrl_fr.html (100%) rename src/main/resources/org/jenkinsci/plugins/oic/{OicSecurityRealm => OicServerManualConfiguration}/help-tokenAuthMethod.html (100%) rename src/main/resources/org/jenkinsci/plugins/oic/{OicSecurityRealm => OicServerManualConfiguration}/help-tokenAuthMethod_fr.html (100%) rename src/main/resources/org/jenkinsci/plugins/oic/{OicSecurityRealm => OicServerManualConfiguration}/help-tokenServerUrl.html (100%) rename src/main/resources/org/jenkinsci/plugins/oic/{OicSecurityRealm => OicServerManualConfiguration}/help-tokenServerUrl_fr.html (100%) rename src/main/resources/org/jenkinsci/plugins/oic/{OicSecurityRealm => OicServerManualConfiguration}/help-useRefreshTokens.html (100%) rename src/main/resources/org/jenkinsci/plugins/oic/{OicSecurityRealm => OicServerManualConfiguration}/help-userInfoServerUrl.html (100%) rename src/main/resources/org/jenkinsci/plugins/oic/{OicSecurityRealm => OicServerManualConfiguration}/help-userInfoServerUrl_fr.html (100%) create mode 100644 src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/help.html create mode 100644 src/main/resources/org/jenkinsci/plugins/oic/OicServerWellKnownConfiguration/config.jelly create mode 100644 src/main/resources/org/jenkinsci/plugins/oic/OicServerWellKnownConfiguration/config.properties create mode 100644 src/main/resources/org/jenkinsci/plugins/oic/OicServerWellKnownConfiguration/help-scopesOverride.html rename src/main/resources/org/jenkinsci/plugins/oic/{OicSecurityRealm/help-overrideScopes_fr.html => OicServerWellKnownConfiguration/help-scopesOverride_fr.html} (68%) rename src/main/resources/org/jenkinsci/plugins/oic/{OicSecurityRealm => OicServerWellKnownConfiguration}/help-wellKnownOpenIDConfigurationUrl.html (52%) rename src/main/resources/org/jenkinsci/plugins/oic/{OicSecurityRealm => OicServerWellKnownConfiguration}/help-wellKnownOpenIDConfigurationUrl_fr.html (58%) create mode 100644 src/main/resources/org/jenkinsci/plugins/oic/OicServerWellKnownConfiguration/help.html diff --git a/pom.xml b/pom.xml index 3e371225..cec48ee7 100644 --- a/pom.xml +++ b/pom.xml @@ -49,6 +49,7 @@ false Max 1836.vccda_4a_122a_a_e + 4.431 diff --git a/src/main/java/org/jenkinsci/plugins/oic/OicSecurityRealm.java b/src/main/java/org/jenkinsci/plugins/oic/OicSecurityRealm.java index 8d922da2..d5643ae2 100644 --- a/src/main/java/org/jenkinsci/plugins/oic/OicSecurityRealm.java +++ b/src/main/java/org/jenkinsci/plugins/oic/OicSecurityRealm.java @@ -36,7 +36,6 @@ import com.google.api.client.http.BasicAuthentication; import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpExecuteInterceptor; -import com.google.api.client.http.HttpHeaders; import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.http.HttpRequestInitializer; @@ -51,13 +50,13 @@ import com.google.api.client.util.Data; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; -import com.google.gson.JsonParseException; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.Extension; import hudson.Util; import hudson.model.Descriptor; +import hudson.model.Descriptor.FormException; import hudson.model.User; import hudson.security.ChainedServletFilter; import hudson.security.SecurityRealm; @@ -69,23 +68,19 @@ import io.burt.jmespath.RuntimeConfiguration; import io.burt.jmespath.jcf.JcfRuntime; import java.io.IOException; +import java.io.InvalidObjectException; +import java.io.ObjectStreamException; import java.io.Serializable; -import java.lang.reflect.Field; import java.net.MalformedURLException; import java.net.URL; import java.net.URLEncoder; -import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.time.Clock; -import java.time.LocalDateTime; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; import java.util.Collections; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -108,6 +103,7 @@ import org.apache.commons.lang.StringUtils; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.DoNotUse; +import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.Header; @@ -138,7 +134,6 @@ * @author Michael Bischoff * @author Steve Arch */ -@SuppressWarnings("deprecation") public class OicSecurityRealm extends SecurityRealm implements Serializable { private static final long serialVersionUID = 1L; @@ -155,12 +150,31 @@ public static enum TokenAuthMethod { private final String clientId; private final Secret clientSecret; - private String wellKnownOpenIDConfigurationUrl = null; - private String tokenServerUrl = null; - private String jwksServerUrl = null; - private TokenAuthMethod tokenAuthMethod; - private String authorizationServerUrl = null; - private String userInfoServerUrl = null; + + /** @deprecated see {@link OicServerWellKnownConfiguration#getWellKnownOpenIDConfigurationUrl()} */ + @Deprecated + private transient String wellKnownOpenIDConfigurationUrl; + + /** @deprecated see {@link OicServerConfiguration#getTokenServerUrl()} */ + @Deprecated + private transient String tokenServerUrl; + + /** @deprecated see {@link OicServerConfiguration#getJwksServerUrl()} */ + @Deprecated + private transient String jwksServerUrl; + + /** @deprecated see {@link OicServerConfiguration#getTokenAuthMethod()} */ + @Deprecated + private transient TokenAuthMethod tokenAuthMethod; + + /** @deprecated see {@link OicServerConfiguration#getAuthorizationServerUrl()} */ + @Deprecated + private transient String authorizationServerUrl; + + /** @deprecated see {@link OicServerConfiguration#getUserInfoServerUrl()} */ + @Deprecated + private transient String userInfoServerUrl; + private String userNameField = "sub"; private transient Expression userNameFieldExpr = null; private String tokenFieldToCheckKey = null; @@ -174,24 +188,36 @@ public static enum TokenAuthMethod { private transient Expression groupsFieldExpr = null; private transient String simpleGroupsFieldName = null; private transient String nestedGroupFieldName = null; - private String scopes = null; + + /** @deprecated see {@link OicServerConfiguration#getScopes()} */ + @Deprecated + private transient String scopes = null; + private final boolean disableSslVerification; private boolean logoutFromOpenidProvider = true; - private String endSessionEndpoint = null; + + /** @deprecated see {@link OicServerConfiguration#getEndSessionUrl()} */ + @Deprecated + private transient String endSessionEndpoint = null; + private String postLogoutRedirectUrl; private boolean escapeHatchEnabled = false; private String escapeHatchUsername = null; private Secret escapeHatchSecret = null; private String escapeHatchGroup = null; - private String automanualconfigure = null; - private boolean useRefreshTokens = false; - /** flag to clear overrideScopes - */ - private transient Boolean overrideScopesDefined = null; + @Deprecated + /** @deprecated with no replacement. See sub classes of {@link OicServerConfiguration} */ + private transient String automanualconfigure = null; - /** Override scopes in wellknown configuration - */ + @Deprecated + /** @deprecated see {@link OicServerWellKnownConfiguration#isUseRefreshTokens()} */ + private transient boolean useRefreshTokens = false; + + private OicServerConfiguration serverConfiguration; + + /** @deprecated see {@link OicServerWellKnownConfiguration#getScopes()} */ + @Deprecated private String overrideScopes = null; /** Flag indicating if root url should be taken from config or request @@ -228,13 +254,10 @@ public static enum TokenAuthMethod { */ private Long allowedTokenExpirationClockSkewSeconds = 60L; - /** Date of wellknown configuration expiration - */ - private transient LocalDateTime wellKnownExpires = null; - /** old field that had an '/' implicitly added at the end, * transient because we no longer want to have this value stored * but it's still needed for backwards compatibility */ + @Deprecated private transient String endSessionUrl; /** Verification of IdToken and UserInfo (in jwt case) @@ -300,7 +323,7 @@ public OicSecurityRealm( TokenAuthMethod.valueOf(StringUtils.defaultIfBlank(tokenAuthMethod, "client_secret_post")); this.userInfoServerUrl = userInfoServerUrl; this.jwksServerUrl = jwksServerUrl; - this.setScopes(scopes); + this.scopes = scopes; this.endSessionEndpoint = endSessionEndpoint; if ("auto".equals(automanualconfigure) @@ -308,7 +331,6 @@ public OicSecurityRealm( && !Util.fixNull(wellKnownOpenIDConfigurationUrl).isEmpty())) { this.automanualconfigure = "auto"; this.wellKnownOpenIDConfigurationUrl = Util.fixEmptyAndTrim(wellKnownOpenIDConfigurationUrl); - this.loadWellKnownOpenIDConfigurationUrl(); } else { this.automanualconfigure = "manual"; this.wellKnownOpenIDConfigurationUrl = null; // Remove the autoconfig URL @@ -326,56 +348,32 @@ public OicSecurityRealm( this.escapeHatchUsername = Util.fixEmptyAndTrim(escapeHatchUsername); this.setEscapeHatchSecret(Secret.fromString(escapeHatchSecret)); this.escapeHatchGroup = Util.fixEmptyAndTrim(escapeHatchGroup); + // hack to avoid rewriting lots of tests :-) + readResolve(); } @DataBoundConstructor public OicSecurityRealm( String clientId, - String clientSecret, - String authorizationServerUrl, - String tokenServerUrl, - String jwksServerUrl, - String tokenAuthMethod, - String userInfoServerUrl, - String endSessionEndpoint, - String scopes, - String automanualconfigure, - Boolean disableSslVerification, - Boolean useRefreshTokens) + Secret clientSecret, + OicServerConfiguration serverConfiguration, + Boolean disableSslVerification) throws IOException { // Needed in DataBoundSetter this.disableSslVerification = Util.fixNull(disableSslVerification, Boolean.FALSE); - this.useRefreshTokens = Util.fixNull(useRefreshTokens, Boolean.FALSE); this.httpTransport = constructHttpTransport(this.disableSslVerification); this.clientId = clientId; - this.clientSecret = clientSecret != null && !clientSecret.toLowerCase().equals(NO_SECRET) - ? Secret.fromString(clientSecret) - : null; - // auto/manual configuration as set in jcasc/config - this.automanualconfigure = Util.fixNull(automanualconfigure); - // previous values of OpenIDConnect configuration - this.authorizationServerUrl = authorizationServerUrl; - this.tokenServerUrl = tokenServerUrl; - this.jwksServerUrl = jwksServerUrl; - this.tokenAuthMethod = - TokenAuthMethod.valueOf(StringUtils.defaultIfBlank(tokenAuthMethod, "client_secret_post")); - this.userInfoServerUrl = userInfoServerUrl; - this.endSessionEndpoint = endSessionEndpoint; - this.setScopes(scopes); + this.clientSecret = clientSecret; + this.serverConfiguration = serverConfiguration; } - protected Object readResolve() { + @SuppressWarnings("deprecated") + protected Object readResolve() throws ObjectStreamException { if (httpTransport == null) { httpTransport = constructHttpTransport(isDisableSslVerification()); } if (!Strings.isNullOrEmpty(endSessionUrl)) { - try { - Field field = getClass().getDeclaredField("endSessionEndpoint"); - field.setAccessible(true); - field.set(this, endSessionUrl + "/"); - } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException e) { - LOGGER.log(Level.SEVERE, "Can't set endSessionEndpoint from old value", e); - } + this.endSessionEndpoint = endSessionUrl + "/"; } // backward compatibility with wrong groupsFieldName split @@ -395,10 +393,39 @@ protected Object readResolve() { this.setTokenFieldToCheckKey(this.tokenFieldToCheckKey); // ensure escapeHatchSecret is encrypted this.setEscapeHatchSecret(this.escapeHatchSecret); + try { + if (automanualconfigure != null) { + if ("auto".equals(automanualconfigure)) { + OicServerWellKnownConfiguration conf = + new OicServerWellKnownConfiguration(wellKnownOpenIDConfigurationUrl); + conf.setScopesOverride(this.overrideScopes); + serverConfiguration = conf; + } else { + OicServerManualConfiguration conf = + new OicServerManualConfiguration(tokenServerUrl, authorizationServerUrl); + if (tokenAuthMethod != null) { + conf.setTokenAuthMethod(tokenAuthMethod); + } + conf.setEndSessionUrl(endSessionEndpoint); + conf.setJwksServerUrl(jwksServerUrl); + conf.setScopes(scopes != null ? scopes : "openid email"); + conf.setUseRefreshTokens(useRefreshTokens); + conf.setUserInfoServerUrl(userInfoServerUrl); + serverConfiguration = conf; + } + } + } catch (FormException e) { + // FormException does not override toString() so looses info on the fields set and the message may not have + // context + // extract if into a better message until this is fixed. + ObjectStreamException ose = new InvalidObjectException(e.getFormField() + ": " + e.getMessage()); + ose.initCause(e); + throw ose; + } return this; } - private static HttpTransport constructHttpTransport(boolean disableSslVerification) { + static HttpTransport constructHttpTransport(boolean disableSslVerification) { NetHttpTransport.Builder builder = new NetHttpTransport.Builder(); builder.setConnectionFactory(new JenkinsAwareConnectionFactory()); @@ -413,6 +440,16 @@ private static HttpTransport constructHttpTransport(boolean disableSslVerificati return builder.build(); } + /** + * Obtain the shared HttpTransport. + * The transport may be invalidated if the realm is saved so should not be cached. + * @return the shared {@code HttpTransport}. + */ + @Restricted(NoExternalUse.class) + HttpTransport getHttpTransport() { + return httpTransport; + } + public String getClientId() { return clientId; } @@ -421,28 +458,9 @@ public Secret getClientSecret() { return clientSecret == null ? Secret.fromString(NO_SECRET) : clientSecret; } - public String getWellKnownOpenIDConfigurationUrl() { - return wellKnownOpenIDConfigurationUrl; - } - - public String getTokenServerUrl() { - return tokenServerUrl; - } - - public String getJwksServerUrl() { - return jwksServerUrl; - } - - public TokenAuthMethod getTokenAuthMethod() { - return tokenAuthMethod; - } - - public String getAuthorizationServerUrl() { - return authorizationServerUrl; - } - - public String getUserInfoServerUrl() { - return userInfoServerUrl; + @Restricted(NoExternalUse.class) // jelly access + public OicServerConfiguration getServerConfiguration() { + return serverConfiguration; } public String getUserNameField() { @@ -469,10 +487,6 @@ public String getGroupsFieldName() { return groupsFieldName; } - public String getScopes() { - return scopes != null ? scopes : "openid email"; - } - public boolean isDisableSslVerification() { return disableSslVerification; } @@ -481,10 +495,6 @@ public boolean isLogoutFromOpenidProvider() { return logoutFromOpenidProvider; } - public String getEndSessionEndpoint() { - return endSessionEndpoint; - } - public String getPostLogoutRedirectUrl() { return postLogoutRedirectUrl; } @@ -505,22 +515,6 @@ public String getEscapeHatchGroup() { return escapeHatchGroup; } - public String getAutomanualconfigure() { - return automanualconfigure; - } - - public boolean isUseRefreshTokens() { - return useRefreshTokens; - } - - public boolean isOverrideScopesDefined() { - return overrideScopes != null; - } - - public String getOverrideScopes() { - return overrideScopes; - } - public boolean isRootURLFromRequest() { return rootURLFromRequest; } @@ -553,112 +547,6 @@ public Long getAllowedTokenExpirationClockSkewSeconds() { return allowedTokenExpirationClockSkewSeconds; } - public boolean isAutoConfigure() { - return "auto".equals(this.automanualconfigure); - } - - /** request wellknown config of provider and update it (if required) - */ - private void loadWellKnownOpenIDConfigurationUrl() { - if (!isAutoConfigure() || this.wellKnownOpenIDConfigurationUrl == null) { - // not configured - return; - } - - LocalDateTime now = LocalDateTime.now(); - if (this.wellKnownExpires != null && this.wellKnownExpires.isBefore(now)) { - // configuration is still fresh - return; - } - - // Get the well-known configuration from the specified URL - try { - URL url = new URL(wellKnownOpenIDConfigurationUrl); - HttpRequest request = httpTransport.createRequestFactory().buildGetRequest(new GenericUrl(url)); - - com.google.api.client.http.HttpResponse response = request.execute(); - WellKnownOpenIDConfigurationResponse config = GsonFactory.getDefaultInstance() - .fromInputStream( - response.getContent(), - Charset.defaultCharset(), - WellKnownOpenIDConfigurationResponse.class); - - this.authorizationServerUrl = Util.fixNull(config.getAuthorizationEndpoint(), this.authorizationServerUrl); - this.tokenServerUrl = Util.fixNull(config.getTokenEndpoint(), this.tokenServerUrl); - this.jwksServerUrl = Util.fixNull(config.getJwksUri(), this.jwksServerUrl); - this.tokenAuthMethod = Util.fixNull(config.getPreferredTokenAuthMethod(), this.tokenAuthMethod); - this.userInfoServerUrl = Util.fixNull(config.getUserinfoEndpoint(), this.userInfoServerUrl); - if (config.getScopesSupported() != null) { - this.setScopes(StringUtils.join(config.getScopesSupported(), " ")); - } - this.applyOverrideScopes(); - this.endSessionEndpoint = Util.fixNull(config.getEndSessionEndpoint(), this.endSessionEndpoint); - - if (config.getGrantTypesSupported() != null) { - this.useRefreshTokens = config.getGrantTypesSupported().contains("refresh_token"); - } - - setWellKnownExpires(response.getHeaders()); - } catch (MalformedURLException e) { - LOGGER.log(Level.SEVERE, "Invalid WellKnown OpenID Configuration URL", e); - } catch (HttpResponseException e) { - LOGGER.log(Level.SEVERE, "Could not get wellknown OpenID Configuration", e); - } catch (JsonParseException e) { - LOGGER.log(Level.SEVERE, "Could not parse wellknown OpenID Configuration", e); - } catch (IOException e) { - LOGGER.log(Level.SEVERE, "Error while loading wellknown OpenID Configuration", e); - } - } - - /** Parse headers to determine expiration date - */ - private void setWellKnownExpires(HttpHeaders headers) { - String expires = Util.fixEmptyAndTrim(headers.getExpires()); - // expires 0 means no cache - // we could (should?) have a look at Cache-Control header and max-age but for simplicity - // we can just leave it default TTL 1h refresh which sounds reasonable for such file - if (expires != null && !"0".equals(expires)) { - ZonedDateTime zdt = ZonedDateTime.parse(expires, DateTimeFormatter.RFC_1123_DATE_TIME); - if (zdt != null) { - this.wellKnownExpires = zdt.toLocalDateTime(); - return; - } - } - - // default to 1 hour refresh - this.wellKnownExpires = LocalDateTime.now().plusSeconds(3600); - } - - @DataBoundSetter - public void setWellKnownOpenIDConfigurationUrl(String wellKnownOpenIDConfigurationUrl) { - if (this.isAutoConfigure() - || (this.automanualconfigure.isEmpty() - && !Util.fixNull(wellKnownOpenIDConfigurationUrl).isEmpty())) { - this.automanualconfigure = "auto"; - this.wellKnownOpenIDConfigurationUrl = wellKnownOpenIDConfigurationUrl; - this.loadWellKnownOpenIDConfigurationUrl(); - } else { - this.automanualconfigure = "manual"; - this.wellKnownOpenIDConfigurationUrl = null; - } - } - - private void applyOverrideScopes() { - if (!"auto".equals(this.automanualconfigure) || this.overrideScopes == null) { - // only applies in "auto" mode when overrideScopes defined - return; - } - if (this.scopes == null) { - this.scopes = overrideScopes; - return; - } - // keep only scopes that are in overrideScopes - HashSet scopesSet = - new HashSet<>(Arrays.asList(this.scopes.trim().split("\\s+"))); - scopesSet.retainAll(Arrays.asList(this.overrideScopes.trim().split("\\s+"))); - this.setScopes(StringUtils.join(scopesSet, " ")); - } - @DataBoundSetter public void setUserNameField(String userNameField) { this.userNameField = Util.fixNull(Util.fixEmptyAndTrim(userNameField), "sub"); @@ -717,11 +605,6 @@ public void setGroupsFieldName(String groupsFieldName) { this.groupsFieldExpr = this.compileJMESPath(groupsFieldName, "groups field"); } - // Not a DataBoundSetter - set in constructor - public void setScopes(String scopes) { - this.scopes = Util.fixEmptyAndTrim(scopes); - } - @DataBoundSetter public void setLogoutFromOpenidProvider(boolean logoutFromOpenidProvider) { this.logoutFromOpenidProvider = logoutFromOpenidProvider; @@ -768,25 +651,6 @@ public void setEscapeHatchGroup(String escapeHatchGroup) { this.escapeHatchGroup = Util.fixEmptyAndTrim(escapeHatchGroup); } - @DataBoundSetter - public void setOverrideScopesDefined(boolean overrideScopesDefined) { - if (overrideScopesDefined) { - this.overrideScopesDefined = Boolean.TRUE; - } else { - this.overrideScopesDefined = Boolean.FALSE; - this.overrideScopes = null; - this.applyOverrideScopes(); - } - } - - @DataBoundSetter - public void setOverrideScopes(String overrideScopes) { - if (this.overrideScopesDefined == null || this.overrideScopesDefined) { - this.overrideScopes = Util.fixEmptyAndTrim(overrideScopes); - this.applyOverrideScopes(); - } - } - @DataBoundSetter public void setRootURLFromRequest(boolean rootURLFromRequest) { this.rootURLFromRequest = rootURLFromRequest; @@ -897,7 +761,7 @@ protected AuthorizationCodeFlow buildAuthorizationCodeFlow() { AccessMethod tokenAccessMethod = BearerToken.queryParameterAccessMethod(); HttpExecuteInterceptor authInterceptor = new ClientParametersAuthentication(clientId, Secret.toString(clientSecret)); - if (TokenAuthMethod.client_secret_basic.equals(tokenAuthMethod)) { + if (TokenAuthMethod.client_secret_basic.equals(serverConfiguration.getTokenAuthMethod())) { tokenAccessMethod = BearerToken.authorizationHeaderAccessMethod(); authInterceptor = new BasicAuthentication(clientId, Secret.toString(clientSecret)); } @@ -905,11 +769,11 @@ protected AuthorizationCodeFlow buildAuthorizationCodeFlow() { tokenAccessMethod, httpTransport, GsonFactory.getDefaultInstance(), - new GenericUrl(tokenServerUrl), + new GenericUrl(serverConfiguration.getTokenServerUrl()), authInterceptor, clientId, - authorizationServerUrl) - .setScopes(Arrays.asList(this.getScopes())); + serverConfiguration.getAuthorizationServerUrl()) + .setScopes(Arrays.asList(serverConfiguration.getScopes())); return builder.build(); } @@ -950,9 +814,6 @@ protected String getValidRedirectUrl(String url) { */ @Restricted(DoNotUse.class) // stapler only public HttpResponse doCommenceLogin(@QueryParameter String from, @Header("Referer") final String referer) { - // reload config if needed - loadWellKnownOpenIDConfigurationUrl(); - final String redirectOnFinish = getValidRedirectUrl(from != null ? from : referer); return new OicSession(from, buildOAuthRedirectUrl()) { @@ -993,7 +854,7 @@ public HttpResponse onSuccess(String authorizationCode, AuthorizationCodeFlow fl } GenericJson userInfo = null; - if (!Strings.isNullOrEmpty(userInfoServerUrl)) { + if (!Strings.isNullOrEmpty(getServerConfiguration().getUserInfoServerUrl())) { userInfo = getUserInfo(flow, response.getAccessToken()); if (userInfo == null) { return HttpResponses.errorWithoutStack(401, "Unauthorized"); @@ -1035,7 +896,7 @@ private OicJsonWebTokenVerifier getJwksVerifier() { } if (jwtVerifier == null) { jwtVerifier = new OicJsonWebTokenVerifier( - jwksServerUrl, + serverConfiguration.getJwksServerUrl(), new OicJsonWebTokenVerifier.Builder().setHttpTransportFactory(new HttpTransportFactory() { @Override public HttpTransport create() { @@ -1083,7 +944,8 @@ public void initialize(HttpRequest request) throws IOException { request.getHeaders().setAuthorization("Bearer " + accessToken); } }); - HttpRequest request = requestFactory.buildGetRequest(new GenericUrl(userInfoServerUrl)); + HttpRequest request = + requestFactory.buildGetRequest(new GenericUrl(serverConfiguration.getUserInfoServerUrl())); request.setThrowExceptionOnExecuteError(false); com.google.api.client.http.HttpResponse response = request.execute(); if (response.isSuccessStatusCode()) { @@ -1289,7 +1151,7 @@ public void doLogout(StaplerRequest req, StaplerResponse rsp) throws IOException OicCredentials credentials = user.getProperty(OicCredentials.class); if (credentials != null) { - if (this.logoutFromOpenidProvider && !Strings.isNullOrEmpty(this.endSessionEndpoint)) { + if (this.logoutFromOpenidProvider && !Strings.isNullOrEmpty(serverConfiguration.getEndSessionUrl())) { // This ensures that token will be expired at the right time with API Key calls, but no refresh can be // made. user.addProperty(new OicCredentials(null, null, null, CLOCK.millis())); @@ -1324,8 +1186,9 @@ static Object getStateAttribute(HttpSession session) { @CheckForNull private String maybeOpenIdLogoutEndpoint(String idToken, String state, String postLogoutRedirectUrl) { - if (this.logoutFromOpenidProvider && !Strings.isNullOrEmpty(this.endSessionEndpoint)) { - StringBuilder openidLogoutEndpoint = new StringBuilder(this.endSessionEndpoint); + final String url = serverConfiguration.getEndSessionUrl(); + if (this.logoutFromOpenidProvider && !Strings.isNullOrEmpty(url)) { + StringBuilder openidLogoutEndpoint = new StringBuilder(url); if (!Strings.isNullOrEmpty(idToken)) { openidLogoutEndpoint.append("?id_token_hint=").append(idToken).append("&"); @@ -1435,7 +1298,7 @@ public boolean handleTokenExpiration(HttpServletRequest httpRequest, HttpServlet } if (isExpired(credentials)) { - if (isUseRefreshTokens() && !Strings.isNullOrEmpty(credentials.getRefreshToken())) { + if (serverConfiguration.isUseRefreshTokens() && !Strings.isNullOrEmpty(credentials.getRefreshToken())) { return refreshExpiredToken(user.getId(), credentials, httpRequest, httpResponse); } else if (!isTokenExpirationCheckDisabled()) { redirectOrRejectRequest(httpRequest, httpResponse); @@ -1540,7 +1403,7 @@ private boolean handleTokenRefreshResponse( return false; } - if (!Strings.isNullOrEmpty(userInfoServerUrl)) { + if (!Strings.isNullOrEmpty(serverConfiguration.getUserInfoServerUrl())) { userInfo = getUserInfo(flow, tokenResponse.getAccessToken()); } @@ -1576,15 +1439,6 @@ private void handleTokenRefreshException(TokenResponseException e, HttpServletRe @Extension public static final class DescriptorImpl extends Descriptor { - public boolean isAuto() { - SecurityRealm realm = Jenkins.get().getSecurityRealm(); - return realm instanceof OicSecurityRealm - && StringUtils.isNotBlank(((OicSecurityRealm) realm).getWellKnownOpenIDConfigurationUrl()); - } - - public boolean isManual() { - return Jenkins.get().getSecurityRealm() instanceof OicSecurityRealm && !isAuto(); - } public String getDisplayName() { return Messages.OicSecurityRealm_DisplayName(); @@ -1608,133 +1462,6 @@ public FormValidation doCheckClientSecret(@QueryParameter String clientSecret) { return FormValidation.ok(); } - @RequirePOST - public FormValidation doCheckWellKnownOpenIDConfigurationUrl( - @QueryParameter String wellKnownOpenIDConfigurationUrl, - @QueryParameter boolean disableSslVerification) { - Jenkins.get().checkPermission(Jenkins.ADMINISTER); - try { - URL url = new URL(wellKnownOpenIDConfigurationUrl); - HttpRequest request = constructHttpTransport(disableSslVerification) - .createRequestFactory() - .buildGetRequest(new GenericUrl(url)); - com.google.api.client.http.HttpResponse response = request.execute(); - - // Try to parse the response. If it's not valid, a JsonParseException will be thrown indicating - // that it's not a valid JSON describing an OpenID Connect endpoint - WellKnownOpenIDConfigurationResponse config = GsonFactory.getDefaultInstance() - .fromInputStream( - response.getContent(), - Charset.defaultCharset(), - WellKnownOpenIDConfigurationResponse.class); - if (config.getAuthorizationEndpoint() == null || config.getTokenEndpoint() == null) { - return FormValidation.warning(Messages.OicSecurityRealm_URLNotAOpenIdEnpoint()); - } - - return FormValidation.ok(); - } catch (MalformedURLException e) { - return FormValidation.error(e, Messages.OicSecurityRealm_NotAValidURL()); - } catch (HttpResponseException e) { - return FormValidation.error( - e, - Messages.OicSecurityRealm_CouldNotRetreiveWellKnownConfig( - e.getStatusCode(), e.getStatusMessage())); - } catch (JsonParseException e) { - return FormValidation.error(e, Messages.OicSecurityRealm_CouldNotParseResponse()); - } catch (IOException e) { - return FormValidation.error(e, Messages.OicSecurityRealm_ErrorRetreivingWellKnownConfig()); - } - } - - @RequirePOST - public FormValidation doCheckTokenServerUrl(@QueryParameter String tokenServerUrl) { - Jenkins.get().checkPermission(Jenkins.ADMINISTER); - if (Util.fixEmptyAndTrim(tokenServerUrl) == null) { - return FormValidation.error(Messages.OicSecurityRealm_TokenServerURLKeyRequired()); - } - try { - new URL(tokenServerUrl); - return FormValidation.ok(); - } catch (MalformedURLException e) { - return FormValidation.error(e, Messages.OicSecurityRealm_NotAValidURL()); - } - } - - @RequirePOST - public FormValidation doCheckJwksServerUrl(@QueryParameter String jwksServerUrl) { - Jenkins.get().checkPermission(Jenkins.ADMINISTER); - if (Util.fixEmptyAndTrim(jwksServerUrl) == null) { - return FormValidation.ok(); - } - try { - new URL(jwksServerUrl); - return FormValidation.ok(); - } catch (MalformedURLException e) { - return FormValidation.error(e, Messages.OicSecurityRealm_NotAValidURL()); - } - } - - @RequirePOST - public FormValidation doCheckTokenAuthMethod(@QueryParameter String tokenAuthMethod) { - Jenkins.get().checkPermission(Jenkins.ADMINISTER); - if (Util.fixEmptyAndTrim(tokenAuthMethod) == null) { - return FormValidation.error(Messages.OicSecurityRealm_TokenAuthMethodRequired()); - } - return FormValidation.ok(); - } - - @RequirePOST - public FormValidation doCheckAuthorizationServerUrl(@QueryParameter String authorizationServerUrl) { - Jenkins.get().checkPermission(Jenkins.ADMINISTER); - if (authorizationServerUrl == null) { - return FormValidation.error(Messages.OicSecurityRealm_TokenServerURLKeyRequired()); - } - try { - new URL(authorizationServerUrl); - return FormValidation.ok(); - } catch (MalformedURLException e) { - return FormValidation.error(e, Messages.OicSecurityRealm_NotAValidURL()); - } - } - - @RequirePOST - public FormValidation doCheckScopes(@QueryParameter String scopes) { - Jenkins.get().checkPermission(Jenkins.ADMINISTER); - if (Util.fixEmptyAndTrim(scopes) == null) { - return FormValidation.ok(Messages.OicSecurityRealm_UsingDefaultScopes()); - } - if (!scopes.toLowerCase().contains("openid")) { - return FormValidation.warning(Messages.OicSecurityRealm_RUSureOpenIdNotInScope()); - } - return FormValidation.ok(); - } - - @RequirePOST - public FormValidation doCheckOverrideScopes(@QueryParameter String overrideScopes) { - Jenkins.get().checkPermission(Jenkins.ADMINISTER); - if (Util.fixEmptyAndTrim(overrideScopes) == null) { - return FormValidation.ok(Messages.OicSecurityRealm_UsingDefaultScopes()); - } - if (!overrideScopes.toLowerCase().contains("openid")) { - return FormValidation.warning(Messages.OicSecurityRealm_RUSureOpenIdNotInScope()); - } - return FormValidation.ok(); - } - - @RequirePOST - public FormValidation doCheckEndSessionEndpoint(@QueryParameter String endSessionEndpoint) { - Jenkins.get().checkPermission(Jenkins.ADMINISTER); - if (Util.fixEmptyAndTrim(endSessionEndpoint) == null) { - return FormValidation.error(Messages.OicSecurityRealm_EndSessionURLKeyRequired()); - } - try { - new URL(endSessionEndpoint); - return FormValidation.ok(); - } catch (MalformedURLException e) { - return FormValidation.error(e, Messages.OicSecurityRealm_NotAValidURL()); - } - } - @RequirePOST public FormValidation doCheckPostLogoutRedirectUrl(@QueryParameter String postLogoutRedirectUrl) { Jenkins.get().checkPermission(Jenkins.ADMINISTER); @@ -1787,5 +1514,9 @@ private FormValidation doCheckFieldName(String fieldName, FormValidation validIf } return FormValidation.ok(); } + + public Descriptor getDefaultServerConfigurationType() { + return Jenkins.get().getDescriptor(OicServerWellKnownConfiguration.class); + } } } diff --git a/src/main/java/org/jenkinsci/plugins/oic/OicServerConfiguration.java b/src/main/java/org/jenkinsci/plugins/oic/OicServerConfiguration.java new file mode 100644 index 00000000..2ae08df3 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/oic/OicServerConfiguration.java @@ -0,0 +1,34 @@ +package org.jenkinsci.plugins.oic; + +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.ExtensionPoint; +import hudson.model.AbstractDescribableImpl; +import org.jenkinsci.plugins.oic.OicSecurityRealm.TokenAuthMethod; + +public abstract class OicServerConfiguration extends AbstractDescribableImpl + implements ExtensionPoint { + + @NonNull + public abstract String getTokenServerUrl(); + + @NonNull + public abstract String getJwksServerUrl(); + + @NonNull + public abstract String getAuthorizationServerUrl(); + + @CheckForNull + public abstract String getUserInfoServerUrl(); + + @NonNull + public abstract String getScopes(); + + @NonNull + public abstract TokenAuthMethod getTokenAuthMethod(); + + @CheckForNull + public abstract String getEndSessionUrl(); + + public abstract boolean isUseRefreshTokens(); +} diff --git a/src/main/java/org/jenkinsci/plugins/oic/OicServerManualConfiguration.java b/src/main/java/org/jenkinsci/plugins/oic/OicServerManualConfiguration.java new file mode 100644 index 00000000..c2281e70 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/oic/OicServerManualConfiguration.java @@ -0,0 +1,204 @@ +package org.jenkinsci.plugins.oic; + +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import hudson.Extension; +import hudson.Util; +import hudson.model.Descriptor; +import hudson.model.Descriptor.FormException; +import hudson.util.FormValidation; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Locale; +import java.util.Objects; +import jenkins.model.Jenkins; +import org.jenkinsci.Symbol; +import org.jenkinsci.plugins.oic.OicSecurityRealm.TokenAuthMethod; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.verb.POST; + +public class OicServerManualConfiguration extends OicServerConfiguration { + + private final String authorizationServerUrl; + private final String tokenServerUrl; + private TokenAuthMethod tokenAuthMethod = TokenAuthMethod.client_secret_post; + private String jwksServerUrl; + private String endSessionUrl; + private String scopes = "openid email"; + private String userInfoServerUrl; + private boolean useRefreshTokens; + + @DataBoundConstructor + public OicServerManualConfiguration(String tokenServerUrl, String authorizationServerUrl) throws FormException { + this.authorizationServerUrl = validateNonNull("authorizationServerUrl", authorizationServerUrl); + this.tokenServerUrl = validateNonNull("tokenServerUrl", tokenServerUrl); + } + + @DataBoundSetter + public void setTokenAuthMethod(TokenAuthMethod tokenAuthMethod) throws FormException { + this.tokenAuthMethod = validateNonNull("tokenAuthMethod", tokenAuthMethod); + } + + @DataBoundSetter + public void setEndSessionUrl(@Nullable String endSessionUrl) { + this.endSessionUrl = endSessionUrl; + } + + @DataBoundSetter + public void setJwksServerUrl(@Nullable String jwksServerUrl) { + this.jwksServerUrl = jwksServerUrl; + } + + @DataBoundSetter + public void setScopes(@NonNull String scopes) { + this.scopes = Objects.requireNonNull(scopes); + } + + @DataBoundSetter + public void setUserInfoServerUrl(@Nullable String userInfoServerUrl) { + this.userInfoServerUrl = userInfoServerUrl; + } + + @DataBoundSetter + public void setUseRefreshTokens(boolean useRefreshTokens) { + this.useRefreshTokens = useRefreshTokens; + } + + @Override + public String getAuthorizationServerUrl() { + return authorizationServerUrl; + } + + @Override + @CheckForNull + public String getEndSessionUrl() { + return endSessionUrl; + } + + @Override + public boolean isUseRefreshTokens() { + return useRefreshTokens; + } + + @Override + public String getJwksServerUrl() { + return jwksServerUrl; + } + + @Override + public String getScopes() { + return scopes; + } + + @Override + public TokenAuthMethod getTokenAuthMethod() { + return tokenAuthMethod; + } + + @Override + public String getTokenServerUrl() { + return tokenServerUrl; + } + + @Override + public String getUserInfoServerUrl() { + return userInfoServerUrl; + } + + private static T validateNonNull(String fieldName, T value) throws FormException { + if (value == null) { + throw new FormException(fieldName + " is mandatory", fieldName); + } + return value; + } + + @Extension + @Symbol("manual") + public static class DescriptorImpl extends Descriptor { + + @Override + public String getDisplayName() { + return Messages.OicServerManualConfiguration_DisplayName(); + } + + @POST + public FormValidation doCheckAuthorizationServerUrl(@QueryParameter String value) { + Jenkins.get().checkPermission(Jenkins.ADMINISTER); + if (value == null) { + return FormValidation.error(Messages.OicSecurityRealm_TokenServerURLKeyRequired()); + } + try { + new URL(value); + return FormValidation.ok(); + } catch (MalformedURLException e) { + return FormValidation.error(e, Messages.OicSecurityRealm_NotAValidURL()); + } + } + + @POST + public FormValidation doCheckEndSessionUrl(@QueryParameter String value) { + Jenkins.get().checkPermission(Jenkins.ADMINISTER); + if (Util.fixEmptyAndTrim(value) == null) { + return FormValidation.error(Messages.OicSecurityRealm_EndSessionURLKeyRequired()); + } + try { + new URL(value); + return FormValidation.ok(); + } catch (MalformedURLException e) { + return FormValidation.error(e, Messages.OicSecurityRealm_NotAValidURL()); + } + } + + @POST + public FormValidation doCheckJwksServerUrl(@QueryParameter String value) { + Jenkins.get().checkPermission(Jenkins.ADMINISTER); + if (Util.fixEmptyAndTrim(value) == null) { + return FormValidation.ok(); + } + try { + new URL(value); + return FormValidation.ok(); + } catch (MalformedURLException e) { + return FormValidation.error(e, Messages.OicSecurityRealm_NotAValidURL()); + } + } + + @POST + public FormValidation doCheckScopes(@QueryParameter String value) { + Jenkins.get().checkPermission(Jenkins.ADMINISTER); + if (Util.fixEmptyAndTrim(value) == null) { + return FormValidation.error(Messages.OicSecurityRealm_ScopesRequired()); + } + if (!value.toLowerCase(Locale.ROOT).contains("openid")) { + return FormValidation.warning(Messages.OicSecurityRealm_RUSureOpenIdNotInScope()); + } + return FormValidation.ok(); + } + + @POST + public FormValidation doCheckTokenServerUrl(@QueryParameter String value) { + Jenkins.get().checkPermission(Jenkins.ADMINISTER); + if (Util.fixEmptyAndTrim(value) == null) { + return FormValidation.error(Messages.OicSecurityRealm_TokenServerURLKeyRequired()); + } + try { + new URL(value); + return FormValidation.ok(); + } catch (MalformedURLException e) { + return FormValidation.error(e, Messages.OicSecurityRealm_NotAValidURL()); + } + } + + @POST + public FormValidation doCheckTokenAuthMethod(@QueryParameter String tokenAuthMethod) { + Jenkins.get().checkPermission(Jenkins.ADMINISTER); + if (Util.fixEmptyAndTrim(tokenAuthMethod) == null) { + return FormValidation.error(Messages.OicSecurityRealm_TokenAuthMethodRequired()); + } + return FormValidation.ok(); + } + } +} diff --git a/src/main/java/org/jenkinsci/plugins/oic/OicServerWellKnownConfiguration.java b/src/main/java/org/jenkinsci/plugins/oic/OicServerWellKnownConfiguration.java new file mode 100644 index 00000000..99a69701 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/oic/OicServerWellKnownConfiguration.java @@ -0,0 +1,272 @@ +package org.jenkinsci.plugins.oic; + +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpHeaders; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpResponseException; +import com.google.api.client.json.gson.GsonFactory; +import com.google.gson.JsonParseException; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import hudson.Extension; +import hudson.RelativePath; +import hudson.Util; +import hudson.model.Descriptor; +import hudson.util.FormValidation; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.Charset; +import java.time.LocalDateTime; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; +import jenkins.model.Jenkins; +import org.apache.commons.lang.StringUtils; +import org.jenkinsci.Symbol; +import org.jenkinsci.plugins.oic.OicSecurityRealm.TokenAuthMethod; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.verb.POST; + +public class OicServerWellKnownConfiguration extends OicServerConfiguration { + + private static final Logger LOGGER = Logger.getLogger(OicServerWellKnownConfiguration.class.getName()); + + private final String wellKnownOpenIDConfigurationUrl; + private String scopesOverride; + + private transient String authorizationServerUrl; + private transient String tokenServerUrl; + private transient String jwksServerUrl; + private transient String endSessionUrl; + private transient String scopes; + private transient String userInfoServerUrl; + private transient boolean useRefreshTokens; + private transient TokenAuthMethod tokenAuthMethod; + + /** + * Time of the wellknown configuration expiration + */ + private transient LocalDateTime wellKnownExpires = null; + + @DataBoundConstructor + public OicServerWellKnownConfiguration(String wellKnownOpenIDConfigurationUrl) { + this.wellKnownOpenIDConfigurationUrl = Objects.requireNonNull(wellKnownOpenIDConfigurationUrl); + } + + @DataBoundSetter + public void setScopesOverride(String scopesOverride) { + this.scopesOverride = Util.fixEmptyAndTrim(scopesOverride); + } + + @Override + public String getAuthorizationServerUrl() { + loadWellKnownConfigIfNeeded(); + return authorizationServerUrl; + } + + @Override + @CheckForNull + public String getEndSessionUrl() { + loadWellKnownConfigIfNeeded(); + return endSessionUrl; + } + + @Override + public String getJwksServerUrl() { + loadWellKnownConfigIfNeeded(); + return jwksServerUrl; + } + + /** + * Returns {@link #getScopesOverride()} if set, otherwise the scopes from the published metadata if set, otherwise "openid email". + */ + @Override + public String getScopes() { + loadWellKnownConfigIfNeeded(); + if (scopesOverride != null) { + return scopesOverride; + } + if (scopes != null) { + return scopes; + } + // server did not advertise anything and no overrides set. + // email may not be supported, but it is relatively common so try anyway + return "openid email"; + } + + public String getScopesOverride() { + return scopesOverride; + } + + public String getWellKnownOpenIDConfigurationUrl() { + return wellKnownOpenIDConfigurationUrl; + } + + @Override + public String getTokenServerUrl() { + loadWellKnownConfigIfNeeded(); + return tokenServerUrl; + } + + @Override + public String getUserInfoServerUrl() { + loadWellKnownConfigIfNeeded(); + return userInfoServerUrl; + } + + @Override + public boolean isUseRefreshTokens() { + loadWellKnownConfigIfNeeded(); + return useRefreshTokens; + } + + @Override + public TokenAuthMethod getTokenAuthMethod() { + loadWellKnownConfigIfNeeded(); + return tokenAuthMethod; + } + + /** + * Obtain the provider configuration from the configured well known URL if it + * has not yet been obtained or requires a refresh. + */ + private void loadWellKnownConfigIfNeeded() { + LocalDateTime now = LocalDateTime.now(); + if (this.wellKnownExpires != null && this.wellKnownExpires.isBefore(now)) { + // configuration is still fresh + return; + } + + // Get the well-known configuration from the specified URL + try { + URL url = new URL(wellKnownOpenIDConfigurationUrl); + OicSecurityRealm realm = (OicSecurityRealm) Jenkins.get().getSecurityRealm(); + HttpRequest request = + realm.getHttpTransport().createRequestFactory().buildGetRequest(new GenericUrl(url)); + + com.google.api.client.http.HttpResponse response = request.execute(); + WellKnownOpenIDConfigurationResponse config = GsonFactory.getDefaultInstance() + .fromInputStream( + response.getContent(), + Charset.defaultCharset(), + WellKnownOpenIDConfigurationResponse.class); + + this.authorizationServerUrl = config.getAuthorizationEndpoint(); + this.tokenServerUrl = config.getTokenEndpoint(); + this.jwksServerUrl = config.getJwksUri(); + this.tokenAuthMethod = config.getPreferredTokenAuthMethod(); + this.userInfoServerUrl = config.getUserinfoEndpoint(); + if (config.getScopesSupported() != null + && !config.getScopesSupported().isEmpty()) { + this.scopes = StringUtils.join(config.getScopesSupported(), " "); + } + this.endSessionUrl = config.getEndSessionEndpoint(); + + if (config.getGrantTypesSupported() != null) { + this.useRefreshTokens = config.getGrantTypesSupported().contains("refresh_token"); + } else { + this.useRefreshTokens = false; + } + + setWellKnownExpires(response.getHeaders()); + } catch (MalformedURLException e) { + LOGGER.log(Level.SEVERE, "Invalid WellKnown OpenID Configuration URL", e); + } catch (HttpResponseException e) { + LOGGER.log(Level.SEVERE, "Could not get wellknown OpenID Configuration", e); + } catch (JsonParseException e) { + LOGGER.log(Level.SEVERE, "Could not parse wellknown OpenID Configuration", e); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Error while loading wellknown OpenID Configuration", e); + } + } + + /** + * Parse headers to determine expiration date + */ + private void setWellKnownExpires(HttpHeaders headers) { + String expires = Util.fixEmptyAndTrim(headers.getExpires()); + // expires 0 means no cache + // we could (should?) have a look at Cache-Control header and max-age but for + // simplicity + // we can just leave it default TTL 1h refresh which sounds reasonable for such + // file + if (expires != null && !"0".equals(expires)) { + ZonedDateTime zdt = ZonedDateTime.parse(expires, DateTimeFormatter.RFC_1123_DATE_TIME); + if (zdt != null) { + this.wellKnownExpires = zdt.toLocalDateTime(); + return; + } + } + + // default to 1 hour refresh + this.wellKnownExpires = LocalDateTime.now().plusSeconds(3600); + } + + @Extension + @Symbol("wellKnown") + public static class DescriptorImpl extends Descriptor { + + @Override + public String getDisplayName() { + return Messages.OicServerWellKnownConfiguration_DisplayName(); + } + + @POST + public FormValidation doCheckWellKnownOpenIDConfigurationUrl( + @QueryParameter String wellKnownOpenIDConfigurationUrl, + @RelativePath("..") @QueryParameter boolean disableSslVerification) { + Jenkins.get().checkPermission(Jenkins.ADMINISTER); + if (wellKnownOpenIDConfigurationUrl == null || wellKnownOpenIDConfigurationUrl.isBlank()) { + return FormValidation.error(Messages.OicSecurityRealm_NotAValidURL()); + } + try { + URL url = new URL(wellKnownOpenIDConfigurationUrl); + HttpRequest request = OicSecurityRealm.constructHttpTransport(disableSslVerification) + .createRequestFactory() + .buildGetRequest(new GenericUrl(url)); + com.google.api.client.http.HttpResponse response = request.execute(); + + // Try to parse the response. If it's not valid, a JsonParseException will be + // thrown indicating + // that it's not a valid JSON describing an OpenID Connect endpoint + WellKnownOpenIDConfigurationResponse config = GsonFactory.getDefaultInstance() + .fromInputStream( + response.getContent(), + Charset.defaultCharset(), + WellKnownOpenIDConfigurationResponse.class); + if (config.getAuthorizationEndpoint() == null || config.getTokenEndpoint() == null) { + return FormValidation.warning(Messages.OicSecurityRealm_URLNotAOpenIdEnpoint()); + } + + return FormValidation.ok(); + } catch (MalformedURLException e) { + return FormValidation.error(e, Messages.OicSecurityRealm_NotAValidURL()); + } catch (HttpResponseException e) { + return FormValidation.error( + e, + Messages.OicSecurityRealm_CouldNotRetreiveWellKnownConfig( + e.getStatusCode(), e.getStatusMessage())); + } catch (JsonParseException e) { + return FormValidation.error(e, Messages.OicSecurityRealm_CouldNotParseResponse()); + } catch (IOException e) { + return FormValidation.error(e, Messages.OicSecurityRealm_ErrorRetreivingWellKnownConfig()); + } + } + + @POST + public FormValidation doCheckOverrideScopes(@QueryParameter String overrideScopes) { + Jenkins.get().checkPermission(Jenkins.ADMINISTER); + if (Util.fixEmptyAndTrim(overrideScopes) == null) { + return FormValidation.ok(Messages.OicSecurityRealm_UsingDefaultScopes()); + } + if (!overrideScopes.toLowerCase().contains("openid")) { + return FormValidation.warning(Messages.OicSecurityRealm_RUSureOpenIdNotInScope()); + } + return FormValidation.ok(); + } + } +} diff --git a/src/main/resources/org/jenkinsci/plugins/oic/Messages.properties b/src/main/resources/org/jenkinsci/plugins/oic/Messages.properties index a33b2e45..b83c38bd 100644 --- a/src/main/resources/org/jenkinsci/plugins/oic/Messages.properties +++ b/src/main/resources/org/jenkinsci/plugins/oic/Messages.properties @@ -13,9 +13,12 @@ OicSecurityRealm.TokenAuthMethodRequired = Token auth method is required. OicSecurityRealm.UsingDefaultUsername = Using ''sub''. OicSecurityRealm.UsingDefaultScopes = Using ''openid email''. OicSecurityRealm.RUSureOpenIdNotInScope = Are you sure you don''t want to include ''openid'' as an scope? +OicSecurityRealm.ScopesRequired = Scopes is required. OicSecurityRealm.EndSessionURLKeyRequired = End Session URL Key is required. OicSecurityRealm.InvalidFieldName = Invalid field name - must be a valid JMESPath expression. OicSecurityRealm.NoIdTokenInResponse = No idtoken was provided in response to token request. OicSecurityRealm.IdTokenParseError = Idtoken could not be parsed. OicSecurityRealm.UsernameNotFound = No field ''{0}'' was supplied in the UserInfo or the IdToken payload to be used as the username. OicSecurityRealm.TokenRequestFailure = Token request failed: {0}" +OicServerWellKnownConfiguration.DisplayName = Discovery via well-known endpoint +OicServerManualConfiguration.DisplayName = Manual entry diff --git a/src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/config.jelly b/src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/config.jelly index 6f758789..42de2c18 100644 --- a/src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/config.jelly @@ -7,58 +7,7 @@ - - - - - - - - - - - - - -
-
-
-
- - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - - -
-
-
+ diff --git a/src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/config.properties b/src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/config.properties index daf01cf1..fb8acda3 100644 --- a/src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/config.properties +++ b/src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/config.properties @@ -1,42 +1,28 @@ AdvancedConfiguration=Advanced configuration -AuthorizationServerUrl=Authorization server url -AutomaticConfiguration=Automatic configuration -Basic=Basic +AllowedTokenExpirationClockSkewSeconds=Token Expiry Expiration Clock Skew +AllowTokenAccessWithoutOicSession=Allow access using a Jenkins API token without an OIDC Session ClientId=Client id ClientSecret=Client secret ConfigurationMode=Configuration mode ConfigureEscapeHatch=Configure 'escape hatch' for when the OpenID Provider is unavailable DisableNonceVerification=Disable Nonce verification DisableSslVerification=Disable ssl verification +DisableTokenExpirationCheck=Disable Token Expiration Check DisableTokenVerification=Disable token verification (JWKS) EmailFieldName=Email field name EnablePKCE=Enable Proof Key for Code Exchange (PKCE) -EndSessionUrl=End session URL for OpenID Provider FullnameFieldName=Full name field name Group=Group GroupsFieldName=Groups field name -JwksServerUrl=Jwks server url LogoutFromOpenIDProvider=Logout from OpenID Provider -ManualConfiguration=Manual configuration -OverrideScopes=Override scopes -Post=Post PostLogoutRedirectUrl=Post logout redirect URL -Scopes=Scopes -Scopes=Scopes Secret=Secret SecurityConfiguration=Security configuration SendScopesInTokenRequest=Send scopes in token request -TokenAuthenticationMethod=Token Authentication Method TokenFieldKeyToCheck=Token Field Key To Check TokenFieldValueToCheck=Token Field Value To Check -TokenServerUrl=Token server url UseRootUrlFromRequest=Use Root URL from request UserFields=User fields -UserInfoServerUrl=UserInfo server url Username=Username UsernameFieldName=User name field name WellknownConfigurationEndpoint=Well-known configuration endpoint -UseRefreshTokens=Enable Token Refresh using Refresh Tokens -DisableTokenExpirationCheck=Disable Token Expiration Check -AllowedTokenExpirationClockSkewSeconds=Token Expiry Expiration Clock Skew -AllowTokenAccessWithoutOicSession=Allow access using a Jenkins API token without an OIDC Session \ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-jwksServerUrl.html b/src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-jwksServerUrl.html deleted file mode 100644 index 5bced9d5..00000000 --- a/src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-jwksServerUrl.html +++ /dev/null @@ -1,4 +0,0 @@ -
- Recommended. jswon webtoken key signature url of the openid connect provider -
- diff --git a/src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-overrideScopes.html b/src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-overrideScopes.html deleted file mode 100644 index 3f192571..00000000 --- a/src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-overrideScopes.html +++ /dev/null @@ -1,5 +0,0 @@ -
- If not overriden, all supported scopes of WellKnown configuration will be used. - When defined, only scopes in the override list will be used (if they are present - in the supported scopes of the configuration endpoint). -
diff --git a/src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-serverConfiguration.html b/src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-serverConfiguration.html new file mode 100644 index 00000000..1860b60d --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-serverConfiguration.html @@ -0,0 +1,3 @@ +
+ How to configure this client +
\ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/config.jelly b/src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/config.jelly new file mode 100644 index 00000000..2a9aa9ea --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/config.jelly @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/config.properties b/src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/config.properties new file mode 100644 index 00000000..56bbf500 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/config.properties @@ -0,0 +1,10 @@ +AuthorizationServerUrl=Authorization server url +Basic=Basic +EndSessionUrl=End session URL for OpenID Provider +JwksServerUrl=Jwks server url +Post=Post +Scopes=Scopes +TokenAuthenticationMethod=Token Authentication Method +TokenServerUrl=Token server url +UseRefreshTokens=Enable Token Refresh using Refresh Tokens +UserInfoServerUrl=UserInfo server url diff --git a/src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-authorizationServerUrl.html b/src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/help-authorizationServerUrl.html similarity index 100% rename from src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-authorizationServerUrl.html rename to src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/help-authorizationServerUrl.html diff --git a/src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-authorizationServerUrl_fr.html b/src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/help-authorizationServerUrl_fr.html similarity index 100% rename from src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-authorizationServerUrl_fr.html rename to src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/help-authorizationServerUrl_fr.html diff --git a/src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-endSessionUrl.html b/src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/help-endSessionUrl.html similarity index 100% rename from src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-endSessionUrl.html rename to src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/help-endSessionUrl.html diff --git a/src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-endSessionUrl_fr.html b/src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/help-endSessionUrl_fr.html similarity index 100% rename from src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-endSessionUrl_fr.html rename to src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/help-endSessionUrl_fr.html diff --git a/src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/help-jwksServerUrl.html b/src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/help-jwksServerUrl.html new file mode 100644 index 00000000..10889b35 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/help-jwksServerUrl.html @@ -0,0 +1,4 @@ +
+ Recommended. json webtoken key signature url of the openid connect provider +
+ diff --git a/src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-jwksServerUrl_fr.html b/src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/help-jwksServerUrl_fr.html similarity index 100% rename from src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-jwksServerUrl_fr.html rename to src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/help-jwksServerUrl_fr.html diff --git a/src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-tokenAuthMethod.html b/src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/help-tokenAuthMethod.html similarity index 100% rename from src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-tokenAuthMethod.html rename to src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/help-tokenAuthMethod.html diff --git a/src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-tokenAuthMethod_fr.html b/src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/help-tokenAuthMethod_fr.html similarity index 100% rename from src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-tokenAuthMethod_fr.html rename to src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/help-tokenAuthMethod_fr.html diff --git a/src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-tokenServerUrl.html b/src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/help-tokenServerUrl.html similarity index 100% rename from src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-tokenServerUrl.html rename to src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/help-tokenServerUrl.html diff --git a/src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-tokenServerUrl_fr.html b/src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/help-tokenServerUrl_fr.html similarity index 100% rename from src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-tokenServerUrl_fr.html rename to src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/help-tokenServerUrl_fr.html diff --git a/src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-useRefreshTokens.html b/src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/help-useRefreshTokens.html similarity index 100% rename from src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-useRefreshTokens.html rename to src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/help-useRefreshTokens.html diff --git a/src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-userInfoServerUrl.html b/src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/help-userInfoServerUrl.html similarity index 100% rename from src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-userInfoServerUrl.html rename to src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/help-userInfoServerUrl.html diff --git a/src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-userInfoServerUrl_fr.html b/src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/help-userInfoServerUrl_fr.html similarity index 100% rename from src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-userInfoServerUrl_fr.html rename to src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/help-userInfoServerUrl_fr.html diff --git a/src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/help.html b/src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/help.html new file mode 100644 index 00000000..3f722989 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/oic/OicServerManualConfiguration/help.html @@ -0,0 +1,4 @@ +
+ Manual configuration of an OpenID Connect (OIDC) client by specifying the URLs for each endpoint. + Manual configuration can be used when Auto Discovery is not available or provides incorrect information. +
\ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/oic/OicServerWellKnownConfiguration/config.jelly b/src/main/resources/org/jenkinsci/plugins/oic/OicServerWellKnownConfiguration/config.jelly new file mode 100644 index 00000000..a3093090 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/oic/OicServerWellKnownConfiguration/config.jelly @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/src/main/resources/org/jenkinsci/plugins/oic/OicServerWellKnownConfiguration/config.properties b/src/main/resources/org/jenkinsci/plugins/oic/OicServerWellKnownConfiguration/config.properties new file mode 100644 index 00000000..548acc14 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/oic/OicServerWellKnownConfiguration/config.properties @@ -0,0 +1,2 @@ +OverrideScopes=Override scopes +WellknownConfigurationEndpoint=Well-known configuration endpoint diff --git a/src/main/resources/org/jenkinsci/plugins/oic/OicServerWellKnownConfiguration/help-scopesOverride.html b/src/main/resources/org/jenkinsci/plugins/oic/OicServerWellKnownConfiguration/help-scopesOverride.html new file mode 100644 index 00000000..2aa2e3aa --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/oic/OicServerWellKnownConfiguration/help-scopesOverride.html @@ -0,0 +1,4 @@ +
+ If not overriden, all supported scopes of WellKnown configuration will be used. + When defined, only scopes in the override list will be used. +
diff --git a/src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-overrideScopes_fr.html b/src/main/resources/org/jenkinsci/plugins/oic/OicServerWellKnownConfiguration/help-scopesOverride_fr.html similarity index 68% rename from src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-overrideScopes_fr.html rename to src/main/resources/org/jenkinsci/plugins/oic/OicServerWellKnownConfiguration/help-scopesOverride_fr.html index 9b409486..f650f39d 100644 --- a/src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-overrideScopes_fr.html +++ b/src/main/resources/org/jenkinsci/plugins/oic/OicServerWellKnownConfiguration/help-scopesOverride_fr.html @@ -1,4 +1,4 @@
Si la liste de scope n'est pas surchargée, tous les scopes définis dans la configuration WellKnown seront utilisés. - Une fois définie, seuls les scopes de la liste de surcharge seront utilisées (s'ils sont présents dans les scopes définis dans l'URL de configuration). + Une fois définie, seuls les scopes de la liste de surcharge seront utilisées.
diff --git a/src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-wellKnownOpenIDConfigurationUrl.html b/src/main/resources/org/jenkinsci/plugins/oic/OicServerWellKnownConfiguration/help-wellKnownOpenIDConfigurationUrl.html similarity index 52% rename from src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-wellKnownOpenIDConfigurationUrl.html rename to src/main/resources/org/jenkinsci/plugins/oic/OicServerWellKnownConfiguration/help-wellKnownOpenIDConfigurationUrl.html index c1e170be..4d22581c 100644 --- a/src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-wellKnownOpenIDConfigurationUrl.html +++ b/src/main/resources/org/jenkinsci/plugins/oic/OicServerWellKnownConfiguration/help-wellKnownOpenIDConfigurationUrl.html @@ -1,6 +1,6 @@
- The well-known registry URI that can be used to automatically configure the endpoints. This is often in the form http://example.com/.well-known/openid-configuration + The well-known registry URI that can be used to automatically configure the endpoints. This is often in the form https://example.com/.well-known/openid-configuration -

Note: If the configuration advertises an logout url then the logout from openid provider ('global logout') is enabled automatically. Save and switch to manual - to override this behavior.

+

Note: If the configuration advertises an logout url then the logout from openid provider ('global logout') is enabled automatically. Switch to manual + to override this behavior.

\ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-wellKnownOpenIDConfigurationUrl_fr.html b/src/main/resources/org/jenkinsci/plugins/oic/OicServerWellKnownConfiguration/help-wellKnownOpenIDConfigurationUrl_fr.html similarity index 58% rename from src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-wellKnownOpenIDConfigurationUrl_fr.html rename to src/main/resources/org/jenkinsci/plugins/oic/OicServerWellKnownConfiguration/help-wellKnownOpenIDConfigurationUrl_fr.html index 69b28121..8104ed15 100644 --- a/src/main/resources/org/jenkinsci/plugins/oic/OicSecurityRealm/help-wellKnownOpenIDConfigurationUrl_fr.html +++ b/src/main/resources/org/jenkinsci/plugins/oic/OicServerWellKnownConfiguration/help-wellKnownOpenIDConfigurationUrl_fr.html @@ -1,5 +1,5 @@
- L'URL de configuration well-known peut être utilisée pour configurer automatiquement les paramètres du serveur. Il est habituellement de la forme http://example.com/.well-known/openid-configuration + L'URL de configuration well-known peut être utilisée pour configurer automatiquement les paramètres du serveur. Il est habituellement de la forme https://example.com/.well-known/openid-configuration -

Note : si la configuration annonce une URL de déconnexion, alors la déconnexion du serveur OpenID ('déconnexion globale') est activée automatiquement. Enregistrez et basculez vers le mode manuel pour changer ce comportement.

+

Note : si la configuration annonce une URL de déconnexion, alors la déconnexion du serveur OpenID ('déconnexion globale') est activée automatiquement. Basculez vers le mode manuel pour changer ce comportement.

\ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/oic/OicServerWellKnownConfiguration/help.html b/src/main/resources/org/jenkinsci/plugins/oic/OicServerWellKnownConfiguration/help.html new file mode 100644 index 00000000..6a93b9ce --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/oic/OicServerWellKnownConfiguration/help.html @@ -0,0 +1,3 @@ +
+ Automatic configuration of the OpenID Connect (OIDC) client from information in the provider's "Well-Known" configuration endpoint. +
\ No newline at end of file diff --git a/src/test/java/org/jenkinsci/plugins/oic/ConfigurationAsCodeTest.java b/src/test/java/org/jenkinsci/plugins/oic/ConfigurationAsCodeTest.java index 1ee9dc1d..304d9269 100644 --- a/src/test/java/org/jenkinsci/plugins/oic/ConfigurationAsCodeTest.java +++ b/src/test/java/org/jenkinsci/plugins/oic/ConfigurationAsCodeTest.java @@ -22,11 +22,12 @@ import static io.jenkins.plugins.casc.misc.Util.getJenkinsRoot; import static io.jenkins.plugins.casc.misc.Util.toStringFromYamlFile; import static io.jenkins.plugins.casc.misc.Util.toYamlString; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; public class ConfigurationAsCodeTest { @@ -42,7 +43,9 @@ public void testConfig() { assertTrue(realm instanceof OicSecurityRealm); OicSecurityRealm oicSecurityRealm = (OicSecurityRealm) realm; - assertEquals("http://localhost/authorize", oicSecurityRealm.getAuthorizationServerUrl()); + assertEquals( + "http://localhost/authorize", + oicSecurityRealm.getServerConfiguration().getAuthorizationServerUrl()); assertEquals("clientId", oicSecurityRealm.getClientId()); assertEquals("clientSecret", Secret.toString(oicSecurityRealm.getClientSecret())); assertTrue(oicSecurityRealm.isDisableSslVerification()); @@ -56,12 +59,18 @@ public void testConfig() { assertEquals("fullNameFieldName", oicSecurityRealm.getFullNameFieldName()); assertEquals("groupsFieldName", oicSecurityRealm.getGroupsFieldName()); assertTrue(oicSecurityRealm.isLogoutFromOpenidProvider()); - assertEquals("scopes", oicSecurityRealm.getScopes()); - assertEquals("http://localhost/token", oicSecurityRealm.getTokenServerUrl()); - assertEquals(TokenAuthMethod.client_secret_post, oicSecurityRealm.getTokenAuthMethod()); + assertEquals("scopes", oicSecurityRealm.getServerConfiguration().getScopes()); + assertEquals( + "http://localhost/token", + oicSecurityRealm.getServerConfiguration().getTokenServerUrl()); + assertEquals( + TokenAuthMethod.client_secret_post, + oicSecurityRealm.getServerConfiguration().getTokenAuthMethod()); assertEquals("userNameField", oicSecurityRealm.getUserNameField()); assertTrue(oicSecurityRealm.isRootURLFromRequest()); - assertEquals("http://localhost/jwks", oicSecurityRealm.getJwksServerUrl()); + assertEquals( + "http://localhost/jwks", + oicSecurityRealm.getServerConfiguration().getJwksServerUrl()); assertFalse(oicSecurityRealm.isDisableTokenVerification()); } @@ -98,7 +107,9 @@ public void testMinimal() throws Exception { assertTrue(realm instanceof OicSecurityRealm); OicSecurityRealm oicSecurityRealm = (OicSecurityRealm) realm; - assertEquals("http://localhost/authorize", oicSecurityRealm.getAuthorizationServerUrl()); + assertEquals( + "http://localhost/authorize", + oicSecurityRealm.getServerConfiguration().getAuthorizationServerUrl()); assertEquals("clientId", oicSecurityRealm.getClientId()); assertEquals("clientSecret", Secret.toString(oicSecurityRealm.getClientSecret())); assertFalse(oicSecurityRealm.isDisableSslVerification()); @@ -106,13 +117,17 @@ public void testMinimal() throws Exception { assertFalse(oicSecurityRealm.isEscapeHatchEnabled()); assertNull(oicSecurityRealm.getFullNameFieldName()); assertNull(oicSecurityRealm.getGroupsFieldName()); - assertEquals("openid email", oicSecurityRealm.getScopes()); - assertEquals("http://localhost/token", oicSecurityRealm.getTokenServerUrl()); - assertEquals(TokenAuthMethod.client_secret_post, oicSecurityRealm.getTokenAuthMethod()); + assertEquals("openid email", oicSecurityRealm.getServerConfiguration().getScopes()); + assertEquals( + "http://localhost/token", + oicSecurityRealm.getServerConfiguration().getTokenServerUrl()); + assertEquals( + TokenAuthMethod.client_secret_post, + oicSecurityRealm.getServerConfiguration().getTokenAuthMethod()); assertEquals("sub", oicSecurityRealm.getUserNameField()); assertTrue(oicSecurityRealm.isLogoutFromOpenidProvider()); assertFalse(oicSecurityRealm.isRootURLFromRequest()); - assertEquals(null, oicSecurityRealm.getJwksServerUrl()); + assertEquals(null, oicSecurityRealm.getServerConfiguration().getJwksServerUrl()); assertFalse(oicSecurityRealm.isDisableTokenVerification()); } @@ -130,16 +145,23 @@ public void testMinimal() throws Exception { @ConfiguredWithCode("ConfigurationAsCodeMinimalWellKnown.yml") public void testMinimalWellKnown() throws Exception { SecurityRealm realm = Jenkins.get().getSecurityRealm(); - - assertTrue(realm instanceof OicSecurityRealm); + assertThat(realm, instanceOf(OicSecurityRealm.class)); OicSecurityRealm oicSecurityRealm = (OicSecurityRealm) realm; String urlBase = String.format("http://localhost:%d", wellKnownMockRule.port()); - assertEquals(urlBase + "/well.known", oicSecurityRealm.getWellKnownOpenIDConfigurationUrl()); - assertEquals(urlBase + "/authorize", oicSecurityRealm.getAuthorizationServerUrl()); - assertEquals(urlBase + "/token", oicSecurityRealm.getTokenServerUrl()); - assertEquals(urlBase + "/jwks", oicSecurityRealm.getJwksServerUrl()); + assertThat(oicSecurityRealm.getServerConfiguration(), instanceOf(OicServerWellKnownConfiguration.class)); + assertEquals( + urlBase + "/well.known", + ((OicServerWellKnownConfiguration) oicSecurityRealm.getServerConfiguration()) + .getWellKnownOpenIDConfigurationUrl()); + assertEquals( + urlBase + "/authorize", + oicSecurityRealm.getServerConfiguration().getAuthorizationServerUrl()); + assertEquals( + urlBase + "/token", oicSecurityRealm.getServerConfiguration().getTokenServerUrl()); + assertEquals( + urlBase + "/jwks", oicSecurityRealm.getServerConfiguration().getJwksServerUrl()); assertEquals("clientId", oicSecurityRealm.getClientId()); assertEquals("clientSecret", Secret.toString(oicSecurityRealm.getClientSecret())); assertFalse(oicSecurityRealm.isDisableSslVerification()); @@ -147,9 +169,12 @@ public void testMinimalWellKnown() throws Exception { assertFalse(oicSecurityRealm.isEscapeHatchEnabled()); assertNull(oicSecurityRealm.getFullNameFieldName()); assertNull(oicSecurityRealm.getGroupsFieldName()); - assertEquals("openid email", oicSecurityRealm.getScopes()); - assertEquals(urlBase + "/token", oicSecurityRealm.getTokenServerUrl()); - assertEquals(TokenAuthMethod.client_secret_post, oicSecurityRealm.getTokenAuthMethod()); + assertEquals("openid email", oicSecurityRealm.getServerConfiguration().getScopes()); + assertEquals( + urlBase + "/token", oicSecurityRealm.getServerConfiguration().getTokenServerUrl()); + assertEquals( + TokenAuthMethod.client_secret_post, + oicSecurityRealm.getServerConfiguration().getTokenAuthMethod()); assertEquals("sub", oicSecurityRealm.getUserNameField()); assertTrue(oicSecurityRealm.isLogoutFromOpenidProvider()); assertFalse(oicSecurityRealm.isDisableTokenVerification()); diff --git a/src/test/java/org/jenkinsci/plugins/oic/DescriptorImplTest.java b/src/test/java/org/jenkinsci/plugins/oic/DescriptorImplTest.java index 25e37a65..f23d6eca 100644 --- a/src/test/java/org/jenkinsci/plugins/oic/DescriptorImplTest.java +++ b/src/test/java/org/jenkinsci/plugins/oic/DescriptorImplTest.java @@ -14,10 +14,11 @@ import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.MatcherAssert.assertThat; import static org.jenkinsci.plugins.oic.TestRealm.AUTO_CONFIG_FIELD; import static org.jenkinsci.plugins.oic.TestRealm.MANUAL_CONFIG_FIELD; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @@ -56,17 +57,14 @@ public void testOicSecurityRealmDescriptorImplManual() throws Exception { "Client secret is required.", descriptor.doCheckClientSecret("").getMessage()); assertEquals(FormValidation.ok(), descriptor.doCheckClientSecret("password")); - assertFalse(descriptor.isAuto()); - assertFalse(descriptor.isManual()); - jenkins.setSecurityRealm(realm); descriptor = (DescriptorImpl) realm.getDescriptor(); - assertNotNull(descriptor); - assertFalse(descriptor.isAuto()); - assertTrue(descriptor.isManual()); + assertThat( + getConfiguredSecuritySecurityRealm().getServerConfiguration(), + instanceOf(OicServerManualConfiguration.class)); } @Test @@ -95,8 +93,9 @@ public void testOicSecurityRealmDescriptorImplAuto() throws Exception { assertNotNull(descriptor); - assertTrue(descriptor.isAuto()); - assertFalse(descriptor.isManual()); + assertThat( + getConfiguredSecuritySecurityRealm().getServerConfiguration(), + instanceOf(OicServerWellKnownConfiguration.class)); } @Test @@ -286,4 +285,8 @@ private void configureWellKnown() { + "\"end_session_endpoint\":\"%s\"}", authUrl, tokenUrl, userInfoUrl, jwksUrl, endSessionUrl)))); } + + private OicSecurityRealm getConfiguredSecuritySecurityRealm() { + return (OicSecurityRealm) jenkins.getSecurityRealm(); + } } diff --git a/src/test/java/org/jenkinsci/plugins/oic/PluginTest.java b/src/test/java/org/jenkinsci/plugins/oic/PluginTest.java index 244c6526..6e315c3d 100644 --- a/src/test/java/org/jenkinsci/plugins/oic/PluginTest.java +++ b/src/test/java/org/jenkinsci/plugins/oic/PluginTest.java @@ -305,17 +305,21 @@ public void testConfigurationWithAutoConfiguration_withScopeOverride() throws Ex configureWellKnown(null, List.of("openid", "profile", "scope1", "scope2", "scope3")); TestRealm oicsr = new TestRealm.Builder(wireMockRule) .WithMinimalDefaults().WithAutomanualconfigure("auto").build(); + jenkins.setSecurityRealm(oicsr); assertEquals( - "All scopes of WellKnown should be used", "openid profile scope1 scope2 scope3", oicsr.getScopes()); + "All scopes of WellKnown should be used", + "openid profile scope1 scope2 scope3", + oicsr.getServerConfiguration().getScopes()); + OicServerWellKnownConfiguration serverConfig = (OicServerWellKnownConfiguration) oicsr.getServerConfiguration(); - oicsr.setOverrideScopes("openid profile scope2 other"); - assertEquals("Predefined scopes of WellKnown should be used", "openid profile scope2", oicsr.getScopes()); + serverConfig.setScopesOverride("openid profile scope2 other"); + assertEquals("scopes should be completely overridden", "openid profile scope2 other", serverConfig.getScopes()); - oicsr.setScopes("openid profile other"); - oicsr.setOverrideScopes(""); - oicsr.setWellKnownOpenIDConfigurationUrl(oicsr.getWellKnownOpenIDConfigurationUrl()); + serverConfig.setScopesOverride(""); assertEquals( - "All scopes of WellKnown should be used", "openid profile scope1 scope2 scope3", oicsr.getScopes()); + "All scopes of WellKnown should be used", + "openid profile scope1 scope2 scope3", + oicsr.getServerConfiguration().getScopes()); } @Test @@ -323,7 +327,10 @@ public void testConfigurationWithAutoConfiguration_withRefreshToken() throws Exc configureWellKnown(null, null, "authorization_code", "refresh_token"); TestRealm oicsr = new TestRealm.Builder(wireMockRule) .WithMinimalDefaults().WithAutomanualconfigure("auto").build(); - assertTrue("Refresh token should be enabled", oicsr.isUseRefreshTokens()); + jenkins.setSecurityRealm(oicsr); + assertTrue( + "Refresh token should be enabled", + oicsr.getServerConfiguration().isUseRefreshTokens()); } @Test diff --git a/src/test/java/org/jenkinsci/plugins/oic/TestRealm.java b/src/test/java/org/jenkinsci/plugins/oic/TestRealm.java index 351dd1d7..06c106b0 100644 --- a/src/test/java/org/jenkinsci/plugins/oic/TestRealm.java +++ b/src/test/java/org/jenkinsci/plugins/oic/TestRealm.java @@ -5,7 +5,7 @@ import hudson.security.SecurityRealm; import io.burt.jmespath.Expression; import java.io.IOException; -import java.lang.reflect.Field; +import java.io.ObjectStreamException; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.StaplerRequest; @@ -209,17 +209,9 @@ public Descriptor getDescriptor() { @Override public HttpResponse doFinishLogin(StaplerRequest request) throws IOException { - try { - Field stateField = OicSession.class.getDeclaredField("state"); - stateField.setAccessible(true); - stateField.set(OicSession.getCurrent(), "state"); - if (!isNonceDisabled()) { - Field nonceField = OicSession.class.getDeclaredField("nonce"); - nonceField.setAccessible(true); - nonceField.set(OicSession.getCurrent(), "nonce"); - } - } catch (Exception e) { - throw new RuntimeException("can't fudge state", e); + OicSession.getCurrent().state = "state"; + if (!isNonceDisabled()) { + OicSession.getCurrent().nonce = "nonce"; } return super.doFinishLogin(request); } @@ -233,7 +225,7 @@ public String getStringFieldFromJMESPath(Object object, String jmespathField) { } @Override - public Object readResolve() { + public Object readResolve() throws ObjectStreamException { return super.readResolve(); } diff --git a/src/test/resources/org/jenkinsci/plugins/oic/ConfigurationAsCode.yml b/src/test/resources/org/jenkinsci/plugins/oic/ConfigurationAsCode.yml index 66faa45a..c223904a 100644 --- a/src/test/resources/org/jenkinsci/plugins/oic/ConfigurationAsCode.yml +++ b/src/test/resources/org/jenkinsci/plugins/oic/ConfigurationAsCode.yml @@ -1,7 +1,13 @@ jenkins: securityRealm: oic: - authorizationServerUrl: http://localhost/authorize + serverConfiguration: + manual: + authorizationServerUrl: http://localhost/authorize + jwksServerUrl: http://localhost/jwks + tokenAuthMethod: client_secret_post + tokenServerUrl: http://localhost/token + scopes: scopes clientId: clientId clientSecret: clientSecret disableSslVerification: true @@ -12,11 +18,7 @@ jenkins: escapeHatchUsername: escapeHatchUsername fullNameFieldName: fullNameFieldName groupsFieldName: groupsFieldName - jwksServerUrl: http://localhost/jwks logoutFromOpenidProvider: true - scopes: scopes - tokenAuthMethod: client_secret_post - tokenServerUrl: http://localhost/token userNameField: userNameField rootURLFromRequest: true sendScopesInTokenRequest: true diff --git a/src/test/resources/org/jenkinsci/plugins/oic/ConfigurationAsCodeExport.yml b/src/test/resources/org/jenkinsci/plugins/oic/ConfigurationAsCodeExport.yml index cc8dbf13..5919a30e 100644 --- a/src/test/resources/org/jenkinsci/plugins/oic/ConfigurationAsCodeExport.yml +++ b/src/test/resources/org/jenkinsci/plugins/oic/ConfigurationAsCodeExport.yml @@ -1,4 +1,3 @@ -authorizationServerUrl: "http://localhost/authorize" clientId: "clientId" disableSslVerification: true emailFieldName: "emailFieldName" @@ -7,13 +6,14 @@ escapeHatchGroup: "escapeHatchGroup" escapeHatchUsername: "escapeHatchUsername" fullNameFieldName: "fullNameFieldName" groupsFieldName: "groupsFieldName" -jwksServerUrl: "http://localhost/jwks" nonceDisabled: true pkceEnabled: true rootURLFromRequest: true -scopes: "scopes" sendScopesInTokenRequest: true -tokenAuthMethod: "client_secret_post" -tokenServerUrl: "http://localhost/token" -useRefreshTokens: false +serverConfiguration: + manual: + authorizationServerUrl: "http://localhost/authorize" + jwksServerUrl: "http://localhost/jwks" + scopes: "scopes" + tokenServerUrl: "http://localhost/token" userNameField: "userNameField" diff --git a/src/test/resources/org/jenkinsci/plugins/oic/ConfigurationAsCodeMinimal.yml b/src/test/resources/org/jenkinsci/plugins/oic/ConfigurationAsCodeMinimal.yml index bd825d0d..dee70839 100644 --- a/src/test/resources/org/jenkinsci/plugins/oic/ConfigurationAsCodeMinimal.yml +++ b/src/test/resources/org/jenkinsci/plugins/oic/ConfigurationAsCodeMinimal.yml @@ -1,7 +1,9 @@ jenkins: securityRealm: oic: - authorizationServerUrl: http://localhost/authorize - tokenServerUrl: http://localhost/token + serverConfiguration: + manual: + authorizationServerUrl: http://localhost/authorize + tokenServerUrl: http://localhost/token clientId: clientId clientSecret: clientSecret diff --git a/src/test/resources/org/jenkinsci/plugins/oic/ConfigurationAsCodeMinimalWellKnown.yml b/src/test/resources/org/jenkinsci/plugins/oic/ConfigurationAsCodeMinimalWellKnown.yml index f1fa9f67..0213c619 100644 --- a/src/test/resources/org/jenkinsci/plugins/oic/ConfigurationAsCodeMinimalWellKnown.yml +++ b/src/test/resources/org/jenkinsci/plugins/oic/ConfigurationAsCodeMinimalWellKnown.yml @@ -1,6 +1,8 @@ jenkins: securityRealm: oic: - wellKnownOpenIDConfigurationUrl: http://localhost:${MOCK_PORT}/well.known clientId: clientId clientSecret: clientSecret + serverConfiguration: + wellKnown: + wellKnownOpenIDConfigurationUrl: http://localhost:${MOCK_PORT}/well.known