Skip to content
Merged
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
1 change: 0 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,6 @@ jobs:
build-args: |
GIT_BRANCH=${{ steps.docker_meta.outputs.version }}
GIT_COMMIT_ID_ABBREV=${{ steps.build_params.outputs.sha8 }}
MAVEN_PROFILE=webapi-docker,tcache
tags: ${{ steps.docker_meta.outputs.tags }}
# Use runtime labels from docker_meta as well as fixed labels
labels: |
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ FROM maven:3.9-eclipse-temurin-21 AS builder

WORKDIR /code

ARG MAVEN_PROFILE=webapi-docker,tcache
ARG MAVEN_PROFILE=webapi-docker,trexsql
ARG MAVEN_PARAMS="" # can use maven options, e.g. -DskipTests=true -DskipUnitTests=true

ARG OPENTELEMETRY_JAVA_AGENT_VERSION=1.17.0
Expand Down
14 changes: 10 additions & 4 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,7 @@
<!-- Preserve parameter names for reflection (required for AspectJ parameter resolution) -->
<arg>-parameters</arg>
</compilerArgs>
<!-- Exclude TrexSQL sources by default (included when -Ptcache is active) -->
<!-- Exclude TrexSQL sources by default (included when -Ptrexsql is active) -->
<excludes>
<exclude>**/trexsql/**</exclude>
</excludes>
Expand Down Expand Up @@ -1051,6 +1051,12 @@
</exclusion>
</exclusions>
</dependency>
<!-- Override oauth2-oidc-sdk version for pac4j 6.x compatibility -->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>oauth2-oidc-sdk</artifactId>
<version>11.20.1</version>
</dependency>
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>pac4j-http</artifactId>
Expand Down Expand Up @@ -1254,15 +1260,15 @@

<profiles>
<profile>
<id>tcache</id>
<id>trexsql</id>
<properties>
<trexsql.enabled>true</trexsql.enabled>
</properties>
<dependencies>
<dependency>
<groupId>com.github.p-hoffmann</groupId>
<artifactId>trexsql-ext</artifactId>
<version>v0.1.18</version>
<version>v0.1.23</version>
</dependency>
</dependencies>
<build>
Expand All @@ -1271,7 +1277,7 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<!-- Include TrexSQL sources when tcache profile is active -->
<!-- Include TrexSQL sources when trexsql profile is active -->
<excludes combine.self="override"/>
</configuration>
</plugin>
Expand Down
28 changes: 19 additions & 9 deletions src/main/java/io/buji/pac4j/filter/CallbackFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.pac4j.core.config.Config;
import org.pac4j.core.engine.CallbackLogic;
import org.pac4j.core.engine.DefaultCallbackLogic;
import org.pac4j.core.engine.savedrequest.SavedRequestHandler;
import org.pac4j.jee.context.JEEFrameworkParameters;

import java.io.IOException;
Expand All @@ -18,28 +19,38 @@ public class CallbackFilter implements Filter {
private String defaultUrl = "/";
private Config config;
private CallbackLogic callbackLogic;

public CallbackFilter() {
this.callbackLogic = new DefaultCallbackLogic();
}

public void setDefaultUrl(String url) {
this.defaultUrl = url;
}

public void setConfig(Config config) {
this.config = config;
}


/**
* Set a custom SavedRequestHandler on the callback logic.
* This allows customizing where users are redirected after authentication.
*/
public void setSavedRequestHandler(SavedRequestHandler savedRequestHandler) {
if (this.callbackLogic instanceof DefaultCallbackLogic) {
((DefaultCallbackLogic) this.callbackLogic).setSavedRequestHandler(savedRequestHandler);
}
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {

HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;

JEEFrameworkParameters parameters = new JEEFrameworkParameters(httpRequest, httpResponse);

// Execute pac4j 6.x callback logic
callbackLogic.perform(
config,
Expand All @@ -48,7 +59,6 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
null, // defaultClient
parameters
);

// Callback logic handles the response, don't continue chain
}
}
2 changes: 2 additions & 0 deletions src/main/java/org/ohdsi/webapi/JerseyConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.filter.EncodingFilter;
import org.glassfish.jersey.server.spi.internal.ValueParamProvider;
import org.ohdsi.webapi.auth.AuthProviderService;
import org.ohdsi.webapi.info.InfoService;
import org.ohdsi.webapi.security.PermissionController;
import org.ohdsi.webapi.security.SSOController;
Expand Down Expand Up @@ -44,6 +45,7 @@ public JerseyConfig(@Value("${jersey.resources.root.package}") String rootPackag

// Register individual services
register(ActivityService.class);
register(AuthProviderService.class);
register(CacheService.class);
register(CDMResultsService.class);
register(CohortAnalysisService.class);
Expand Down
92 changes: 73 additions & 19 deletions src/main/java/org/ohdsi/webapi/OidcConfCreator.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@
package org.ohdsi.webapi;

import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.oauth2.sdk.pkce.CodeChallengeMethod;
import org.pac4j.oidc.config.OidcConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

Expand All @@ -30,6 +33,11 @@
@Component
public class OidcConfCreator {

private static final Logger logger = LoggerFactory.getLogger(OidcConfCreator.class);

private volatile OidcConfiguration cachedConfiguration;
private final Object lock = new Object();

@Value("${security.oid.clientId}")
private String clientId;

Expand All @@ -38,7 +46,10 @@ public class OidcConfCreator {

@Value("${security.oid.url}")
private String url;


@Value("${security.oid.externalUrl:}")
private String externalUrl;

@Value("${security.oid.logoutUrl}")
private String logoutUrl;

Expand All @@ -47,31 +58,74 @@ public class OidcConfCreator {

@Value("#{${security.oid.customParams:{T(java.util.Collections).emptyMap()}}}")
private Map<String, String> customParams = new HashMap<>();

@Value("${security.oauth.callback.api}")
private String oauthApiCallback;

/**
* Returns the external OIDC URL for browser-facing endpoints.
* If externalUrl is set, returns it; otherwise returns the discovery URL.
*/
public String getExternalUrl() {
if (externalUrl != null && !externalUrl.isEmpty()) {
return externalUrl;
}
// Fall back to discovery URL, removing the .well-known path if present
if (url != null && url.contains("/.well-known/")) {
return url.substring(0, url.indexOf("/.well-known/"));
}
return url;
}

public OidcConfiguration build() {
OidcConfiguration conf = new OidcConfiguration();
conf.setClientId(clientId);
conf.setSecret(apiSecret);
conf.setDiscoveryURI(url);
conf.setLogoutUrl(logoutUrl);
conf.setWithState(true);
conf.setUseNonce(true);

if (customParams != null) {
customParams.forEach(conf::addCustomParam);
OidcConfiguration cached = cachedConfiguration;
if (cached != null) {
return cached;
}

String scopes = "openid";
if (extraScopes != null && !extraScopes.isEmpty()){
scopes += " ";
scopes += extraScopes;
synchronized (lock) {
cached = cachedConfiguration;
if (cached != null) {
return cached;
}

OidcConfiguration conf = new OidcConfiguration();
conf.setClientId(clientId);
conf.setSecret(apiSecret);
conf.setDiscoveryURI(url);
conf.setLogoutUrl(logoutUrl);
conf.setWithState(true);
conf.setUseNonce(true);

if (customParams != null) {
customParams.forEach(conf::addCustomParam);
}

String scopes = "openid";
if (extraScopes != null && !extraScopes.isEmpty()) {
scopes += " ";
scopes += extraScopes;
}
conf.setScope(scopes);
conf.setPreferredJwsAlgorithm(JWSAlgorithm.RS256);
conf.setPkceMethod(CodeChallengeMethod.S256);

try {
logger.info("Initializing OIDC configuration with discovery URL: {}", url);
conf.init();

var resolver = conf.getOpMetadataResolver();
if (resolver != null && resolver.load() != null) {
cachedConfiguration = conf;
} else {
logger.error("OIDC metadata resolver returned null");
}
} catch (Exception e) {
logger.error("Failed to initialize OIDC configuration", e);
}

return conf;
}
conf.setScope(scopes);
conf.setPreferredJwsAlgorithm(JWSAlgorithm.RS256);
return conf;
}

}
97 changes: 97 additions & 0 deletions src/main/java/org/ohdsi/webapi/auth/AuthProviderInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* Copyright 2024 Observational Health Data Sciences and Informatics [OHDSI.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.ohdsi.webapi.auth;

import com.fasterxml.jackson.annotation.JsonInclude;

/**
* DTO representing an authentication provider configuration for Atlas frontend.
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public class AuthProviderInfo {

private String name;
private String url;
private boolean ajax;
private String icon;
private boolean isUseCredentialsForm;
private String logoutUrl;

public AuthProviderInfo() {
}

public AuthProviderInfo(String name, String url, boolean ajax, String icon, boolean isUseCredentialsForm) {
this.name = name;
this.url = url;
this.ajax = ajax;
this.icon = icon;
this.isUseCredentialsForm = isUseCredentialsForm;
}

public AuthProviderInfo(String name, String url, boolean ajax, String icon, boolean isUseCredentialsForm, String logoutUrl) {
this(name, url, ajax, icon, isUseCredentialsForm);
this.logoutUrl = logoutUrl;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getUrl() {
return url;
}

public void setUrl(String url) {
this.url = url;
}

public boolean isAjax() {
return ajax;
}

public void setAjax(boolean ajax) {
this.ajax = ajax;
}

public String getIcon() {
return icon;
}

public void setIcon(String icon) {
this.icon = icon;
}

public boolean isUseCredentialsForm() {
return isUseCredentialsForm;
}

public void setUseCredentialsForm(boolean isUseCredentialsForm) {
this.isUseCredentialsForm = isUseCredentialsForm;
}

public String getLogoutUrl() {
return logoutUrl;
}

public void setLogoutUrl(String logoutUrl) {
this.logoutUrl = logoutUrl;
}
}
Loading