Skip to content

Commit 1367275

Browse files
Nanne Baarsmp911de
Nanne Baars
authored andcommitted
Support JWT Authentication
Closes gh-689 Original pull request: gh-802
1 parent d877da4 commit 1367275

File tree

5 files changed

+435
-0
lines changed

5 files changed

+435
-0
lines changed

spring-vault-core/pom.xml

+7
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,13 @@
314314
<scope>test</scope>
315315
</dependency>
316316

317+
<dependency>
318+
<groupId>com.nimbusds</groupId>
319+
<artifactId>nimbus-jose-jwt</artifactId>
320+
<version>9.30.2</version>
321+
<scope>test</scope>
322+
</dependency>
323+
317324
<!-- Logging -->
318325

319326
<dependency>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright 2017-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.vault.authentication;
17+
18+
import java.util.HashMap;
19+
import java.util.Map;
20+
import java.util.Optional;
21+
import org.apache.commons.logging.Log;
22+
import org.apache.commons.logging.LogFactory;
23+
import org.springframework.util.Assert;
24+
import org.springframework.util.StringUtils;
25+
import org.springframework.vault.VaultException;
26+
import org.springframework.vault.support.VaultResponse;
27+
import org.springframework.vault.support.VaultToken;
28+
import org.springframework.web.client.RestClientException;
29+
import org.springframework.web.client.RestOperations;
30+
31+
/**
32+
* JWT implementation of {@link ClientAuthentication}. {@link JwtAuthentication} uses a
33+
* JSON Web Token to login into Vault. JWT and Role are sent in the login request to Vault
34+
* to obtain a {@link VaultToken}.
35+
*
36+
* @author Nanne Baars
37+
* @since 3.0.4
38+
* @see JwtAuthenticationOptions
39+
* @see RestOperations
40+
* @see <a href="https://www.vaultproject.io/api-docs/auth/jwt">Vault Auth Backend:
41+
* JWT</a>
42+
*/
43+
public class JwtAuthentication implements ClientAuthentication, AuthenticationStepsFactory {
44+
45+
public static final String DEFAULT_JWT_AUTHENTICATION_PATH = "jwt";
46+
47+
private static final Log logger = LogFactory.getLog(JwtAuthentication.class);
48+
49+
private final JwtAuthenticationOptions options;
50+
51+
private final RestOperations restOperations;
52+
53+
/**
54+
* Create a {@link JwtAuthentication} using {@link JwtAuthenticationOptions} and
55+
* {@link RestOperations}.
56+
* @param options must not be {@literal null}.
57+
* @param restOperations must not be {@literal null}.
58+
*/
59+
public JwtAuthentication(JwtAuthenticationOptions options, RestOperations restOperations) {
60+
Assert.notNull(options, "JwtAuthenticationOptions must not be null");
61+
Assert.notNull(restOperations, "RestOperations must not be null");
62+
63+
this.options = options;
64+
this.restOperations = restOperations;
65+
}
66+
67+
private static Map<String, String> getJwtLogin(String role, String jwt) {
68+
Map<String, String> login = new HashMap<>();
69+
70+
login.put("jwt", jwt);
71+
if (StringUtils.hasText(role)) {
72+
login.put("role", role);
73+
}
74+
75+
return login;
76+
}
77+
78+
@Override
79+
public AuthenticationSteps getAuthenticationSteps() {
80+
return AuthenticationSteps.fromSupplier(options.getJwtSupplier())
81+
.map(token -> getJwtLogin(options.getRole(), token))
82+
.login(getLoginPath());
83+
}
84+
85+
@Override
86+
public VaultToken login() throws VaultException {
87+
Map<String, String> login = getJwtLogin(this.options.getRole(), this.options.getJwtSupplier().get());
88+
89+
try {
90+
VaultResponse response = this.restOperations.postForObject(getLoginPath(), login, VaultResponse.class);
91+
92+
Assert.state(response != null && response.getAuth() != null, "Auth field must not be null");
93+
94+
logger.debug("Login successful using JWT authentication");
95+
96+
return LoginTokenUtil.from(response.getAuth());
97+
}
98+
catch (RestClientException e) {
99+
throw VaultLoginException.create("JWT", e);
100+
}
101+
}
102+
103+
private String getLoginPath() {
104+
return AuthenticationUtil
105+
.getLoginPath(Optional.ofNullable(options.getPath()).orElse(DEFAULT_JWT_AUTHENTICATION_PATH));
106+
}
107+
108+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/*
2+
* Copyright 2017-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.vault.authentication;
17+
18+
import java.util.function.Supplier;
19+
import org.springframework.lang.Nullable;
20+
import org.springframework.util.Assert;
21+
22+
/**
23+
* Authentication options for {@link JwtAuthentication}.
24+
* <p>
25+
* Authentication options provide the role and the JWT. {@link JwtAuthenticationOptions}
26+
* can be constructed using {@link #builder()}. Instances of this class are immutable once
27+
* constructed.
28+
* <p>
29+
*
30+
* @author Nanne Baars
31+
* @since 3.0.4
32+
* @see JwtAuthentication
33+
* @see #builder()
34+
*/
35+
public class JwtAuthenticationOptions {
36+
37+
/**
38+
* Path of the JWT authentication backend mount. Optional and defaults to
39+
* {@literal jwt}.
40+
*/
41+
@Nullable
42+
private final String path;
43+
44+
/**
45+
* Name of the role against which the login is being attempted. Defaults to configured
46+
* default_role if not provided. See
47+
* <a href="https://developer.hashicorp.com/vault/api-docs/auth/jwt#configure">Vault
48+
* JWT configuration</a>
49+
*/
50+
@Nullable
51+
private final String role;
52+
53+
/**
54+
* Supplier instance to obtain a service account JSON Web Tokens.
55+
*/
56+
private final Supplier<String> jwtSupplier;
57+
58+
private JwtAuthenticationOptions(String role, Supplier<String> jwtSupplier, String path) {
59+
60+
this.role = role;
61+
this.jwtSupplier = jwtSupplier;
62+
this.path = path;
63+
}
64+
65+
/**
66+
* @return a new {@link JwtAuthenticationOptionsBuilder}.
67+
*/
68+
public static JwtAuthenticationOptionsBuilder builder() {
69+
return new JwtAuthenticationOptionsBuilder();
70+
}
71+
72+
/**
73+
* @return name of the role against which the login is being attempted.
74+
*/
75+
public String getRole() {
76+
return this.role;
77+
}
78+
79+
/**
80+
* @return JSON Web Token.
81+
*/
82+
public Supplier<String> getJwtSupplier() {
83+
return this.jwtSupplier;
84+
}
85+
86+
/**
87+
* @return the path of the kubernetes authentication backend mount.
88+
*/
89+
public String getPath() {
90+
return this.path;
91+
}
92+
93+
/**
94+
* Builder for {@link JwtAuthenticationOptions}.
95+
*/
96+
public static class JwtAuthenticationOptionsBuilder {
97+
98+
private String role;
99+
100+
private Supplier<String> jwtSupplier;
101+
102+
private String path;
103+
104+
/**
105+
* Configure the role.
106+
* @param role name of the role against which the login is being attempted, must
107+
* not be {@literal null} or empty.
108+
* @return {@code this} {@link JwtAuthenticationOptionsBuilder}.
109+
*/
110+
public JwtAuthenticationOptionsBuilder role(String role) {
111+
112+
Assert.hasText(role, "Role must not be empty");
113+
114+
this.role = role;
115+
return this;
116+
}
117+
118+
/**
119+
* Configure the mount path.
120+
* @param path must not be {@literal null} or empty.
121+
* @return {@code this} {@link JwtAuthenticationOptionsBuilder}.
122+
*/
123+
public JwtAuthenticationOptionsBuilder path(String path) {
124+
125+
Assert.hasText(path, "Path must not be empty");
126+
127+
this.path = path;
128+
return this;
129+
}
130+
131+
/**
132+
* Configure the {@link Supplier} to obtain a JWT authentication token.
133+
* @param jwtSupplier must not be {@literal null}.
134+
* @return {@code this} {@link JwtAuthenticationOptionsBuilder}.
135+
*/
136+
public JwtAuthenticationOptionsBuilder jwt(Supplier<String> jwtSupplier) {
137+
138+
Assert.notNull(jwtSupplier, "Jwt supplier must not be null");
139+
140+
this.jwtSupplier = jwtSupplier;
141+
return this;
142+
}
143+
144+
/**
145+
* Build a new {@link JwtAuthenticationOptions} instance.
146+
* @return a new {@link JwtAuthenticationOptions}.
147+
*/
148+
public JwtAuthenticationOptions build() {
149+
150+
Assert.notNull(this.jwtSupplier, "JWT must not be null");
151+
152+
return new JwtAuthenticationOptions(this.role, this.jwtSupplier, this.path);
153+
}
154+
155+
}
156+
157+
}

0 commit comments

Comments
 (0)