Skip to content

Commit 53f14ab

Browse files
andreasgebauermp911de
authored andcommitted
Add support for GCP IAM credentials API.
We now support the IAM Credentials API in addition to the deprecated IAM API for signing JWT. Closes gh-600. Original pull request: gh-619.
1 parent 5ad1508 commit 53f14ab

File tree

10 files changed

+861
-0
lines changed

10 files changed

+861
-0
lines changed

spring-vault-core/pom.xml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,25 @@
194194
</exclusion>
195195
</exclusions>
196196
</dependency>
197+
<dependency>
198+
<groupId>com.google.cloud</groupId>
199+
<artifactId>google-cloud-iamcredentials</artifactId>
200+
<exclusions>
201+
<exclusion>
202+
<groupId>com.fasterxml.jackson.core</groupId>
203+
<artifactId>jackson-core</artifactId>
204+
</exclusion>
205+
<exclusion>
206+
<groupId>org.apache.httpcomponents</groupId>
207+
<artifactId>httpclient</artifactId>
208+
</exclusion>
209+
<exclusion>
210+
<artifactId>commons-logging</artifactId>
211+
<groupId>commons-logging</groupId>
212+
</exclusion>
213+
</exclusions>
214+
<optional>true</optional>
215+
</dependency>
197216

198217
<dependency>
199218
<groupId>com.google.auth</groupId>
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2021 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 org.springframework.util.Assert;
19+
20+
import com.google.auth.oauth2.GoogleCredentials;
21+
import com.google.auth.oauth2.ServiceAccountCredentials;
22+
23+
/**
24+
* Default implementation of {@link GcpCredentialsAccountIdAccessor}. Used by
25+
* {@link GcpIamCredentialsAuthentication}.
26+
*
27+
* @author Andreas Gebauer
28+
* @since 2.4
29+
* @see GcpIamCredentialsAuthentication
30+
*/
31+
enum DefaultGcpCredentialsAccessors implements GcpCredentialsAccountIdAccessor {
32+
33+
INSTANCE;
34+
35+
/**
36+
* Get a the service account id (email) to be placed in the signed JWT.
37+
* @param credentials credentials object to obtain the service account id from.
38+
* @return the service account id to use.
39+
*/
40+
@Override
41+
public String getServiceAccountId(GoogleCredentials credentials) {
42+
43+
Assert.notNull(credentials, "GoogleCredentials must not be null");
44+
Assert.isInstanceOf(ServiceAccountCredentials.class, credentials,
45+
"The configured GoogleCredentials does not represent a service account. Configure the service account id with GcpIamCredentialsAuthenticationOptionsBuilder#serviceAccountId(String).");
46+
47+
return ((ServiceAccountCredentials) credentials).getAccount();
48+
}
49+
50+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2021 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 com.google.auth.oauth2.GoogleCredentials;
19+
20+
/**
21+
* Interface to obtain a service account id for GCP IAM credentials authentication.
22+
* Implementations are used by {@link GcpIamCredentialsAuthentication}.
23+
*
24+
* @author Andreas Gebauer
25+
* @since 2.4
26+
* @see GcpIamCredentialsAuthentication
27+
*/
28+
@FunctionalInterface
29+
public interface GcpCredentialsAccountIdAccessor {
30+
31+
/**
32+
* Get a the service account id (email) to be placed in the signed JWT.
33+
* @param credentials credential object to obtain the service account id from.
34+
* @return the service account id to use.
35+
*/
36+
String getServiceAccountId(GoogleCredentials credentials);
37+
38+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2021 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.io.IOException;
19+
import java.util.function.Supplier;
20+
21+
import com.google.auth.oauth2.GoogleCredentials;
22+
import com.google.auth.oauth2.ServiceAccountCredentials;
23+
24+
/**
25+
* Interface to obtain a {@link ServiceAccountCredentials} for GCP IAM credentials
26+
* authentication. Implementations are used by {@link GcpIamCredentialsAuthentication}.
27+
*
28+
* @author Andreas Gebauer
29+
* @since 2.4
30+
* @see GcpIamCredentialsAuthentication
31+
*/
32+
@FunctionalInterface
33+
public interface GcpCredentialsSupplier extends Supplier<GoogleCredentials> {
34+
35+
/**
36+
* Exception-safe helper to get {@link ServiceAccountCredentials} from
37+
* {@link #getCredentials}.
38+
* @return the ServiceAccountCredentials for JWT signing.
39+
*/
40+
@Override
41+
default GoogleCredentials get() {
42+
43+
try {
44+
return getCredentials();
45+
}
46+
catch (IOException e) {
47+
throw new IllegalStateException("Cannot obtain GoogleCredential", e);
48+
}
49+
}
50+
51+
/**
52+
* Get a {@link GoogleCredentials} for GCP IAM credentials authentication via JWT
53+
* signing.
54+
* @return the {@link GoogleCredentials}.
55+
* @throws IOException if the credentials lookup fails.
56+
*/
57+
GoogleCredentials getCredentials() throws IOException;
58+
59+
}

spring-vault-core/src/main/java/org/springframework/vault/authentication/GcpIamAuthentication.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,9 @@
6464
* @see <a href=
6565
* "https://cloud.google.com/iam/reference/rest/v1/projects.serviceAccounts/signJwt">GCP:
6666
* projects.serviceAccounts.signJwt</a>
67+
* @deprecated Use {@link GcpIamCredentialsAuthentication} instead.
6768
*/
69+
@Deprecated
6870
public class GcpIamAuthentication extends GcpJwtAuthenticationSupport implements ClientAuthentication {
6971

7072
private static final JsonFactory JSON_FACTORY = new JacksonFactory();
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
* Copyright 2021 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.io.IOException;
19+
import java.time.Instant;
20+
import java.util.Collections;
21+
import java.util.LinkedHashMap;
22+
import java.util.Map;
23+
24+
import org.springframework.util.Assert;
25+
import org.springframework.vault.VaultException;
26+
import org.springframework.vault.support.VaultToken;
27+
import org.springframework.web.client.RestOperations;
28+
29+
import com.google.api.client.http.HttpTransport;
30+
import com.google.api.client.json.JsonFactory;
31+
import com.google.api.client.json.jackson2.JacksonFactory;
32+
import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider;
33+
import com.google.api.gax.rpc.TransportChannelProvider;
34+
import com.google.auth.oauth2.GoogleCredentials;
35+
import com.google.cloud.iam.credentials.v1.IamCredentialsClient;
36+
import com.google.cloud.iam.credentials.v1.IamCredentialsSettings;
37+
import com.google.cloud.iam.credentials.v1.ServiceAccountName;
38+
import com.google.cloud.iam.credentials.v1.SignJwtResponse;
39+
import com.google.cloud.iam.credentials.v1.stub.IamCredentialsStubSettings;
40+
41+
/**
42+
* GCP IAM credentials login implementation using GCP IAM service accounts to legitimate
43+
* its authenticity via JSON Web Token.
44+
* <p/>
45+
* This authentication method uses Googles IAM Credentials API to obtain a signed token
46+
* for a specific {@link com.google.api.client.auth.oauth2.Credential}. Service account
47+
* details are obtained from a {@link GoogleCredentials} that can be retrieved either from
48+
* a JSON file or the runtime environment (GAE, GCE).
49+
* <p/>
50+
* {@link GcpIamCredentialsAuthentication} uses Google Java API that uses synchronous API.
51+
*
52+
* @author Andreas Gebauer
53+
* @since 2.4
54+
* @see GcpIamCredentialsAuthenticationOptions
55+
* @see HttpTransport
56+
* @see GoogleCredentials
57+
* @see GoogleCredentials#getApplicationDefault()
58+
* @see RestOperations
59+
* @see <a href="https://www.vaultproject.io/docs/auth/gcp.html">Auth Backend: gcp
60+
* (IAM)</a>
61+
* @see <a href=
62+
* "https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/signJwt">GCP:
63+
* projects.serviceAccounts.signJwt</a>
64+
*/
65+
public class GcpIamCredentialsAuthentication extends GcpJwtAuthenticationSupport implements ClientAuthentication {
66+
67+
private static final JsonFactory JSON_FACTORY = new JacksonFactory();
68+
69+
private final GcpIamCredentialsAuthenticationOptions options;
70+
71+
private final TransportChannelProvider transportChannelProvider;
72+
73+
private final GoogleCredentials credentials;
74+
75+
/**
76+
* Create a new instance of {@link GcpIamCredentialsAuthentication} given
77+
* {@link GcpIamCredentialsAuthenticationOptions} and {@link RestOperations}. This
78+
* constructor initializes {@link InstantiatingGrpcChannelProvider} for Google API
79+
* usage.
80+
* @param options must not be {@literal null}.
81+
* @param restOperations HTTP client for for Vault login, must not be {@literal null}.
82+
*/
83+
public GcpIamCredentialsAuthentication(GcpIamCredentialsAuthenticationOptions options,
84+
RestOperations restOperations) {
85+
this(options, restOperations, IamCredentialsStubSettings.defaultGrpcTransportProviderBuilder().build());
86+
}
87+
88+
/**
89+
* Create a new instance of {@link GcpIamCredentialsAuthentication} given
90+
* {@link GcpIamCredentialsAuthenticationOptions}, {@link RestOperations} and
91+
* {@link TransportChannelProvider}.
92+
* @param options must not be {@literal null}.
93+
* @param restOperations HTTP client for for Vault login, must not be {@literal null}.
94+
* @param transportChannelProvider Provider for transport channel Google API use, must
95+
* not be {@literal null}.
96+
*/
97+
public GcpIamCredentialsAuthentication(GcpIamCredentialsAuthenticationOptions options,
98+
RestOperations restOperations, TransportChannelProvider transportChannelProvider) {
99+
100+
super(restOperations);
101+
102+
Assert.notNull(options, "GcpAuthenticationOptions must not be null");
103+
Assert.notNull(restOperations, "RestOperations must not be null");
104+
Assert.notNull(transportChannelProvider, "TransportChannelProvider must not be null");
105+
106+
this.options = options;
107+
this.transportChannelProvider = transportChannelProvider;
108+
this.credentials = options.getCredentialSupplier().get();
109+
}
110+
111+
@Override
112+
public VaultToken login() throws VaultException {
113+
114+
String signedJwt = signJwt();
115+
116+
return doLogin("GCP-IAM", signedJwt, this.options.getPath(), this.options.getRole());
117+
}
118+
119+
protected String signJwt() {
120+
121+
String serviceAccount = getServiceAccountId();
122+
Map<String, Object> jwtPayload = getJwtPayload(this.options, serviceAccount);
123+
124+
try {
125+
IamCredentialsSettings credentialsSettings = IamCredentialsSettings.newBuilder()
126+
.setCredentialsProvider(() -> this.credentials)
127+
.setTransportChannelProvider(this.transportChannelProvider).build();
128+
try (IamCredentialsClient iamCredentialsClient = IamCredentialsClient.create(credentialsSettings)) {
129+
String payload = JSON_FACTORY.toString(jwtPayload);
130+
ServiceAccountName serviceAccountName = ServiceAccountName.of("-", serviceAccount);
131+
SignJwtResponse response = iamCredentialsClient.signJwt(serviceAccountName, Collections.emptyList(),
132+
payload);
133+
return response.getSignedJwt();
134+
}
135+
}
136+
catch (IOException e) {
137+
throw new VaultLoginException("Cannot sign JWT", e);
138+
}
139+
}
140+
141+
private String getServiceAccountId() {
142+
return this.options.getServiceAccountIdAccessor().getServiceAccountId(this.credentials);
143+
}
144+
145+
private static Map<String, Object> getJwtPayload(GcpIamCredentialsAuthenticationOptions options,
146+
String serviceAccount) {
147+
148+
Instant validUntil = options.getClock().instant().plus(options.getJwtValidity());
149+
150+
Map<String, Object> payload = new LinkedHashMap<>();
151+
152+
payload.put("sub", serviceAccount);
153+
payload.put("aud", "vault/" + options.getRole());
154+
payload.put("exp", validUntil.getEpochSecond());
155+
156+
return payload;
157+
}
158+
159+
}

0 commit comments

Comments
 (0)