Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JENKINS-73849] Adding token and SSL verification disabling check for FIPS #1

Open
wants to merge 16 commits into
base: pac4j
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 47 additions & 2 deletions src/main/java/org/jenkinsci/plugins/oic/OicSecurityRealm.java
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
import javax.servlet.http.HttpSession;
import jenkins.model.Jenkins;
import jenkins.security.ApiTokenProperty;
import jenkins.security.FIPS140;
import jenkins.security.SecurityListener;
import jenkins.util.SystemProperties;
import org.apache.commons.lang.StringUtils;
Expand Down Expand Up @@ -306,16 +307,30 @@ public OicSecurityRealm(
Secret clientSecret,
OicServerConfiguration serverConfiguration,
Boolean disableSslVerification)
throws IOException {
throws IOException, FormException {
// Needed in DataBoundSetter
this.disableSslVerification = Util.fixNull(disableSslVerification, Boolean.FALSE);
if (FIPS140.useCompliantAlgorithms() && this.disableSslVerification) {
throw new FormException(
Messages.OicSecurityRealm_DisableSslVerificationFipsMode(), "disableSslVerification");
}
this.clientId = clientId;
this.clientSecret = clientSecret;
this.serverConfiguration = serverConfiguration;
}

@SuppressWarnings("deprecated")
protected Object readResolve() throws ObjectStreamException {
// Fail if migrating to a FIPS non-compliant config
if (FIPS140.useCompliantAlgorithms()) {
if (isDisableSslVerification()) {
throw new IllegalStateException(Messages.OicSecurityRealm_DisableSslVerificationFipsMode());
}
if (isDisableTokenVerification()) {
throw new IllegalStateException(Messages.OicSecurityRealm_DisableTokenVerificationFipsMode());
}
}

if (!Strings.isNullOrEmpty(endSessionUrl)) {
this.endSessionEndpoint = endSessionUrl + "/";
}
Expand Down Expand Up @@ -643,7 +658,11 @@ public void setPkceEnabled(boolean pkceEnabled) {
}

@DataBoundSetter
public void setDisableTokenVerification(boolean disableTokenVerification) {
public void setDisableTokenVerification(boolean disableTokenVerification) throws FormException {
if (FIPS140.useCompliantAlgorithms() && disableTokenVerification) {
throw new FormException(
Messages.OicSecurityRealm_DisableTokenVerificationFipsMode(), "disableTokenVerification");
}
this.disableTokenVerification = disableTokenVerification;
}

Expand Down Expand Up @@ -1371,6 +1390,22 @@ public FormValidation doCheckTokenFieldToCheckKey(@QueryParameter String tokenFi
return this.doCheckFieldName(tokenFieldToCheckKey, FormValidation.ok());
}

@RequirePOST
public FormValidation doCheckDisableSslVerification(@QueryParameter Boolean disableSslVerification) {
if (FIPS140.useCompliantAlgorithms() && disableSslVerification) {
return FormValidation.error(Messages.OicSecurityRealm_DisableSslVerificationFipsMode());
}
return FormValidation.ok();
}

@RequirePOST
public FormValidation doCheckDisableTokenVerification(@QueryParameter Boolean disableTokenVerification) {
if (FIPS140.useCompliantAlgorithms() && disableTokenVerification) {
return FormValidation.error(Messages.OicSecurityRealm_DisableTokenVerificationFipsMode());
}
return FormValidation.ok();
}

// method to check fieldName matches JMESPath format
private FormValidation doCheckFieldName(String fieldName, FormValidation validIfNull) {
Jenkins.get().checkPermission(Jenkins.ADMINISTER);
Expand All @@ -1388,4 +1423,14 @@ public Descriptor<OicServerConfiguration> getDefaultServerConfigurationType() {
return Jenkins.get().getDescriptor(OicServerWellKnownConfiguration.class);
}
}

private static TokenAuthMethod toSupportedAuthMode(ClientAuthenticationMethod auth) {
String value = auth.getValue();
for (TokenAuthMethod tam : TokenAuthMethod.values()) {
if (tam.toString().equals(value)) {
return tam;
}
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,7 @@ OicSecurityRealm.UsernameNotFound = No field ''{0}'' was supplied in the UserInf
OicSecurityRealm.SSLErrorRetreivingWellKnownConfig = The server presented an invalid or incorrect TLS certificate.
OicSecurityRealm.TokenRequestFailure = Token request failed: {0}"
OicSecurityRealm.TokenRefreshFailure = Unable to refresh access token
OicSecurityRealm.DisableSslVerificationFipsMode = SSL verification can not be disabled in FIPS mode
OicSecurityRealm.DisableTokenVerificationFipsMode = Token verification can not be disabled in FIPS mode
OicServerWellKnownConfiguration.DisplayName = Discovery via well-known endpoint
OicServerManualConfiguration.DisplayName = Manual entry
112 changes: 112 additions & 0 deletions src/test/java/org/jenkinsci/plugins/oic/OicSecurityRealmFipsTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package org.jenkinsci.plugins.oic;

import hudson.Util;
import hudson.model.Descriptor;
import hudson.util.FormValidation;
import hudson.util.Secret;
import java.io.IOException;
import jenkins.security.FIPS140;
import org.hamcrest.Matcher;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.reactor.ReactorException;
import org.jvnet.hudson.test.FlagRule;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.WithoutJenkins;
import org.jvnet.hudson.test.recipes.LocalData;

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.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThrows;
import static org.jvnet.hudson.test.JenkinsMatchers.hasKind;

public class OicSecurityRealmFipsTest {

@ClassRule
public static FlagRule<String> fipsFlag = FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "true");

@Rule
public NonFailingOnStartupJenkinsRule j = new NonFailingOnStartupJenkinsRule();

@Test
@WithoutJenkins
public void settingNonCompliantValuesNotAllowedTest() throws IOException, Descriptor.FormException {
OicSecurityRealm realm = new OicSecurityRealm("clientId", Secret.fromString("secret"), null, false);
Descriptor.FormException ex = assertThrows(
Descriptor.FormException.class,
() -> new OicSecurityRealm("clientId", Secret.fromString("secret"), null, true));
assertThat(
"Exception contains the reason",
ex.getMessage(),
containsString("SSL verification can not be disabled"));
realm.setDisableTokenVerification(false);
ex = assertThrows(Descriptor.FormException.class, () -> realm.setDisableTokenVerification(true));
assertThat(
"Exception contains the reason",
ex.getMessage(),
containsString("Token verification can not be disabled"));
}

@Test
@WithoutJenkins
public void validationWarnsOfInvalidValuesTest() {
OicSecurityRealm.DescriptorImpl descriptor = new OicSecurityRealm.DescriptorImpl();
FormValidation response = descriptor.doCheckDisableSslVerification(true);
assertThat(
"States SSL verification can not be disabled",
response,
allOf(
hasKind(FormValidation.Kind.ERROR),
withMessageContaining("SSL verification can not be disabled")));
response = descriptor.doCheckDisableSslVerification(false);
assertThat("Validation is ok", response, hasKind(FormValidation.Kind.OK));

response = descriptor.doCheckDisableTokenVerification(true);
assertThat(
"States token verification can not be disabled",
response,
allOf(
hasKind(FormValidation.Kind.ERROR),
withMessageContaining("Token verification can not be disabled")));
response = descriptor.doCheckDisableTokenVerification(false);
assertThat("Validation is ok", response.kind, is(FormValidation.Kind.OK));
}

// This test is not strictly needed as per
// https://github.com/jenkinsci/jep/blob/master/jep/237/README.adoc#backwards-compatibility
// Just adding it to cover corner cases
@Test
@LocalData
public void failsOnMigrationTest() {
assertThat(
"We should get a ReactorException, startup failed", j.getError(), instanceOf(ReactorException.class));
}

// Simple JenkinsRule extension that doesn't make test fail on startup errors, so we can check the error.
public static class NonFailingOnStartupJenkinsRule extends JenkinsRule {
private Throwable error;

@Override
public void before() throws Throwable {
try {
super.before();
} catch (Throwable t) {
error = t;
}
}

public Throwable getError() {
return error;
}
}

private static Matcher<FormValidation> withMessageContaining(String message) {
// the FormValidation message will be escaped for HTML, so we escape what we expect.
return hasProperty("message", containsString(Util.escape(message)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
import com.github.tomakehurst.wiremock.junit.WireMockRule;
import hudson.model.Descriptor;
import hudson.util.Secret;
import java.io.IOException;
import org.acegisecurity.AuthenticationManager;
Expand Down Expand Up @@ -65,20 +66,20 @@ public void testAuthenticate_withUsernamePasswordAuthenticationToken() throws Ex
}

@Test
public void testGetAuthenticationGatewayUrl() throws IOException {
public void testGetAuthenticationGatewayUrl() throws IOException, Descriptor.FormException {
TestRealm realm = new TestRealm(wireMockRule);
assertEquals("securityRealm/escapeHatch", realm.getAuthenticationGatewayUrl());
}

@Test
public void testShouldSetNullClientSecretWhenSecretIsNull() throws IOException {
public void testShouldSetNullClientSecretWhenSecretIsNull() throws IOException, Descriptor.FormException {
TestRealm realm = new TestRealm.Builder(wireMockRule)
.WithMinimalDefaults().WithClient("id without secret", null).build();
assertEquals("none", Secret.toString(realm.getClientSecret()));
}

@Test
public void testGetValidRedirectUrl() throws IOException {
public void testGetValidRedirectUrl() throws IOException, Descriptor.FormException {
// root url is http://localhost:????/jenkins/
final String rootUrl = jenkinsRule.jenkins.getRootUrl();

Expand All @@ -95,7 +96,7 @@ public void testGetValidRedirectUrl() throws IOException {
}

@Test
public void testShouldReturnRootUrlWhenRedirectUrlIsInvalid() throws IOException {
public void testShouldReturnRootUrlWhenRedirectUrlIsInvalid() throws IOException, Descriptor.FormException {
// root url is http://localhost:????/jenkins/
String rootUrl = jenkinsRule.jenkins.getRootUrl();

Expand All @@ -110,7 +111,7 @@ public void testShouldReturnRootUrlWhenRedirectUrlIsInvalid() throws IOException
}

@Test
public void testShouldCheckEscapeHatchWithPlainPassword() throws IOException {
public void testShouldCheckEscapeHatchWithPlainPassword() throws IOException, Descriptor.FormException {
final String escapeHatchUsername = "aUsername";
final String escapeHatchPassword = "aSecretPassword";

Expand All @@ -127,7 +128,7 @@ public void testShouldCheckEscapeHatchWithPlainPassword() throws IOException {
}

@Test
public void testShouldCheckEscapeHatchWithHashedPassword() throws IOException {
public void testShouldCheckEscapeHatchWithHashedPassword() throws IOException, Descriptor.FormException {
final String escapeHatchUsername = "aUsername";
final String escapeHatchPassword = "aSecretPassword";
final String escapeHatchCryptedPassword = BCrypt.hashpw(escapeHatchPassword, BCrypt.gensalt());
Expand Down
4 changes: 3 additions & 1 deletion src/test/java/org/jenkinsci/plugins/oic/PluginTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import hudson.model.Descriptor;
import hudson.model.User;
import hudson.tasks.Mailer;
import hudson.util.VersionNumber;
Expand Down Expand Up @@ -177,7 +178,8 @@ private void browseLoginPage() throws IOException, SAXException {
webClient.goTo(jenkins.getSecurityRealm().getLoginUrl());
}

private void configureTestRealm(@NonNull Consumer<OicSecurityRealm> consumer) throws IOException {
private void configureTestRealm(@NonNull Consumer<OicSecurityRealm> consumer)
throws IOException, Descriptor.FormException {
var securityRealm = new TestRealm(wireMockRule);
consumer.accept(securityRealm);
jenkins.setSecurityRealm(securityRealm);
Expand Down
14 changes: 7 additions & 7 deletions src/test/java/org/jenkinsci/plugins/oic/TestRealm.java
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ public Builder WithDisableSslVerification(boolean disableSslVerification) {
return this;
}

public TestRealm build() throws IOException {
public TestRealm build() throws IOException, Descriptor.FormException {
return new TestRealm(this);
}

Expand Down Expand Up @@ -177,7 +177,7 @@ public OicServerConfiguration buildServerConfiguration() {
}
}

public TestRealm(Builder builder) throws IOException {
public TestRealm(Builder builder) throws IOException, Descriptor.FormException {
super(
builder.clientId,
builder.clientSecret,
Expand All @@ -203,18 +203,18 @@ public TestRealm(Builder builder) throws IOException {
}

public TestRealm(WireMockRule wireMockRule, String userInfoServerUrl, String emailFieldName, String groupsFieldName)
throws IOException {
throws IOException, Descriptor.FormException {
this(new Builder(wireMockRule)
.WithUserInfoServerUrl(userInfoServerUrl)
.WithEmailFieldName(emailFieldName)
.WithGroupsFieldName(groupsFieldName));
}

public TestRealm(WireMockRule wireMockRule) throws IOException {
public TestRealm(WireMockRule wireMockRule) throws IOException, Descriptor.FormException {
this(new Builder(wireMockRule).WithMinimalDefaults());
}

public TestRealm(WireMockRule wireMockRule, String userInfoServerUrl) throws IOException {
public TestRealm(WireMockRule wireMockRule, String userInfoServerUrl) throws IOException, Descriptor.FormException {
this(new Builder(wireMockRule).WithMinimalDefaults().WithUserInfoServerUrl(userInfoServerUrl));
}

Expand All @@ -224,7 +224,7 @@ public TestRealm(
String emailFieldName,
String groupFieldName,
boolean automanualconfigure)
throws IOException {
throws IOException, Descriptor.FormException {
this(new Builder(wireMockRule)
.WithMinimalDefaults()
.WithUserInfoServerUrl(userInfoServerUrl)
Expand All @@ -243,7 +243,7 @@ public TestRealm(
String escapeHatchUsername,
String escapeHatchSecret,
String escapeHatchGroup)
throws IOException {
throws IOException, Descriptor.FormException {
this(new Builder(wireMockRule)
.WithMinimalDefaults()
.WithUserInfoServerUrl(userInfoServerUrl)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version='1.1' encoding='UTF-8'?>
<hudson>
<disabledAdministrativeMonitors/>
<version>2.426.3</version>
<numExecutors>2</numExecutors>
<mode>NORMAL</mode>
<useSecurity>true</useSecurity>
<securityRealm class="org.jenkinsci.plugins.oic.OicSecurityRealm" plugin="oic-auth@4.999999-SNAPSHOT">
<clientId>client-id</clientId>
<clientSecret>{AQAAABAAAAAQ6eIZw98uUCRkQ7ug3nSTzQ4EKjZWLpSQFIxRg9AMWNo=}</clientSecret>
<userNameField>sub</userNameField>
<disableSslVerification>false</disableSslVerification>
<logoutFromOpenidProvider>false</logoutFromOpenidProvider>
<escapeHatchEnabled>false</escapeHatchEnabled>
<escapeHatchSecret>{AQAAABAAAABAPfWEOWZptuocLQqIjwfXTQ6Ky/3yq4CIWvdUYhH4jX1CuCzrFvr2ICIiBIIpp79McMBCTHq9QhVjfgbPN9XPEbjsFiWihgKHZYF+3+wz3kM=}</escapeHatchSecret>
<serverConfiguration class="org.jenkinsci.plugins.oic.OicServerWellKnownConfiguration">
<wellKnownOpenIDConfigurationUrl>https://example.test</wellKnownOpenIDConfigurationUrl>
</serverConfiguration>
<rootURLFromRequest>false</rootURLFromRequest>
<sendScopesInTokenRequest>false</sendScopesInTokenRequest>
<pkceEnabled>false</pkceEnabled>
<disableTokenVerification>true</disableTokenVerification>
<nonceDisabled>false</nonceDisabled>
<tokenExpirationCheckDisabled>false</tokenExpirationCheckDisabled>
<allowTokenAccessWithoutOicSession>false</allowTokenAccessWithoutOicSession>
<allowedTokenExpirationClockSkewSeconds>0</allowedTokenExpirationClockSkewSeconds>
</securityRealm>
</hudson>