Skip to content

Commit

Permalink
[ALS-6103] Architectural changes to support multiple auth providers (#…
Browse files Browse the repository at this point in the history
…183)

- Removed `OpenAuthenticationController`, `OktaAuthenticationController`, and `AuthController`.

- Created the `AuthenticationService` interface. All authentication service classes **MUST** implement this interface. This interface contains three methods: `authenticate`, `getProvider()`, and `isEnabled()`.
  - `authenticate`: Implements the specific authentication logic for each authentication service.
  - `getProvider`: Returns the name of the provider, e.g., `fence`, `open`, `auth0`, etc. These values must correspond to the `idpProvider` value in the authentication controller path `/auth/authentication/{idpProvider}`. This value is used to look up the correct authentication service in the `AuthenticationServiceRegistry` class.
  - `isEnabled`: Returns `true` or `false` based on the corresponding `application.properties` value.

- Created the `AuthenticationServiceRegistry`. This service registry maintains a map of all enabled `AuthenticationService` services and provides a `getAuthenticationService` method that returns an `AuthenticationService` based on the provider string.

- Created the `AuthenticationController`. This controller uses the `AuthenticationServiceRegistry` to dynamically delegate authentication to the correct `AuthenticationService` based on the `{idpProvider}` path variable.
  • Loading branch information
