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 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 + + + + +