Skip to content
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
11 changes: 7 additions & 4 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

<groupId>org.vaulttec.sonarqube.auth.oidc</groupId>
<artifactId>sonar-auth-oidc-plugin</artifactId>
<version>2.1.2-SNAPSHOT</version>
<version>3.0.0-SNAPSHOT</version>
<packaging>sonar-plugin</packaging>
<name>OpenID Connect Authentication for SonarQube</name>
<description>OpenID Connect Authentication for SonarQube</description>
Expand All @@ -19,11 +19,13 @@
</licenses>

<properties>
<java.version>1.8</java.version>
<java.version>11</java.version>
<maven.compiler.source>11</maven.compiler.source>
<maven.complier.target>11</maven.complier.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<sonar.pluginClass>org.vaulttec.sonarqube.auth.oidc.AuthOidcPlugin</sonar.pluginClass>
<sonar.pluginKey>authoidc</sonar.pluginKey>
<sonar-plugin-api.version>7.4</sonar-plugin-api.version>
<sonar-plugin-api.version>11.0.0.2664</sonar-plugin-api.version>
<nimbusds-oidc-sdk.version>10.14.2</nimbusds-oidc-sdk.version>
<nimbusds-jose-jwt.version>9.31</nimbusds-jose-jwt.version>

Expand Down Expand Up @@ -128,8 +130,9 @@
</build>

<dependencies>

<dependency>
<groupId>org.sonarsource.sonarqube</groupId>
<groupId>org.sonarsource.api.plugin</groupId>
<artifactId>sonar-plugin-api</artifactId>
<version>${sonar-plugin-api.version}</version>
<scope>provided</scope>
Expand Down
21 changes: 11 additions & 10 deletions src/main/java/org/vaulttec/sonarqube/auth/oidc/AutoLoginFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
Expand All @@ -28,12 +27,16 @@
import javax.servlet.http.HttpServletResponse;

import org.sonar.api.server.ServerSide;
import org.sonar.api.server.http.HttpRequest;
import org.sonar.api.server.http.HttpResponse;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonar.api.web.ServletFilter;
import org.sonar.api.web.FilterChain;
import org.sonar.api.web.HttpFilter;
import org.sonar.api.web.UrlPattern;

@ServerSide
public class AutoLoginFilter extends ServletFilter {
public class AutoLoginFilter extends HttpFilter {

private static final Logger LOGGER = Loggers.get(AutoLoginFilter.class);

Expand All @@ -53,28 +56,26 @@ public UrlPattern doGetPattern() {
}

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

// Skip if disabled via request parameter
if (referrer == null || !referrer.endsWith(SKIP_REQUEST_PARAM)) {
String loginPageUrl = config.getBaseUrl() + OIDC_URL + config.getContextPath() + "/projects";
LOGGER.debug("Redirecting to OIDC login page: {}", loginPageUrl);
((HttpServletResponse) response).sendRedirect(loginPageUrl);
response.sendRedirect(loginPageUrl);
return;
}
}
chain.doFilter(request, response);
}

