Skip to content

Commit 7fcf779

Browse files
committed
initial commit
1 parent 35b710e commit 7fcf779

File tree

14 files changed

+480
-1
lines changed

14 files changed

+480
-1
lines changed

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* text=auto eol=lf

.github/workflows/pipeline.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: PIPELINE
2+
3+
on:
4+
push:
5+
branches:
6+
- 'master'
7+
workflow_dispatch:
8+
9+
jobs:
10+
bump:
11+
uses: UnterrainerInformatik/bump-semver-workflow/.github/workflows/workflow.yml@master
12+
13+
build:
14+
name: Build and publish to Maven Central 🚀
15+
needs: bump
16+
uses: UnterrainerInformatik/maven-central-workflow/.github/workflows/workflow.yml@master
17+
with:
18+
major_version: ${{ needs.bump.outputs.major_version }}
19+
minor_version: ${{ needs.bump.outputs.minor_version }}
20+
build_version: ${{ needs.bump.outputs.build_version }}
21+
maven_profiles: release-to-sonatype
22+
maven_args: -Dmaven.test.skip=true
23+
secrets:
24+
SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
25+
SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
26+
GPG_SECRET_KEY: ${{ secrets.GPG_SECRET_KEY }}
27+
GPG_OWNERTRUST: ${{ secrets.GPG_OWNERTRUST }}
28+
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}

.gitignore

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,14 @@
2121

2222
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
2323
hs_err_pid*
24-
replay_pid*
24+
/target/
25+
26+
src/main/java/info/unterrainer/commons/jreutils/Information.java
27+
28+
.settings/
29+
30+
.classpath
31+
32+
bin/
33+
34+
dependency-reduced-pom.xml

.project

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<projectDescription>
3+
<name>serialization</name>
4+
<comment></comment>
5+
<projects>
6+
</projects>
7+
<buildSpec>
8+
<buildCommand>
9+
<name>org.eclipse.jdt.core.javabuilder</name>
10+
<arguments>
11+
</arguments>
12+
</buildCommand>
13+
<buildCommand>
14+
<name>org.eclipse.m2e.core.maven2Builder</name>
15+
<arguments>
16+
</arguments>
17+
</buildCommand>
18+
</buildSpec>
19+
<natures>
20+
<nature>org.eclipse.jdt.core.javanature</nature>
21+
<nature>org.eclipse.m2e.core.maven2Nature</nature>
22+
</natures>
23+
</projectDescription>

Information.template

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package @package@;
2+
3+
public class Information {
4+
public static final String name = "@name@";
5+
public static final String buildTime = "@buildTime@";
6+
public static final String pomVersion = "@pomVersion@";
7+
}

