| New file |
| | |
| | | /* |
| | | * The contents of this file are subject to the terms of the Common Development and |
| | | * Distribution License (the License). You may not use this file except in compliance with the |
| | | * License. |
| | | * |
| | | * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the |
| | | * specific language governing permission and limitations under the License. |
| | | * |
| | | * When distributing Covered Software, include this CDDL Header Notice in each file and include |
| | | * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL |
| | | * Header, with the fields enclosed by brackets [] replaced by your own identifying |
| | | * information: "Portions copyright [year] [name of copyright owner]". |
| | | * |
| | | * Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap.authz; |
| | | |
| | | import static org.assertj.core.api.Assertions.assertThat; |
| | | import static org.forgerock.opendj.rest2ldap.authz.Authorization.newFileAccessTokenResolver; |
| | | |
| | | import java.io.IOException; |
| | | import java.nio.charset.StandardCharsets; |
| | | import java.nio.file.Files; |
| | | import java.nio.file.Path; |
| | | import java.util.Comparator; |
| | | |
| | | import org.forgerock.http.oauth2.AccessTokenException; |
| | | import org.forgerock.http.oauth2.AccessTokenInfo; |
| | | import org.forgerock.http.oauth2.AccessTokenResolver; |
| | | import org.forgerock.testng.ForgeRockTestCase; |
| | | import org.testng.annotations.AfterMethod; |
| | | import org.testng.annotations.BeforeMethod; |
| | | import org.testng.annotations.Test; |
| | | |
| | | /** |
| | | * The file resolver treats the bearer token as the name of a file inside the configured token directory. The |
| | | * token comes verbatim from the {@code Authorization} header of an unauthenticated request, so a token which is |
| | | * not a plain file name must never select a file elsewhere on the file system (GHSA-c5gp-3768-jc59). |
| | | */ |
| | | @Test |
| | | @SuppressWarnings("javadoc") |
| | | public class FileAccessTokenResolverTest extends ForgeRockTestCase { |
| | | |
| | | private static final String VALID_TOKEN_JSON = |
| | | "{ \"scope\": [ \"read\" ], \"expireTime\": " + (System.currentTimeMillis() + 3_600_000L) + " }"; |
| | | |
| | | private Path root; |
| | | private Path tokenDir; |
| | | private Path outsideToken; |
| | | private AccessTokenResolver resolver; |
| | | |
| | | @BeforeMethod |
| | | public void setUp() throws Exception { |
| | | root = Files.createTempDirectory("file-access-token-resolver"); |
| | | tokenDir = Files.createDirectory(root.resolve("tokens")); |
| | | writeToken(tokenDir.resolve("valid-token")); |
| | | // A valid, unexpired token file one level above the token directory. |
| | | outsideToken = writeToken(root.resolve("outside-token")); |
| | | resolver = newFileAccessTokenResolver(tokenDir.toString()); |
| | | } |
| | | |
| | | @AfterMethod |
| | | public void tearDown() throws Exception { |
| | | Files.walk(root).sorted(Comparator.reverseOrder()).forEach(p -> p.toFile().delete()); |
| | | } |
| | | |
| | | @Test |
| | | public void resolvesTokenFileByName() throws Exception { |
| | | final AccessTokenInfo token = resolver.resolve(null, "valid-token").getOrThrow(); |
| | | assertThat(token.getToken()).isEqualTo("valid-token"); |
| | | assertThat(token.getScopes()).containsExactly("read"); |
| | | } |
| | | |
| | | @Test |
| | | public void resolvesTokenNameContainingDots() throws Exception { |
| | | writeToken(tokenDir.resolve("header.payload..signature")); |
| | | final AccessTokenInfo token = resolver.resolve(null, "header.payload..signature").getOrThrow(); |
| | | assertThat(token.getScopes()).containsExactly("read"); |
| | | } |
| | | |
| | | @Test(expectedExceptions = AccessTokenException.class) |
| | | public void rejectsTokenEscapingTheDirectoryWithParentReferences() throws Exception { |
| | | assertThat(outsideToken).exists(); |
| | | resolver.resolve(null, "../outside-token").getOrThrow(); |
| | | } |
| | | |
| | | @Test(expectedExceptions = AccessTokenException.class) |
| | | public void rejectsTokenEscapingTheDirectoryThroughAChildFirst() throws Exception { |
| | | resolver.resolve(null, "valid-token/../../outside-token").getOrThrow(); |
| | | } |
| | | |
| | | @Test(expectedExceptions = AccessTokenException.class) |
| | | public void rejectsAbsoluteTokenPath() throws Exception { |
| | | resolver.resolve(null, outsideToken.toAbsolutePath().toString()).getOrThrow(); |
| | | } |
| | | |
| | | @Test(expectedExceptions = AccessTokenException.class) |
| | | public void rejectsTokenNamingAFileInASubdirectory() throws Exception { |
| | | writeToken(Files.createDirectory(tokenDir.resolve("sub")).resolve("valid-token")); |
| | | resolver.resolve(null, "sub/valid-token").getOrThrow(); |
| | | } |
| | | |
| | | @Test(expectedExceptions = AccessTokenException.class) |
| | | public void rejectsTokenWithBackslashSeparator() throws Exception { |
| | | // Fails closed on every platform, whether or not the platform treats the backslash as a separator. |
| | | resolver.resolve(null, "..\\outside-token").getOrThrow(); |
| | | } |
| | | |
| | | @Test(expectedExceptions = AccessTokenException.class) |
| | | public void rejectsTokenNamingTheDirectoryItself() throws Exception { |
| | | resolver.resolve(null, ".").getOrThrow(); |
| | | } |
| | | |
| | | @Test(expectedExceptions = AccessTokenException.class) |
| | | public void rejectsEmptyToken() throws Exception { |
| | | resolver.resolve(null, "").getOrThrow(); |
| | | } |
| | | |
| | | @Test(expectedExceptions = AccessTokenException.class) |
| | | public void rejectsUnknownTokenFile() throws Exception { |
| | | resolver.resolve(null, "unknown-token").getOrThrow(); |
| | | } |
| | | |
| | | private static Path writeToken(final Path file) throws IOException { |
| | | return Files.write(file, VALID_TOKEN_JSON.getBytes(StandardCharsets.UTF_8)); |
| | | } |
| | | } |