@Override
public void init(FilterConfig filterConfig) throws ServletException {
public void init(){
// Not needed here
}

@Override
public void destroy() {
// Not needed here
Expand Down
37 changes: 28 additions & 9 deletions src/main/java/org/vaulttec/sonarqube/auth/oidc/OidcClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@
package org.vaulttec.sonarqube.auth.oidc;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.io.UnsupportedEncodingException;
import java.net.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

Expand Down Expand Up @@ -67,6 +69,7 @@
import com.nimbusds.openid.connect.sdk.validators.IDTokenValidator;

import org.sonar.api.server.ServerSide;
import org.sonar.api.server.http.HttpRequest;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;

Expand Down Expand Up @@ -97,20 +100,36 @@ public AuthenticationRequest createAuthenticationRequest(String callbackUrl, Str
return request;
}

public AuthorizationCode getAuthorizationCode(HttpServletRequest callbackRequest) {
public AuthorizationCode getAuthorizationCode(HttpRequest callbackRequest) {
LOGGER.debug("Retrieving authorization code from callback request's query parameters: {}",
callbackRequest.getQueryString());
AuthenticationResponse authResponse;
try {
HTTPRequest request = ServletUtils.createHTTPRequest(callbackRequest);
authResponse = AuthenticationResponseParser.parse(request.getURL().toURI(), request.getQueryParameters());
} catch (ParseException | URISyntaxException | IOException e) {
throw new IllegalStateException("Error while parsing callback request", e);
URI uri = new URI(callbackRequest.getRequestURL());

Map<String, List<String>> queryParams = new HashMap<>();
String queryString = callbackRequest.getQueryString();
if (queryString != null && !queryString.isEmpty() ) {
String[] pairs = queryString.split("&");
for (String pair: pairs) {
int idx = pair.indexOf("=");
if (idx > 0) {
String key = URLDecoder.decode(pair.substring(0,idx), "UTF-8");
String value = URLDecoder.decode(pair.substring(idx + 1), "UTF-8");
queryParams.computeIfAbsent(key, k -> new ArrayList<>()).add(value);
}
}
}
authResponse = AuthenticationResponseParser.parse(uri, queryParams);
} catch (ParseException | URISyntaxException | UnsupportedEncodingException e) {
throw new IllegalStateException("Error while processing callback request", e);
}

if (authResponse instanceof AuthenticationErrorResponse) {
ErrorObject error = ((AuthenticationErrorResponse) authResponse).getErrorObject();
throw new IllegalStateException("Authentication request failed: " + error.toJSONObject());
}

AuthorizationCode authorizationCode = ((AuthenticationSuccessResponse) authResponse).getAuthorizationCode();
LOGGER.debug("Authorization code: {}", authorizationCode.getValue());
return authorizationCode;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public void init(InitContext context) {
public void callback(CallbackContext context) {
LOGGER.debug("Handling authentication response");
context.verifyCsrfState();
AuthorizationCode authorizationCode = client.getAuthorizationCode(context.getRequest());
AuthorizationCode authorizationCode = client.getAuthorizationCode(context.getHttpRequest());
UserInfo userInfo = client.getUserInfo(authorizationCode, context.getCallbackUrl());
UserIdentity userIdentity = userIdentityFactory.create(userInfo);
LOGGER.debug("Authenticating user '{}' with groups {}", userIdentity.getProviderLogin(), userIdentity.getGroups());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,18 @@
package org.vaulttec.sonarqube.auth.oidc;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.*;
import static org.vaulttec.sonarqube.auth.oidc.OidcConfiguration.LOGIN_STRATEGY_DEFAULT_VALUE;

import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
import com.nimbusds.openid.connect.sdk.validators.IDTokenValidator;
import org.sonar.api.config.Configuration;

import org.sonar.api.config.internal.MapSettings;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

public abstract class AbstractOidcTest {

Expand All @@ -36,27 +38,41 @@ public abstract class AbstractOidcTest {
public static final String STATE = "state";
public static final String VALID_CODE = "valid_code";

protected MapSettings settings = new MapSettings();
protected OidcConfiguration config = new OidcConfiguration(settings.asConfig());
protected OidcConfiguration oidcConfig;
protected Configuration config;


protected void setSettings(boolean enabled) {
setSettings(enabled, ISSUER_URI);
}

protected void setSettings(boolean enabled, String issuerUri) {
Map<String, String> settings = new HashMap<>();
if (enabled) {
settings.setProperty(OidcConfiguration.ENABLED, true);
settings.setProperty(OidcConfiguration.ISSUER_URI, issuerUri);
settings.setProperty(OidcConfiguration.CLIENT_ID, "id");
settings.setProperty(OidcConfiguration.CLIENT_SECRET, "secret");
settings.setProperty(OidcConfiguration.ID_TOKEN_SIG_ALG, "RS256");
settings.setProperty(OidcConfiguration.LOGIN_STRATEGY, LOGIN_STRATEGY_DEFAULT_VALUE);
settings.setProperty(OidcConfiguration.GROUPS_SYNC, true);
settings.setProperty(OidcConfiguration.GROUPS_SYNC_CLAIM_NAME, "myGroups");
settings.setProperty(OidcConfiguration.SCOPES, "openid email profile");
settings.put(property(OidcConfiguration.ENABLED), "true");
settings.put(property(OidcConfiguration.ISSUER_URI), issuerUri);
settings.put(property(OidcConfiguration.CLIENT_ID), "id");
settings.put(property(OidcConfiguration.CLIENT_SECRET), "secret");
settings.put(property(OidcConfiguration.ID_TOKEN_SIG_ALG), "RS256");
settings.put(property(OidcConfiguration.LOGIN_STRATEGY), LOGIN_STRATEGY_DEFAULT_VALUE);
settings.put(property(OidcConfiguration.GROUPS_SYNC), "true");
settings.put(property(OidcConfiguration.GROUPS_SYNC_CLAIM_NAME), "myGroups");
settings.put(property(OidcConfiguration.SCOPES), "openid email profile");
} else {
settings.setProperty(OidcConfiguration.ENABLED, false);
settings.put(property(OidcConfiguration.ENABLED), "false");
}

config = mock(Configuration.class);
for(Map.Entry<String, String> entry: settings.entrySet()){
when(config.get(entry.getKey())).thenReturn(Optional.of(entry.getValue()));
when(config.getBoolean(entry.getKey())).thenReturn(Optional.of(Boolean.parseBoolean(entry.getValue())));
}

this.oidcConfig = new OidcConfiguration(config);
}

protected static String property(String suffix){
return "sonar.auth" + OidcIdentityProvider.KEY + "." + suffix;
}

protected OIDCProviderMetadata getProviderMetadata(String issuerUri) {
Expand Down Expand Up @@ -84,8 +100,8 @@ protected OIDCProviderMetadata getProviderMetadata(String issuerUri) {
}

protected OidcClient createSpyOidcClient() {
OidcClient client = spy(new OidcClient(config));
doReturn(getProviderMetadata(config.issuerUri())).when(client).getProviderMetadata();
OidcClient client = spy(new OidcClient(oidcConfig));
doReturn(getProviderMetadata(oidcConfig.issuerUri())).when(client).getProviderMetadata();
doReturn(mock(IDTokenValidator.class)).when(client).createValidator(any(), any());
return client;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,36 +17,58 @@
*/
package org.vaulttec.sonarqube.auth.oidc;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.Test;
import org.sonar.api.Plugin;
import org.sonar.api.SonarQubeSide;
import org.sonar.api.SonarRuntime;
import org.sonar.api.internal.PluginContextImpl;
import org.sonar.api.internal.SonarRuntimeImpl;
import org.sonar.api.*;
import org.sonar.api.utils.Version;

import static org.assertj.core.api.Assertions.assertThat;

public class AuthOidcPluginTest {

AuthOidcPlugin underTest = new AuthOidcPlugin();



@Test
public void test_server_side_extensions() throws Exception {
SonarRuntime runtime = SonarRuntimeImpl.forSonarQube(Version.create(7, 6), SonarQubeSide.SERVER);
Plugin.Context context = new PluginContextImpl.Builder().setSonarRuntime(runtime).build();
Plugin.Context context = setupContext(SonarQubeSide.SERVER);
underTest.define(context);

assertThat(context.getExtensions()).hasSize(20);
}

@Test
public void test_scnner_side_extensions() throws Exception {
SonarRuntime runtime = SonarRuntimeImpl.forSonarQube(Version.create(7, 6), SonarQubeSide.SCANNER);
Plugin.Context context = new PluginContextImpl.Builder().setSonarRuntime(runtime).build();
Plugin.Context context = setupContext(SonarQubeSide.SCANNER);
underTest.define(context);

assertThat(context.getExtensions()).isEmpty();
}


private Plugin.Context setupContext(SonarQubeSide side){

SonarRuntime runtime = new SonarRuntime() {
@Override
public Version getApiVersion() {
return Version.create(9, 9);
}

@Override
public SonarProduct getProduct() {
return SonarProduct.SONARQUBE;
}

@Override
public SonarQubeSide getSonarQubeSide() {
return side;
}

@Override
public SonarEdition getEdition() {
return SonarEdition.COMMUNITY;
}
};

return new Plugin.Context(runtime);
}

}
Loading