Skip to content

Commit

Permalink
fixes #44 - adds Auto-Login servlet filter
Browse files Browse the repository at this point in the history
If enabled redirects from SonarQube's login page to the IdP authentication page. This behaviour can be temporarily disabled by using the URL "<sonarServerBaseURL>/?auto-login=false" in a new browser session (without cookie from previous SonarQube login).
  • Loading branch information
tjuerge committed Aug 22, 2021
1 parent c0c8058 commit 1b7f1c5
Show file tree
Hide file tree
Showing 9 changed files with 320 additions and 53 deletions.
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,17 @@ Optionally the groups a user is associated in SonarQube can be synchronized with

### Server Base URL

`Server base URL` property must be set either by setting the
URL from SonarQube administration page (General -\> Server base URL).
SonarQube's `Server base URL` property must be set either by setting the
URL from SonarQube administration page (General -\> Server base URL) or the property `sonar.core.serverBaseURL` in the `sonar.properties`.

**In this URL no trailing slash is allowed!** Otherwise the redirects from the identity provider back to the SonarQube server are not created correctly.

### Force user authentication

If the plugin's Auto-Login feature is enabled then SonarQube's `Force user authentication` property must be enabled either from SonarQube administration page (Security -\> Force user authentication) or the property `sonar.forceAuthentication` in the `sonar.properties`.

**Otherwise the plugin won't be able to automatically redirect to the IdP's login page.**

### Network Proxy

