From 4d886d218c923b61295fdcecb6fc6ff667bbd69a Mon Sep 17 00:00:00 2001 From: Ewan Cahen Date: Fri, 15 Nov 2024 16:14:12 +0100 Subject: [PATCH 1/2] feat: on refreshing auth token add check to see if user is admin --- authentication/README.md | 3 +- .../rsd/authentication/JwtCreator.java | 17 +++--- .../rsd/authentication/PostgrestAccount.java | 27 ++++++++++ .../authentication/PostgrestAccountTest.java | 54 +++++++++++++++++++ docker-compose.yml | 2 +- 5 files changed, 95 insertions(+), 8 deletions(-) create mode 100644 authentication/src/test/java/nl/esciencecenter/rsd/authentication/PostgrestAccountTest.java diff --git a/authentication/README.md b/authentication/README.md index 3324e8fd5..ee4192b49 100644 --- a/authentication/README.md +++ b/authentication/README.md @@ -28,7 +28,8 @@ extra_hosts: - "host.docker.internal:host-gateway" ``` 2. In `nginx.conf`, replace `server auth:7000;` with `server host.docker.internal:7000;` -3. (Optionally) allow TCP traffic on port 7000 of your firewall if logging in seems to hang forever or if you get `504 Gateway Timeout` responses. +3. (Optional) For refreshing your tokens to work, set `RSD_AUTH_URL=http://nginx/auth` in your `.env`. +4. (Optional) Allow TCP traffic on port 7000 of your firewall if signing in seems to hang forever or if you get `504 Gateway Timeout` responses. Remember to undo these changes before committing! diff --git a/authentication/src/main/java/nl/esciencecenter/rsd/authentication/JwtCreator.java b/authentication/src/main/java/nl/esciencecenter/rsd/authentication/JwtCreator.java index 56d3ad08a..a200d81f6 100644 --- a/authentication/src/main/java/nl/esciencecenter/rsd/authentication/JwtCreator.java +++ b/authentication/src/main/java/nl/esciencecenter/rsd/authentication/JwtCreator.java @@ -14,26 +14,28 @@ import com.auth0.jwt.interfaces.DecodedJWT; import com.google.gson.Gson; +import java.io.IOException; import java.util.Date; import java.util.Map; import java.util.Objects; +import java.util.UUID; public class JwtCreator { static final long ONE_HOUR_IN_MILLISECONDS = 3600_000L; // 60 * 60 * 1000 - private final String signingSecret; private final Algorithm signingAlgorithm; + private static final String RSD_ADMIN_ROLE = "rsd_admin"; + private static final String RSD_USER_ROLE = "rsd_user"; public JwtCreator(String signingSecret) { Objects.requireNonNull(signingSecret); - this.signingSecret = signingSecret; - this.signingAlgorithm = Algorithm.HMAC256(this.signingSecret); + this.signingAlgorithm = Algorithm.HMAC256(signingSecret); } String createUserJwt(AccountInfo accountInfo) { return JWT.create() .withClaim("iss", "rsd_auth") - .withClaim("role", accountInfo.isAdmin() ? "rsd_admin" : "rsd_user") + .withClaim("role", accountInfo.isAdmin() ? RSD_ADMIN_ROLE : RSD_USER_ROLE) .withClaim("account", accountInfo.account().toString()) .withClaim("name", accountInfo.name()) .withClaim("data", accountInfo.data()) @@ -45,19 +47,22 @@ String createUserJwt(AccountInfo accountInfo) { String createAdminJwt() { return JWT.create() .withClaim("iss", "rsd_auth") - .withClaim("role", "rsd_admin") + .withClaim("role", RSD_ADMIN_ROLE) .withExpiresAt(new Date(System.currentTimeMillis() + ONE_HOUR_IN_MILLISECONDS)) .sign(signingAlgorithm); } - String refreshToken(String token) { + String refreshToken(String token) throws IOException, InterruptedException { DecodedJWT oldJwt = JWT.decode(token); + UUID accountId = UUID.fromString(oldJwt.getClaim("account").asString()); + boolean isAdmin = PostgrestAccount.isAdmin(accountId); String payloadEncoded = oldJwt.getPayload(); String payloadDecoded = Main.decode(payloadEncoded); Gson gson = new Gson(); Map claimsMap = gson.>fromJson(payloadDecoded, Map.class); return JWT.create() .withPayload(claimsMap) + .withClaim("role", isAdmin ? RSD_ADMIN_ROLE : RSD_USER_ROLE) .withExpiresAt(new Date(System.currentTimeMillis() + ONE_HOUR_IN_MILLISECONDS)) .sign(signingAlgorithm); } diff --git a/authentication/src/main/java/nl/esciencecenter/rsd/authentication/PostgrestAccount.java b/authentication/src/main/java/nl/esciencecenter/rsd/authentication/PostgrestAccount.java index 4670dd859..a4b39181b 100644 --- a/authentication/src/main/java/nl/esciencecenter/rsd/authentication/PostgrestAccount.java +++ b/authentication/src/main/java/nl/esciencecenter/rsd/authentication/PostgrestAccount.java @@ -8,6 +8,7 @@ package nl.esciencecenter.rsd.authentication; import com.google.gson.JsonArray; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; @@ -100,6 +101,32 @@ else if (accountsWithSub.size() == 1) { } } + public static boolean isAdmin(UUID accountId) throws IOException, InterruptedException { + Objects.requireNonNull(accountId); + String backendUri = Config.backendBaseUrl(); + URI accountUrl = URI.create(backendUri + "/admin_account?account_id=eq.%s".formatted(accountId)); + JwtCreator jwtCreator = new JwtCreator(Config.jwtSigningSecret()); + String token = jwtCreator.createAdminJwt(); + + String response = getAsAdmin(accountUrl, token); + return parseIsAdminResponse(accountId, response); + } + + static boolean parseIsAdminResponse(UUID accountId, String response) { + Objects.requireNonNull(accountId); + Objects.requireNonNull(response); + JsonElement jsonTree = JsonParser.parseString(response); + return jsonTree.isJsonArray() + && jsonTree.getAsJsonArray().size() == 1 + && jsonTree.getAsJsonArray().get(0).getAsJsonObject().get("account_id").isJsonPrimitive() + && jsonTree.getAsJsonArray() + .get(0) + .getAsJsonObject() + .getAsJsonPrimitive("account_id") + .getAsString() + .equals(accountId.toString()); + } + public void coupleLogin(UUID accountId, OpenIdInfo openIdInfo, OpenidProvider provider) throws IOException, InterruptedException { String backendUri = Config.backendBaseUrl(); JwtCreator jwtCreator = new JwtCreator(Config.jwtSigningSecret()); diff --git a/authentication/src/test/java/nl/esciencecenter/rsd/authentication/PostgrestAccountTest.java b/authentication/src/test/java/nl/esciencecenter/rsd/authentication/PostgrestAccountTest.java new file mode 100644 index 000000000..0f4841641 --- /dev/null +++ b/authentication/src/test/java/nl/esciencecenter/rsd/authentication/PostgrestAccountTest.java @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: 2024 Ewan Cahen (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +package nl.esciencecenter.rsd.authentication; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +class PostgrestAccountTest { + + @Test + void givenEmtpyArray_whenCheckingIfAdmin_thenFalseReturned() { + String emptyArray = "[]"; + UUID adminUuid = UUID.randomUUID(); + + Assertions.assertFalse(PostgrestAccount.parseIsAdminResponse(adminUuid, emptyArray)); + } + + @Test + void givenResponseWithNullValue_whenCheckingIfAdmin_thenFalseReturned() { + String emptyArray = "[{\"account_id\": null}]"; + UUID adminUuid = UUID.randomUUID(); + + Assertions.assertFalse(PostgrestAccount.parseIsAdminResponse(adminUuid, emptyArray)); + } + + @Test + void givenArrayOfSizeOneWithCorrectUuid_whenCheckingIfAdmin_thenTrueReturned() { + UUID adminUuid = UUID.randomUUID(); + String successResponse = "[{\"account_id\": \"%s\"}]".formatted(adminUuid); + + Assertions.assertTrue(PostgrestAccount.parseIsAdminResponse(adminUuid, successResponse)); + } + + @Test + void givenArrayOfSizeOneWithIncorrectUuid_whenCheckingIfAdmin_thenFalseReturned() { + UUID adminUuid = UUID.randomUUID(); + String successResponse = "[{\"account_id\": \"%s\"}]".formatted(UUID.randomUUID()); + + Assertions.assertFalse(PostgrestAccount.parseIsAdminResponse(adminUuid, successResponse)); + } + + @Test + void givenArrayOfSizeTwoWithCorrectUuid_whenCheckingIfAdmin_thenFalseReturned() { + UUID adminUuid = UUID.randomUUID(); + String wrongIdResponse = "[{\"account_id\": \"%s\"}, {\"account_id\": \"%s\"}]".formatted(adminUuid, UUID.randomUUID()); + + Assertions.assertFalse(PostgrestAccount.parseIsAdminResponse(adminUuid, wrongIdResponse)); + } +} diff --git a/docker-compose.yml b/docker-compose.yml index b1d47ba05..80a6e1e13 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -53,7 +53,7 @@ services: auth: build: ./authentication - image: rsd/auth:1.5.0 + image: rsd/auth:1.6.0 ports: - 5005:5005 expose: From ca2e70461d157ea2208ae612135efa17da3a979b Mon Sep 17 00:00:00 2001 From: Ewan Cahen Date: Mon, 18 Nov 2024 11:48:31 +0100 Subject: [PATCH 2/2] chore: upgrade auth dependencies --- authentication/pom.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/authentication/pom.xml b/authentication/pom.xml index 418113192..82be58ade 100644 --- a/authentication/pom.xml +++ b/authentication/pom.xml @@ -34,7 +34,7 @@ SPDX-License-Identifier: Apache-2.0 org.apache.maven.plugins maven-dependency-plugin - 3.7.1 + 3.8.1 @@ -101,21 +101,21 @@ SPDX-License-Identifier: Apache-2.0 io.javalin javalin - 6.2.0 + 6.3.0 org.jetbrains annotations - 24.1.0 + 26.0.1 org.slf4j slf4j-simple - 2.0.13 + 2.0.16 @@ -143,14 +143,14 @@ SPDX-License-Identifier: Apache-2.0 org.slf4j slf4j-api - 2.0.13 + 2.0.16 ch.qos.logback logback-classic - 1.5.6 + 1.5.12