diff --git a/pom.xml b/pom.xml index b75b67b9..f4315b86 100644 --- a/pom.xml +++ b/pom.xml @@ -49,6 +49,7 @@ false Max 1836.vccda_4a_122a_a_e + 4.347 diff --git a/src/main/java/org/jenkinsci/plugins/oic/OicSecurityRealm.java b/src/main/java/org/jenkinsci/plugins/oic/OicSecurityRealm.java index a2bfa264..3acdfb56 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,10 @@ private FormValidation doCheckFieldName(String fieldName, FormValidation validIf } return FormValidation.ok(); } + + @Restricted(NoExternalUse.class) // jelly only + 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..c543ff26 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/oic/OicServerConfiguration.java @@ -0,0 +1,37 @@ +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 java.io.Serializable; +import org.jenkinsci.plugins.oic.OicSecurityRealm.TokenAuthMethod; + +public abstract class OicServerConfiguration extends AbstractDescribableImpl + implements ExtensionPoint, Serializable { + + private static final long serialVersionUID = 1L; + + @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..9edb8592 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/oic/OicServerManualConfiguration.java @@ -0,0 +1,206 @@ +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 static final long serialVersionUID = 1L; + + 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..a8f29250 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/oic/OicServerWellKnownConfiguration.java @@ -0,0 +1,274 @@ +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 long serialVersionUID = 1L; + + 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(); + } + 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..24beeeb0 100644 --- a/src/main/resources/org/jenkinsci/plugins/oic/Messages.properties +++ b/src/main/resources/org/jenkinsci/plugins/oic/Messages.properties @@ -3,7 +3,7 @@ OicLogoutAction.OicLogout = Oic Logout OicSecurityRealm.DisplayName = Login with Openid Connect OicSecurityRealm.ClientIdRequired = Client id is required. OicSecurityRealm.ClientSecretRequired = Client secret is required. -OicSecurityRealm.URLNotAOpenIdEnpoint = URL does seem to describe OpenID Connect endpoints +OicSecurityRealm.URLNotAOpenIdEnpoint = URL does not seem to describe OpenID Connect endpoints OicSecurityRealm.NotAValidURL = Not a valid url. OicSecurityRealm.CouldNotRetreiveWellKnownConfig = Could not retrieve well-known config {0,number,integer} {1} OicSecurityRealm.CouldNotParseResponse = Could not parse response @@ -11,11 +11,13 @@ OicSecurityRealm.ErrorRetreivingWellKnownConfig = Error when retrieving well-kno OicSecurityRealm.TokenServerURLKeyRequired = Token Server Url Key is required. 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.RUSureOpenIdNotInScope = Are you sure you don''t want to include ''openid'' as a 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/Messages_pl.properties b/src/main/resources/org/jenkinsci/plugins/oic/Messages_pl.properties index 407af4db..e0947203 100644 --- a/src/main/resources/org/jenkinsci/plugins/oic/Messages_pl.properties +++ b/src/main/resources/org/jenkinsci/plugins/oic/Messages_pl.properties @@ -13,6 +13,5 @@ OicSecurityRealm.ErrorRetreivingWellKnownConfig = B\u0142\u0105d przy pobieraniu OicSecurityRealm.TokenServerURLKeyRequired = Wymagany jest klucz do Token Server URL. OicSecurityRealm.TokenAuthMethodRequired = Wymagana jest metoda uwierzytelniania tokena. OicSecurityRealm.UsingDefaultUsername = U\u017cyj 'sub'. -OicSecurityRealm.UsingDefaultScopes = U\u017cyj 'OpenID email'. OicSecurityRealm.RUSureOpenIdNotInScope = Czy na pewno nie chcesz uwzgl\u0119dnia\u0107 "OpenID" jako zakresu? OicSecurityRealm.EndSessionURLKeyRequired = Klucz URL sesji ko\u0144cowej jest wymagany. diff --git a/src/main/resources/org/jenkinsci/plugins/oic/Messages_zh.properties b/src/main/resources/org/jenkinsci/plugins/oic/Messages_zh.properties index 76b761f9..c0a703f3 100644 --- a/src/main/resources/org/jenkinsci/plugins/oic/Messages_zh.properties +++ b/src/main/resources/org/jenkinsci/plugins/oic/Messages_zh.properties @@ -13,6 +13,5 @@ OicSecurityRealm.ErrorRetreivingWellKnownConfig = \u7372\u53d6\u516c\u8a8d\u7d44 OicSecurityRealm.TokenServerURLKeyRequired = \u9700\u8981\u63d0\u4f9b\u6b0a\u6756\u4f3a\u670d\u5668\u4e4b Url \u91d1\u9470 OicSecurityRealm.TokenAuthMethodRequired = \u9700\u8981\u958b\u555f\u6b0a\u6756 Token \u9a57\u8b49\u65b9\u6cd5 OicSecurityRealm.UsingDefaultUsername = \u4f7f\u7528 sub -OicSecurityRealm.UsingDefaultScopes = \u4f7f\u7528 openid \u96fb\u5b50\u4fe1\u7bb1 OicSecurityRealm.RUSureOpenIdNotInScope = \u662f\u5426\u78ba\u8a8d\u4e0d\u5c07 \u300copenid\u300d\u4f5c\u70ba\u4e00\u500b\u7bc4\u570d scope \uff1f OicSecurityRealm.EndSessionURLKeyRequired = \u9700\u8981\u63d0\u4f9b\u7d42\u6b62 Session \u4e4b Url \u91d1\u9470 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..e279d863 100644 --- a/src/test/java/org/jenkinsci/plugins/oic/DescriptorImplTest.java +++ b/src/test/java/org/jenkinsci/plugins/oic/DescriptorImplTest.java @@ -1,7 +1,5 @@ package org.jenkinsci.plugins.oic; -import com.github.tomakehurst.wiremock.core.WireMockConfiguration; -import com.github.tomakehurst.wiremock.junit.WireMockRule; import hudson.util.FormValidation; import java.io.IOException; import jenkins.model.Jenkins; @@ -11,21 +9,16 @@ import org.junit.Test; import org.jvnet.hudson.test.JenkinsRule; -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; public class DescriptorImplTest { - @Rule - public WireMockRule wireMockRule = new WireMockRule(new WireMockConfiguration().dynamicPort(), true); - @Rule public JenkinsRule jenkinsRule = new JenkinsRule(); @@ -38,10 +31,7 @@ public void setUp() { @Test public void testOicSecurityRealmDescriptorImplManual() throws Exception { - configureWellKnown(); - TestRealm realm = new TestRealm(wireMockRule, null, null, null, MANUAL_CONFIG_FIELD); - - OicSecurityRealm.DescriptorImpl descriptor = (DescriptorImpl) realm.getDescriptor(); + OicSecurityRealm.DescriptorImpl descriptor = (DescriptorImpl) jenkins.getDescriptor(OicSecurityRealm.class); assertNotNull(descriptor); @@ -56,27 +46,22 @@ public void testOicSecurityRealmDescriptorImplManual() throws Exception { "Client secret is required.", descriptor.doCheckClientSecret("").getMessage()); assertEquals(FormValidation.ok(), descriptor.doCheckClientSecret("password")); - assertFalse(descriptor.isAuto()); - assertFalse(descriptor.isManual()); - + TestRealm realm = new TestRealm( + new TestRealm.Builder("http://ignored.test/").WithAutomanualconfigure(MANUAL_CONFIG_FIELD)); jenkins.setSecurityRealm(realm); descriptor = (DescriptorImpl) realm.getDescriptor(); - assertNotNull(descriptor); - assertFalse(descriptor.isAuto()); - assertTrue(descriptor.isManual()); + assertThat( + getConfiguredSecuritySecurityRealm().getServerConfiguration(), + instanceOf(OicServerManualConfiguration.class)); } @Test public void testOicSecurityRealmDescriptorImplAuto() throws Exception { - configureWellKnown(); - TestRealm realm = new TestRealm(wireMockRule, null, null, null, AUTO_CONFIG_FIELD); - - OicSecurityRealm.DescriptorImpl descriptor = (DescriptorImpl) realm.getDescriptor(); - - assertNotNull(descriptor); + OicSecurityRealm.DescriptorImpl descriptor = + (DescriptorImpl) jenkins.getDescriptorOrDie(OicSecurityRealm.class); assertEquals("Login with Openid Connect", descriptor.getDisplayName()); assertEquals("Client id is required.", descriptor.doCheckClientId(null).getMessage()); @@ -89,66 +74,23 @@ public void testOicSecurityRealmDescriptorImplAuto() throws Exception { "Client secret is required.", descriptor.doCheckClientSecret("").getMessage()); assertEquals(FormValidation.ok(), descriptor.doCheckClientSecret("password")); + TestRealm realm = + new TestRealm(new TestRealm.Builder("http://ignored.test/").WithAutomanualconfigure(AUTO_CONFIG_FIELD)); jenkins.setSecurityRealm(realm); - descriptor = (DescriptorImpl) realm.getDescriptor(); + descriptor = (DescriptorImpl) jenkins.getSecurityRealm().getDescriptor(); assertNotNull(descriptor); - assertTrue(descriptor.isAuto()); - assertFalse(descriptor.isManual()); - } - - @Test - public void doCheckTokenServerUrl() throws IOException { - configureWellKnown(); - TestRealm realm = new TestRealm(wireMockRule, null, null, null, AUTO_CONFIG_FIELD); - - OicSecurityRealm.DescriptorImpl descriptor = (DescriptorImpl) realm.getDescriptor(); - - assertNotNull(descriptor); - assertEquals( - "Token Server Url Key is required.", - descriptor.doCheckTokenServerUrl(null).getMessage()); - assertTrue(descriptor.doCheckTokenServerUrl("").getMessage().contains("is required.")); - assertEquals(FormValidation.ok(), descriptor.doCheckTokenServerUrl("http://localhost")); - } - - @Test - public void doCheckAuthorizationServerUrl() throws IOException { - configureWellKnown(); - TestRealm realm = new TestRealm(wireMockRule, null, null, null, AUTO_CONFIG_FIELD); - - OicSecurityRealm.DescriptorImpl descriptor = (DescriptorImpl) realm.getDescriptor(); - - assertNotNull(descriptor); - assertEquals( - "Token Server Url Key is required.", - descriptor.doCheckAuthorizationServerUrl(null).getMessage()); - assertTrue(descriptor.doCheckAuthorizationServerUrl("").getMessage().contains("Not a valid url.")); - assertEquals(FormValidation.ok(), descriptor.doCheckAuthorizationServerUrl("http://localhost")); - } - - @Test - public void doCheckJwksServerUrl() throws IOException { - configureWellKnown(); - TestRealm realm = new TestRealm(wireMockRule, null, null, null, AUTO_CONFIG_FIELD); - - OicSecurityRealm.DescriptorImpl descriptor = (DescriptorImpl) realm.getDescriptor(); - - assertNotNull(descriptor); - assertEquals(FormValidation.ok(), descriptor.doCheckJwksServerUrl(null)); - assertEquals(FormValidation.ok(), descriptor.doCheckJwksServerUrl("")); - assertEquals(FormValidation.ok(), descriptor.doCheckJwksServerUrl("http://localhost/jwks")); + assertThat( + getConfiguredSecuritySecurityRealm().getServerConfiguration(), + instanceOf(OicServerWellKnownConfiguration.class)); } @Test public void doCheckUserNameField() throws IOException { - configureWellKnown(); - TestRealm realm = new TestRealm(wireMockRule, null, null, null, AUTO_CONFIG_FIELD); - - OicSecurityRealm.DescriptorImpl descriptor = (DescriptorImpl) realm.getDescriptor(); - assertNotNull(descriptor); + OicSecurityRealm.DescriptorImpl descriptor = + (DescriptorImpl) jenkins.getDescriptorOrDie(OicSecurityRealm.class); assertEquals( FormValidation.ok("Using 'sub'.").getMessage(), @@ -161,11 +103,8 @@ public void doCheckUserNameField() throws IOException { @Test public void doCheckFullNameFieldName() throws IOException { - configureWellKnown(); - TestRealm realm = new TestRealm(wireMockRule, null, null, null, AUTO_CONFIG_FIELD); - - OicSecurityRealm.DescriptorImpl descriptor = (DescriptorImpl) realm.getDescriptor(); - assertNotNull(descriptor); + OicSecurityRealm.DescriptorImpl descriptor = + (DescriptorImpl) jenkins.getDescriptorOrDie(OicSecurityRealm.class); assertEquals(FormValidation.ok(), descriptor.doCheckFullNameFieldName("")); assertEquals(FormValidation.Kind.ERROR, descriptor.doCheckFullNameFieldName("]not valid").kind); @@ -174,11 +113,8 @@ public void doCheckFullNameFieldName() throws IOException { @Test public void doCheckEmailFieldName() throws IOException { - configureWellKnown(); - TestRealm realm = new TestRealm(wireMockRule, null, null, null, AUTO_CONFIG_FIELD); - - OicSecurityRealm.DescriptorImpl descriptor = (DescriptorImpl) realm.getDescriptor(); - assertNotNull(descriptor); + OicSecurityRealm.DescriptorImpl descriptor = + (DescriptorImpl) jenkins.getDescriptorOrDie(OicSecurityRealm.class); assertEquals(FormValidation.ok(), descriptor.doCheckEmailFieldName("")); assertEquals(FormValidation.Kind.ERROR, descriptor.doCheckEmailFieldName("]not valid").kind); @@ -187,11 +123,8 @@ public void doCheckEmailFieldName() throws IOException { @Test public void doCheckGroupsFieldName() throws IOException { - configureWellKnown(); - TestRealm realm = new TestRealm(wireMockRule, null, null, null, AUTO_CONFIG_FIELD); - - OicSecurityRealm.DescriptorImpl descriptor = (DescriptorImpl) realm.getDescriptor(); - assertNotNull(descriptor); + OicSecurityRealm.DescriptorImpl descriptor = + (DescriptorImpl) jenkins.getDescriptorOrDie(OicSecurityRealm.class); assertEquals(FormValidation.ok(), descriptor.doCheckGroupsFieldName("")); assertEquals(FormValidation.Kind.ERROR, descriptor.doCheckGroupsFieldName("]not valid").kind); @@ -200,66 +133,18 @@ public void doCheckGroupsFieldName() throws IOException { @Test public void doCheckTokenFieldToCheckKey() throws IOException { - configureWellKnown(); - TestRealm realm = new TestRealm(wireMockRule, null, null, null, AUTO_CONFIG_FIELD); - - OicSecurityRealm.DescriptorImpl descriptor = (DescriptorImpl) realm.getDescriptor(); - assertNotNull(descriptor); + OicSecurityRealm.DescriptorImpl descriptor = + (DescriptorImpl) jenkins.getDescriptorOrDie(OicSecurityRealm.class); assertEquals(FormValidation.ok(), descriptor.doCheckTokenFieldToCheckKey("")); assertEquals(FormValidation.Kind.ERROR, descriptor.doCheckTokenFieldToCheckKey("]not valid").kind); assertEquals(FormValidation.ok(), descriptor.doCheckTokenFieldToCheckKey("akey")); } - @Test - public void doCheckScopes() throws IOException { - configureWellKnown(); - TestRealm realm = new TestRealm(wireMockRule, null, null, null, AUTO_CONFIG_FIELD); - - OicSecurityRealm.DescriptorImpl descriptor = (DescriptorImpl) realm.getDescriptor(); - assertNotNull(descriptor); - - assertEquals( - FormValidation.ok("Using 'openid email'.").getMessage(), - descriptor.doCheckScopes(null).getMessage()); - assertEquals( - FormValidation.ok("Using 'openid email'.").getMessage(), - descriptor.doCheckScopes("").getMessage()); - - assertEquals( - FormValidation.warning("Are you sure you don't want to include 'openid' as an scope?") - .getMessage(), - descriptor.doCheckScopes("email username").getMessage()); - - assertEquals(FormValidation.ok(), descriptor.doCheckScopes("openid")); - } - - @Test - public void doCheckEndSessionEndpoint() throws IOException { - configureWellKnown(); - TestRealm realm = new TestRealm(wireMockRule, null, null, null, AUTO_CONFIG_FIELD); - - OicSecurityRealm.DescriptorImpl descriptor = (DescriptorImpl) realm.getDescriptor(); - assertNotNull(descriptor); - - assertEquals( - "End Session URL Key is required.", - descriptor.doCheckEndSessionEndpoint(null).getMessage()); - assertEquals( - "End Session URL Key is required.", - descriptor.doCheckEndSessionEndpoint("").getMessage()); - assertTrue( - descriptor.doCheckEndSessionEndpoint("not a url").getMessage().contains("Not a valid url.")); - assertEquals(FormValidation.ok(), descriptor.doCheckEndSessionEndpoint("http://localhost")); - } - @Test public void doCheckPostLogoutRedirectUrl() throws IOException { - configureWellKnown(); - TestRealm realm = new TestRealm(wireMockRule, null, null, null, AUTO_CONFIG_FIELD); - - OicSecurityRealm.DescriptorImpl descriptor = (DescriptorImpl) realm.getDescriptor(); - assertNotNull(descriptor); + OicSecurityRealm.DescriptorImpl descriptor = + (DescriptorImpl) jenkins.getDescriptorOrDie(OicSecurityRealm.class); assertEquals(FormValidation.ok(), descriptor.doCheckPostLogoutRedirectUrl(null)); assertEquals(FormValidation.ok(), descriptor.doCheckPostLogoutRedirectUrl("")); @@ -270,20 +155,7 @@ public void doCheckPostLogoutRedirectUrl() throws IOException { assertEquals(FormValidation.ok(), descriptor.doCheckPostLogoutRedirectUrl("http://localhost")); } - private void configureWellKnown() { - String authUrl = "http://localhost:" + wireMockRule.port() + "/authorization"; - String tokenUrl = "http://localhost:" + wireMockRule.port() + "/token"; - String userInfoUrl = "http://localhost:" + wireMockRule.port() + "/userinfo"; - String jwksUrl = "null"; - String endSessionUrl = "null"; - - wireMockRule.stubFor(get(urlPathEqualTo("/well.known")) - .willReturn(aResponse() - .withHeader("Content-Type", "text/html; charset=utf-8") - .withBody(String.format( - "{\"authorization_endpoint\": \"%s\", \"token_endpoint\":\"%s\", " - + "\"userinfo_endpoint\":\"%s\",\"jwks_uri\":\"%s\", \"scopes_supported\": null, " - + "\"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/OicServerManualConfigurationTest.java b/src/test/java/org/jenkinsci/plugins/oic/OicServerManualConfigurationTest.java new file mode 100644 index 00000000..8f2309f3 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/oic/OicServerManualConfigurationTest.java @@ -0,0 +1,107 @@ +package org.jenkinsci.plugins.oic; + +import hudson.Util; +import hudson.util.FormValidation; +import java.io.IOException; +import org.hamcrest.Matcher; +import org.jenkinsci.plugins.oic.OicServerManualConfiguration.DescriptorImpl; +import org.junit.ClassRule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasProperty; +import static org.hamcrest.Matchers.is; +import static org.jvnet.hudson.test.JenkinsMatchers.hasKind; + +public class OicServerManualConfigurationTest { + + @ClassRule + public static JenkinsRule jenkinsRule = new JenkinsRule(); + + @Test + public void ddoCheckTokenServerUrl() throws IOException { + DescriptorImpl descriptor = getDescriptor(); + + assertThat( + descriptor.doCheckTokenServerUrl(null), + allOf(hasKind(FormValidation.Kind.ERROR), withMessage("Token Server Url Key is required."))); + assertThat( + descriptor.doCheckTokenServerUrl(""), + allOf(hasKind(FormValidation.Kind.ERROR), withMessage("Token Server Url Key is required."))); + assertThat(descriptor.doCheckTokenServerUrl("http://localhost"), hasKind(FormValidation.Kind.OK)); + } + + @Test + public void doCheckAuthorizationServerUrl() throws IOException { + DescriptorImpl descriptor = getDescriptor(); + + assertThat( + descriptor.doCheckAuthorizationServerUrl(null), + allOf(hasKind(FormValidation.Kind.ERROR), withMessage("Token Server Url Key is required."))); + assertThat( + descriptor.doCheckAuthorizationServerUrl(""), + allOf(hasKind(FormValidation.Kind.ERROR), withMessageContaining("Not a valid url."))); + assertThat(descriptor.doCheckAuthorizationServerUrl("http://localhost"), hasKind(FormValidation.Kind.OK)); + } + + @Test + public void doCheckJwksServerUrl() throws IOException { + DescriptorImpl descriptor = getDescriptor(); + + assertThat(descriptor.doCheckJwksServerUrl(null), hasKind(FormValidation.Kind.OK)); + assertThat(descriptor.doCheckJwksServerUrl(""), hasKind(FormValidation.Kind.OK)); + assertThat(descriptor.doCheckJwksServerUrl("http://localhost.jwks"), hasKind(FormValidation.Kind.OK)); + } + + @Test + public void doCheckScopes() throws IOException { + DescriptorImpl descriptor = getDescriptor(); + + assertThat( + descriptor.doCheckScopes(null), + allOf(hasKind(FormValidation.Kind.ERROR), withMessage("Scopes is required."))); + assertThat( + descriptor.doCheckScopes(""), + allOf(hasKind(FormValidation.Kind.ERROR), withMessage("Scopes is required."))); + assertThat( + descriptor.doCheckScopes("email username"), + allOf( + hasKind(FormValidation.Kind.WARNING), + withMessage("Are you sure you don't want to include 'openid' as a scope?"))); + + assertThat(descriptor.doCheckScopes("openid"), hasKind(FormValidation.Kind.OK)); + } + + @Test + public void doCheckEndSessionEndpoint() throws IOException { + DescriptorImpl descriptor = getDescriptor(); + + assertThat( + descriptor.doCheckEndSessionUrl(null), + allOf(hasKind(FormValidation.Kind.ERROR), withMessage("End Session URL Key is required."))); + assertThat( + descriptor.doCheckEndSessionUrl(""), + allOf(hasKind(FormValidation.Kind.ERROR), withMessage("End Session URL Key is required."))); + assertThat( + descriptor.doCheckEndSessionUrl("not a url"), + allOf(hasKind(FormValidation.Kind.ERROR), withMessageContaining("Not a valid url."))); + assertThat(descriptor.doCheckEndSessionUrl("http://localhost.jwks"), hasKind(FormValidation.Kind.OK)); + } + + private static DescriptorImpl getDescriptor() { + return (DescriptorImpl) jenkinsRule.jenkins.getDescriptor(OicServerManualConfiguration.class); + } + + private static Matcher withMessage(String message) { + // the FormValidation message will be escaped for HTML, so we escape what we expect. + return hasProperty("message", is(Util.escape(message))); + } + + private static Matcher withMessageContaining(String message) { + // the FormValidation message will be escaped for HTML, so we escape what we expect. + return hasProperty("message", containsString(Util.escape(message))); + } +} diff --git a/src/test/java/org/jenkinsci/plugins/oic/OicServerWellKnownConfigurationTest.java b/src/test/java/org/jenkinsci/plugins/oic/OicServerWellKnownConfigurationTest.java new file mode 100644 index 00000000..0a27fb31 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/oic/OicServerWellKnownConfigurationTest.java @@ -0,0 +1,108 @@ +package org.jenkinsci.plugins.oic; + +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import hudson.Util; +import hudson.util.FormValidation; +import java.io.IOException; +import org.hamcrest.Matcher; +import org.jenkinsci.plugins.oic.OicServerWellKnownConfiguration.DescriptorImpl; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; + +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.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasProperty; +import static org.hamcrest.Matchers.is; +import static org.jvnet.hudson.test.JenkinsMatchers.hasKind; + +public class OicServerWellKnownConfigurationTest { + + @ClassRule + public static JenkinsRule jenkinsRule = new JenkinsRule(); + + @Rule + public WireMockRule wireMockRule = new WireMockRule(new WireMockConfiguration().dynamicPort(), true); + + @Test + public void doCheckWellKnownOpenIDConfigurationUrl() throws IOException { + configureWireMockWellKnownEndpoint(); + DescriptorImpl descriptor = getDescriptor(); + + assertThat( + descriptor.doCheckWellKnownOpenIDConfigurationUrl(null, false), + allOf(hasKind(FormValidation.Kind.ERROR), withMessage("Not a valid url."))); + assertThat( + descriptor.doCheckWellKnownOpenIDConfigurationUrl("", false), + allOf(hasKind(FormValidation.Kind.ERROR), withMessage("Not a valid url."))); + assertThat( + descriptor.doCheckWellKnownOpenIDConfigurationUrl( + wireMockRule.url("/.well-known/openid-configuration"), false), + hasKind(FormValidation.Kind.OK)); + + assertThat( + descriptor.doCheckWellKnownOpenIDConfigurationUrl( + jenkinsRule.jenkins.getRootUrl() + "/api/json", false), + allOf( + hasKind(FormValidation.Kind.WARNING), + withMessage("URL does not seem to describe OpenID Connect endpoints"))); + assertThat( + descriptor.doCheckWellKnownOpenIDConfigurationUrl(jenkinsRule.jenkins.getRootUrl() + "/api/xml", false), + allOf( + hasKind(FormValidation.Kind.ERROR), + withMessageContaining("Error when retrieving well-known config"))); + } + + @Test + public void doCheckOverrideScopes() throws IOException { + DescriptorImpl descriptor = getDescriptor(); + + assertThat(descriptor.doCheckOverrideScopes(null), hasKind(FormValidation.Kind.OK)); + assertThat(descriptor.doCheckOverrideScopes(""), hasKind(FormValidation.Kind.OK)); + assertThat( + descriptor.doCheckOverrideScopes("openid email profile address phone offline_access"), + hasKind(FormValidation.Kind.OK)); + assertThat( + descriptor.doCheckOverrideScopes("blah"), + allOf( + hasKind(FormValidation.Kind.WARNING), + withMessage("Are you sure you don't want to include 'openid' as a scope?"))); + } + + private void configureWireMockWellKnownEndpoint() { + String authUrl = "http://localhost:" + wireMockRule.port() + "/authorization"; + String tokenUrl = "http://localhost:" + wireMockRule.port() + "/token"; + String userInfoUrl = "http://localhost:" + wireMockRule.port() + "/userinfo"; + String jwksUrl = "null"; + String endSessionUrl = "null"; + + wireMockRule.stubFor(get(urlPathEqualTo("/.well-known/openid-configuration")) + .willReturn(aResponse() + .withHeader("Content-Type", "text/html; charset=utf-8") + .withBody(String.format( + "{\"authorization_endpoint\": \"%s\", \"token_endpoint\":\"%s\", " + + "\"userinfo_endpoint\":\"%s\",\"jwks_uri\":\"%s\", \"scopes_supported\": null, " + + "\"end_session_endpoint\":\"%s\"}", + authUrl, tokenUrl, userInfoUrl, jwksUrl, endSessionUrl)))); + } + + private static DescriptorImpl getDescriptor() { + return (DescriptorImpl) jenkinsRule.jenkins.getDescriptor(OicServerWellKnownConfiguration.class); + } + + private static Matcher withMessage(String message) { + // the FormValidation message will be escaped for HTML, so we escape what we expect. + return hasProperty("message", is(Util.escape(message))); + } + + private static Matcher withMessageContaining(String message) { + // the FormValidation message will be escaped for HTML, so we escape what we expect. + return hasProperty("message", containsString(Util.escape(message))); + } +} 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