first commit
This commit is contained in:
commit
4df08a68e0
35
.gitignore
vendored
Normal file
35
.gitignore
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
target/
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### Eclipse ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
### Mac OS ###
|
||||
.DS_Store
|
||||
42
README.md
Normal file
42
README.md
Normal file
@ -0,0 +1,42 @@
|
||||
# adfs-auth-ms
|
||||
|
||||
Jednoduchy REST wrapper nad puvodni logikou `AdfsAuthMediator`.
|
||||
|
||||
## Co dela
|
||||
|
||||
- nacte konfiguraci z YAML
|
||||
- vytvori `client_assertion` stejne jako puvodni Java callout
|
||||
- zavola ADFS token endpoint
|
||||
- drzi access token v pametove cache, dokud nevyprsi
|
||||
- vystavi endpoint `GET /token` a `GET /health`
|
||||
|
||||
## Konfigurace
|
||||
|
||||
Konfigurace je v `config/config.yaml` nebo ve classpath `config.yaml`.
|
||||
|
||||
Klice v sekci `adfs` zachovavaji stejne nazvy jako puvodni policy:
|
||||
|
||||
- `tokenUrl`
|
||||
- `audience`
|
||||
- `resource`
|
||||
- `clientId`
|
||||
- `certificate`
|
||||
- `privateKey`
|
||||
- `proxyHost`
|
||||
- `proxyPort`
|
||||
- `proxyUser`
|
||||
- `proxyPassword`
|
||||
|
||||
## Spusteni
|
||||
|
||||
```bash
|
||||
mvn package
|
||||
java -jar target/adfs-auth-ms-1.0-SNAPSHOT.jar
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/health
|
||||
curl http://localhost:8080/token
|
||||
```
|
||||
20
adfs.crt
Normal file
20
adfs.crt
Normal file
@ -0,0 +1,20 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDLTCCAhWgAwIBAgIQMa6VTqqCHbVHuWPdFQ4JtzANBgkqhkiG9w0BAQsFADAb
|
||||
MRkwFwYDVQQDDBBmcy5rb21lcmNwb2oubG9jMB4XDTI2MDYzMDEzNDcwMVoXDTI3
|
||||
MDYzMDE0MDcwMVowGzEZMBcGA1UEAwwQZnMua29tZXJjcG9qLmxvYzCCASIwDQYJ
|
||||
KoZIhvcNAQEBBQADggEPADCCAQoCggEBANVbpYlq8O2QLxWcZHjBnBCoj69Aq9fH
|
||||
NS7psQuFeYvc2legnbgcz0sIE/RSUzRw9t09RaHApig0TOkhYtezuETDEQdGqoay
|
||||
W8sRaCKMFms5S3dL4MFBhVyerAiIXLOh5qfreshXh+XdRfHv0AYOx83JrXy4z9xh
|
||||
AALQ23udKdRPbd/acYN/TAgiioclQsTZo8Qn7DzZvwjItzX5pvWqkSDeBJCNIDJ9
|
||||
+d7w4SJuSrm4+dFbiHd7NT/qjfQikAw0kLiDBHXqBdWpBBzbssZW3iPxep5u9kQn
|
||||
mnOEeGbI1egkCXBzm1Fxr8MBMg4j2NL/zG80FzHoyWBsfG2XGNQOe8ECAwEAAaNt
|
||||
MGswDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD
|
||||
ATAbBgNVHREEFDASghBmcy5rb21lcmNwb2oubG9jMB0GA1UdDgQWBBR1NFW+QJLH
|
||||
kFdSHbmoLOX2PoeAOTANBgkqhkiG9w0BAQsFAAOCAQEAUf487OGTFkNZCcdBdNsQ
|
||||
YsNqxFz69y01oELQ8hX38So46MnnDewUiWu7taPOCdsdPNMOfsUzFEYMvEGJJd5i
|
||||
GaUHNAqP4I0RXEeSLH3H5T2OYkBvduDxbUBCCtIbX0BbveNiNcti/vklgimJN1JK
|
||||
LRuEIMZ0iucuDAzCkgD7ZoLPk9qGP2Jc0AKmuFLRRmxNFTePTWBOOBTThUo/RieM
|
||||
k1yDnPMjzWgTLSZbtJgfjFJ9XuTmIwNukdDFFDzNV0OfyFcOnV9x703rTT5S/oWb
|
||||
ceFCFFzA6tXF4ezdGgMfqyjuXOR24jy0xQ8rdrP1zWeH+Une2yzuG0ILvq/RtXDW
|
||||
zQ==
|
||||
-----END CERTIFICATE-----
|
||||
83
pom.xml
Normal file
83
pom.xml
Normal file
@ -0,0 +1,83 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>cz.trask</groupId>
|
||||
<artifactId>adfs-auth-ms</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.source>11</maven.compiler.source>
|
||||
<maven.compiler.target>11</maven.compiler.target>
|
||||
<jackson.version>2.18.2</jackson.version>
|
||||
<log4j.version>2.24.3</log4j.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.dataformat</groupId>
|
||||
<artifactId>jackson-dataformat-yaml</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-api</artifactId>
|
||||
<version>${log4j.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-core</artifactId>
|
||||
<version>${log4j.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
<configuration>
|
||||
<release>${maven.compiler.target}</release>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<version>3.7.1</version>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<mainClass>cz.trask.adfsauthms.AdfsAuthMsServer</mainClass>
|
||||
</manifest>
|
||||
</archive>
|
||||
<descriptorRefs>
|
||||
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||
</descriptorRefs>
|
||||
<appendAssemblyId>false</appendAssemblyId>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>make-assembly</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>false</filtering>
|
||||
</resource>
|
||||
</resources>
|
||||
</build>
|
||||
</project>
|
||||
117
src/main/java/cz/trask/adfsauthms/AdfsAuthMsServer.java
Normal file
117
src/main/java/cz/trask/adfsauthms/AdfsAuthMsServer.java
Normal file
@ -0,0 +1,117 @@
|
||||
package cz.trask.adfsauthms;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.security.KeyStore;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import com.sun.net.httpserver.HttpsConfigurator;
|
||||
import com.sun.net.httpserver.HttpsServer;
|
||||
|
||||
import cz.trask.adfsauthms.config.AppConfig;
|
||||
import cz.trask.adfsauthms.config.AppConfig.ContextConfig;
|
||||
import cz.trask.adfsauthms.config.ConfigurationManager;
|
||||
import cz.trask.adfsauthms.context.BaseContextHandler;
|
||||
import cz.trask.adfsauthms.service.AdfsTokenService;
|
||||
|
||||
public class AdfsAuthMsServer {
|
||||
|
||||
private static final Logger logger = LogManager.getLogger(AdfsAuthMsServer.class);
|
||||
|
||||
private final HttpServer server;
|
||||
|
||||
public AdfsAuthMsServer() throws Exception {
|
||||
logger.debug("Initializing AdfsAuthMsServer");
|
||||
ConfigurationManager configurationManager = ConfigurationManager.load();
|
||||
AppConfig config = configurationManager.getConfig();
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
AdfsTokenService tokenService = new AdfsTokenService(config.getAdfs(), objectMapper);
|
||||
|
||||
this.server = createServer(config);
|
||||
List<ContextConfig> contexts = config.getServer().getContexts().getContext();
|
||||
for (ContextConfig contextConfig : contexts) {
|
||||
logger.debug("Creating context: {} -> {}", contextConfig.getPath(), contextConfig.getClassName());
|
||||
BaseContextHandler handler = instantiateHandler(contextConfig.getClassName());
|
||||
handler.init(config, tokenService, objectMapper);
|
||||
server.createContext(contextConfig.getPath(), handler);
|
||||
}
|
||||
|
||||
server.setExecutor(Executors.newFixedThreadPool(config.getServer().getThreads()));
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
try {
|
||||
AdfsAuthMsServer server = new AdfsAuthMsServer();
|
||||
server.start();
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to start ADFS auth microservice", e);
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
public void start() {
|
||||
server.start();
|
||||
logger.info("ADFS auth microservice started on {}", server.getAddress());
|
||||
}
|
||||
|
||||
private HttpServer createServer(AppConfig config) throws Exception {
|
||||
String type = config.getServer().getType();
|
||||
int port = config.getServer().getPort();
|
||||
if ("https".equalsIgnoreCase(type)) {
|
||||
HttpsServer httpsServer = HttpsServer.create(new InetSocketAddress(port), 0);
|
||||
httpsServer.setHttpsConfigurator(new HttpsConfigurator(buildSslContext(config)));
|
||||
return httpsServer;
|
||||
}
|
||||
return HttpServer.create(new InetSocketAddress(port), 0);
|
||||
}
|
||||
|
||||
private SSLContext buildSslContext(AppConfig config) throws Exception {
|
||||
AppConfig.TlsConfig tlsConfig = config.getServer().getTls();
|
||||
if (tlsConfig == null || tlsConfig.getPath() == null || tlsConfig.getPassphrase() == null) {
|
||||
throw new IllegalArgumentException("TLS configuration is required for https server.type");
|
||||
}
|
||||
|
||||
KeyStore keyStore = KeyStore.getInstance("JKS");
|
||||
try (FileInputStream inputStream = new FileInputStream(tlsConfig.getPath())) {
|
||||
keyStore.load(inputStream, tlsConfig.getPassphrase().toCharArray());
|
||||
}
|
||||
|
||||
char[] privateKeyPassphrase = tlsConfig.getPrivatekey() != null
|
||||
&& tlsConfig.getPrivatekey().getPassphrase() != null
|
||||
? tlsConfig.getPrivatekey().getPassphrase().toCharArray()
|
||||
: tlsConfig.getPassphrase().toCharArray();
|
||||
|
||||
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
|
||||
keyManagerFactory.init(keyStore, privateKeyPassphrase);
|
||||
|
||||
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("SunX509");
|
||||
trustManagerFactory.init(keyStore);
|
||||
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);
|
||||
return sslContext;
|
||||
}
|
||||
|
||||
private BaseContextHandler instantiateHandler(String className) throws Exception {
|
||||
if (className == null || className.isBlank()) {
|
||||
throw new IllegalArgumentException("Context handler class is missing.");
|
||||
}
|
||||
|
||||
Class<?> handlerClass = Class.forName(className);
|
||||
Object instance = handlerClass.getDeclaredConstructor().newInstance();
|
||||
if (!(instance instanceof BaseContextHandler)) {
|
||||
throw new IllegalArgumentException("Class " + className + " is not a BaseContextHandler");
|
||||
}
|
||||
return (BaseContextHandler) instance;
|
||||
}
|
||||
}
|
||||
97
src/main/java/cz/trask/adfsauthms/config/AdfsConfig.java
Normal file
97
src/main/java/cz/trask/adfsauthms/config/AdfsConfig.java
Normal file
@ -0,0 +1,97 @@
|
||||
package cz.trask.adfsauthms.config;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class AdfsConfig {
|
||||
|
||||
private String tokenUrl;
|
||||
private String audience;
|
||||
private String resource;
|
||||
private List<String> clientIds;
|
||||
private String certificate;
|
||||
private String privateKey;
|
||||
private String proxyHost;
|
||||
private String proxyPort;
|
||||
private String proxyUser;
|
||||
private String proxyPassword;
|
||||
|
||||
public String getTokenUrl() {
|
||||
return tokenUrl;
|
||||
}
|
||||
|
||||
public void setTokenUrl(String tokenUrl) {
|
||||
this.tokenUrl = tokenUrl;
|
||||
}
|
||||
|
||||
public String getAudience() {
|
||||
return audience;
|
||||
}
|
||||
|
||||
public void setAudience(String audience) {
|
||||
this.audience = audience;
|
||||
}
|
||||
|
||||
public String getResource() {
|
||||
return resource;
|
||||
}
|
||||
|
||||
public void setResource(String resource) {
|
||||
this.resource = resource;
|
||||
}
|
||||
|
||||
public List<String> getClientIds() {
|
||||
return clientIds;
|
||||
}
|
||||
|
||||
public void setClientIds(List<String> clientIds) {
|
||||
this.clientIds = clientIds;
|
||||
}
|
||||
|
||||
public String getCertificate() {
|
||||
return certificate;
|
||||
}
|
||||
|
||||
public void setCertificate(String certificate) {
|
||||
this.certificate = certificate;
|
||||
}
|
||||
|
||||
public String getPrivateKey() {
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
public void setPrivateKey(String privateKey) {
|
||||
this.privateKey = privateKey;
|
||||
}
|
||||
|
||||
public String getProxyHost() {
|
||||
return proxyHost;
|
||||
}
|
||||
|
||||
public void setProxyHost(String proxyHost) {
|
||||
this.proxyHost = proxyHost;
|
||||
}
|
||||
|
||||
public String getProxyPort() {
|
||||
return proxyPort;
|
||||
}
|
||||
|
||||
public void setProxyPort(String proxyPort) {
|
||||
this.proxyPort = proxyPort;
|
||||
}
|
||||
|
||||
public String getProxyUser() {
|
||||
return proxyUser;
|
||||
}
|
||||
|
||||
public void setProxyUser(String proxyUser) {
|
||||
this.proxyUser = proxyUser;
|
||||
}
|
||||
|
||||
public String getProxyPassword() {
|
||||
return proxyPassword;
|
||||
}
|
||||
|
||||
public void setProxyPassword(String proxyPassword) {
|
||||
this.proxyPassword = proxyPassword;
|
||||
}
|
||||
}
|
||||
159
src/main/java/cz/trask/adfsauthms/config/AppConfig.java
Normal file
159
src/main/java/cz/trask/adfsauthms/config/AppConfig.java
Normal file
@ -0,0 +1,159 @@
|
||||
package cz.trask.adfsauthms.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class AppConfig {
|
||||
|
||||
private ServerConfig server = new ServerConfig();
|
||||
private AdfsConfig adfs = new AdfsConfig();
|
||||
private String backendUrl;
|
||||
|
||||
public ServerConfig getServer() {
|
||||
return server;
|
||||
}
|
||||
|
||||
public void setServer(ServerConfig server) {
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
public AdfsConfig getAdfs() {
|
||||
return adfs;
|
||||
}
|
||||
|
||||
public void setAdfs(AdfsConfig adfs) {
|
||||
this.adfs = adfs;
|
||||
}
|
||||
|
||||
public String getBackendUrl() {
|
||||
return backendUrl;
|
||||
}
|
||||
|
||||
public void setBackendUrl(String backendUrl) {
|
||||
this.backendUrl = backendUrl;
|
||||
}
|
||||
|
||||
public static class ServerConfig {
|
||||
private String type = "http";
|
||||
private int port = 8080;
|
||||
private int threads = 8;
|
||||
private TlsConfig tls = new TlsConfig();
|
||||
private ContextsConfig contexts = new ContextsConfig();
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
public void setPort(int port) {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public int getThreads() {
|
||||
return threads;
|
||||
}
|
||||
|
||||
public void setThreads(int threads) {
|
||||
this.threads = threads;
|
||||
}
|
||||
|
||||
public TlsConfig getTls() {
|
||||
return tls;
|
||||
}
|
||||
|
||||
public void setTls(TlsConfig tls) {
|
||||
this.tls = tls;
|
||||
}
|
||||
|
||||
public ContextsConfig getContexts() {
|
||||
return contexts;
|
||||
}
|
||||
|
||||
public void setContexts(ContextsConfig contexts) {
|
||||
this.contexts = contexts;
|
||||
}
|
||||
}
|
||||
|
||||
public static class TlsConfig {
|
||||
private String path;
|
||||
private String passphrase;
|
||||
private PrivateKeyConfig privatekey = new PrivateKeyConfig();
|
||||
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public void setPath(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public String getPassphrase() {
|
||||
return passphrase;
|
||||
}
|
||||
|
||||
public void setPassphrase(String passphrase) {
|
||||
this.passphrase = passphrase;
|
||||
}
|
||||
|
||||
public PrivateKeyConfig getPrivatekey() {
|
||||
return privatekey;
|
||||
}
|
||||
|
||||
public void setPrivatekey(PrivateKeyConfig privatekey) {
|
||||
this.privatekey = privatekey;
|
||||
}
|
||||
}
|
||||
|
||||
public static class PrivateKeyConfig {
|
||||
private String passphrase;
|
||||
|
||||
public String getPassphrase() {
|
||||
return passphrase;
|
||||
}
|
||||
|
||||
public void setPassphrase(String passphrase) {
|
||||
this.passphrase = passphrase;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ContextsConfig {
|
||||
private List<ContextConfig> context = new ArrayList<>();
|
||||
|
||||
public List<ContextConfig> getContext() {
|
||||
return context;
|
||||
}
|
||||
|
||||
public void setContext(List<ContextConfig> context) {
|
||||
this.context = context;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ContextConfig {
|
||||
private String path;
|
||||
private String className;
|
||||
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public void setPath(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public String getClassName() {
|
||||
return className;
|
||||
}
|
||||
|
||||
@com.fasterxml.jackson.annotation.JsonProperty("class")
|
||||
public void setClassName(String className) {
|
||||
this.className = className;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
package cz.trask.adfsauthms.config;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
public class ConfigurationManager {
|
||||
|
||||
private static final Logger logger = LogManager.getLogger(ConfigurationManager.class);
|
||||
|
||||
private static final String CONFIG_DIRECTORY = "config";
|
||||
private static final String CONFIG_FILE_NAME = "config.yaml";
|
||||
|
||||
private final ObjectMapper mapper;
|
||||
private final AppConfig config;
|
||||
|
||||
private ConfigurationManager() throws IOException {
|
||||
this.mapper = new ObjectMapper(new YAMLFactory());
|
||||
this.mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
try (InputStream inputStream = openConfig()) {
|
||||
this.config = mapper.readValue(inputStream, AppConfig.class);
|
||||
}
|
||||
}
|
||||
|
||||
public static ConfigurationManager load() throws IOException {
|
||||
return new ConfigurationManager();
|
||||
}
|
||||
|
||||
public AppConfig getConfig() {
|
||||
return config;
|
||||
}
|
||||
|
||||
private InputStream openConfig() throws IOException {
|
||||
Path configPath = Paths.get(CONFIG_DIRECTORY, CONFIG_FILE_NAME);
|
||||
if (Files.isReadable(configPath)) {
|
||||
logger.info("Loading configuration from file: {}", configPath.toAbsolutePath());
|
||||
return Files.newInputStream(configPath);
|
||||
}
|
||||
|
||||
InputStream classpathStream = ConfigurationManager.class.getClassLoader().getResourceAsStream(CONFIG_FILE_NAME);
|
||||
if (classpathStream != null) {
|
||||
logger.info("Loading configuration from classpath: {}", CONFIG_FILE_NAME);
|
||||
return classpathStream;
|
||||
}
|
||||
|
||||
logger.error("Configuration file '{}' not found in {} or classpath", CONFIG_FILE_NAME, CONFIG_DIRECTORY);
|
||||
throw new IOException("Cannot find configuration file '" + CONFIG_DIRECTORY + "/" + CONFIG_FILE_NAME + "'.");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,117 @@
|
||||
package cz.trask.adfsauthms.context;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.sun.net.httpserver.Headers;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpHandler;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import cz.trask.adfsauthms.config.AppConfig;
|
||||
import cz.trask.adfsauthms.service.AdfsTokenService;
|
||||
|
||||
public abstract class BaseContextHandler implements HttpHandler {
|
||||
|
||||
protected final Logger logger = LogManager.getLogger(getClass());
|
||||
|
||||
protected static final String METHOD_GET = "GET";
|
||||
protected static final String METHOD_POST = "POST";
|
||||
protected static final String CONTENT_TYPE_JSON = "application/json;charset=UTF-8";
|
||||
|
||||
protected static final String HEADER_CONTENT_TYPE = "Content-Type";
|
||||
protected static final String HEADER_CACHE_CONTROL = "Cache-Control";
|
||||
protected static final String HEADER_PRAGMA = "Pragma";
|
||||
protected static final String HEADER_EXPIRES = "Expires";
|
||||
protected static final String HEADER_AUTHORIZATION = "Authorization";
|
||||
|
||||
protected static final String VAL_NO_CACHE = "no-cache, no-store, must-revalidate";
|
||||
protected static final String VAL_PRAGMA_NO_CACHE = "no-cache";
|
||||
protected static final String VAL_EXPIRES_ZERO = "0";
|
||||
|
||||
protected AppConfig appConfig;
|
||||
protected AdfsTokenService tokenService;
|
||||
protected ObjectMapper objectMapper;
|
||||
|
||||
public void init(AppConfig appConfig, AdfsTokenService tokenService, ObjectMapper objectMapper) {
|
||||
this.appConfig = appConfig;
|
||||
this.tokenService = tokenService;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(HttpExchange exchange) throws IOException {
|
||||
String method = exchange.getRequestMethod();
|
||||
String path = exchange.getRequestURI().getPath();
|
||||
logger.debug("Received request: {} {}", method, path);
|
||||
|
||||
try {
|
||||
processRequest(exchange);
|
||||
} catch (Exception e) {
|
||||
logger.error("Error processing request: {} {}", method, path, e);
|
||||
sendError(exchange, 500, "internal_error", "An unexpected error occurred.");
|
||||
}
|
||||
}
|
||||
|
||||
protected void processRequest(HttpExchange exchange) throws IOException {
|
||||
String method = exchange.getRequestMethod();
|
||||
if (METHOD_GET.equalsIgnoreCase(method)) {
|
||||
processRequestGet(exchange);
|
||||
} else if (METHOD_POST.equalsIgnoreCase(method)) {
|
||||
processRequestPost(exchange);
|
||||
} else {
|
||||
sendError(exchange, 405, "method_not_allowed", "Unsupported method: " + method);
|
||||
}
|
||||
}
|
||||
|
||||
protected void processRequestGet(HttpExchange exchange) throws IOException {
|
||||
sendError(exchange, 405, "method_not_allowed", "GET not supported.");
|
||||
}
|
||||
|
||||
protected void processRequestPost(HttpExchange exchange) throws IOException {
|
||||
sendError(exchange, 405, "method_not_allowed", "POST not supported.");
|
||||
}
|
||||
|
||||
protected void sendJson(HttpExchange exchange, int statusCode, Object payload) throws IOException {
|
||||
byte[] responseBytes = objectMapper.writeValueAsBytes(payload);
|
||||
sendResponse(exchange, statusCode, CONTENT_TYPE_JSON, responseBytes);
|
||||
}
|
||||
|
||||
protected void sendResponse(HttpExchange exchange, int statusCode, String contentType, byte[] body) throws IOException {
|
||||
int length = body != null ? body.length : -1;
|
||||
logger.debug("Sending response: HTTP {} ({} bytes)", statusCode, length);
|
||||
Headers headers = exchange.getResponseHeaders();
|
||||
if (contentType != null) {
|
||||
headers.set(HEADER_CONTENT_TYPE, contentType);
|
||||
}
|
||||
headers.set(HEADER_CACHE_CONTROL, VAL_NO_CACHE);
|
||||
headers.set(HEADER_PRAGMA, VAL_PRAGMA_NO_CACHE);
|
||||
headers.set(HEADER_EXPIRES, VAL_EXPIRES_ZERO);
|
||||
exchange.sendResponseHeaders(statusCode, length > 0 ? length : 0);
|
||||
if (body != null && body.length > 0) {
|
||||
try (OutputStream outputStream = exchange.getResponseBody()) {
|
||||
outputStream.write(body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void sendError(HttpExchange exchange, int statusCode, String error, String errorDescription)
|
||||
throws IOException {
|
||||
logger.debug("Sending error response: HTTP {} - {}: {}", statusCode, error, errorDescription);
|
||||
Map<String, String> response = new LinkedHashMap<>();
|
||||
response.put("error", error);
|
||||
response.put("error_description", errorDescription);
|
||||
sendJson(exchange, statusCode, response);
|
||||
}
|
||||
|
||||
protected String getRequestBody(InputStream requestBody) throws IOException {
|
||||
return new String(requestBody.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
19
src/main/java/cz/trask/adfsauthms/context/HealthHandler.java
Normal file
19
src/main/java/cz/trask/adfsauthms/context/HealthHandler.java
Normal file
@ -0,0 +1,19 @@
|
||||
package cz.trask.adfsauthms.context;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
|
||||
public class HealthHandler extends BaseContextHandler {
|
||||
|
||||
@Override
|
||||
protected void processRequestGet(HttpExchange exchange) throws IOException {
|
||||
Map<String, Object> response = new LinkedHashMap<>();
|
||||
response.put("status", "UP");
|
||||
response.put("timestamp", Instant.now().toString());
|
||||
sendJson(exchange, 200, response);
|
||||
}
|
||||
}
|
||||
203
src/main/java/cz/trask/adfsauthms/context/ProcessHandler.java
Normal file
203
src/main/java/cz/trask/adfsauthms/context/ProcessHandler.java
Normal file
@ -0,0 +1,203 @@
|
||||
package cz.trask.adfsauthms.context;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import com.sun.net.httpserver.Headers;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
|
||||
import cz.trask.adfsauthms.dto.TokenPayloadIdp;
|
||||
|
||||
public class ProcessHandler extends BaseContextHandler {
|
||||
|
||||
private static final String AUTH_PREFIX_BEARER = "Bearer ";
|
||||
|
||||
private static final String ERR_SERVER_ERROR = "server_error";
|
||||
private static final String ERR_TOO_MANY_REQUESTS = "too_many_requests";
|
||||
private static final String ERR_INVALID_CONFIG = "invalid_configuration";
|
||||
private static final String ERR_PROCESS_FAILED = "process_failed";
|
||||
|
||||
private final AtomicInteger clientIdIndex = new AtomicInteger(0);
|
||||
|
||||
@Override
|
||||
protected void processRequest(HttpExchange exchange) throws IOException {
|
||||
handleProxy(exchange);
|
||||
}
|
||||
|
||||
private void handleProxy(HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
List<String> clientIds = appConfig.getAdfs().getClientIds();
|
||||
if (clientIds == null || clientIds.isEmpty()) {
|
||||
logger.error("No client IDs configured in adfs.clientIds");
|
||||
sendError(exchange, 500, ERR_SERVER_ERROR, "ADFS Client IDs not configured.");
|
||||
return;
|
||||
}
|
||||
|
||||
int index = clientIdIndex.get() % clientIds.size();
|
||||
String currentClientId = clientIds.get(index);
|
||||
|
||||
TokenPayloadIdp token = tokenService.getToken(currentClientId);
|
||||
String accessToken = token.getAccessToken();
|
||||
|
||||
String backendBaseUrl = appConfig.getBackendUrl();
|
||||
if (backendBaseUrl == null || backendBaseUrl.isBlank()) {
|
||||
logger.error("backend_url is not configured");
|
||||
sendError(exchange, 500, ERR_SERVER_ERROR, "Backend URL not configured.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine target URL
|
||||
String contextPath = exchange.getHttpContext().getPath();
|
||||
String fullPath = exchange.getRequestURI().getPath();
|
||||
String relativePath = fullPath.substring(contextPath.length());
|
||||
String query = exchange.getRequestURI().getRawQuery();
|
||||
|
||||
URI baseUri = new URI(backendBaseUrl);
|
||||
String targetPath = baseUri.getPath();
|
||||
if (targetPath == null) targetPath = "";
|
||||
if (targetPath.endsWith("/") && relativePath.startsWith("/")) {
|
||||
targetPath += relativePath.substring(1);
|
||||
} else if (!targetPath.endsWith("/") && !relativePath.startsWith("/") && !relativePath.isEmpty()) {
|
||||
targetPath += "/" + relativePath;
|
||||
} else {
|
||||
targetPath += relativePath;
|
||||
}
|
||||
|
||||
String targetQuery = baseUri.getRawQuery();
|
||||
if (query != null && !query.isEmpty()) {
|
||||
if (targetQuery == null || targetQuery.isEmpty()) {
|
||||
targetQuery = query;
|
||||
} else {
|
||||
targetQuery += "&" + query;
|
||||
}
|
||||
}
|
||||
|
||||
URI targetUri = new URI(baseUri.getScheme(), baseUri.getAuthority(), targetPath, null, null);
|
||||
String targetUrl = targetUri.toString();
|
||||
if (targetQuery != null && !targetQuery.isEmpty()) {
|
||||
targetUrl += "?" + targetQuery;
|
||||
}
|
||||
|
||||
String method = exchange.getRequestMethod();
|
||||
byte[] body = null;
|
||||
if (exchange.getRequestBody() != null) {
|
||||
body = exchange.getRequestBody().readAllBytes();
|
||||
}
|
||||
|
||||
logger.info("Forwarding {} request to backend: {} (Client ID: {})", method, targetUrl, currentClientId);
|
||||
BackendResponse backendResponse = callBackend(targetUrl, method, exchange.getRequestHeaders(), body, accessToken);
|
||||
|
||||
int retryCount = 0;
|
||||
int maxRetries = clientIds.size() - 1;
|
||||
while (backendResponse.statusCode == 429 && retryCount < maxRetries) {
|
||||
retryCount++;
|
||||
int nextIndex = clientIdIndex.incrementAndGet() % clientIds.size();
|
||||
String retryClientId = clientIds.get(nextIndex);
|
||||
|
||||
logger.warn("Backend returned 429. Retrying ({}/{}) with next client ID: {}...",
|
||||
retryCount, maxRetries, retryClientId);
|
||||
|
||||
token = tokenService.getToken(retryClientId);
|
||||
accessToken = token.getAccessToken();
|
||||
|
||||
logger.info("Retrying request to backend with new token (Client ID: {})", retryClientId);
|
||||
backendResponse = callBackend(targetUrl, method, exchange.getRequestHeaders(), body, accessToken);
|
||||
}
|
||||
|
||||
if (backendResponse.statusCode == 429) {
|
||||
logger.error("Throttling limit reached. Exhausted all {} identities and backend still returns 429.", clientIds.size());
|
||||
sendError(exchange, 429, ERR_TOO_MANY_REQUESTS,
|
||||
"All configured client identities are currently throttled by the backend after " + retryCount + " retries.");
|
||||
return;
|
||||
}
|
||||
|
||||
sendResponse(exchange, backendResponse.statusCode, backendResponse.contentType, backendResponse.body);
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
sendError(exchange, 400, ERR_INVALID_CONFIG, e.getMessage());
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to process request and forward to backend", e);
|
||||
sendError(exchange, 502, ERR_PROCESS_FAILED, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private BackendResponse callBackend(String targetUrl, String method, Headers requestHeaders, byte[] requestBody, String token) throws Exception {
|
||||
HttpURLConnection connection = null;
|
||||
try {
|
||||
URL url = new URI(targetUrl).toURL();
|
||||
connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setRequestMethod(method);
|
||||
|
||||
// Forward headers
|
||||
for (Map.Entry<String, List<String>> entry : requestHeaders.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
if (shouldForwardHeader(key)) {
|
||||
for (String value : entry.getValue()) {
|
||||
connection.addRequestProperty(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
connection.setRequestProperty(HEADER_AUTHORIZATION, AUTH_PREFIX_BEARER + token);
|
||||
connection.setConnectTimeout(5000);
|
||||
connection.setReadTimeout(5000);
|
||||
|
||||
if (requestBody != null && requestBody.length > 0) {
|
||||
connection.setDoOutput(true);
|
||||
try (var os = connection.getOutputStream()) {
|
||||
os.write(requestBody);
|
||||
}
|
||||
}
|
||||
|
||||
int responseCode = connection.getResponseCode();
|
||||
String contentType = connection.getContentType();
|
||||
logger.debug("Backend response code: {}, Content-Type: {}", responseCode, contentType);
|
||||
|
||||
InputStream responseStream = responseCode >= 400 ? connection.getErrorStream() : connection.getInputStream();
|
||||
byte[] body = null;
|
||||
if (responseStream != null) {
|
||||
body = responseStream.readAllBytes();
|
||||
}
|
||||
|
||||
return new BackendResponse(responseCode, contentType, body);
|
||||
} finally {
|
||||
if (connection != null) {
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldForwardHeader(String headerName) {
|
||||
if (headerName == null) return false;
|
||||
String lowerCaseHeader = headerName.toLowerCase();
|
||||
return !lowerCaseHeader.equals("host") &&
|
||||
!lowerCaseHeader.equals("authorization") &&
|
||||
!lowerCaseHeader.equals("content-length") &&
|
||||
!lowerCaseHeader.equals("connection") &&
|
||||
!lowerCaseHeader.equals("keep-alive") &&
|
||||
!lowerCaseHeader.equals("proxy-authenticate") &&
|
||||
!lowerCaseHeader.equals("proxy-authorization") &&
|
||||
!lowerCaseHeader.equals("te") &&
|
||||
!lowerCaseHeader.equals("trailers") &&
|
||||
!lowerCaseHeader.equals("transfer-encoding") &&
|
||||
!lowerCaseHeader.equals("upgrade");
|
||||
}
|
||||
|
||||
private static class BackendResponse {
|
||||
final int statusCode;
|
||||
final String contentType;
|
||||
final byte[] body;
|
||||
|
||||
BackendResponse(int statusCode, String contentType, byte[] body) {
|
||||
this.statusCode = statusCode;
|
||||
this.contentType = contentType;
|
||||
this.body = body;
|
||||
}
|
||||
}
|
||||
}
|
||||
41
src/main/java/cz/trask/adfsauthms/dto/TokenPayloadIdp.java
Normal file
41
src/main/java/cz/trask/adfsauthms/dto/TokenPayloadIdp.java
Normal file
@ -0,0 +1,41 @@
|
||||
package cz.trask.adfsauthms.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class TokenPayloadIdp {
|
||||
|
||||
@JsonProperty("access_token")
|
||||
private String accessToken;
|
||||
|
||||
@JsonProperty("token_type")
|
||||
private String tokenType;
|
||||
|
||||
@JsonProperty("expires_in")
|
||||
private int expiresIn;
|
||||
|
||||
public String getAccessToken() {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
public void setAccessToken(String accessToken) {
|
||||
this.accessToken = accessToken;
|
||||
}
|
||||
|
||||
public String getTokenType() {
|
||||
return tokenType;
|
||||
}
|
||||
|
||||
public void setTokenType(String tokenType) {
|
||||
this.tokenType = tokenType;
|
||||
}
|
||||
|
||||
public int getExpiresIn() {
|
||||
return expiresIn;
|
||||
}
|
||||
|
||||
public void setExpiresIn(int expiresIn) {
|
||||
this.expiresIn = expiresIn;
|
||||
}
|
||||
}
|
||||
285
src/main/java/cz/trask/adfsauthms/service/AdfsTokenService.java
Normal file
285
src/main/java/cz/trask/adfsauthms/service/AdfsTokenService.java
Normal file
@ -0,0 +1,285 @@
|
||||
package cz.trask.adfsauthms.service;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Authenticator;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.PasswordAuthentication;
|
||||
import java.net.Proxy;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.Signature;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.util.Base64;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import cz.trask.adfsauthms.config.AdfsConfig;
|
||||
import cz.trask.adfsauthms.dto.TokenPayloadIdp;
|
||||
|
||||
public class AdfsTokenService {
|
||||
|
||||
private static final Logger logger = LogManager.getLogger(AdfsTokenService.class);
|
||||
|
||||
private static final String PARAM_RESOURCE = "resource";
|
||||
private static final String PARAM_CLIENT_ID = "client_id";
|
||||
private static final String PARAM_CLIENT_ASSERTION_TYPE = "client_assertion_type";
|
||||
private static final String PARAM_CLIENT_ASSERTION = "client_assertion";
|
||||
private static final String PARAM_GRANT_TYPE = "grant_type";
|
||||
|
||||
private static final String GRANT_TYPE_CLIENT_CREDENTIALS = "client_credentials";
|
||||
private static final String CLIENT_ASSERTION_TYPE =
|
||||
"urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
|
||||
|
||||
private static final String HEADER_CONTENT_TYPE = "Content-Type";
|
||||
private static final String CONTENT_TYPE_FORM = "application/x-www-form-urlencoded";
|
||||
private static final String METHOD_POST = "POST";
|
||||
|
||||
private final AdfsConfig config;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
private volatile TokenPayloadIdp cachedToken;
|
||||
|
||||
public AdfsTokenService(AdfsConfig config, ObjectMapper objectMapper) {
|
||||
this.config = config;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
public synchronized TokenPayloadIdp getToken(String clientIdOverride) throws Exception {
|
||||
validateConfig();
|
||||
if (clientIdOverride == null && cachedToken != null && cachedToken.getAccessToken() != null
|
||||
&& !cachedToken.getAccessToken().isBlank()
|
||||
&& !isJwtExpired(cachedToken.getAccessToken())) {
|
||||
logger.debug("Returning cached ADFS token");
|
||||
return cachedToken;
|
||||
}
|
||||
|
||||
logger.info("Fetching new ADFS token from {} (Client ID: {})", config.getTokenUrl(),
|
||||
clientIdOverride != null ? clientIdOverride : getDefaultClientId());
|
||||
TokenPayloadIdp fetchedToken = fetchToken(clientIdOverride);
|
||||
if (clientIdOverride == null) {
|
||||
this.cachedToken = fetchedToken;
|
||||
}
|
||||
return fetchedToken;
|
||||
}
|
||||
|
||||
private String getDefaultClientId() {
|
||||
if (config.getClientIds() != null && !config.getClientIds().isEmpty()) {
|
||||
return config.getClientIds().get(0);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public synchronized void invalidateCache() {
|
||||
logger.debug("Invalidating cached ADFS token");
|
||||
this.cachedToken = null;
|
||||
}
|
||||
|
||||
private TokenPayloadIdp fetchToken(String clientIdOverride) throws Exception {
|
||||
Proxy proxy = buildProxy();
|
||||
if (proxy != null) {
|
||||
logger.debug("Using proxy for ADFS token fetch: {}", config.getProxyHost());
|
||||
}
|
||||
String effectiveClientId = clientIdOverride != null ? clientIdOverride : getDefaultClientId();
|
||||
String clientAssertion = generateJwtAssertion(effectiveClientId);
|
||||
logger.debug("Generated JWT client assertion for ADFS: {}", clientAssertion);
|
||||
String formData = buildFormData(effectiveClientId, clientAssertion);
|
||||
|
||||
HttpURLConnection connection = null;
|
||||
try {
|
||||
URL url = new URI(config.getTokenUrl()).toURL();
|
||||
connection = (HttpURLConnection) url.openConnection(proxy != null ? proxy : Proxy.NO_PROXY);
|
||||
connection.setRequestMethod(METHOD_POST);
|
||||
connection.setDoInput(true);
|
||||
connection.setDoOutput(true);
|
||||
connection.setRequestProperty(HEADER_CONTENT_TYPE, CONTENT_TYPE_FORM);
|
||||
|
||||
try (OutputStream outputStream = connection.getOutputStream()) {
|
||||
outputStream.write(formData.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
int responseCode = connection.getResponseCode();
|
||||
logger.debug("ADFS response code: {}", responseCode);
|
||||
InputStream responseStream = responseCode >= 400 ? connection.getErrorStream() : connection.getInputStream();
|
||||
String responseBody = readFully(responseStream);
|
||||
if (responseCode >= 400) {
|
||||
logger.debug("ADFS error body: {}", responseBody);
|
||||
throw new IllegalStateException("ADFS returned HTTP " + responseCode + ": " + responseBody);
|
||||
}
|
||||
logger.debug("ADFS token fetched successfully");
|
||||
return objectMapper.readValue(responseBody, TokenPayloadIdp.class);
|
||||
} finally {
|
||||
if (connection != null) {
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Proxy buildProxy() {
|
||||
if (isBlank(config.getProxyHost()) || isBlank(config.getProxyPort())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int port = Integer.parseInt(config.getProxyPort());
|
||||
if (!isBlank(config.getProxyUser()) && !isBlank(config.getProxyPassword())) {
|
||||
Authenticator.setDefault(new Authenticator() {
|
||||
@Override
|
||||
protected PasswordAuthentication getPasswordAuthentication() {
|
||||
return new PasswordAuthentication(config.getProxyUser(), config.getProxyPassword().toCharArray());
|
||||
}
|
||||
});
|
||||
}
|
||||
return new Proxy(Proxy.Type.HTTP, new InetSocketAddress(config.getProxyHost(), port));
|
||||
}
|
||||
|
||||
private String buildFormData(String clientId, String clientAssertion) throws Exception {
|
||||
Map<String, String> params = new LinkedHashMap<>();
|
||||
params.put(PARAM_RESOURCE, config.getResource());
|
||||
params.put(PARAM_CLIENT_ID, clientId);
|
||||
params.put(PARAM_CLIENT_ASSERTION_TYPE, CLIENT_ASSERTION_TYPE);
|
||||
params.put(PARAM_CLIENT_ASSERTION, clientAssertion);
|
||||
params.put(PARAM_GRANT_TYPE, GRANT_TYPE_CLIENT_CREDENTIALS);
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (Map.Entry<String, String> entry : params.entrySet()) {
|
||||
if (builder.length() > 0) {
|
||||
builder.append('&');
|
||||
}
|
||||
builder.append(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8.name()));
|
||||
builder.append('=');
|
||||
builder.append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8.name()));
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private String generateJwtAssertion(String clientId) throws Exception {
|
||||
String certPem = new String(Base64.getDecoder().decode(config.getCertificate().replaceAll("\\s+", "")), StandardCharsets.UTF_8)
|
||||
.replace("-----BEGIN CERTIFICATE-----", "")
|
||||
.replace("-----END CERTIFICATE-----", "")
|
||||
.replaceAll("\\s+", "");
|
||||
X509Certificate cert = getCertificate(certPem);
|
||||
|
||||
String keyPem = new String(Base64.getDecoder().decode(config.getPrivateKey().replaceAll("\\s+", "")), StandardCharsets.UTF_8)
|
||||
.replace("-----BEGIN PRIVATE KEY-----", "")
|
||||
.replace("-----END PRIVATE KEY-----", "")
|
||||
.replaceAll("\\s+", "");
|
||||
PrivateKey privateKey = getPrivateKey(keyPem);
|
||||
|
||||
MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
|
||||
String x5t = Base64.getUrlEncoder().withoutPadding().encodeToString(sha1.digest(cert.getEncoded()));
|
||||
|
||||
long now = System.currentTimeMillis() / 1000L;
|
||||
|
||||
Map<String, Object> header = new LinkedHashMap<>();
|
||||
header.put("alg", "RS256");
|
||||
header.put("typ", "JWT");
|
||||
header.put("x5t", x5t);
|
||||
header.put("kid", x5t);
|
||||
|
||||
Map<String, Object> claims = new LinkedHashMap<>();
|
||||
claims.put("iss", clientId);
|
||||
claims.put("sub", clientId);
|
||||
claims.put("aud", config.getAudience());
|
||||
claims.put("jti", UUID.randomUUID().toString());
|
||||
claims.put("iat", now);
|
||||
claims.put("nbf", now);
|
||||
claims.put("exp", now + 600);
|
||||
|
||||
String headerPart = Base64.getUrlEncoder().withoutPadding()
|
||||
.encodeToString(objectMapper.writeValueAsBytes(header));
|
||||
String claimsPart = Base64.getUrlEncoder().withoutPadding()
|
||||
.encodeToString(objectMapper.writeValueAsBytes(claims));
|
||||
String signingInput = headerPart + "." + claimsPart;
|
||||
|
||||
Signature signature = Signature.getInstance("SHA256withRSA");
|
||||
signature.initSign(privateKey);
|
||||
signature.update(signingInput.getBytes(StandardCharsets.UTF_8));
|
||||
String signaturePart = Base64.getUrlEncoder().withoutPadding().encodeToString(signature.sign());
|
||||
return signingInput + "." + signaturePart;
|
||||
}
|
||||
|
||||
private boolean isJwtExpired(String jwt) throws Exception {
|
||||
String[] parts = jwt.split("\\.");
|
||||
if (parts.length != 3) {
|
||||
throw new IllegalArgumentException("Invalid JWT format");
|
||||
}
|
||||
|
||||
byte[] decodedBytes = Base64.getUrlDecoder().decode(parts[1]);
|
||||
Map<String, Object> claims = objectMapper.readValue(decodedBytes, Map.class);
|
||||
Number exp = (Number) claims.get("exp");
|
||||
if (exp == null) {
|
||||
throw new IllegalArgumentException("JWT does not contain 'exp' claim");
|
||||
}
|
||||
return exp.longValue() * 1000L < System.currentTimeMillis();
|
||||
}
|
||||
|
||||
private X509Certificate getCertificate(String certificatePem) throws Exception {
|
||||
String normalized = certificatePem.replace("-----BEGIN CERTIFICATE-----", "")
|
||||
.replace("-----END CERTIFICATE-----", "")
|
||||
.replaceAll("\\s", "");
|
||||
byte[] certBytes = Base64.getDecoder().decode(normalized);
|
||||
CertificateFactory factory = CertificateFactory.getInstance("X.509");
|
||||
return (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(certBytes));
|
||||
}
|
||||
|
||||
private PrivateKey getPrivateKey(String privateKeyPem) throws Exception {
|
||||
String normalized = privateKeyPem.replaceAll("-----BEGIN (.*) PRIVATE KEY-----", "")
|
||||
.replaceAll("-----END (.*) PRIVATE KEY-----", "")
|
||||
.replaceAll("\\s", "");
|
||||
byte[] keyBytes = Base64.getDecoder().decode(normalized);
|
||||
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
||||
return keyFactory.generatePrivate(spec);
|
||||
}
|
||||
|
||||
private String readFully(InputStream inputStream) throws Exception {
|
||||
if (inputStream == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
try (InputStream in = inputStream; ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
||||
byte[] buffer = new byte[4096];
|
||||
for (int read = in.read(buffer); read >= 0; read = in.read(buffer)) {
|
||||
baos.write(buffer, 0, read);
|
||||
}
|
||||
return baos.toString(StandardCharsets.UTF_8.name());
|
||||
}
|
||||
}
|
||||
|
||||
private void validateConfig() {
|
||||
requireValue(config.getTokenUrl(), "tokenUrl");
|
||||
requireValue(config.getAudience(), "audience");
|
||||
requireValue(config.getResource(), "resource");
|
||||
if (config.getClientIds() == null || config.getClientIds().isEmpty()) {
|
||||
throw new IllegalArgumentException("Missing or empty config value: clientIds");
|
||||
}
|
||||
requireValue(config.getCertificate(), "certificate");
|
||||
requireValue(config.getPrivateKey(), "privateKey");
|
||||
}
|
||||
|
||||
private void requireValue(String value, String name) {
|
||||
if (isBlank(value)) {
|
||||
throw new IllegalArgumentException("Missing or empty config value: " + name);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isBlank(String value) {
|
||||
return value == null || value.trim().isEmpty();
|
||||
}
|
||||
}
|
||||
25
src/main/resources/config.yaml
Normal file
25
src/main/resources/config.yaml
Normal file
@ -0,0 +1,25 @@
|
||||
server:
|
||||
type: http
|
||||
port: 8080
|
||||
threads: 8
|
||||
contexts:
|
||||
context:
|
||||
- path: /process
|
||||
class: cz.trask.adfsauthms.context.ProcessHandler
|
||||
- path: /health
|
||||
class: cz.trask.adfsauthms.context.HealthHandler
|
||||
|
||||
adfs:
|
||||
tokenUrl: "https://fs.komercpoj.loc/adfs/oauth2/token"
|
||||
audience: "https://fs.komercpoj.loc/adfs/oauth2/token"
|
||||
resource: "urn:kamma:api"
|
||||
clientIds:
|
||||
- "db2edf05-7af2-4523-97f9-39f930afc634"
|
||||
certificate: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURFRENDQWZpZ0F3SUJBZ0lRRjhOQTNUQytVWUpJL1VRNTNTSFVGakFOQmdrcWhraUc5dzBCQVFVRkFEQWIKTVJrd0Z3WURWUVFEREJCUFFYVjBhQ0JEYkdsbGJuUWdTbGRVTUI0WERUSTJNRFl6TURFME1UazBOVm9YRFRJMwpNRFl6TURFME16azBOVm93R3pFWk1CY0dBMVVFQXd3UVQwRjFkR2dnUTJ4cFpXNTBJRXBYVkRDQ0FTSXdEUVlKCktvWklodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCQU1UVzFpL0tNUFY1cTl2TXgzTGV4V1lsNFdFTmJ6UDAKQVV5ZjhlNm1GcHdEcE5PWUVXV1RQYnAwUm5HRmY4SGpCaUFwTWFDSFZCbmJqMC81Q3pSWWk0N3lWWEMzaGRiQgpYdDdZRGNCeEduYnN5Zy9LdTB5SFRQRW5YblB5VCtpYXUzS2JqQXgyOGU1OXhzRHlFMzZxaktveGRTbURZT0M4CmpzM0hsMHNGTWNjd3JzZ0JxOFEyYllIVUt1TERORmRveE1DaEhOTHlxOE1sNFdnNmVHdWtMN2p4R0tGNXlzczEKM096emJoZkc5dEpWY290WUt0WDlHUThUckdpREF2UGFOREs2WXMxUTZWbDNKVjY5dmdET1ZSVlJCUzhpWmdLaQpQbTdob1hHb2s3VVEzV2R0c3dpU09ReE1iMWZhVDFLOWp0T3lmTm1vNjgyUkluU1VJSi9sTm9rQ0F3RUFBYU5RCk1FNHdEZ1lEVlIwUEFRSC9CQVFEQWdXZ01CMEdBMVVkSlFRV01CUUdDQ3NHQVFVRkJ3TUNCZ2dyQmdFRkJRY0QKQVRBZEJnTlZIUTRFRmdRVTlRNUJSVnpRVDVNbG5mY3N6VzFwUHZiZmRud3dEUVlKS29aSWh2Y05BUUVGQlFBRApnZ0VCQUpUMVlIYk03UCtrYi9wSmJ5bENLUUd6OEZ6eGFpeDVyay9MdndJNDVmcVJTQ0NUaERmemUrdTNiK05ZCjcxV2V0Z2Y2cUhubkVrdkRnVXJyWXU1QUhrZE9WWXlDUkJnN0dWKzIrQUtpWExEYUsrV0FnM2U4Zkk2cFNvOUMKeXdvc0lNNFcyaWJWSnhzVWNIZGhTUFBVVlkrVzNSY1B4VlZmWmV4eTZXUXBsVlV0Z3JYQmpYSEdEWE5YQUw3awo4ZUFwSEpDQVByKytabktQc3VzSWFyeVNqRXRrZXR0UHViMGp0MnJlajdqaHJuU1EyRlg4czhqcU43L0lCeWRlCjd2N2YwMlowMjVPV2hZRHozRVVQaWZLOXpmM2Nac2hZRDJ3OE1jdGVUZkZKaUxPMzM2bGhCRjBNVW5laDFEaDQKOWhSVEp2NVUwNXd3bnRRbWc3bloxOFpJRGNFPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg=="
|
||||
privateKey: "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRREUxdFl2eWpEMWVhdmIKek1keTNzVm1KZUZoRFc4ejlBRk1uL0h1cGhhY0E2VFRtQkZsa3oyNmRFWnhoWC9CNHdZZ0tUR2doMVFaMjQ5UAorUXMwV0l1TzhsVnd0NFhXd1Y3ZTJBM0FjUnAyN01vUHlydE1oMHp4SjE1ejhrL29tcnR5bTR3TWR2SHVmY2JBCjhoTitxb3lxTVhVcGcyRGd2STdOeDVkTEJUSEhNSzdJQWF2RU5tMkIxQ3Jpd3pSWGFNVEFvUnpTOHF2REplRm8KT25ocnBDKzQ4UmloZWNyTE5kenM4MjRYeHZiU1ZYS0xXQ3JWL1JrUEU2eG9nd0x6MmpReXVtTE5VT2xaZHlWZQp2YjRBemxVVlVRVXZJbVlDb2o1dTRhRnhxSk8xRU4xbmJiTUlramtNVEc5WDJrOVN2WTdUc256WnFPdk5rU0owCmxDQ2Y1VGFKQWdNQkFBRUNnZ0VBTlk3ZktKR3pVSmVTazNQY3NQeThhVmJWUkRzTGp1OU5pelBaK0QxbzJYcUUKVnE2QnpVbUsramk1RWhwbzhMQjg4ak5ETlpLYzU0dytLbHh4R0FVZnMvMXNFZ2RkZTFtU0hzQmF2WW9JMFdNTQpkOCtKdHdENWhvUlh4dVNmcVlLd2pqYVVuSytnbEd2VXNKc3Rnc0dWRkpud2J5TTRNTzRkVFJDSnNmZnYyMnFCClNVRVh2MVFzRmhlbTBySnFuendPRlpycXVObXlvSnhTUzREYUJ4bmtqY3VHVG90Mk9qMlpVRjBTZW5HaU16NVkKRjZkR2dtWHNDTXBtc1k2czBrRUtDcExGMXIwS2VaUG8vZmVheU1GUDdONUtxVmxUSWExVGJjS2dQaWJFMGRqNwpXckhwR0l0Rm40ZHhJc2FCQlJ2dTlkZ2JJTVJuc0NhOFE1bURwbVg1SVFLQmdRRFVJQndFZjd1S0JqQ0M3OFFHClZzTmRLY1RuWTRrVDVxOFZiRlpVdkI2M052N0NDYmVCc1d0ZDRSMDFNUnkwVnhVUG4zekpWYk1DNUN5ckFHZ1EKTVRrVzlZQmVxb1A3aEk1ZnM5a1B5cG42SHlJN1NZS0ZldHF5RW5Ca1B4RklPSzUzRXJRWGxkZlRWbUdGdWpNUwpXYVpkOWpEOURmcGZKSzR6S2xuWHlXVlZkd0tCZ1FEdGpWVW9kNldRVEp5eHl1Vjk2Z29xbEl4L2t2UFNoQ2pqCkVqM2ZXY21HTGVBaUU2Z0lBNUh3YnFGM0tCSUVaSURpVVlJTjNkUUxLUlpVdXhDRkxpak9Ea3lBUkV1WFFUeFkKaUl0dWR3a3BmTS90OXdaQ09YSUczSWNXMDUvZHJrSzl5eXloMkRGTEJuZ3JERitoMzhDbWo0bExoc2RadDBQcgpXZjUxWnB6VC93S0JnR3d5aEpmMjN4MmowcEsyNFhHcVI3UDVYaW40Snk4emR4S2lVOWFjcmI0ZUd3dTJFUmZoCit6WERZVGFFZW5PeUIxZ1VyWDIwYkw2SXpBL2RBVGRoSkJHRjM1aHB1VEJOaUtGZ0J0TjdMOWJZa29sVEVYUXMKR2VqQ1p2bDdBY0dveDdTTW9iZDJBc1FWUjJFQ2ZKSmJqL1JWWXQ5d2hjaUoyU0RYOVVPUHdsUTVBb0dBWWp5VwpRTnZwemRqQTNBMktCaDRwQWg3WVUxR1VIelNrSy9NNVB3cEVlb2F5TDZWdFVaTVlZUk4vRm1XdHZiOUtSVTFyCnVReEpTaXc5bmVDV0hsMU9acGduTHN3UGJvZDl5eWI4Y2p3cnY4cHJ1bjd6U2FPejhmNTBwdzN4Q0oydDRBc0wKZEFxUnAvTU84czUxSmQ0QUwyRWdaK2xldTAwOGV6R0dOMHF1QkVVQ2dZRUF1VDZRS0FRUndxeWV4WlZPbmJPZwowZWVtelJSWDJYQStVV3J6YTRKbzV2MnZhYTR0cXhuQ2prQ1BaTldTc0diWUlTZVJNUUV0UXEwdUl0ZW9zMTFWCjJWM25BNXhwZ1ErbmZWNnpyQTFYK3VGekltWHpTbWgxSkpkK0RGUVhNT2xBWWNGY2J2aFdYWUVoaS9SR01DT3kKdU1MNWxBa2swV2JXRHFicFNGM1RMMlk9Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0="
|
||||
proxyHost: ""
|
||||
proxyPort: ""
|
||||
proxyUser: ""
|
||||
proxyPassword: ""
|
||||
|
||||
backendUrl: "https://calc.kamma.cz/add?x=543&y=123"
|
||||
13
src/main/resources/log4j2.xml
Normal file
13
src/main/resources/log4j2.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="WARN">
|
||||
<Appenders>
|
||||
<Console name="Console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n"/>
|
||||
</Console>
|
||||
</Appenders>
|
||||
<Loggers>
|
||||
<Root level="debug">
|
||||
<AppenderRef ref="Console"/>
|
||||
</Root>
|
||||
</Loggers>
|
||||
</Configuration>
|
||||
Loading…
x
Reference in New Issue
Block a user