Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auth with Keycloak #183

Merged
merged 20 commits into from
Apr 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 17 additions & 11 deletions backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,18 @@
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.17.0</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-keycloak-authorization</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-panache</artifactId>
Expand All @@ -96,6 +108,11 @@
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.18.1</version>
</dependency>
</dependencies>
<build>
<plugins>
Expand Down Expand Up @@ -123,17 +140,6 @@
</compilerArgs>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<configuration>
<reportsDirectory>${project.build.directory}/surefire-reports</reportsDirectory>
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire-plugin.version}</version>
Expand Down
25 changes: 19 additions & 6 deletions backend/src/main/java/fr/zelytra/session/SessionSocket.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
import fr.zelytra.session.server.SotServer;
import fr.zelytra.session.socket.MessageType;
import fr.zelytra.session.socket.SocketMessage;
import fr.zelytra.session.socket.security.SocketSecurityEntity;
import io.quarkus.logging.Log;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.websocket.*;
import jakarta.websocket.server.PathParam;
Expand All @@ -21,8 +21,8 @@
import java.io.IOException;
import java.util.concurrent.*;

@ServerEndpoint(value = "/sessions/{sessionId}") // WebSocket endpoint
@ApplicationScoped
// WebSocket endpoint
@ServerEndpoint(value = "/sessions/{token}/{sessionId}")
public class SessionSocket {

private final ExecutorService executor = Executors.newSingleThreadExecutor();
Expand All @@ -32,6 +32,9 @@ public class SessionSocket {
@ConfigProperty(name = "app.version")
String appVersion;

@ConfigProperty(name = "quarkus.oidc.auth-server-url")
String realmURL;

@Inject
SessionManager sessionManager;

Expand All @@ -58,7 +61,7 @@ public void onOpen(Session session) {


@OnMessage
public void onMessage(String message, Session session, @PathParam("sessionId") String sessionId) throws IOException {
public void onMessage(String message, Session session, @PathParam("sessionId") String sessionId, @PathParam("token") String token) throws IOException {

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
Expand All @@ -73,7 +76,7 @@ public void onMessage(String message, Session session, @PathParam("sessionId") S
switch (socketMessage.messageType()) {
case CONNECT -> {
Player player = objectMapper.convertValue(socketMessage.data(), Player.class);
handleConnectMessage(player, session, sessionId);
handleConnectMessage(player, session, sessionId, token);
}
case UPDATE -> {
Player player = objectMapper.convertValue(socketMessage.data(), Player.class);
Expand Down Expand Up @@ -136,13 +139,23 @@ private void handleLeaveServerMessage(Session session, SotServer sotServer) {
}

// Extracted method to handle CONNECT messages
private void handleConnectMessage(Player player, Session session, String sessionId) {
public void handleConnectMessage(Player player, Session session, String sessionId, String token) throws IOException {
// Cancel the timeout task since we've received the message
Future<?> timeoutTask = sessionTimeoutTasks.remove(session.getId());
if (timeoutTask != null) {
timeoutTask.cancel(true);
}

// Checking security
SocketSecurityEntity socketSecurity = SocketSecurityEntity.websocketUser.get(token);
if (socketSecurity == null || !socketSecurity.isValid()) {
Log.info("Invalid token, session will be closed");
sessionManager.sendDataToPlayer(session, MessageType.CONNECTION_REFUSED, null);
session.close();
return;
}
SocketSecurityEntity.websocketUser.remove(token);

// Refuse connection from client with different version
if (player.getClientVersion() == null || !player.getClientVersion().equalsIgnoreCase(appVersion)) {
Log.warn("[" + player.getUsername() + "] Client is out of date, connection refused (" + player.getClientVersion() + ")");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ public enum MessageType {
OUTDATED_CLIENT,
SESSION_NOT_FOUND,
KEEP_ALIVE,
CONNECTION_REFUSED,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package fr.zelytra.session.socket.security;

import io.quarkus.logging.Log;
import io.quarkus.security.Authenticated;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;

@Authenticated
@Path("/socket")
public class SocketSecurityEndpoints {

@GET
@Path("/register")
@Produces(MediaType.TEXT_PLAIN)
public Response registerClient() {
Log.info("[GET] /socket/register");
SocketSecurityEntity socketSecurity = new SocketSecurityEntity();
return Response.ok(socketSecurity.getKey()).build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package fr.zelytra.session.socket.security;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

public class SocketSecurityEntity {

public static final Map<String, SocketSecurityEntity> websocketUser = new HashMap<>();

private final String key;

private final long validity;

public SocketSecurityEntity() {
this.validity = new Date().toInstant().plusSeconds(30).toEpochMilli();
this.key = UUID.randomUUID().toString();
websocketUser.put(this.key, this);
}

public boolean isValid() {
return this.validity >= new Date().toInstant().toEpochMilli();
}

public String getKey() {
return key;
}
}
20 changes: 18 additions & 2 deletions backend/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,21 @@ quarkus.hibernate-orm.database.generation=update
%test.quarkus.hibernate-orm.dialect=org.hibernate.dialect.H2Dialect
%test.quarkus.http.port=9090

proxy.check.api.key=${PROXY_CHECK_API_KEY:test}
app.version=0.1.5
# OIDC Configuration
%prod,dev.quarkus.oidc.auth-server-url=${KEYCLOAK_HOST:http://127.0.0.1:2604/auth}/realms/Betterfleet
%prod,dev.quarkus.oidc.client-id=${KEYCLOAK_CLIENT_ID:application}

# Debug
#quarkus.log.category."io.quarkus.oidc".level=DEBUG
#quarkus.log.category."io.smallrye.jwt".level=DEBUG

# Custom variables
proxy.check.api.key=${PROXY_CHECK_API_KEY}
app.version=0.1.5

# CORS configuration
quarkus.http.cors=true
quarkus.http.cors.origins=*
quarkus.http.cors.methods=GET,PUT,POST,DELETE,OPTIONS
quarkus.http.cors.access-control-max-age=24H
quarkus.http.cors.exposed-headers=Access-Control-Allow-Origin,Access-Control-Allow-Credentials
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import fr.zelytra.session.fleet.Fleet;
import fr.zelytra.session.player.Player;
import fr.zelytra.session.socket.MessageType;
import fr.zelytra.session.socket.security.SocketSecurityEntity;
import io.quarkus.test.InjectMock;
import io.quarkus.test.common.http.TestHTTPResource;
import io.quarkus.test.junit.QuarkusTest;
Expand Down Expand Up @@ -50,18 +51,22 @@ class SessionSocketTest {
void setup() throws URISyntaxException, DeploymentException, IOException {
Mockito.doReturn(null).when(executorService).submit(any(Runnable.class));
String sessionId = sessionManager.createSession();
this.uri = new URI("ws://" + websocketEndpoint.getHost() + ":" + websocketEndpoint.getPort() + "/sessions/" + sessionId);
SocketSecurityEntity socketSecurity = new SocketSecurityEntity();
this.uri = new URI("ws://" + websocketEndpoint.getHost() + ":" + websocketEndpoint.getPort() + "/sessions/" + socketSecurity.getKey() + "/" + sessionId);
betterFleetClient = new BetterFleetClient();
ContainerProvider.getWebSocketContainer().connectToServer(betterFleetClient, uri);
}

@Test
void stressTest() throws IOException, InterruptedException, EncodeException, DeploymentException {
void stressTest() throws IOException, InterruptedException, EncodeException, DeploymentException, URISyntaxException {
List<Player> fakePlayers = generateFakePlayer(50);
String fleetId = "";
for (Player player : fakePlayers) {

BetterFleetClient playerClient = new BetterFleetClient();
SocketSecurityEntity socketSecurity = new SocketSecurityEntity();
URI uri = new URI("ws://" + websocketEndpoint.getHost() + ":" + websocketEndpoint.getPort() + "/sessions/" + socketSecurity.getKey() + "/" + fleetId);

ContainerProvider.getWebSocketContainer().connectToServer(playerClient, uri);
playerClient.sendMessage(MessageType.CONNECT, player);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package fr.zelytra.session.socket.security;

import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import org.junit.jupiter.api.Test;

@QuarkusTest
public class SocketSecurityEndpointsTest {

private static final String RANDOM_BEARER_TOKEN = "337aab0f-b547-489b-9dbd-a54dc7bdf20d";

@Test
void testPermitAll() {
RestAssured.given()
.when()
.header("Authorization", "Bearer: " + RANDOM_BEARER_TOKEN)
.get("/socket/register")
.then()
.statusCode(401);
}
}
6 changes: 5 additions & 1 deletion deployment/.env
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
POSTGRES_USER=
POSTGRES_PASSWORD=
PUBLIC_QUARKUS_HOSTNAME=
PUBLIC_QUARKUS_HOSTNAME=
KEYCLOAK_PASSWORD=
PUBLIC_KEYCLOAK_HOSTNAME=
MICROSOFT_CLIENT_ID=
MICROSOFT_CLIENT_SECRET=
8 changes: 7 additions & 1 deletion deployment/dev/.env
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
POSTGRES_USER=m7CaHqTPqojQceii
POSTGRES_PASSWORD=A7aXfgh4@BM#7x85
POSTGRES_PASSWORD=A7aXfgh4@BM#7x85
KEYCLOAK_USER=admin
PUBLIC_QUARKUS_HOSTNAME=http://127.0.0.1:2601
KEYCLOAK_PASSWORD=pa$$w0rd
PUBLIC_KEYCLOAK_HOSTNAME=http://127.0.0.1:2604
MICROSOFT_CLIENT_ID=123
MICROSOFT_CLIENT_SECRET=123
47 changes: 45 additions & 2 deletions deployment/dev/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,55 @@ services:
ports:
- "2600:5432"
volumes:
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
- /psql-data/app:/var/lib/postgresql/data
- ./psql-data/app:/var/lib/postgresql/data
- /etc/localtime:/etc/localtime:ro
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: BetterFleet
PGDATA: /var/lib/postgresql/data
restart: unless-stopped

postgres-auth:
image: postgres:14.2-alpine
container_name: betterfleet-postgres-auth
ports:
- "2603:5432"
volumes:
- ./psql-data/auth:/var/lib/postgresql/data/pgdata
- /etc/localtime:/etc/localtime:ro
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: Keycloak
PGDATA: /var/lib/postgresql/data/pgdata
restart: unless-stopped

keycloak:
image: quay.io/keycloak/keycloak:latest
container_name: betterfleet-keycloak
depends_on:
- postgres-auth
ports:
- "2604:8080"
volumes:
- ./keycloak/config:/opt/keycloak/data/import
- /etc/localtime:/etc/localtime:ro
command: start --import-realm
environment:
KC_HOSTNAME_STRICT: "false"
KC_HTTP_RELATIVE_PATH: /auth
KC_DB: postgres
KC_DB_URL_HOST: postgres-auth
PROXY_ADDRESS_FORWARDING: "true"
KC_PROXY: edge
MICROSOFT_CLIENT_ID: ${MICROSOFT_CLIENT_ID}
MICROSOFT_CLIENT_SECRET: ${MICROSOFT_CLIENT_SECRET}
KC_DB_URL_DATABASE: Keycloak
KC_VAULT_FILE: /var/keycloak/vault
KC_DB_USERNAME: ${POSTGRES_USER}
KC_DB_PASSWORD: ${POSTGRES_PASSWORD}
KEYCLOAK_ADMIN: ${KEYCLOAK_USER}
KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_PASSWORD}
KC_OVERRIDE: "false"
restart: unless-stopped
Loading
Loading