pom.xml

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<project xmlns="http://maven.apache.org/POM/4.0.0"
2+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
5+
<parent>
6+
<groupId>info.unterrainer.commons</groupId>
7+
<artifactId>parent-javalin-pom</artifactId>
8+
<version>1.0.2</version>
9+
</parent>
10+
11+
<modelVersion>4.0.0</modelVersion>
12+
<artifactId>websocket-server</artifactId>
13+
<version>1.0.0</version>
14+
<name>WebsocketServer</name>
15+
<packaging>jar</packaging>
16+
17+
<properties>
18+
<mainclass>info.unterrainer.commons.websocketserver.WebsocketServer</mainclass>
19+
<name>Websocket-Server</name>
20+
<package-path>info/unterrainer/commons/websocketserver</package-path>
21+
<packg-string>info.unterrainer.commons.websocketserver</packg-string>
22+
</properties>
23+
24+
<dependencies>
25+
<!--Websocket Server-->
26+
<!-- And add org.keycloak:keycloak-core to ignoredUnused below...-->
27+
<dependency>
28+
<groupId>org.eclipse.jetty.websocket</groupId>
29+
<artifactId>websocket-api</artifactId>
30+
<version>9.4.38.v20210224</version>
31+
<scope>compile</scope>
32+
</dependency>
33+
<dependency>
34+
<groupId>org.keycloak</groupId>
35+
<artifactId>keycloak-admin-client</artifactId>
36+
<version>26.0.4</version>
37+
</dependency>
38+
<dependency>
39+
<groupId>info.unterrainer.commons</groupId>
40+
<artifactId>http-server</artifactId>
41+
<version>1.0.0</version>
42+
</dependency>
43+
</dependencies>
44+
45+
<build>
46+
<pluginManagement>
47+
<plugins>
48+
<plugin>
49+
<groupId>org.apache.maven.plugins</groupId>
50+
<artifactId>maven-dependency-plugin</artifactId>
51+
<executions>
52+
<execution>
53+
<id>analyze</id>
54+
<configuration>
55+
<ignoredUsedUndeclaredDependencies
56+
combine.children="append">
57+
<ignoredUsedUndeclaredDependencies>org.keycloak:keycloak-core</ignoredUsedUndeclaredDependencies>
58+
<ignoredUsedUndeclaredDependencies>org.keycloak:keycloak-common</ignoredUsedUndeclaredDependencies>
59+
</ignoredUsedUndeclaredDependencies>
60+
</configuration>
61+
</execution>
62+
</executions>
63+
</plugin>
64+
</plugins>
65+
</pluginManagement>
66+
</build>
67+
</project>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package info.unterrainer.websocketserver;
2+
3+
import java.util.Set;
4+
import java.util.concurrent.ConcurrentHashMap;
5+
6+
import io.javalin.websocket.WsConnectContext;
7+
import io.javalin.websocket.WsMessageContext;
8+
import lombok.extern.slf4j.Slf4j;
9+
10+
@Slf4j
11+
public class AiComm extends WsOauthHandlerBase {
12+
13+
private Set<WsConnectContext> connectedWsClients = ConcurrentHashMap.newKeySet();
14+
15+
@Override
16+
public void onConnect(WsConnectContext ctx) throws Exception {
17+
super.onConnect(ctx);
18+
connectedWsClients.add(ctx);
19+
ctx.send("Welcome to our websocket-server!");
20+
}
21+
22+
@Override
23+
public void onMessage(WsMessageContext ctx) throws Exception {
24+
super.onMessage(ctx);
25+
26+
// Broadcast to all connected WS clients.
27+
for (WsConnectContext client : connectedWsClients) {
28+
if (client.session.isOpen()) {
29+
client.send("Echo from server: [" + ctx.message() + "]");
30+
}
31+
}
32+
}
33+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package info.unterrainer.websocketserver;
2+
3+
import java.io.IOException;
4+
import java.math.BigInteger;
5+
import java.net.URI;
6+
import java.net.http.HttpClient;
7+
import java.net.http.HttpRequest;
8+
import java.net.http.HttpResponse;
9+
import java.security.KeyFactory;
10+
import java.security.PublicKey;
11+
import java.security.spec.RSAPublicKeySpec;
12+
import java.util.Base64;
13+
14+
import org.keycloak.TokenVerifier;
15+
import org.keycloak.common.VerificationException;
16+
import org.keycloak.representations.AccessToken;
17+
18+
import com.fasterxml.jackson.databind.JsonNode;
19+
import com.fasterxml.jackson.databind.ObjectMapper;
20+
21+
import info.unterrainer.commons.httpserver.exceptions.ForbiddenException;
22+
import info.unterrainer.commons.httpserver.exceptions.UnauthorizedException;
23+
import lombok.RequiredArgsConstructor;
24+
import lombok.extern.slf4j.Slf4j;
25+
26+
@Slf4j
27+
@RequiredArgsConstructor
28+
public class JwtTokenHandler {
29+
30+
private final String host;
31+
private final String realm;
32+
33+
private String authUrl;
34+
private PublicKey publicKey = null;
35+
36+
public void initPublicKey() {
37+
String correctedHost = host;
38+
String correctedRealm = realm;
39+
40+
if (publicKey != null)
41+
return;
42+
if (!correctedHost.endsWith("/"))
43+
correctedHost += "/";
44+
if (!correctedRealm.startsWith("/"))
45+
correctedRealm = "/" + correctedRealm;
46+
47+
authUrl = correctedHost + "realms" + correctedRealm + "/protocol/openid-connect/certs";
48+
try {
49+
log.info("Getting public key from: [{}]", authUrl);
50+
publicKey = fetchPublicKey(authUrl);
51+
} catch (Exception e) {
52+
log.error("There was an error fetching the PublicKey from the openIdConnect-server [{}].", authUrl);
53+
throw new IllegalStateException(e);
54+
}
55+
}
56+
57+
private PublicKey fetchPublicKey(String jwksUrl) throws Exception {
58+
ObjectMapper objectMapper = new ObjectMapper();
59+
HttpClient client = HttpClient.newHttpClient();
60+
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(jwksUrl)).GET().build();
61+
62+
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
63+
64+
if (response.statusCode() >= 300) {
65+
throw new IOException("Failed to fetch JWKS: HTTP " + response.statusCode());
66+
}
67+
68+
JsonNode jwks = objectMapper.readTree(response.body());
69+
// Just take the first key for now.
70+
JsonNode key = jwks.get("keys").get(0);
71+
72+
String modulusBase64 = key.get("n").asText();
73+
String exponentBase64 = key.get("e").asText();
74+
75+
byte[] modulusBytes = Base64.getUrlDecoder().decode(modulusBase64);
76+
byte[] exponentBytes = Base64.getUrlDecoder().decode(exponentBase64);
77+
78+
BigInteger modulus = new BigInteger(1, modulusBytes);
79+
BigInteger exponent = new BigInteger(1, exponentBytes);
80+
81+
RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, exponent);
82+
KeyFactory factory = KeyFactory.getInstance("RSA");
83+
return factory.generatePublic(spec);
84+
}
85+
86+
public void checkAccess(String authorizationHeader) {
87+
try {
88+
TokenVerifier<AccessToken> tokenVerifier = persistUserInfoInContext(authorizationHeader);
89+
if (tokenVerifier == null)
90+
throw new UnauthorizedException();
91+
92+
initPublicKey();
93+
tokenVerifier.publicKey(publicKey);
94+
try {
95+
tokenVerifier.verifySignature();
96+
} catch (VerificationException e) {
97+
throw new UnauthorizedException(
98+
"Error verifying token from user with publicKey obtained from keycloak.", e);
99+
}
100+
101+
try {
102+
tokenVerifier.verify();
103+
throw new ForbiddenException();
104+
} catch (VerificationException e) {
105+
throw new ForbiddenException();
106+
}
107+
} catch (Exception e) {
108+
log.error("Error checking token.", e);
109+
throw e;
110+
}
111+
}
112+
113+
private TokenVerifier<AccessToken> persistUserInfoInContext(String authorizationHeader) {
114+
if (authorizationHeader == null || authorizationHeader.isBlank())
115+
return null;
116+
117+
try {
118+
TokenVerifier<AccessToken> tokenVerifier = TokenVerifier.create(authorizationHeader, AccessToken.class);
119+
AccessToken token = tokenVerifier.getToken();
120+
if (!token.isActive()) {
121+
log.warn("Token is inactive.");
122+
return null;
123+
}
124+
// Disabled to enable getting token from side-channels like 'localhost'.
125+
/*
126+
* if (!token.getIssuer().equalsIgnoreCase(authUrl)) {
127+
* setTokenRejectionReason(ctx, "Token has wrong real-url."); return null; }
128+
*/
129+
return tokenVerifier;
130+
131+
} catch (VerificationException e) {
132+
log.warn("Token was checked and deemed invalid.", e);
133+
return null;
134+
}
135+
}
136+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package info.unterrainer.websocketserver;
2+
3+
import java.util.HashSet;
4+
import java.util.function.Consumer;
5+
6+
import org.jetbrains.annotations.NotNull;
7+
8+
import io.javalin.Javalin;
9+
import io.javalin.websocket.WsHandler;
10+
import lombok.extern.slf4j.Slf4j;
11+
12+
@Slf4j
13+
public class WebsocketServer {
14+
15+
private String keycloakHost;
16+
private String realm;
17+
private JwtTokenHandler tokenHandler;
18+
19+
private Javalin wss;
20+
private boolean isOauthEnabled = false;
21+
22+
public WebsocketServer() {
23+
wss = Javalin.create();
24+
}
25+
26+
public WebsocketServer(String keycloakHost, String keycloakRealm) {
27+
this.keycloakHost = keycloakHost;
28+
this.realm = keycloakRealm;
29+
30+
try {
31+
tokenHandler = new JwtTokenHandler(this.keycloakHost, this.realm);
32+
tokenHandler.initPublicKey();
33+
wss = Javalin.create();
34+
isOauthEnabled = true;
35+
} catch (Exception e) {
36+
// Exceptions will terminate a request later on, but should not terminate the
37+
// main-thread here.
38+
}
39+
}
40+
41+
public WebsocketServer start(int port) {
42+
wss.start(port);
43+
log.debug("Websocket server started on port: {}", port);
44+
return this;
45+
}
46+
47+
public WebsocketServer ws(@NotNull String path, @NotNull Consumer<WsHandler> ws) {
48+
wss.ws(path, ws, new HashSet<>());
49+
return this;
50+
}
51+
52+
public WebsocketServer wsOauth(@NotNull String path, @NotNull WsOauthHandlerBase handler) {
53+
if (!isOauthEnabled) {
54+
throw new IllegalStateException("Websocket server is not configured for OAuth2/JWT support.");
55+
}
56+
57+
handler.setTokenHandler(tokenHandler);
58+
wss.ws(path, ws -> {
59+
ws.onConnect(handler::onConnect);
60+
ws.onMessage(handler::onMessage);
61+
ws.onClose(handler::onClose);
62+
ws.onError(handler::onError);
63+
});
64+
return this;
65+
}
66+
}

0 commit comments

Comments
 (0)