diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..830747b
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,11 @@
+FROM eclipse-temurin:17 AS builder
+WORKDIR /app
+COPY . .
+# only build security-server module
+RUN ./mvnw clean verify -pl security-server -am
+
+FROM eclipse-temurin:17-jre
+WORKDIR /app
+COPY --from=builder /app/security-server/target/security-server-3.2.0-SNAPSHOT.jar /app/app.jar
+COPY --from=builder /app/security-server/target/libs /app/libs
+ENTRYPOINT ["java", "-cp", "/app/app.jar:/app/libs/*", "app.VitruvSecurityServerApp"]
diff --git a/pom.xml b/pom.xml
index c471608..bd750e8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -38,6 +38,7 @@
remote
p2wrappers
+ security-server
diff --git a/security-server/.gitignore b/security-server/.gitignore
new file mode 100644
index 0000000..5036730
--- /dev/null
+++ b/security-server/.gitignore
@@ -0,0 +1,32 @@
+# VS Code
+.vscode/
+
+# Maven
+target/
+*.log
+*.tmp
+.mvn/wrapper/*.jar
+
+# Eclipse
+META-INF
+build.properties
+plugin.properties
+.project
+.classpath
+.settings/
+bin/
+
+# IntelliJ
+../.idea/
+*.iml
+
+# Project VitruvServer
+zIgnore
+
+# Let's Encrypt
+certs/
+*.pem
+*.der
+
+# OIDC-Provider data
+*.env
\ No newline at end of file
diff --git a/security-server/README.md b/security-server/README.md
new file mode 100644
index 0000000..57a47c1
--- /dev/null
+++ b/security-server/README.md
@@ -0,0 +1,169 @@
+# Vitruv Security Server
+Vitruv Security Server is a Java-based server application built with Maven,
+that ensures secure communication between clients and the internal Vitruv Server.
+It follows a lightweight and direct architecture without relying on frameworks like Spring.
+
+The system is designed to:
+- Authenticate clients via the _FeLS_ identity provider (using the OIDC protocol).
+- Protect communication through TLS encryption.
+- _Security Server_ checks if request is authorized.
+- Forward authorized requests to the internal _Vitruv Server_.
+
+The application runs containerized in a Docker environment on a bwCloud instance.
+A cron job automatically renews the required TLS certificate from Let’s Encrypt via Certbot.
+
+---
+## Guide: How to Deploy
+
+This guide explains step by setp how to deploy the Vitruv Security Server in a Docker environment.
+Important files are located in the [`deployment/`](./deployment) directory.
+
+### 1. Build a new Docker Image (optional)
+_Only required if you need to modify or update the Vitruv Security Server._
+#### 1.1 Build the image
+Run the following command at the root level of the `Vitruv-Server` project:
+```sh
+docker build -t vitruv-security-server:vX.Y . #e.g.: v1.7
+```
+#### 1.2 Push to Docker Hub
+```sh
+docker login
+docker tag vitruv-security-server:vX.Y bluesbird/vitruv-security-server:vX.Y
+docker push bluesbird/vitruv-security-server:vX.Y
+```
+_Note: `bluesbird` has to be replaced with your Docker Hub username._
+
+---
+
+### 2. Pull Docker Image
+The following steps are executed in the target environment (e.g. [bwCloud](https://www.bw-cloud.org/)).
+
+1. Open an SSH session to your target VM:
+ ```sh
+ ssh -i your_ssh_key ubuntu@193.196.39.34 #user@your-server-ip
+ ```
+
+2. Pull latest Docker image:
+
+ ```sh
+ docker pull bluesbird/vitruv-security-server:vX.Y
+ ```
+
+---
+
+### 3. Get TLS Certificates from [Let's Encrypt](https://letsencrypt.org/)
+Run the following command to generate TLS certificates via [Certbot](https://certbot.eff.org/):
+```sh
+certbot certonly --standalone -d
+```
+Certificates are generated at `/etc/letsencrypt/live/`.
+
+_Note: `standalone` mode is used since no web server (such as Traefik or Nginx) is running.
+For alternatives, see [Certbot documentation](https://eff-certbot.readthedocs.io/en/latest/using.html#getting-certificates-and-choosing-plugins)._
+
+---
+
+### 4. Copy and Configure important Files
+**Note**: For this step you need OIDC client credentials for your domain.
+In case you do did not receive them, contact Dr. Matthias Bonn ([matthias.bonn@kit.edu](https://www.scc.kit.edu/dienste/openid-connect.php#:~:text=matthias.bonn%40kit.edu)).
+
+Copy the [`deployment/`](./deployment) directory to your target environment. This includes:
+- [`.env`](./deployment/.env) → configure environment variables. This includes the OIDC client credentials.
+- **Important: sensitive data** never commit this file!
+ Note: To recieve OIDC credentials request,
+- [`docker-compose.yml`](./deployment/docker-compose.yml) → adjust the image name, tag and domain.
+- [`renew_certificates.sh`](./deployment/renew_certificates.sh) → adjust paths and domain.
+
+Follow the `TODO` comments in each file.
+It is recommended to keep these files in the same directory.
+Further information about these files is provided [here](#deployment-directory).
+
+---
+
+### 5. Start the Docker Container
+1. Run the following command to start the container using [`docker-compose.yml`](./deployment/docker-compose.yml):
+ ```sh
+ docker-compose up -d
+ ```
+ _Note: `-d` flag for detached mode._
+
+
+2. Monitor container (optional):
+ ```sh
+ docker logs -f vitruv-security-server
+ ```
+ Alternatively, the `server.log` file can be monitored inside the container:
+ ```sh
+ docker exec -it vitruv-security-server /bin/bash
+ cat logs/server.log
+ ```
+
+---
+
+### 6. Set Up Automatic Certificate Renewal
+To automate the renewal process, add a cronjob that uses the [`renew_certificates.sh`](./deployment/renew_certificates.sh) script.
+The script must be executable so that the cron daemon can run it without errors.
+
+1. Make the script executable:
+ ```sh
+ chmod 775 renew_certificates.sh
+ ```
+ _Note: This sets read, write, and execute permissions._
+
+2. Open the crontab:
+ ```sh
+ crontab -e
+ ```
+3. Add the following line:
+ ```sh
+ 0 4 * * * /path/to/renew_certificates.sh >> /path/to/renew_certificates.log 2>&1
+ ```
+ _Note: This cronjob runs daily at 4 AM and logs output to `/path/to/renew_certificates.log` (generated automatically)._
+
+---
+## Workflows
+The following workflows illustrate the interaction as implemented between the client, the Security Server, the Vitruv Server, and FeLS.
+
+### 1. Authentication Process via FeLS
+This process occurs when no valid Access Token or Refresh Token is available.
+
+1. The client sends an HTTPS request to the Security Server.
+2. The Security Server detects that no valid Access Token or Refresh Token is present.
+3. The client is redirected to the FeLS SSO authentication page for authentication.
+4. After successful authentication, FeLS sends Access, ID and Refresh Tokens to the Security Server:
+5. The Security Server validates the ID Token. If successful, all tokens are send to the client.
+6. The client can now send authorized requests to the Vitruv Server (see [next](#2-request-handling-with-tokens) workflow).
+
+
+### 2. Request Handling with Tokens
+This process occurs when the client provides either a valid Access Token or a valid Refresh Token.
+
+1. The client sends an HTTPS request with an Access Token to the Security Server.
+2. The server checks the Access Token:
+ - If Access Token is valid → Request is forwarded to the Vitruv Server.
+ - If Access Token is invalid/missing:
+ - If a valid Refresh Token is available → The server attempts to refresh the Access Token and issue a new Refresh Token via FeLS before forwarding the request to the Vitruv Server.
+ - Else → The client is redirected to the FeLS SSO authentication page (→ triggering the [Authentication Process](#1-authentication-process-via-fels)).
+3. The Security Server returns the Vitruv Server's response to the client.
+---
+
+
+## Finding: Refreshment of Access Token
+An unexpected behavior was observed when refreshing expired Access Tokens via the FeLS identity provider.
+Instead of receiving a new JWT Access Token, the newly issued Access Token is an opaque token, similar to the Refresh Token.
+This opaque token is immediately rejected as invalid when used, triggering a loop in the token refresh process.
+While this does not affect the behaviour from an end user perspective, this is still a resource-wasting behaviour and a potential security risk.
+The FeLS administrator confirmed this behavior is unintended and will investigate the issue.
+
+**Update (27.02.2025):** After asking again, Mr. Michael Simon (SCC) said that changes were made and the issue should be re-tested.
+A quick test revealed a change: no Access Token is returned at all, but the new Refresh Token remains functional and can still be used.
+This needs further investigation.
+
+---
+
+## Further Useful Links
+- **Live Server:** [www.vitruv-server.org](https://www.vitruv-server.org) (Hosted on [bwCloud](https://www.bw-cloud.org/))
+- **Docker Images:** [Docker Hub - Vitruv Server](https://hub.docker.com/r/bluesbird/vitruvserver/tags)
+- **OIDC Client Configuration:** [FeLS Project](https://fels.scc.kit.edu/project)
+- **Original Development Repository:** [GitHub - Vitruv Server](https://github.com/bluesbird/VitruvServer)
+- **Vitruv Repository:** [GitHub - Vitruv-Server](https://github.com/vitruv-tools/Vitruv-Server)
diff --git a/security-server/pom.xml b/security-server/pom.xml
new file mode 100644
index 0000000..9c6cf54
--- /dev/null
+++ b/security-server/pom.xml
@@ -0,0 +1,212 @@
+
+
+ 4.0.0
+
+
+ tools.vitruv
+ tools.vitruv.server
+ 3.2.0-SNAPSHOT
+
+
+ security-server
+ jar
+
+ Vitruv-Security-Server
+
+
+
+ 3.3
+ 2.2.0
+ 1.6.0
+
+
+
+
+
+ emf-compare
+ EMF Compare
+ p2
+ https://download.eclipse.org/modeling/emf/compare/updates/releases/${repo.emf-compare.version}
+
+
+ sdq-commons
+ SDQ Commons
+ https://kit-sdq.github.io/updatesite/release/commons/${repo.sdq-commons.version}
+ p2
+
+
+ xannotations
+ XAnnotations
+ p2
+ https://kit-sdq.github.io/updatesite/release/xannotations/${repo.xannotations.version}
+
+
+
+
+
+
+ central
+ Maven Central
+ https://repo1.maven.org/maven2/
+
+ false
+
+
+
+
+
+ artifactory.openntf.org
+ artifactory.openntf.org
+ https://artifactory.openntf.org/openntf
+
+ false
+
+
+
+
+
+
+
+ src/main/resources
+
+ **/*
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ 17
+ 17
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ 3.4.2
+
+
+
+ app.VitruvSecurityServerApp
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+ 3.5.0
+
+
+ copy-dependencies
+ package
+
+ copy-dependencies
+
+
+ ${project.build.directory}/libs
+
+
+
+
+
+
+
+ org.openntf.maven
+ p2-layout-resolver
+ 1.9.0
+ true
+
+
+
+
+
+
+
+ tools.vitruv
+ tools.vitruv.framework.remote
+ ${project.version}
+
+
+ tools.vitruv
+ tools.vitruv.framework.views
+
+
+ tools.vitruv
+ tools.vitruv.framework.vsum
+
+
+ tools.vitruv
+ tools.vitruv.framework.testutils.integration
+ ${project.version}
+
+
+ tools.vitruv
+ tools.vitruv.framework.testutils.deprecated
+ ${project.version}
+
+
+ tools.vitruv
+ tools.vitruv.framework.applications
+ ${project.version}
+
+
+
+
+ com.nimbusds
+ oauth2-oidc-sdk
+ 10.9
+
+
+ com.nimbusds
+ nimbus-jose-jwt
+ 10.0.1
+
+
+
+
+ org.apache.logging.log4j
+ log4j-api
+ 2.23.1
+
+
+ org.apache.logging.log4j
+ log4j-core
+ 2.23.1
+
+
+ org.apache.logging.log4j
+ log4j-slf4j2-impl
+ 2.23.1
+ runtime
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+ org.eclipse.emf
+ org.eclipse.emf.common
+
+
+ org.eclipse.emfcloud
+ emfjson-jackson
+
+
+ io.micrometer
+ micrometer-core
+
+
+
\ No newline at end of file
diff --git a/security-server/src/main/java/app/VitruvSecurityServerApp.java b/security-server/src/main/java/app/VitruvSecurityServerApp.java
new file mode 100644
index 0000000..d1a9cb2
--- /dev/null
+++ b/security-server/src/main/java/app/VitruvSecurityServerApp.java
@@ -0,0 +1,60 @@
+package app;
+
+import config.ConfigManager;
+import oidc.OIDCClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import server.SecurityServerManager;
+import server.VitruvServerManager;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * The main application class initializes and starts both the Security Server and the Vitruv Server.
+ * It also manages the OIDC client for authentication and schedules a periodic log to indicate
+ * that the application is still running.
+ */
+public class VitruvSecurityServerApp {
+
+ public static final Logger logger = LoggerFactory.getLogger(VitruvSecurityServerApp.class);
+ private static OIDCClient oidcClient;
+ private static ConfigManager config;
+
+ /**
+ * Starts the Vitruv and Security servers and initializes the OIDC client.
+ *
+ * @param args unused
+ * @throws Exception If server initialization fails
+ */
+ public static void main(String[] args) throws Exception {
+ logger.info("Starting initialization of servers and OIDC client...");
+
+ config = new ConfigManager();
+
+ final VitruvServerManager vitruvServerManager = new VitruvServerManager(config.getVitruvServerPort());
+ vitruvServerManager.start();
+
+ final SecurityServerManager securityServerManager =
+ new SecurityServerManager(config.getHttpsServerPort(), config.getVitruvServerPort(), config.getTlsPassword());
+ securityServerManager.start();
+
+ final String redirectURI = config.getDomainProtocol() + "://" + config.getDomainName() + "/callback";
+ logger.debug("redirectURI: {}", redirectURI);
+ oidcClient = new OIDCClient(config.getOidcClientId(), config.getOidcClientSecret(), redirectURI);
+
+ logger.info("Initialization completed.");
+
+ // Periodic server notification (can be omitted)
+ final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
+ scheduler.scheduleAtFixedRate(() -> logger.info("still running.."), 0, 1, TimeUnit.DAYS);
+ }
+
+ public static OIDCClient getOidcClient() {
+ return oidcClient;
+ }
+ public static ConfigManager getServerConfig() {
+ return config;
+ }
+}
\ No newline at end of file
diff --git a/security-server/src/main/java/config/ConfigManager.java b/security-server/src/main/java/config/ConfigManager.java
new file mode 100644
index 0000000..5ecd439
--- /dev/null
+++ b/security-server/src/main/java/config/ConfigManager.java
@@ -0,0 +1,67 @@
+package config;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.InputStream;
+import java.util.Properties;
+
+/**
+ * Manages configuration properties loaded from 'config.properties' and environment variables for sensitive data.
+ */
+public class ConfigManager {
+ private static final Logger logger = LoggerFactory.getLogger(ConfigManager.class);
+ private final Properties properties = new Properties();
+ private final static String CONFIG_FILE_NAME = "config.properties";
+
+ /**
+ * Loads configuration properties from 'config.properties'.
+ *
+ * @throws Exception if the config file is missing or cannot be loaded
+ */
+ public ConfigManager() throws Exception {
+ try (InputStream input = getClass().getClassLoader().getResourceAsStream(CONFIG_FILE_NAME)) {
+ if (input == null) {
+ logger.error("Config file not found: " + CONFIG_FILE_NAME);
+ throw new Exception("Config file not found: " + CONFIG_FILE_NAME);
+ }
+ properties.load(input);
+ }
+ }
+
+ public int getVitruvServerPort() {
+ return Integer.parseInt(properties.getProperty("vitruv-server.port"));
+ }
+
+ public int getHttpsServerPort() {
+ return Integer.parseInt(properties.getProperty("https-server.port"));
+ }
+
+ public String getDomainProtocol() {
+ return properties.getProperty("domain.protocol");
+ }
+
+ public String getDomainName() {
+ return properties.getProperty("domain.name");
+ }
+
+ public String getCertChainPath() {
+ return properties.getProperty("cert.chain.path");
+ }
+
+ public String getCertKeyPath() {
+ return properties.getProperty("cert.key.path");
+ }
+
+ public String getOidcClientId() {
+ return System.getenv("OIDC_CLIENT_ID");
+ }
+
+ public String getOidcClientSecret() {
+ return System.getenv("OIDC_CLIENT_SECRET");
+ }
+
+ public String getTlsPassword() {
+ return System.getenv("TLS_PASSWORD");
+ }
+}
\ No newline at end of file
diff --git a/security-server/src/main/java/handler/AuthEndpointHandler.java b/security-server/src/main/java/handler/AuthEndpointHandler.java
new file mode 100644
index 0000000..685b08c
--- /dev/null
+++ b/security-server/src/main/java/handler/AuthEndpointHandler.java
@@ -0,0 +1,40 @@
+package handler;
+
+import app.VitruvSecurityServerApp;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+/**
+ * Handles the `/auth` endpoint. Redirects clients to the OIDC authorization page provided by FeLS to initiate
+ * SSO authentication, since only authenticated users can use endpoints of the Vitruv Server.
+ */
+public class AuthEndpointHandler implements HttpHandler {
+
+ private static final Logger logger = LoggerFactory.getLogger(AuthEndpointHandler.class);
+
+ /**
+ * Redirects incoming requests to the OIDC authentication URL of FeLS.
+ *
+ * @param exchange HTTP exchange containing the request and response data
+ * @throws IOException if io error occurs during redirect
+ */
+ @Override
+ public void handle(HttpExchange exchange) throws IOException {
+ try {
+ String authorizationUrl = VitruvSecurityServerApp.getOidcClient().getAuthorizationRequestURI().toString();
+ logger.info("SSO redirect to authentication URL: {}", authorizationUrl);
+ exchange.getResponseHeaders().set("Location", authorizationUrl);
+ // code 302 for redirect, -1 for empty body
+ exchange.sendResponseHeaders(302, -1);
+ } catch (Exception e) {
+ logger.error("Error generating authentication URL: {}", e.getMessage());
+ exchange.sendResponseHeaders(500, 0);
+ } finally {
+ exchange.close();
+ }
+ }
+}
diff --git a/security-server/src/main/java/handler/CallbackEndpointHandler.java b/security-server/src/main/java/handler/CallbackEndpointHandler.java
new file mode 100644
index 0000000..389b64c
--- /dev/null
+++ b/security-server/src/main/java/handler/CallbackEndpointHandler.java
@@ -0,0 +1,105 @@
+package handler;
+
+import app.VitruvSecurityServerApp;
+import com.nimbusds.oauth2.sdk.AccessTokenResponse;
+import com.nimbusds.oauth2.sdk.token.Tokens;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+/**
+ * Handles the `/callback` endpoint. It processes incoming authorization codes from FeLS and exchanges them for ID, Access,
+ * and Refresh Tokens. After validation of ID Token, all tokens are sent to the client. The tokens are stored as
+ * HTTP-only cookies for secure client-side storage, and secure flag ensures HTTPS usage of the cookies.
+ */
+public class CallbackEndpointHandler implements HttpHandler {
+
+ private static final Logger logger = LoggerFactory.getLogger(CallbackEndpointHandler.class);
+
+ /**
+ * Processes incoming requests, exchanges the authorization code for tokens, and sets authentication cookies.
+ *
+ * @param exchange HTTP exchange containing the request and response data
+ * @throws IOException if io error occurs during handling
+ */
+ @Override
+ public void handle(HttpExchange exchange) throws IOException {
+ logger.info("Handling callback endpoint: {}", exchange.getRequestURI());
+
+ String query = exchange.getRequestURI().getQuery();
+ String authorizationCode = getAuthorizationCode(query);
+
+ if (authorizationCode == null) {
+ handleMissingAuthCode(exchange);
+ return;
+ }
+ logger.debug("OIDC Authorization code received: {}", authorizationCode);
+
+ try {
+ // get tokens
+ AccessTokenResponse tokenResponse = VitruvSecurityServerApp.getOidcClient().exchangeAuthorizationCode(authorizationCode);
+ Tokens tokens = tokenResponse.getTokens();
+
+ String idToken = tokenResponse.getCustomParameters().get("id_token").toString();
+ String accessToken = tokens.getAccessToken().getValue();
+ String refreshToken = tokens.getRefreshToken().getValue();
+ logger.debug("ID Token: {}", idToken);
+ logger.debug("Access Token: {}", accessToken);
+ logger.debug("Refresh Token: {}", refreshToken);
+
+ // validate ID Token
+ VitruvSecurityServerApp.getOidcClient().validateIDToken(idToken);
+
+ handleSuccessResponse(exchange, idToken, accessToken, refreshToken);
+
+ } catch (Exception e) {
+ logger.error("Error during token exchange: {}", e.getMessage());
+ handleUnsuccessfulResponse(exchange);
+ } finally {
+ exchange.close();
+ }
+ }
+
+ private void handleMissingAuthCode(HttpExchange exchange) throws IOException {
+ String response = "Authentication failed. OIDC Authorization code not found in the callback request.";
+ logger.info(response);
+ exchange.sendResponseHeaders(400, response.getBytes().length);
+ exchange.getResponseBody().write(response.getBytes());
+ exchange.close();
+ }
+
+ private void handleSuccessResponse(HttpExchange exchange, String idToken, String accessToken, String refreshToken) throws IOException {
+ // set cookies
+ exchange.getResponseHeaders().add("Set-Cookie", "id_token=" + idToken + "; Path=/; HttpOnly; Secure; SameSite=Strict");
+ exchange.getResponseHeaders().add("Set-Cookie", "access_token=" + accessToken + "; Path=/; HttpOnly; Secure; SameSite=Strict");
+ exchange.getResponseHeaders().add("Set-Cookie", "refresh_token=" + refreshToken + "; Path=/; HttpOnly; Secure; SameSite=Strict");
+
+ // set body
+ String response = "You were successfully authenticated by FeLS! Your requests are authorized now." + "\n\n"
+ + "Access Token (JWT; expires in 1 hour):\n" + accessToken + "\n\n"
+ + "ID Token (JWT; expires in 1 hour):\n" + idToken + "\n\n"
+ + "Refresh Token (Opaque; this token is not further required):\n" + refreshToken;
+ exchange.sendResponseHeaders(200, response.getBytes().length);
+ exchange.getResponseBody().write(response.getBytes());
+ }
+
+ private void handleUnsuccessfulResponse(HttpExchange exchange) throws IOException {
+ String response = "Token exchange failed.";
+ exchange.sendResponseHeaders(500, response.getBytes().length);
+ exchange.getResponseBody().write(response.getBytes());
+ }
+
+ private String getAuthorizationCode(String query) {
+ if (query == null) return null;
+ for (String pair : query.split("&")) {
+ String[] keyValue = pair.split("=");
+ if (keyValue.length == 2 && keyValue[0].equals("code")) {
+ return keyValue[1];
+ }
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/security-server/src/main/java/handler/TokenValidationHandler.java b/security-server/src/main/java/handler/TokenValidationHandler.java
new file mode 100644
index 0000000..d6a6fc8
--- /dev/null
+++ b/security-server/src/main/java/handler/TokenValidationHandler.java
@@ -0,0 +1,112 @@
+package handler;
+
+import app.VitruvSecurityServerApp;
+import com.nimbusds.oauth2.sdk.AccessTokenResponse;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import util.TokenUtils;
+
+import java.io.IOException;
+
+/**
+ * Acts as a security wrapper for the VitruvRequestHandler, ensuring only valid Access Tokens are processed.
+ * If a token is expired or missing, it attempts renewal using a Refresh Token.
+ * If unsuccessful, clients are redirected to the '/auth' endpoint.
+ */
+public class TokenValidationHandler implements HttpHandler {
+ private static final Logger logger = LoggerFactory.getLogger(TokenValidationHandler.class);
+ private final HttpHandler next;
+ private final AuthEndpointHandler authEndpointHandler = new AuthEndpointHandler();
+
+ /**
+ * @param next The handler to call if the request is authorized.
+ */
+ public TokenValidationHandler(HttpHandler next) {
+ this.next = next;
+ }
+
+ /**
+ * Checks access token validity, handles refresh if needed, or redirects to authentication.
+ *
+ * @param exchange HTTP exchange containing request data
+ * @throws IOException if forwarding or redirecting fails
+ */
+ @Override
+ public void handle(HttpExchange exchange) throws IOException {
+ if (exchange.getRequestURI().toString().equals("/favicon.ico")) {
+ logger.info("Ignore '/favicon.ico' request.");
+ return;
+ }
+
+ logger.info("\nNew Request: '{}'", exchange.getRequestURI().toString());
+ try {
+ String accessToken = TokenUtils.extractToken(exchange, "access_token");
+
+ // check if Access Token is valid
+ if (accessToken != null && VitruvSecurityServerApp.getOidcClient().isAccessTokenValid(accessToken)) {
+ next.handle(exchange);
+ }
+ else {
+ // try to refresh Access Token
+ handleTokenRefresh(exchange);
+ }
+ } catch (Exception e) {
+ logger.error("An error occurred while validating Access Token: {}\n-> Redirecting to SSO.", e.getMessage());
+ authEndpointHandler.handle(exchange);
+ }
+ }
+
+ /**
+ * Attempts to refresh the access token using the refresh token. If unsuccessful, redirects to authentication.
+ *
+ * @param exchange HTTP exchange containing request data
+ * @throws IOException if token refresh fails
+ */
+ private void handleTokenRefresh(HttpExchange exchange) throws IOException {
+ String refreshToken = TokenUtils.extractToken(exchange, "refresh_token");
+
+ if (refreshToken == null) {
+ logger.warn("No valid Access Token and no Refresh Token found.\n-> Redirecting to SSO.");
+ authEndpointHandler.handle(exchange);
+ return;
+ }
+
+ // try to refresh Access Token and Refresh Token
+ try {
+ replaceTokens(exchange, refreshToken);
+
+ logger.info("Access Token successfully refreshed. Processing request.");
+ next.handle(exchange);
+
+ } catch (Exception e) {
+ logger.error("Failed to refresh Access Token: {}\n-> Redirecting to SSO.", e.getMessage());
+ authEndpointHandler.handle(exchange);
+ }
+ }
+
+ /**
+ * Clears old tokens and sets new access and refresh tokens as cookies.
+ *
+ * @param exchange HTTP exchange containing request data
+ * @param refreshToken refresh Token
+ * @throws Exception if token refresh fails
+ */
+ private void replaceTokens(HttpExchange exchange, String refreshToken) throws Exception {
+ AccessTokenResponse newTokens = VitruvSecurityServerApp.getOidcClient().refreshAccessToken(refreshToken);
+ String newAccessToken = newTokens.getTokens().getAccessToken().getValue();
+ String newRefreshToken = newTokens.getTokens().getRefreshToken().getValue();
+
+ logger.debug("New Access Token: {}", newAccessToken);
+ logger.debug("New Refresh Token: {}", newRefreshToken);
+
+ // remove old tokens
+ exchange.getResponseHeaders().add("Set-Cookie", "access_token=; Max-Age=0; Path=/; HttpOnly; Secure; SameSite=Strict");
+ exchange.getResponseHeaders().add("Set-Cookie", "refresh_token=; Max-Age=0; Path=/; HttpOnly; Secure; SameSite=Strict");
+
+ // set new tokens
+ exchange.getResponseHeaders().add("Set-Cookie", "access_token=" + newAccessToken + "; Path=/; HttpOnly; Secure; SameSite=Strict");
+ exchange.getResponseHeaders().add("Set-Cookie", "refresh_token=" + newRefreshToken + "; Path=/; HttpOnly; Secure; SameSite=Strict");
+ }
+}
diff --git a/security-server/src/main/java/handler/VitruvRequestHandler.java b/security-server/src/main/java/handler/VitruvRequestHandler.java
new file mode 100644
index 0000000..1b95744
--- /dev/null
+++ b/security-server/src/main/java/handler/VitruvRequestHandler.java
@@ -0,0 +1,104 @@
+package handler;
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+/**
+ * Handles generic HTTPS requests, forwarding them to the internal Vitruv Server.
+ * Responds to root endpoint (`/`) requests with a welcome message.
+ */
+public class VitruvRequestHandler implements HttpHandler {
+
+ private final int forwardPort;
+ private static final Logger logger = LoggerFactory.getLogger(VitruvRequestHandler.class);
+
+ public VitruvRequestHandler(int forwardPort) {
+ this.forwardPort = forwardPort;
+ }
+
+ /**
+ * Forwards the request to the Vitruv server or responds directly if root path.
+ *
+ * @param exchange exchange containing the request from the client and used to send the response
+ */
+ @Override
+ public void handle(HttpExchange exchange) {
+ try {
+ String requestURI = exchange.getRequestURI().toString();
+
+ // check for root request
+ if (requestURI.equals("/")) {
+ handleRootRequest(exchange);
+ return;
+ }
+
+ logger.info("Redirect request '{}' to VitruvServer at port {}", requestURI, this.forwardPort);
+
+ HttpURLConnection connection = createConnectionToInternVitruvServer(exchange);
+ forwardRequestAndResponse(exchange, connection);
+
+ } catch (Exception e) {
+ logger.error("An error occurred while handling the HTTP request: {}", e.getMessage());
+ }
+ }
+
+ private void handleRootRequest(HttpExchange exchange) throws IOException {
+ String response = "Welcome to Vitruv-Server :)\n\n"
+ + "Important information can be found here: https://github.com/vitruv-tools";
+ exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=UTF-8");
+ exchange.sendResponseHeaders(200, response.getBytes().length);
+
+ try (OutputStream os = exchange.getResponseBody()) {
+ os.write(response.getBytes());
+ }
+
+ }
+
+ private HttpURLConnection createConnectionToInternVitruvServer(HttpExchange exchange) throws IOException {
+ final String vitruvHost = "http://localhost:" + this.forwardPort;
+ final String fullUri = vitruvHost + exchange.getRequestURI().toString();
+
+ // redirect HTTP request to Vitruv
+ final HttpURLConnection connection = (HttpURLConnection) new URL(fullUri).openConnection();
+ connection.setRequestMethod(exchange.getRequestMethod());
+ exchange.getRequestHeaders().forEach((key, values) -> {
+ for (String value : values) {
+ connection.setRequestProperty(key, value);
+ }
+ });
+
+ return connection;
+ }
+
+ private void forwardRequestAndResponse(HttpExchange exchange, HttpURLConnection connection) throws IOException {
+ // redirect body
+ if (exchange.getRequestBody().available() > 0) {
+ connection.setDoOutput(true);
+ try (OutputStream os = connection.getOutputStream()) {
+ os.write(exchange.getRequestBody().readAllBytes());
+ }
+ }
+
+ // read answer
+ final int responseCode = connection.getResponseCode();
+ InputStream responseStream = responseCode >= 400
+ ? connection.getErrorStream()
+ : connection.getInputStream();
+
+ // return answer to client
+ exchange.sendResponseHeaders(responseCode, responseStream.available());
+ try (OutputStream os = exchange.getResponseBody()) {
+ os.write(responseStream.readAllBytes());
+ }
+
+ logger.debug("Response code: {}", responseCode);
+ }
+}
diff --git a/security-server/src/main/java/interaction/UserInteractorManager.java b/security-server/src/main/java/interaction/UserInteractorManager.java
new file mode 100644
index 0000000..44e6488
--- /dev/null
+++ b/security-server/src/main/java/interaction/UserInteractorManager.java
@@ -0,0 +1,74 @@
+package interaction;
+
+import org.eclipse.xtext.xbase.lib.Functions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import tools.vitruv.change.interaction.InteractionResultProvider;
+import tools.vitruv.change.interaction.InternalUserInteractor;
+import tools.vitruv.change.interaction.UserInteractionListener;
+import tools.vitruv.change.interaction.builder.*;
+
+// TODO: adjust or remove
+/**
+ * Provides empty implementations for various user interaction dialogs.
+ * This class should be adjusted before productive usage.
+ */
+public class UserInteractorManager {
+
+ private static final Logger logger = LoggerFactory.getLogger(UserInteractorManager.class);
+
+ /**
+ * A dummy InternalUserInteractor.
+ *
+ * @return stub of an InternalUserInteractor
+ */
+ public static InternalUserInteractor createInternalUserInteractor() {
+ return new InternalUserInteractor() {
+ @Override
+ public NotificationInteractionBuilder getNotificationDialogBuilder() {
+ logger.warn("getNotificationDialogBuilder() is not implemented.");
+ return null;
+ }
+
+ @Override
+ public ConfirmationInteractionBuilder getConfirmationDialogBuilder() {
+ logger.warn("getConfirmationDialogBuilder() is not implemented.");
+ return null;
+ }
+
+ @Override
+ public TextInputInteractionBuilder getTextInputDialogBuilder() {
+ logger.warn("getTextInputDialogBuilder() is not implemented.");
+ return null;
+ }
+
+ @Override
+ public MultipleChoiceSingleSelectionInteractionBuilder getSingleSelectionDialogBuilder() {
+ logger.warn("getSingleSelectionDialogBuilder() is not implemented.");
+ return null;
+ }
+
+ @Override
+ public MultipleChoiceMultiSelectionInteractionBuilder getMultiSelectionDialogBuilder() {
+ logger.warn("getMultiSelectionDialogBuilder() is not implemented.");
+ return null;
+ }
+
+ @Override
+ public void registerUserInputListener(UserInteractionListener userInteractionListener) {
+ logger.warn("registerUserInputListener() is not implemented.");
+ }
+
+ @Override
+ public void deregisterUserInputListener(UserInteractionListener userInteractionListener) {
+ logger.warn("deregisterUserInputListener() is not implemented.");
+ }
+
+ @Override
+ public AutoCloseable replaceUserInteractionResultProvider(Functions.Function1 super InteractionResultProvider, ? extends InteractionResultProvider> function1) {
+ logger.warn("replaceUserInteractionResultProvider() is not implemented.");
+ return null;
+ }
+ };
+ }
+}
diff --git a/security-server/src/main/java/oidc/OIDCClient.java b/security-server/src/main/java/oidc/OIDCClient.java
new file mode 100644
index 0000000..f9898ff
--- /dev/null
+++ b/security-server/src/main/java/oidc/OIDCClient.java
@@ -0,0 +1,245 @@
+package oidc;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSVerifier;
+import com.nimbusds.jose.crypto.RSASSAVerifier;
+import com.nimbusds.jose.jwk.JWK;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
+import com.nimbusds.jose.proc.JWSKeySelector;
+import com.nimbusds.jose.proc.JWSVerificationKeySelector;
+import com.nimbusds.jose.proc.SecurityContext;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.jwt.proc.DefaultJWTProcessor;
+import com.nimbusds.oauth2.sdk.*;
+import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
+import com.nimbusds.oauth2.sdk.auth.Secret;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.oauth2.sdk.id.Issuer;
+import com.nimbusds.oauth2.sdk.token.RefreshToken;
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderConfigurationRequest;
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URL;
+import java.security.interfaces.RSAPublicKey;
+import java.text.ParseException;
+import java.util.Date;
+
+/**
+ * Handles the OIDC communication flow with the FeLS identity provider.
+ * Supports authorization requests, token exchange, validation, and token refresh using the refresh token.
+ */
+public class OIDCClient {
+
+ private static final Logger logger = LoggerFactory.getLogger(OIDCClient.class);
+ private final String clientId;
+ private final String clientSecret;
+ private final URI redirectUri;
+ private OIDCProviderMetadata providerMetadata;
+ /**
+ * Static discovery URI for the FeLS OIDC provider. Not configurable, as only FeLS is supported.
+ */
+ private static final String DISCOVERY_URI = "https://fels.scc.kit.edu/oidc/realms/fels";
+
+ /**
+ * Initializes the OIDC client and fetches the provider metadata.
+ *
+ * @throws Exception if metadata discovery fails
+ */
+ public OIDCClient(String clientId, String clientSecret, String redirectUri) throws Exception {
+ this.clientId = clientId;
+ this.clientSecret = clientSecret;
+ this.redirectUri = new URI(redirectUri);
+ discoverProviderMetadata();
+
+ logger.info("OIDC Client started.");
+ }
+
+ /**
+ * Discovers the OIDC provider metadata using the discovery endpoint.
+ *
+ * @throws Exception if discovery fails
+ */
+ private void discoverProviderMetadata() throws Exception {
+ Issuer issuer = new Issuer(new URI(DISCOVERY_URI));
+
+ OIDCProviderConfigurationRequest request = new OIDCProviderConfigurationRequest(issuer);
+ OIDCProviderMetadata metadata = OIDCProviderMetadata.parse(request.toHTTPRequest().send().getContentAsJSONObject());
+ this.providerMetadata = metadata;
+
+ logger.info("Metadata Issuer: {}", issuer);
+ logger.debug("Provider Metadata discovered: {}", metadata);
+ }
+
+ public URI getAuthorizationRequestURI() {
+ AuthorizationRequest request = new AuthorizationRequest.Builder(new ResponseType(ResponseType.Value.CODE), new ClientID(clientId))
+ .endpointURI(providerMetadata.getAuthorizationEndpointURI())
+ .redirectionURI(redirectUri)
+ .scope(new Scope("openid", "profile", "email"))
+ .build();
+ return request.toURI();
+ }
+
+ /**
+ * Exchanges an authorization code for access, ID, and refresh tokens.
+ *
+ * @param code Authorization code
+ * @return AccessTokenResponse
+ * @throws Exception if the token exchange fails
+ */
+ public AccessTokenResponse exchangeAuthorizationCode(String code) throws Exception {
+ AuthorizationCode authorizationCode = new AuthorizationCode(code);
+ TokenRequest request = new TokenRequest(
+ providerMetadata.getTokenEndpointURI(),
+ new ClientSecretBasic(new ClientID(clientId), new Secret(clientSecret)),
+ new AuthorizationCodeGrant(authorizationCode, redirectUri));
+
+ TokenResponse response = TokenResponse.parse(request.toHTTPRequest().send());
+
+ if (!response.indicatesSuccess()) {
+ logger.error("Token request failed: " + response.toErrorResponse().getErrorObject().getDescription());
+ throw new Exception("Token request failed: " + response.toErrorResponse().getErrorObject().getDescription());
+ }
+
+ return response.toSuccessResponse();
+ }
+
+ /**
+ * Validates the ID token's signature and claims.
+ *
+ * @param idTokenString ID Token as String
+ * @throws Exception if the token is invalid
+ */
+ public void validateIDToken(String idTokenString) throws Exception {
+ SignedJWT idToken = SignedJWT.parse(idTokenString);
+ // create the JWT processor for validating signature & claims
+ DefaultJWTProcessor jwtProcessor = new DefaultJWTProcessor<>();
+ // load the JWK set from "https://fels.scc.kit.edu/oidc/realms/fels/protocol/openid-connect/certs"
+ URL jwkSetURL = new URL(providerMetadata.getJWKSetURI().toString());
+ JWKSet jwkSet = JWKSet.load(jwkSetURL);
+ ImmutableJWKSet jwkSource = new ImmutableJWKSet<>(jwkSet);
+ // set up JWS key selector with RS256
+ JWSKeySelector keySelector = new JWSVerificationKeySelector<>(
+ JWSAlgorithm.RS256,
+ jwkSource
+ );
+ jwtProcessor.setJWSKeySelector(keySelector);
+ // verify idToken & extract JWTClaimsSet
+ JWTClaimsSet claimsSet = jwtProcessor.process(idToken, null);
+ logger.debug("Claims: {}", claimsSet.toJSONObject());
+
+ validateClaims(claimsSet);
+
+ logger.debug("Email of user: " + claimsSet.getClaim("email").toString());
+ logger.info("ID Token is valid.");
+ }
+
+ /**
+ * Validates the claims of the ID Token.
+ *
+ * @param claimsSet set of claims
+ * @throws Exception if a claim is invalid
+ */
+ private void validateClaims(JWTClaimsSet claimsSet) throws Exception {
+ // validate issuer
+ String issuer = claimsSet.getIssuer();
+ if (!issuer.equals(providerMetadata.getIssuer().toString())) {
+ throw new Exception("Invalid ID Token issuer: " + issuer);
+ }
+
+ // validate audience
+ String audience = claimsSet.getAudience().get(0);
+ if (!audience.equals(clientId)) {
+ throw new Exception("Invalid ID Token audience: " + audience);
+ }
+ }
+
+ /**
+ * Refreshes the access token using a refresh token.
+ *
+ * @param refreshToken Refresh Token
+ * @return new Access and Refresh Token
+ * @throws Exception if refresh fails
+ */
+ public AccessTokenResponse refreshAccessToken(String refreshToken) throws Exception {
+ TokenRequest request = new TokenRequest(
+ providerMetadata.getTokenEndpointURI(),
+ new ClientSecretBasic(new ClientID(clientId), new Secret(clientSecret)),
+ new RefreshTokenGrant(new RefreshToken(refreshToken))
+ );
+
+ TokenResponse response = TokenResponse.parse(request.toHTTPRequest().send());
+
+ if (!response.indicatesSuccess()) {
+ throw new Exception(response.toErrorResponse().getErrorObject().getDescription());
+ }
+ return response.toSuccessResponse();
+ }
+
+ /**
+ * Checks whether an access token is valid and not expired.
+ *
+ * @param accessToken Access Token (JWT)
+ * @return true if the token is valid, false otherwise
+ */
+ public boolean isAccessTokenValid(String accessToken) {
+ try {
+ SignedJWT signedJWT = SignedJWT.parse(accessToken);
+ Date expiration = signedJWT.getJWTClaimsSet().getExpirationTime();
+
+ // check if token is expired
+ if (expiration == null || expiration.before(new Date())) {
+ logger.error("Access Token expired");
+ return false;
+ }
+
+ // check if signature is valid
+ return validateSignature(signedJWT);
+ } catch (ParseException e) {
+ logger.error("Error parsing Access Token: {}", e.getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * Verifies the signature of a signed Access Token.
+ *
+ * @param signedJWT Token to be verified
+ * @return true if the signature is valid, false otherwise
+ */
+ private boolean validateSignature(SignedJWT signedJWT) {
+ try {
+ // fetch JWKS URI and find matching key
+ URL jwkSetURL = new URL(providerMetadata.getJWKSetURI().toString());
+ JWKSet jwkSet = JWKSet.load(jwkSetURL);
+ JWK key = jwkSet.getKeyByKeyId(signedJWT.getHeader().getKeyID());
+
+ if (key == null) {
+ logger.error("No matching key found for kid={}.", signedJWT.getHeader().getKeyID());
+ return false;
+ }
+
+ // verify signature
+ RSAPublicKey publicKey = (RSAPublicKey) key.toRSAKey().toPublicKey();
+ JWSVerifier verifier = new RSASSAVerifier(publicKey);
+ boolean isValid = signedJWT.verify(verifier);
+
+ if (!isValid) {
+ logger.error("Invalid JWT signature.");
+ } else {
+ logger.debug("Valid JWT signature.");
+ }
+ return isValid;
+
+ } catch (ParseException | IOException | JOSEException e) {
+ logger.error("Signature validation failed: {}", e.getMessage());
+ return false;
+ }
+ }
+}
diff --git a/security-server/src/main/java/server/SecurityServerManager.java b/security-server/src/main/java/server/SecurityServerManager.java
new file mode 100644
index 0000000..3c9e4f5
--- /dev/null
+++ b/security-server/src/main/java/server/SecurityServerManager.java
@@ -0,0 +1,134 @@
+package server;
+
+import app.VitruvSecurityServerApp;
+import com.sun.net.httpserver.HttpsConfigurator;
+import com.sun.net.httpserver.HttpsParameters;
+import com.sun.net.httpserver.HttpsServer;
+import handler.AuthEndpointHandler;
+import handler.CallbackEndpointHandler;
+import handler.TokenValidationHandler;
+import handler.VitruvRequestHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import util.TLSUtils;
+
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.net.InetSocketAddress;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+
+/**
+ * Configures and launches the Security Server using TLS encryption.
+ * It defines endpoints for token validation, initiating the OIDC authentication process, and handling the callback response.
+ * The class also establishes the SSL context.
+ */
+public class SecurityServerManager {
+ private static final Logger logger = LoggerFactory.getLogger(SecurityServerManager.class);
+ private final int port;
+ private final int forwardPort;
+ private final char[] tlsPassword;
+ private HttpsServer securityServer;
+
+ public SecurityServerManager(int port, int forwardPort, String tlsPassword) {
+ this.port = port;
+ this.forwardPort = forwardPort;
+ this.tlsPassword = tlsPassword == null ? null : tlsPassword.toCharArray();
+ }
+
+ /**
+ * Sets up TLS context, configures HTTPS server, registers endpoints,
+ * and starts the server.
+ *
+ * @throws Exception if TLS or server setup fails
+ */
+ public void start() throws Exception {
+ final SSLContext sslContext = createSSLContext();
+
+ securityServer = HttpsServer.create(new InetSocketAddress(port), 0);
+ securityServer.setHttpsConfigurator(new HttpsConfigurator(sslContext) {
+ @Override
+ public void configure(HttpsParameters params) {
+ configureHttpsParameters(params, getSSLContext());
+ }
+ });
+ registerEndpoints();
+ securityServer.setExecutor(null);
+ securityServer.start();
+
+ logger.info("Security Server started on port {} with forwardPort {}.", port, forwardPort);
+ }
+
+ /**
+ * Registers all HTTP endpoints handled by the Security Server. See handler classes for further details.
+ */
+ private void registerEndpoints() {
+ // Vitruv endpoints (secured through TokenValidationHandler wrapper)
+ securityServer.createContext("/", new TokenValidationHandler(new VitruvRequestHandler(forwardPort)));
+
+ // Security Server specific endpoints
+ securityServer.createContext("/auth", new AuthEndpointHandler());
+ securityServer.createContext("/callback", new CallbackEndpointHandler());
+ }
+
+ private void configureHttpsParameters(HttpsParameters params, SSLContext sslContext) {
+ SSLEngine engine = sslContext.createSSLEngine();
+ params.setNeedClientAuth(false);
+ params.setCipherSuites(engine.getEnabledCipherSuites());
+ params.setProtocols(engine.getEnabledProtocols());
+ params.setSSLParameters(sslContext.getDefaultSSLParameters());
+ }
+
+ /**
+ * Creates and initializes the SSLContext using the TLS certificate and private key.
+ *
+ * @return initiated SSLContext
+ * @throws Exception if loading or initialization fails
+ */
+ private SSLContext createSSLContext() throws Exception {
+ try {
+ CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
+ X509Certificate certificate;
+
+ // load certificate
+ try (InputStream certChainStream = new FileInputStream(VitruvSecurityServerApp.getServerConfig().getCertChainPath())) {
+ logger.debug("certChainStream: {}", certChainStream);
+ certificate = (X509Certificate) certificateFactory.generateCertificate(certChainStream);
+ }
+
+ // load private key
+ try (InputStream keyStream = new FileInputStream(VitruvSecurityServerApp.getServerConfig().getCertKeyPath())) {
+ logger.debug("keyStream: {}", keyStream);
+
+ byte[] keyBytes = keyStream.readAllBytes();
+ PrivateKey privateKey = TLSUtils.convertToPkcs8Key(keyBytes);
+
+ logger.debug("Private Key Algorithm: {}", privateKey.getAlgorithm());
+ logger.debug("Private Key Format: {}", privateKey.getFormat());
+
+ KeyStore ks = KeyStore.getInstance("PKCS12");
+ ks.load(null, tlsPassword);
+
+ // add certificate and private key to key store
+ ks.setKeyEntry("alias", privateKey, tlsPassword, new Certificate[]{certificate});
+
+ KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
+ kmf.init(ks, tlsPassword);
+
+ // create new SSL (TLS) context
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(kmf.getKeyManagers(), null, null);
+ return sslContext;
+ }
+ } catch (Exception e) {
+ logger.error("Error occurred while trying to load SSL context: {}", e.getMessage());
+ throw new Exception("Failed to initialize SSL context", e);
+ }
+ }
+}
diff --git a/security-server/src/main/java/server/VitruvServerManager.java b/security-server/src/main/java/server/VitruvServerManager.java
new file mode 100644
index 0000000..42917e4
--- /dev/null
+++ b/security-server/src/main/java/server/VitruvServerManager.java
@@ -0,0 +1,57 @@
+package server;
+
+import interaction.UserInteractorManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import tools.vitruv.framework.remote.server.VitruvServer;
+import tools.vitruv.framework.views.ViewTypeFactory;
+import tools.vitruv.framework.vsum.VirtualModelBuilder;
+
+import java.nio.file.Path;
+
+import static tools.vitruv.framework.views.ViewTypeFactory.createIdentityMappingViewType;
+
+/**
+ * Handles generic HTTPS requests, forwarding them to the internal Vitruv Server.
+ * Additionally, it responds to root endpoint (`/`) requests with a welcome message.
+ */
+public class VitruvServerManager {
+
+ private static final Logger logger = LoggerFactory.getLogger(VitruvServerManager.class);
+ private static final String STORAGE_FOLDER_PATH = "StorageFolder";
+ private final int port;
+ private VitruvServer server;
+
+ public VitruvServerManager(int port) {
+ this.port = port;
+ }
+
+ /**
+ * Initializes the Vitruv server and starts it.
+ *
+ * @throws Exception if the server cannot be started.
+ */
+ public void start() throws Exception {
+ server = new VitruvServer(() -> {
+ final VirtualModelBuilder vsumBuilder = new VirtualModelBuilder();
+
+ /////////////////////////////////////////////////////////////////////////////////
+ /////// Testing Area ///////
+ // TODO: this needs to be adjusted
+
+ final Path pathDir = Path.of(STORAGE_FOLDER_PATH);
+ vsumBuilder.withStorageFolder(pathDir);
+
+ vsumBuilder.withUserInteractor(UserInteractorManager.createInternalUserInteractor());
+
+ vsumBuilder.withViewType(ViewTypeFactory.createIdentityMappingViewType("DefaultView"));
+ vsumBuilder.withViewType(createIdentityMappingViewType("MyViewTypeBob17"));
+ vsumBuilder.withViewType(createIdentityMappingViewType("MyViewTypeBob18"));
+ /////////////////////////////////////////////////////////////////////////////////
+
+ return vsumBuilder.buildAndInitialize();
+ }, port);
+ server.start();
+ logger.info("VitruvServer started on port " + port);
+ }
+}
\ No newline at end of file
diff --git a/security-server/src/main/java/util/TLSUtils.java b/security-server/src/main/java/util/TLSUtils.java
new file mode 100644
index 0000000..ffa9bba
--- /dev/null
+++ b/security-server/src/main/java/util/TLSUtils.java
@@ -0,0 +1,51 @@
+package util;
+
+import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.PrivateKey;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.Base64;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Utility class for TLS-specific tasks.
+ */
+public class TLSUtils {
+
+ private static final String PRIVATE_KEY_PATTERN = "-----BEGIN PRIVATE KEY-----([A-Za-z0-9+/=\\s]+)-----END PRIVATE KEY-----";
+
+ /**
+ * Converts a PEM-formatted private key into a PKCS#8-encoded PrivateKey object using the elliptic curve algorithm.
+ *
+ * @param pemKey contains PEM key
+ * @return parsed PrivateKey
+ * @throws GeneralSecurityException if parsing or conversion fails
+ */
+ public static PrivateKey convertToPkcs8Key(byte[] pemKey) throws GeneralSecurityException {
+ try {
+ // extract private key
+ String pem = new String(pemKey);
+ Pattern pattern = Pattern.compile(PRIVATE_KEY_PATTERN, Pattern.DOTALL);
+ Matcher matcher = pattern.matcher(pem);
+
+ if (!matcher.find()) {
+ throw new GeneralSecurityException("Invalid PEM format");
+ }
+
+ String base64Key = matcher.group(1).replaceAll("\\s", "");
+ byte[] keyBytes = Base64.getDecoder().decode(base64Key);
+
+ // convert to PKCS8
+ PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
+
+ try {
+ return KeyFactory.getInstance("EC").generatePrivate(spec);
+ } catch (Exception e) {
+ throw new GeneralSecurityException("Unsupported key format: " + e.getMessage(), e);
+ }
+ } catch (Exception e) {
+ throw new GeneralSecurityException("Failed to parse private key", e);
+ }
+ }
+}
diff --git a/security-server/src/main/java/util/TokenUtils.java b/security-server/src/main/java/util/TokenUtils.java
new file mode 100644
index 0000000..3f521b8
--- /dev/null
+++ b/security-server/src/main/java/util/TokenUtils.java
@@ -0,0 +1,36 @@
+package util;
+
+import com.sun.net.httpserver.HttpExchange;
+
+import java.util.List;
+
+/**
+ * Utility for tokens.
+ */
+public class TokenUtils {
+
+ /**
+ * Extracts the value of a specific token from the Cookie header.
+ *
+ * @param exchange HTTP exchange containing the request
+ * @param tokenType name of the token to extract (e.g. "access_token")
+ * @return token value, or null if not found
+ */
+ public static String extractToken(HttpExchange exchange, String tokenType) {
+ List cookieHeaders = exchange.getRequestHeaders().get("Cookie");
+
+ if (cookieHeaders == null || cookieHeaders.isEmpty()) {
+ return null;
+ }
+
+ String[] tokens = cookieHeaders.get(0).split(";");
+
+ for (String token : tokens) {
+ token = token.trim();
+ if (token.startsWith(tokenType + "=")) {
+ return token.substring((tokenType + "=").length());
+ }
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/security-server/src/main/resources/config.properties b/security-server/src/main/resources/config.properties
new file mode 100644
index 0000000..23593af
--- /dev/null
+++ b/security-server/src/main/resources/config.properties
@@ -0,0 +1,11 @@
+## Server Config
+vitruv-server.port=8080
+https-server.port=8443
+
+# Domain Config
+domain.protocol=https
+domain.name=vitruv-server.org
+
+# Certificate Path for TLS
+cert.chain.path=certs/fullchain.pem
+cert.key.path=certs/privkey.pem
diff --git a/security-server/src/main/resources/log4j2.xml b/security-server/src/main/resources/log4j2.xml
new file mode 100644
index 0000000..3c43e05
--- /dev/null
+++ b/security-server/src/main/resources/log4j2.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ debug
+
+
+
+
+