Skip to content

Commit

Permalink
Merge pull request #49 from morincer/csrf-token-2020
Browse files Browse the repository at this point in the history
Csrf token 2020
  • Loading branch information
morincer authored Jul 24, 2020
2 parents 29546ca + eeb5b2f commit e6526af
Show file tree
Hide file tree
Showing 18 changed files with 194 additions and 32 deletions.
12 changes: 1 addition & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,7 @@ Please refer the example of set up for [Okta](./docs/OktaSetup.md) if you need s

* Make sure you have properly set up users on the TeamCity side - their usernames should match the SAML assertion name ID (usually - e-mail).

* **Make sure your CORS settings allow posts from the IdP site**. Go to the Adminstration -> Diagnostics -> Internal Properties -> Edit internal property and add/edit line:

```
rest.cors.origins=<value of the remote host address>
```

> **Teamcity 2020.1+ Update**
>
>Starting from version 2020.1 Teamcity changed the way they do CSRF protection: along with standard origin check they now require additional token to be acquired and sent as a header by remote host (read details and rationale [here](https://www.jetbrains.com/help/teamcity/csrf-protection.html)).
>
>Unfortunately, at the moment it is not clear how to make this mechanism work with SAML flows - as IdPs don't normally send custom headers to SPs consumers. If you configured CORS and get "token is missing" error, the possible workaround would be to disable the token checking by setting internal property teamcity.csrf.paranoid=false
* (Optional) Review CORS configuration - the plugin adds exception to the Teamcity's CORS filter - if request is POST sent to the SAML login callback URL and it contains a SAML message - it is considered CORS safe. If such behavior is by some reason is not acceptable the filter exception can be disabled on the plugin configuration page (Misc -> CORS Filter Exception checkbox)

* Logout and click the "Login with SSO" button to test.

Expand Down
2 changes: 1 addition & 1 deletion build/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<parent>
<artifactId>saml-authentication</artifactId>
<groupId>org.gromozeka.teamcity</groupId>
<version>1.2</version>
<version>1.2.1</version>
</parent>

<artifactId>build</artifactId>
Expand Down
3 changes: 1 addition & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.gromozeka.teamcity</groupId>
<artifactId>saml-authentication</artifactId>
<version>1.2</version>
<version>1.2.1</version>
<packaging>pom</packaging>
<properties>
<pluginVersion>1.2</pluginVersion>
<teamcity-version-lib>2020.1</teamcity-version-lib>
<teamcity-version-runtime>2020.1.2</teamcity-version-runtime>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
Expand Down
2 changes: 1 addition & 1 deletion saml-authentication-server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<artifactId>saml-authentication</artifactId>
<groupId>org.gromozeka.teamcity</groupId>
<version>1.2</version>
<version>1.2.1</version>
</parent>
<artifactId>saml-authentication-server</artifactId>
<packaging>jar</packaging>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,18 @@ public class SamlAuthenticationScheme extends HttpAuthenticationSchemeAdapter {
private RootUrlHolder rootUrlHolder;
private SamlPluginSettingsStorage settingsStorage;
private UserModel userModel;
private LoginConfiguration loginConfiguration;


public SamlAuthenticationScheme(
@NotNull RootUrlHolder rootUrlHolder,
@NotNull final SamlPluginSettingsStorage settingsStorage,
@NotNull UserModel userModel) {
@NotNull UserModel userModel,
@NotNull LoginConfiguration loginConfiguration) {
this.rootUrlHolder = rootUrlHolder;
this.settingsStorage = settingsStorage;
this.userModel = userModel;
this.loginConfiguration = loginConfiguration;
}

@NotNull
Expand Down Expand Up @@ -262,7 +265,7 @@ public String getCertificateBase64Encoded(X509Certificate cert) throws Certifica
return "";
}

public static boolean isConfigured(LoginConfiguration loginConfiguration) {
public boolean isConfigured() {
boolean authModuleConfigured = loginConfiguration
.getConfiguredAuthModules(AuthModuleType.class).stream()
.anyMatch(t -> t.getType().getClass().getName().equals(SamlAuthenticationScheme.class.getName()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,24 @@ public class SamlLoginPageExtension extends SimplePageExtension {
private final SamlPluginSettingsStorage settingsStorage;
private static final String PAGE_EXTENSION_ID = "SamlLogin";
private LoginConfiguration loginConfiguration;
private SamlAuthenticationScheme scheme;

public SamlLoginPageExtension(@NotNull PagePlaces pagePlaces,
@NotNull PluginDescriptor descriptor,
@NotNull final SamlPluginSettingsStorage settingsStorage,
@NotNull LoginConfiguration loginConfiguration) {
@NotNull SamlAuthenticationScheme scheme) {
super(pagePlaces,
PlaceId.LOGIN_PAGE,
PAGE_EXTENSION_ID,
descriptor.getPluginResourcesPath("SamlLogin.jsp"));
this.loginConfiguration = loginConfiguration;
this.scheme = scheme;
register();
this.settingsStorage = settingsStorage;
}

@Override
public boolean isAvailable(@NotNull HttpServletRequest request) {
return SamlAuthenticationScheme.isConfigured(loginConfiguration);
return scheme.isConfigured();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import jetbrains.buildServer.serverSide.ServerPaths;
import jetbrains.buildServer.serverSide.auth.LoginConfiguration;
import jetbrains.buildServer.users.UserModel;
import jetbrains.buildServer.web.SamlCsrfCheck;
import jetbrains.buildServer.web.openapi.PagePlaces;
import jetbrains.buildServer.web.openapi.PluginDescriptor;
import jetbrains.buildServer.web.openapi.WebControllerManager;
Expand All @@ -23,15 +24,15 @@ public class SamlPluginConfiguration {

@Bean
SamlAuthenticationScheme samlAuthenticationScheme(LoginConfiguration loginConfiguration, SamlPluginSettingsStorage samlPluginSettingsStorage, UserModel userModel, RootUrlHolder rootUrlHolder) {
SamlAuthenticationScheme samlAuthenticationScheme = new SamlAuthenticationScheme(rootUrlHolder, samlPluginSettingsStorage, userModel);
SamlAuthenticationScheme samlAuthenticationScheme = new SamlAuthenticationScheme(rootUrlHolder, samlPluginSettingsStorage, userModel, loginConfiguration);
loginConfiguration.registerAuthModuleType(samlAuthenticationScheme);

return samlAuthenticationScheme;
}

@Bean
SamlLoginPageExtension samlLoginPageExtension(@NotNull PagePlaces pagePlaces, @NotNull PluginDescriptor descriptor, SamlPluginSettingsStorage settingsStorage, LoginConfiguration loginConfiguration) {
return new SamlLoginPageExtension(pagePlaces, descriptor, settingsStorage, loginConfiguration);
SamlLoginPageExtension samlLoginPageExtension(@NotNull PagePlaces pagePlaces, @NotNull PluginDescriptor descriptor, SamlPluginSettingsStorage settingsStorage, SamlAuthenticationScheme scheme) {
return new SamlLoginPageExtension(pagePlaces, descriptor, settingsStorage, scheme);
}

@Bean
Expand All @@ -45,8 +46,8 @@ SamlCallbackController samlCallbackController(SBuildServer server, WebController
}

@Bean
SamlSettingsAdminPage samlPluginAdminPage(@NotNull PagePlaces pagePlaces, @NotNull PluginDescriptor descriptor, SamlPluginSettingsStorage settingsStorage, LoginConfiguration loginConfiguration) {
return new SamlSettingsAdminPage(pagePlaces, descriptor, settingsStorage, loginConfiguration);
SamlSettingsAdminPage samlPluginAdminPage(@NotNull PagePlaces pagePlaces, @NotNull PluginDescriptor descriptor, SamlPluginSettingsStorage settingsStorage, SamlAuthenticationScheme samlAuthenticationScheme) {
return new SamlSettingsAdminPage(pagePlaces, descriptor, settingsStorage, samlAuthenticationScheme);
}

@Bean
Expand All @@ -61,4 +62,9 @@ SamlPluginSettingsStorage samlPluginSettingsStorage(ServerPaths serverPaths) thr
SamlSettingsJsonController samlSettingsAjaxController(WebControllerManager controllerManager, SamlAuthenticationScheme samlAuthenticationScheme) throws IOException {
return new SamlSettingsJsonController(samlAuthenticationScheme, samlPluginSettingsStorage(null), controllerManager);
}

@Bean
SamlCsrfCheck samlCsrfCheck(SamlAuthenticationScheme scheme, SamlPluginSettingsStorage settingsStorage) {
return new SamlCsrfCheck(scheme, settingsStorage);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,18 @@

public class SamlSettingsAdminPage extends AdminPage {
private final PluginDescriptor pluginDescriptor;
private LoginConfiguration loginConfiguration;
private SamlAuthenticationScheme samlAuthenticationScheme;
private final Logger LOG = Loggers.SERVER;
private SamlPluginSettingsStorage settingsStorage;

protected SamlSettingsAdminPage(@NotNull PagePlaces pagePlaces,
@NotNull PluginDescriptor pluginDescriptor,
@NotNull SamlPluginSettingsStorage settingsStorage,
@NotNull LoginConfiguration loginConfiguration) {
@NotNull SamlAuthenticationScheme samlAuthenticationScheme) {
super(pagePlaces);
this.settingsStorage = settingsStorage;
this.pluginDescriptor = pluginDescriptor;
this.loginConfiguration = loginConfiguration;
this.samlAuthenticationScheme = samlAuthenticationScheme;
setPluginName(SamlPluginConstants.PLUGIN_NAME);
setIncludeUrl(pluginDescriptor.getPluginResourcesPath("SamlPluginAdminPage.jsp"));
setTabTitle("SAML Settings");
Expand Down Expand Up @@ -59,6 +59,6 @@ public void fillModel(@NotNull Map<String, Object> model, @NotNull HttpServletRe
public boolean isAvailable(@NotNull HttpServletRequest request) {
return super.isAvailable(request)
&& checkHasGlobalPermission(request, Permission.CHANGE_SERVER_SETTINGS)
&& SamlAuthenticationScheme.isConfigured(loginConfiguration);
&& samlAuthenticationScheme.isConfigured();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public class SamlPluginSettings {
private String allowedPostfixes = null;
private boolean compressRequest = true;
private boolean strict = true;
private boolean samlCorsFilter = true;

SamlAttributeMappingSettings emailAttributeMapping = new SamlAttributeMappingSettings();
SamlAttributeMappingSettings nameAttributeMapping = new SamlAttributeMappingSettings();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package jetbrains.buildServer.web;

import jetbrains.buildServer.auth.saml.plugin.SamlAuthenticationScheme;
import jetbrains.buildServer.auth.saml.plugin.SamlPluginConstants;
import jetbrains.buildServer.auth.saml.plugin.SamlPluginSettingsStorage;
import jetbrains.buildServer.log.Loggers;
import jetbrains.buildServer.serverSide.auth.LoginConfiguration;
import lombok.var;
import org.jetbrains.annotations.NotNull;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.net.URL;

public class SamlCsrfCheck implements CsrfCheck {

private SamlAuthenticationScheme scheme;
private SamlPluginSettingsStorage settingsStorage;

public SamlCsrfCheck(SamlAuthenticationScheme scheme, SamlPluginSettingsStorage settingsStorage) {
this.scheme = scheme;
this.settingsStorage = settingsStorage;
}

@Override
public CheckResult isSafe(@NotNull HttpServletRequest request) {

if (!scheme.isConfigured()) return UNKNOWN;

try {
if (!this.settingsStorage.load().isSamlCorsFilter()) {
Loggers.AUTH.debug("SAML CORS filter is disabled by plugin configuration - skipping");
return UNKNOWN;
}

Loggers.AUTH.debug("Evaluating SAML CORS filter conditions");

URL callbackUrl = scheme.getCallbackUrl();
var requestURL = new URL(request.getRequestURL().toString());

if (callbackUrl == null) return UNKNOWN;

if ("POST".equals(request.getMethod()) && callbackUrl.getPath().equals(requestURL.getPath())) {
var parameter = request.getParameter(SamlPluginConstants.SAML_RESPONSE_REQUEST_PARAMETER);
if (StringUtils.isEmpty(parameter)) {
Loggers.AUTH.debug("SAML CORS Check: " + SamlPluginConstants.SAML_RESPONSE_REQUEST_PARAMETER + " is not found in the request - responding with UNKNOWN result");
return UNKNOWN;
}

Loggers.AUTH.debug("CSRF validated via SAML callback target");

return CheckResult.safe();
}

} catch (Exception e) {
Loggers.AUTH.error(e.getMessage(), e);
}

return UNKNOWN;
}

@NotNull
@Override
public String describe(boolean b) {
return "SAML login requests to SSO callback URL are CORS-safe";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,15 @@ export interface SamlSettings {
ssoLoginButtonName?: string;
strict?: boolean;

samlCorsFilter?: boolean;
compressRequest?: boolean;

createUsersAutomatically?: boolean;
limitToPostfixes?: boolean;
allowedPostfixes?: string;



emailAttributeMapping?: SamlAttributeMapping;
nameAttributeMapping?: SamlAttributeMapping;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@
<template v-slot:content><input type="checkbox" v-model="settings.strict"/></template>
<template v-slot:note>Runs additional checks on SAML message</template>
</RunnerFormRow>
<RunnerFormRow>
<template v-slot:label>SAML Callback CORS Filer Exception</template>
<template v-slot:content><input type="checkbox" v-model="settings.samlCorsFilter"/></template>
<template v-slot:note>Adds CORS filter exception for POST requests sent to the login callback URL</template>
</RunnerFormRow>
<RunnerFormRow>
<template v-slot:label>Hide Login Form</template>
<template v-slot:content><input type="checkbox" v-model="settings.hideLoginForm"/></template>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import jetbrains.buildServer.auth.saml.plugin.pojo.SamlAttributeMappingSettings;
import jetbrains.buildServer.auth.saml.plugin.pojo.SamlPluginSettings;
import jetbrains.buildServer.controllers.interceptors.auth.HttpAuthenticationResult;
import jetbrains.buildServer.serverSide.auth.LoginConfiguration;
import jetbrains.buildServer.users.SUser;
import jetbrains.buildServer.users.UserModel;
import lombok.var;
Expand Down Expand Up @@ -69,7 +70,9 @@ public void setUp() throws Exception {

this.settingsStorage.settings.setStrict(false);

this.scheme = new SamlAuthenticationScheme(rootUrlHolder, settingsStorage, userModel);
LoginConfiguration loginConfiguration = mock(LoginConfiguration.class);

this.scheme = new SamlAuthenticationScheme(rootUrlHolder, settingsStorage, userModel, loginConfiguration);
}

@After
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.onelogin.saml2.util.Util;
import jetbrains.buildServer.controllers.AuthorizationInterceptor;
import jetbrains.buildServer.serverSide.SBuildServer;
import jetbrains.buildServer.serverSide.auth.LoginConfiguration;
import jetbrains.buildServer.users.UserModel;
import jetbrains.buildServer.web.openapi.WebControllerManager;
import lombok.var;
Expand Down Expand Up @@ -48,7 +49,9 @@ public void setUp() throws Exception {
this.interceptor = mock(AuthorizationInterceptor.class);
this.userModel = mock(UserModel.class);

scheme = new SamlAuthenticationScheme(server, settingsStorage, userModel);
LoginConfiguration loginConfiguration = mock(LoginConfiguration.class);

scheme = new SamlAuthenticationScheme(server, settingsStorage, userModel, loginConfiguration);
controller = new SamlLoginController(this.server, this.webControllerManager, this.interceptor, this.scheme, this.settingsStorage);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import jetbrains.buildServer.RootUrlHolder;
import jetbrains.buildServer.auth.saml.plugin.pojo.MetadataImport;
import jetbrains.buildServer.serverSide.auth.LoginConfiguration;
import jetbrains.buildServer.users.UserModel;
import jetbrains.buildServer.web.openapi.WebControllerManager;
import lombok.var;
Expand Down Expand Up @@ -45,7 +46,8 @@ public void setUp() throws Exception {

this.userModel = mock(UserModel.class);

samlAuthenticationScheme = new SamlAuthenticationScheme(rootUrlHolder, settingsStorage, userModel);
LoginConfiguration loginConfiguration = mock(LoginConfiguration.class);
samlAuthenticationScheme = new SamlAuthenticationScheme(rootUrlHolder, settingsStorage, userModel, loginConfiguration);

controller = new SamlSettingsJsonController(this.samlAuthenticationScheme, this.settingsStorage, this.webControllerManager);
}
Expand Down
Loading

0 comments on commit e6526af

Please sign in to comment.