getGitHubLogin(String token) {
+ return Map.of("token", token);
+ }
+
+}
diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/GitHubAuthenticationOptions.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/GitHubAuthenticationOptions.java
new file mode 100644
index 00000000..3edc0ba4
--- /dev/null
+++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/GitHubAuthenticationOptions.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2017-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.vault.authentication;
+
+import java.util.function.Supplier;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+
+/**
+ * Authentication options for {@link GitHubAuthentication}.
+ *
+ * Authentication options provide the role and the token.
+ * {@link GitHubAuthenticationOptions} can be constructed using {@link #builder()}.
+ * Instances of this class are immutable once constructed.
+ *
+ * @author Nanne Baars
+ * @author Mark Paluch
+ * @since 3.2
+ * @see GitHubAuthentication
+ * @see #builder()
+ */
+public class GitHubAuthenticationOptions {
+
+ public static final String DEFAULT_GITHUB_AUTHENTICATION_PATH = "github";
+
+ /**
+ * Path of the GitHub authentication backend mount. Optional and defaults to
+ * {@literal github}.
+ */
+ private final String path;
+
+ /**
+ * Supplier instance to obtain the GitHub personal access token.
+ */
+ private final Supplier tokenSupplier;
+
+ private GitHubAuthenticationOptions(Supplier tokenSupplier, String path) {
+
+ this.tokenSupplier = tokenSupplier;
+ this.path = path;
+ }
+
+ /**
+ * @return a new {@link GitHubAuthenticationOptions}.
+ */
+ public static GithubAuthenticationOptionsBuilder builder() {
+ return new GithubAuthenticationOptionsBuilder();
+ }
+
+ /**
+ * @return access token to use.
+ */
+ public Supplier getTokenSupplier() {
+ return this.tokenSupplier;
+ }
+
+ /**
+ * @return the path of the GitHub authentication backend mount.
+ */
+ public String getPath() {
+ return this.path;
+ }
+
+ /**
+ * Builder for {@link GitHubAuthenticationOptions}.
+ */
+ public static class GithubAuthenticationOptionsBuilder {
+
+ private String path = DEFAULT_GITHUB_AUTHENTICATION_PATH;
+
+ @Nullable
+ private Supplier tokenSupplier;
+
+ /**
+ * Configure the mount path.
+ * @param path must not be {@literal null} or empty.
+ * @return {@code this} {@link GithubAuthenticationOptionsBuilder}.
+ */
+ public GithubAuthenticationOptionsBuilder path(String path) {
+
+ Assert.hasText(path, "Path must not be empty");
+
+ this.path = path;
+ return this;
+ }
+
+ /**
+ * Configure the GitHub token. Vault authentication will use this token as
+ * singleton. If you want to provide a dynamic token that can change over time,
+ * see {@link #tokenSupplier(Supplier)}.
+ * @param token must not be {@literal null}.
+ * @return {@code this} {@link GithubAuthenticationOptionsBuilder}.
+ */
+ public GithubAuthenticationOptionsBuilder token(String token) {
+
+ Assert.hasText(token, "Token must not be empty");
+
+ return tokenSupplier(() -> token);
+ }
+
+ /**
+ * Configure the {@link Supplier} to obtain a token.
+ * @param tokenSupplier must not be {@literal null}.
+ * @return {@code this} {@link GithubAuthenticationOptionsBuilder}.
+ */
+ public GithubAuthenticationOptionsBuilder tokenSupplier(Supplier tokenSupplier) {
+
+ Assert.notNull(tokenSupplier, "Token supplier must not be null");
+
+ this.tokenSupplier = tokenSupplier;
+ return this;
+ }
+
+ /**
+ * Build a new {@link GitHubAuthenticationOptions} instance.
+ * @return a new {@link GitHubAuthenticationOptions}.
+ */
+ public GitHubAuthenticationOptions build() {
+
+ Assert.notNull(this.tokenSupplier, "Token must not be null");
+
+ return new GitHubAuthenticationOptions(this.tokenSupplier, this.path);
+ }
+
+ }
+
+}
diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/GitHubAuthenticationIntegrationTest.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/GitHubAuthenticationIntegrationTest.java
new file mode 100644
index 00000000..5893afd7
--- /dev/null
+++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/GitHubAuthenticationIntegrationTest.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2017-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.vault.authentication;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.io.IOException;
+import java.util.Map;
+import okhttp3.mockwebserver.Dispatcher;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.vault.support.VaultToken;
+import org.springframework.vault.util.IntegrationTestSupport;
+import org.springframework.vault.util.Settings;
+import org.springframework.vault.util.TestRestTemplateFactory;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * Integration tests for {@link GitHubAuthentication} using
+ * {@link AuthenticationStepsExecutor}.
+ *
+ * @author Nanne Baars
+ * @author Mark Paluch
+ */
+class GitHubAuthenticationIntegrationTest extends IntegrationTestSupport {
+
+ private static final int organizationId = 1;
+
+ private final MockWebServer gitHubMockServer = new MockWebServer();
+
+ @BeforeEach
+ void before() throws Exception {
+
+ if (!prepare().hasAuth("github")) {
+ prepare().mountAuth("github");
+ }
+
+ prepare().getVaultOperations()
+ .doWithSession(
+ restOperations -> restOperations.postForEntity("auth/github/config", Map.of("organization_id", 1,
+ "base_url", "http://localhost:%d".formatted(gitHubMockServer.getPort())), Map.class));
+ }
+
+ @AfterEach
+ void after() throws IOException {
+ gitHubMockServer.shutdown();
+ }
+
+ @Test
+ void shouldLoginSuccessfully() {
+ RestTemplate restTemplate = TestRestTemplateFactory.create(Settings.createSslConfiguration());
+ setupGithubMockServer(gitHubUserResponse(), gitHubOrganizationResponse(organizationId),
+ gitHubTeamResponse(organizationId));
+
+ GitHubAuthentication authentication = new GitHubAuthentication(
+ GitHubAuthenticationOptions.builder().tokenSupplier(() -> "TOKEN").build(), restTemplate);
+ VaultToken loginToken = authentication.login();
+
+ assertThat(loginToken.getToken()).isNotNull();
+ }
+
+ @Test
+ void shouldFailIfOrganizationIsNotTheSame() {
+ RestTemplate restTemplate = TestRestTemplateFactory.create(Settings.createSslConfiguration());
+ var wrongOrganizationId = organizationId + 1;
+ setupGithubMockServer(gitHubUserResponse(), gitHubOrganizationResponse(wrongOrganizationId),
+ gitHubTeamResponse(wrongOrganizationId));
+
+ GitHubAuthentication authentication = new GitHubAuthentication(
+ GitHubAuthenticationOptions.builder().tokenSupplier(() -> "TOKEN2").build(), restTemplate);
+
+ assertThatThrownBy(authentication::login).isInstanceOf(VaultLoginException.class)
+ .hasMessageContaining("Cannot login using GitHub: user is not part of required org");
+ }
+
+ private String gitHubUserResponse() {
+ return """
+ {
+ "login": "octocat",
+ "id": 100
+ }
+ """;
+ }
+
+ private String gitHubOrganizationResponse(int organizationId) {
+ return """
+ [
+ {
+ "login": "Foo bar organization",
+ "id": %d
+ }
+ ]
+ """.formatted(organizationId);
+ }
+
+ private String gitHubTeamResponse(int organizationId) {
+ return """
+ [
+ {
+ "id": 45,
+ "name": "Justice League",
+ "slug": "justice-league",
+ "organization": {
+ "id": %d
+ }
+ }
+ ]
+ """.formatted(organizationId);
+ }
+
+ private void setupGithubMockServer(String userJson, String orgJson, String teamJson) {
+ gitHubMockServer.setDispatcher(new Dispatcher() {
+
+ @Override
+ public MockResponse dispatch(RecordedRequest request) {
+
+ return switch (request.getPath()) {
+ case "/user" -> new MockResponse().setResponseCode(200).setBody(userJson);
+ case "/user/orgs?per_page=100" -> new MockResponse().setResponseCode(200).setBody(orgJson);
+ case "/user/teams?per_page=100" -> new MockResponse().setResponseCode(200).setBody(teamJson);
+ default -> new MockResponse().setResponseCode(404);
+ };
+ }
+ });
+ }
+
+}
diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/JwtAuthenticationIntegrationTest.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/JwtAuthenticationIntegrationTest.java
index fea4e794..9be1e9ec 100644
--- a/spring-vault-core/src/test/java/org/springframework/vault/authentication/JwtAuthenticationIntegrationTest.java
+++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/JwtAuthenticationIntegrationTest.java
@@ -43,7 +43,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.springframework.vault.authentication.JwtAuthentication.DEFAULT_JWT_AUTHENTICATION_PATH;
/**
- * Integration tests for {@link KubernetesAuthentication} using
+ * Integration tests for {@link JwtAuthentication} using
* {@link AuthenticationStepsExecutor}.
*
* @author Nanne Baars