If a [network proxy](https://docs.oracle.com/javase/8/docs/api/java/net/doc-files/net-properties.html#Proxies) is used with SonarQube (via `http[s].proxy[Host|Port]` properties in the `sonar.properties`) and the host name of the identity provider is not resolvable by this proxy then the IdP's host name must be excluded from being resolved by the proxy. This is done by defining the property `http.nonProxyHosts` in the `sonar.properties`.
Expand Down Expand Up @@ -46,11 +52,15 @@ If a [network proxy](https://docs.oracle.com/javase/8/docs/api/java/net/doc-file
- Configure the plugin for the OpenID Connect client (a client secret is only required for clients with access type 'confidential')
![SonarQube Plugin Configuration](docs/images/plugin-config.png)

- If Auto-Login is enabled then the logout from SonarQube is not possible anymore. This is because logout redirects to SonarQube's login page which triggers the Auto-Login.

**To skip Auto-Login use the URL `<sonarServerBaseURL>/?auto-login=false` in a new browser session (without cookie from previous SonarQube login).**

- For synchronizing groups the name of the custom userinfo claim must be the same as defined in the identity provider's mapper

## Tested with

* SonarQube 7.9.1, 8.2
* Keycloak 4.8.1.Final
* SonarQube 7.9.1, 8.2, 8.5.1
* Keycloak 4.8.1.Final, 12.0.4
* JetBrains Hub 2017.4
* Okta 2018.25
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ public class AuthOidcPlugin implements Plugin {

@Override
public void define(Context context) {
context.addExtensions(OidcConfiguration.class, OidcClient.class, OidcIdentityProvider.class, UserIdentityFactory.class);
context.addExtensions(OidcConfiguration.class, OidcClient.class, OidcIdentityProvider.class,
UserIdentityFactory.class, AutoLoginFilter.class);
context.addExtensions(OidcConfiguration.definitions());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* OpenID Connect Authentication for SonarQube
* Copyright (c) 2021 Torsten Juergeleit
* mailto:torsten AT vaulttec DOT org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.vaulttec.sonarqube.auth.oidc;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.sonar.api.web.ServletFilter;

public class AutoLoginFilter extends ServletFilter {

private static final String LOGIN_URL = "/sessions/new";
private static final String OIDC_URL = "/sessions/init/" + OidcIdentityProvider.KEY + "?return_to=/projects";
private static final String SKIP_REQUEST_PARAM = "auto_login=false";

private final OidcConfiguration config;

public AutoLoginFilter(OidcConfiguration config) {
this.config = config;
}

@Override
public UrlPattern doGetPattern() {
return UrlPattern.create(LOGIN_URL);
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (config.isEnabled() && config.isAutoLogin()) {
if (request instanceof HttpServletRequest) {
String referrer = ((HttpServletRequest) request).getHeader("referer");

// Skip if disabled via request parameter
if (referrer == null || !referrer.endsWith(SKIP_REQUEST_PARAM)) {
((HttpServletResponse) response).sendRedirect(config.getBaseUrl() + OIDC_URL);
return;
}
}
}
chain.doFilter(request, response);
}

@Override
public void init(FilterConfig filterConfig) throws ServletException {
}

@Override
public void destroy() {
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import javax.annotation.CheckForNull;

import org.sonar.api.CoreProperties;
import org.sonar.api.config.Configuration;
import org.sonar.api.config.PropertyDefinition;
import org.sonar.api.server.ServerSide;
Expand All @@ -37,39 +39,42 @@
public class OidcConfiguration {

private static final String CATEGORY = CATEGORY_SECURITY;
private static final String SUBCATEGORY = "oidc";
private static final String SUBCATEGORY = OidcIdentityProvider.KEY;

private static final String ENABLED = "sonar.auth.oidc.enabled";
private static final String ISSUER_URI = "sonar.auth.oidc.issuerUri";
private static final String CLIENT_ID = "sonar.auth.oidc.clientId.secured";
private static final String CLIENT_SECRET = "sonar.auth.oidc.clientSecret.secured";
private static final String ALLOW_USERS_TO_SIGN_UP = "sonar.auth.oidc.allowUsersToSignUp";
private static final String ENABLED = "sonar.auth." + OidcIdentityProvider.KEY + ".enabled";
private static final String AUTO_LOGIN = "sonar.auth." + OidcIdentityProvider.KEY + ".autoLogin";
private static final String ISSUER_URI = "sonar.auth." + OidcIdentityProvider.KEY + ".issuerUri";
private static final String CLIENT_ID = "sonar.auth." + OidcIdentityProvider.KEY + ".clientId.secured";
private static final String CLIENT_SECRET = "sonar.auth." + OidcIdentityProvider.KEY + ".clientSecret.secured";
private static final String ALLOW_USERS_TO_SIGN_UP = "sonar.auth." + OidcIdentityProvider.KEY + ".allowUsersToSignUp";

private static final String SCOPES = "sonar.auth.oidc.scopes";
private static final String SCOPES = "sonar.auth." + OidcIdentityProvider.KEY + ".scopes";
private static final String SCOPES_DEFAULT_VALUE = "openid email profile";

static final String LOGIN_STRATEGY = "sonar.auth.oidc.loginStrategy";
static final String LOGIN_STRATEGY = "sonar.auth." + OidcIdentityProvider.KEY + ".loginStrategy";
static final String LOGIN_STRATEGY_UNIQUE = "Unique";
static final String LOGIN_STRATEGY_PROVIDER_ID = "Same as OpenID Connect login";
static final String LOGIN_STRATEGY_PREFERRED_USERNAME = "Preferred username";
static final String LOGIN_STRATEGY_EMAIL = "Email";
static final String LOGIN_STRATEGY_CUSTOM_CLAIM = "Custom claim";
static final String LOGIN_STRATEGY_DEFAULT_VALUE = LOGIN_STRATEGY_PREFERRED_USERNAME;

private static final String LOGIN_STRATEGY_CUSTOM_CLAIM_NAME = "sonar.auth.oidc.loginStrategy.customClaim.name";
private static final String LOGIN_STRATEGY_CUSTOM_CLAIM_NAME = "sonar.auth." + OidcIdentityProvider.KEY
+ ".loginStrategy.customClaim.name";
private static final String LOGIN_STRATEGY_CUSTOM_CLAIM_NAME_DEFAULT_VALUE = "upn";

private static final String GROUPS_SYNC = "sonar.auth.oidc.groupsSync";
private static final String GROUPS_SYNC_CLAIM_NAME = "sonar.auth.oidc.groupsSync.claimName";
private static final String GROUPS_SYNC = "sonar.auth." + OidcIdentityProvider.KEY + ".groupsSync";
private static final String GROUPS_SYNC_CLAIM_NAME = "sonar.auth." + OidcIdentityProvider.KEY
+ ".groupsSync.claimName";
private static final String GROUPS_SYNC_CLAIM_NAME_DEFAULT_VALUE = "groups";

private static final String ICON_PATH = "sonar.auth.oidc.iconPath";
private static final String ICON_PATH = "sonar.auth." + OidcIdentityProvider.KEY + ".iconPath";
private static final String ICON_PATH_DEFAULT_VALUE = "/static/authoidc/openid.svg";

private static final String BACKGROUND_COLOR = "sonar.auth.oidc.backgroundColor";
private static final String BACKGROUND_COLOR = "sonar.auth." + OidcIdentityProvider.KEY + ".backgroundColor";
private static final String BACKGROUND_COLOR_DEFAULT_VALUE = "#236a97";

private static final String LOGIN_BUTTON_TEXT = "sonar.auth.oidc.loginButtonText";
private static final String LOGIN_BUTTON_TEXT = "sonar.auth." + OidcIdentityProvider.KEY + ".loginButtonText";
private static final String LOGIN_BUTTON_TEXT_DEFAULT_VALUE = "OpenID Connect";

private final Configuration config;
Expand All @@ -78,10 +83,23 @@ public OidcConfiguration(Configuration config) {
this.config = config;
}

public String getBaseUrl() {

Optional<String> baseUrl = config.get(CoreProperties.SERVER_BASE_URL);
if (baseUrl.isPresent()) {
return baseUrl.get();
}
return "";
}

public boolean isEnabled() {
return config.getBoolean(ENABLED).orElse(false) && issuerUri() != null && clientId() != null;
}

public boolean isAutoLogin() {
return config.getBoolean(AUTO_LOGIN).orElse(false);
}

@CheckForNull
public String issuerUri() {
return config.get(ISSUER_URI).orElse(null);
Expand Down Expand Up @@ -136,13 +154,17 @@ public static List<PropertyDefinition> definitions() {
int index = 1;
return Arrays.asList(
PropertyDefinition.builder(ENABLED).name("Enabled")
.description(
"Enable OpenID Connect users to login. Value is ignored if client ID and secret are not defined.")
.description("Enable OpenID Connect users to login. "
+ "Value is ignored if client ID and secret are not defined.")
.category(CATEGORY).subCategory(SUBCATEGORY).type(BOOLEAN).defaultValue(valueOf(false)).index(index++)
.build(),
PropertyDefinition.builder(AUTO_LOGIN).name("Auto-Login")
.description("Skip the SonarQube login page and forward to OpenID Connect authentication. "
+ "Auto-Login can be skipped by using the URL \"&lt;sonarServerBaseURL&gt;/?auto-login=false\".").category(CATEGORY)
.subCategory(SUBCATEGORY).type(BOOLEAN).defaultValue(valueOf(false)).index(index++).build(),
PropertyDefinition.builder(ISSUER_URI).name("Issuer URI")
.description("The issuer URI of an OpenID Connect provider."
+ " This URI is used to retrieve the provider's metadata via OpenID Connect Discovery from the path \"/.well-known/openid-configuration\".")
.description("The issuer URI of an OpenID Connect provider. "
+ "This URI is used to retrieve the provider's metadata via OpenID Connect Discovery from the path \"/.well-known/openid-configuration\".")
.category(CATEGORY).subCategory(SUBCATEGORY).type(STRING).index(index++).build(),
PropertyDefinition.builder(CLIENT_ID).name("Client ID").description("The ID of an OpenID Connect Client.")
.category(CATEGORY).subCategory(SUBCATEGORY).type(STRING).index(index++).build(),
Expand All @@ -154,8 +176,9 @@ public static List<PropertyDefinition> definitions() {
.description("OAuth scopes ('openid' is required) to pass in the Open ID Connect authorize request.")
.category(CATEGORY).subCategory(SUBCATEGORY).type(STRING).defaultValue(SCOPES_DEFAULT_VALUE).index(index++)
.build(),
PropertyDefinition.builder(ALLOW_USERS_TO_SIGN_UP).name("Allow users to sign-up").description(
"Allow new users to authenticate. When set to 'false', only existing users will be able to authenticate to the server.")
PropertyDefinition.builder(ALLOW_USERS_TO_SIGN_UP).name("Allow users to sign-up")
.description("Allow new users to authenticate. "
+ "When set to 'false', only existing users will be able to authenticate to the server.")
.category(CATEGORY).subCategory(SUBCATEGORY).type(BOOLEAN).defaultValue(valueOf(true)).index(index++)
.build(),
PropertyDefinition.builder(LOGIN_STRATEGY).name("Login generation strategy").description(format(
Expand Down Expand Up @@ -195,7 +218,6 @@ public static List<PropertyDefinition> definitions() {
PropertyDefinition.builder(LOGIN_BUTTON_TEXT).name("Login button text")
.description("The text in SonarQube's login button added to 'Log in with '.").category(CATEGORY)
.subCategory(SUBCATEGORY).type(STRING).defaultValue(LOGIN_BUTTON_TEXT_DEFAULT_VALUE).index(index).build());

}

}
5 changes: 4 additions & 1 deletion src/main/resources/org/sonar/l10n/authoidc.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
property.category.security.oidc=OpenID Connect
property.category.security.oidc.description=In order to enable OpenID Connect authentication:<ul><li>The property 'sonar.core.serverBaseURL' must be set to a URL <strong>WITHOUT A TRAILING SLASH</strong></li></ul>
property.category.security.oidc.description=In order to enable OpenID Connect authentication:<ul>\
<li>The property 'sonar.core.serverBaseURL' must be set to a URL <strong>WITHOUT A TRAILING SLASH</strong></li>\
<li>If the Auto-Login feature is enabled then the property 'sonar.forceAuthentication' must be set to 'true' as well</li>\
</ul>
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public class AuthOidcPluginTest {
public void test_extensions() throws Exception {
underTest.define(context);

assertThat(context.getExtensions()).hasSize(17);
assertThat(context.getExtensions()).hasSize(19);
}

private static class MockContext extends Plugin.Context {
Expand Down
Loading

0 comments on commit 1b7f1c5

Please sign in to comment.