Gcolon021 committed Jul 10, 2024
1 parent 6477da2 commit 13c4304
Show file tree
Hide file tree
Showing 61 changed files with 1,983 additions and 3,194 deletions.
22 changes: 16 additions & 6 deletions Jenkinsfile.fence
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,37 @@ pipeline {
parameters {
string(name: 'DOCKER_REGISTRY', description: 'Docker registry URL (e.g., ECR URL)', defaultValue: 'hms-dbmi')
string(name: 'REPOSITORY_NAME', description: 'Docker repository name', defaultValue: 'psama')
string(name: 'DATASOURCE_URL', description: 'Datasource URL', defaultValue: '${database_host_address}')
string(name: 'DATASOURCE_USERNAME', description: 'Datasource username', defaultValue: '${database_app_user_secret_name}')
string(name: 'STACK_SPECIFIC_APPLICATION_ID', description: 'Application ID for base query', defaultValue: '${application_id_for_base_query}')
}

environment {
DOCKER_BUILD_ARGS = "-f ./pic-sure-auth-services/Dockerfile"
DOCKER_BUILD_ARGS = "-f ./pic-sure-auth-services/bdc.Dockerfile"
GIT_BRANCH_SHORT = sh(script: 'echo ${GIT_BRANCH} | cut -d "/" -f 2', returnStdout: true).trim()
GIT_COMMIT_SHORT = sh(script: 'echo ${GIT_COMMIT} | cut -c1-7', returnStdout: true).trim()
IMAGE_TAG = "${GIT_BRANCH_SHORT}_${GIT_COMMIT_SHORT}"
LATEST_TAG = "latest"
}

stages {
stage('build') {
stage('Build Docker Image') {
steps {
sh "docker build ${DOCKER_BUILD_ARGS} -t ${params.DOCKER_REGISTRY}/${params.REPOSITORY_NAME}:${IMAGE_TAG} ."
sh "docker tag ${params.DOCKER_REGISTRY}/${params.REPOSITORY_NAME}:${IMAGE_TAG} ${params.DOCKER_REGISTRY}/${params.REPOSITORY_NAME}:${LATEST_TAG}"
script {
// Define the build args
def buildArgs = " --build-arg DATASOURCE_URL=${env.DATASOURCE_URL} " +
" --build-arg DATASOURCE_USERNAME=${env.DATASOURCE_USERNAME} " +
" --build-arg STACK_SPECIFIC_APPLICATION_ID=${env.STACK_SPECIFIC_APPLICATION_ID} "

sh "docker build ${DOCKER_BUILD_ARGS} ${buildArgs} -t ${params.DOCKER_REGISTRY}/${params.REPOSITORY_NAME}:${IMAGE_TAG} ."
sh "docker tag ${params.DOCKER_REGISTRY}/${params.REPOSITORY_NAME}:${IMAGE_TAG} ${params.DOCKER_REGISTRY}/${params.REPOSITORY_NAME}:${LATEST_TAG}"
}
}
}
stage('deploy') {
stage('Deploy Docker Image') {
steps {
sh "docker save ${params.DOCKER_REGISTRY}/${params.REPOSITORY_NAME}:${LATEST_TAG} | gzip > psama.tar.gz"
sh "aws s3 --sse=AES256 cp psama.tar.gz s3://$S3_BUCKET_NAME/releases/jenkins_pipeline_build_${pipeline_build_id}/psama.tar.gz"
sh "aws s3 --sse=AES256 cp psama.tar.gz s3://$S3_BUCKET_NAME/releases/psama/psama.tar.gz"
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions config/psama/bdc/psama-db-config.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
spring.datasource.driver-class-name=com.amazonaws.secretsmanager.sql.AWSSecretsManagerMySQLDriver
spring.datasource.url=jdbc-secretsmanager:mysql://${DATASOURCE_URL}/auth?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&autoReconnectForPools=true
spring.datasource.username=${DATASOURCE_USERNAME}
31 changes: 31 additions & 0 deletions config/psama/psama.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# This is a template file that can be used to configure the application. Pass this to the dockerfile and
# the application will be configured with the values provided in this file.

# These properties are used to configure our applications JWT token.
# The JWT token is used to authenticate the user and authorize them to access the application
# after initial login.
APPLICATION_CLIENT_SECRET=
APPLICATION_CLIENT_SECRET_IS_BASE_64=false

# Fence IDP Configuration
FENCE_IDP_PROVIDER_IS_ENABLED=false
FENCE_CLIENT_ID=
FENCE_CLIENT_SECRET=
FENCE_IDP_PROVIDER_URI=

# AIM AHEAD Authorized Access (A4) OKTA IDP Configuration
A4_OKTA_IDP_PROVIDER_IS_ENABLED=false
A4_OKTA_CLIENT_SECRET=
A4_OKTA_CLIENT_ID=
A4_OKTA_CONNECTION_ID=
A4_OKTA_IDP_PROVIDER_URI=

# Auth0 IDP Configuration (Generally, this is used for the All-In-One and Authorized Access)
AUTH0_IDP_PROVIDER_IS_ENABLED=false
AUTH0_HOST=
AUTH0_DENIED_EMAIL_ENABLED=

# Open Access IDP Configuration
OPEN_IDP_PROVIDER_IS_ENABLED=false

SYSTEM_NAME=PIC-SURE BioDataCatalyst
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# We have migrated our naming convention away from "FENCE_" to a more generalized "MANAGED_".

-- Update roles table
UPDATE role
SET name = REPLACE(name, 'FENCE', 'MANAGED')
WHERE name LIKE '%FENCE%';

UPDATE role
SET description = REPLACE(description, 'FENCE', 'MANAGED')
WHERE description LIKE '%FENCE%';

-- Update privileges table
UPDATE privilege
SET name = REPLACE(name, 'FENCE', 'MANAGED')
WHERE name LIKE '%FENCE%';

UPDATE privilege
SET description = REPLACE(description, 'FENCE', 'MANAGED')
WHERE description LIKE '%FENCE%';

-- Update access rules table
UPDATE access_rule
SET name = REPLACE(name, 'FENCE', 'MANAGED')
WHERE name LIKE '%FENCE%';

UPDATE access_rule
SET description = REPLACE(description, 'FENCE', 'MANAGED')
WHERE description LIKE '%FENCE%';
37 changes: 37 additions & 0 deletions pic-sure-auth-services/bdc.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
FROM maven:3.9.6-amazoncorretto-21 as build

# Copy the source code into the container
COPY ./ /app

# Change the working directory
WORKDIR /app

# Build the jar
RUN mvn clean install -DskipTests

FROM amazoncorretto:21.0.1-alpine3.18

ARG DATASOURCE_URL
ARG DATASOURCE_USERNAME
ARG STACK_SPECIFIC_APPLICATION_ID

ENV DATASOURCE_URL=${DATASOURCE_URL}
ENV DATASOURCE_USERNAME=${DATASOURCE_USERNAME}
ENV STACK_SPECIFIC_APPLICATION_ID=${application_id_for_base_query}

# Copy jar and access token from maven build
COPY --from=build /app/pic-sure-auth-services/target/pic-sure-auth-services.jar /pic-sure-auth-service.jar

# Copy additional bdc configuration files. Root of the project
COPY config/psama/bdc/psama-db-config.properties /config/psama-db-config.properties

# Set SPRING_CONFIG_ADDITIONAL_LOCATION
ENV SPRING_CONFIG_ADDITIONAL_LOCATION=file:/config/psama-db-config.properties

# Copy the AWS certificate
COPY pic-sure-auth-services/aws_certs/certificate.der /certificate.der

# Import the certificate into the Java trust store
RUN keytool -noprompt -import -alias aws_cert -keystore $JAVA_HOME/lib/security/cacerts -storepass changeit -file /certificate.der

ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /pic-sure-auth-service.jar"]
10 changes: 10 additions & 0 deletions pic-sure-auth-services/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@
<version>3.2.4</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
<version>3.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.1.10</version>
</dependency>
<!-- https://mvnrepository.com/artifact/net.bytebuddy/byte-buddy -->
<dependency>
<groupId>net.bytebuddy</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import edu.harvard.hms.dbmi.avillach.auth.service.impl.CustomUserDetailService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
Expand All @@ -10,6 +12,7 @@
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;

@Configuration
@EnableCaching
public class ApplicationConfig {

private final CustomUserDetailService customUserDetailService;
Expand All @@ -30,4 +33,9 @@ public AuthenticationProvider authenticationProvider() {
provider.setUserDetailsService(customUserDetailService);
return provider;
}

@Bean("customKeyGenerator")
public KeyGenerator generator() {
return new CustomKeyGenerator();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package edu.harvard.hms.dbmi.avillach.auth.config;

import edu.harvard.hms.dbmi.avillach.auth.entity.User;
import org.springframework.cache.interceptor.KeyGenerator;

import java.lang.reflect.Method;

public class CustomKeyGenerator implements KeyGenerator {

@Override
public Object generate(Object target, Method method, Object... params) {
for (Object param : params) {
if (param instanceof User user) {
return user.getEmail();
}
}

throw new IllegalArgumentException("No valid params found. Cannot generate cache key");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,14 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.sessionManagement((session) -> session.sessionCreationPolicy(STATELESS))
.authenticationProvider(authenticationProvider)
.authorizeHttpRequests((authorizeRequests) ->
authorizeRequests.requestMatchers("/actuator/health", "/actuator/info", "/authentication/**", "/swagger.yaml", "/swagger.json","/authentication", "/okta/authentication", "/open/authentication").permitAll()
authorizeRequests.requestMatchers(
"/actuator/health",
"/actuator/info",
"/authentication",
"/authentication/**",
"/swagger.yaml",
"/swagger.json"
).permitAll()
.anyRequest().authenticated()
)
.httpBasic(AbstractHttpConfigurer::disable)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import edu.harvard.hms.dbmi.avillach.auth.entity.AccessRule;
import edu.harvard.hms.dbmi.avillach.auth.model.response.PICSUREResponse;
import edu.harvard.hms.dbmi.avillach.auth.service.impl.authorization.AccessRuleService;
import edu.harvard.hms.dbmi.avillach.auth.service.impl.AccessRuleService;
import io.swagger.v3.oas.annotations.*;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.security.RolesAllowed;
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package edu.harvard.hms.dbmi.avillach.auth.rest;

import edu.harvard.hms.dbmi.avillach.auth.model.response.PICSUREResponse;
import edu.harvard.hms.dbmi.avillach.auth.service.AuthenticationService;
import edu.harvard.hms.dbmi.avillach.auth.service.impl.authentication.AuthenticationServiceRegistry;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;


/**
* <p>The authentication endpoint for PSAMA.</p>
*/
@Tag(name = "Authentication")
@Controller
@RequestMapping("/")
public class AuthenticationController {

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

private final AuthenticationServiceRegistry authenticationServiceRegistry;

@Autowired
public AuthenticationController(AuthenticationServiceRegistry authenticationServiceRegistry) {
this.authenticationServiceRegistry = authenticationServiceRegistry;
}

@Operation(description = "The authentication endpoint for retrieving a valid user token")
@PostMapping(path = "/authentication/{idpProvider}", consumes = "application/json", produces = "application/json")
public ResponseEntity<?> authentication(
@PathVariable("idpProvider") String idpProvider,
@Parameter(required = true, description = "A json object that includes all Oauth authentication needs, for example, access_token and redirectURI")
@RequestBody Map<String, String> authRequest, HttpServletRequest request) throws IOException {
logger.debug("authentication() starting...");
logger.debug("authentication() requestHost: {}", request.getServerName());

if (authRequest == null) {
logger.error("authentication() authRequest is null");
return ResponseEntity.badRequest().body("authRequest is null");
}

AuthenticationService authenticationService = authenticationServiceRegistry.getAuthenticationService(idpProvider);
if (authenticationService == null) {
logger.error("authentication() authenticationService is null");
return ResponseEntity.badRequest().body("authenticationService is null");
}

HashMap<String, String> authenticate = authenticationService.authenticate(authRequest, request.getServerName());
if (authenticate != null && !authenticate.isEmpty()) {
logger.info("authentication() User authenticated successfully.");
return PICSUREResponse.success(authenticate);
}

logger.error("authentication() User not authenticated.");
return PICSUREResponse.unauthorizedError("User not authenticated.");
}
}
Loading

0 comments on commit 13c4304

Please sign in to comment.