Skip to content

Commit 768d195

Browse files
committed
impl(v3): add support for creating OAuth SA creds from files
1 parent c201e56 commit 768d195

11 files changed

Lines changed: 257 additions & 36 deletions

google/cloud/credentials.cc

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,13 @@ std::shared_ptr<Credentials> MakeImpersonateServiceAccountCredentials(
4848
std::shared_ptr<Credentials> MakeServiceAccountCredentials(
4949
std::string json_object, Options opts) {
5050
return std::make_shared<internal::ServiceAccountConfig>(
51-
std::move(json_object), std::move(opts));
51+
std::move(json_object), absl::nullopt, std::move(opts));
52+
}
53+
54+
std::shared_ptr<Credentials> MakeServiceAccountCredentialsFromFile(
55+
std::string const& file_path, Options opts) {
56+
return std::make_shared<internal::ServiceAccountConfig>(
57+
absl::nullopt, file_path, std::move(opts));
5258
}
5359

5460
std::shared_ptr<Credentials> MakeExternalAccountCredentials(

google/cloud/credentials.h

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,58 @@ std::shared_ptr<Credentials> MakeImpersonateServiceAccountCredentials(
301301
std::shared_ptr<Credentials> MakeServiceAccountCredentials(
302302
std::string json_object, Options opts = {});
303303

304+
/**
305+
* Creates service account credentials from a service account key contained in
306+
* a file.
307+
*
308+
* A [service account] is an account for an application or compute workload
309+
* instead of an individual end user. The recommended practice is to use
310+
* Google Default Credentials, which relies on the configuration of the Google
311+
* Cloud system hosting your application (GCE, GKE, Cloud Run) to authenticate
312+
* your workload or application. But sometimes you may need to create and
313+
* download a [service account key], for example, to use a service account
314+
* when running your application on a system that is not part of Google Cloud.
315+
*
316+
* Service account credentials are used in this latter case.
317+
*
318+
* You can create multiple service account keys for a single service account.
319+
* When you create a service account key, the key is returned as string, in the
320+
* format described by [aip/4112]. This string contains an id for the service
321+
* account, as well as the cryptographical materials (a RSA private key)
322+
* required to authenticate the caller.
323+
*
324+
* Therefore, services account keys should be treated as any other secret
325+
* with security implications. Think of them as unencrypted passwords. Do not
326+
* store them where unauthorized persons or programs may read them.
327+
*
328+
* As stated above, most applications should probably use default credentials,
329+
* maybe pointing them to a file with these contents. Using this function may be
330+
* useful when the service account key is obtained from Cloud Secret Manager or
331+
* a similar service.
332+
*
333+
* [aip/4112]: https://google.aip.dev/auth/4112
334+
* [service account]: https://cloud.google.com/iam/docs/overview#service_account
335+
* [service account key]:
336+
* https://cloud.google.com/iam/docs/creating-managing-service-account-keys#iam-service-account-keys-create-cpp
337+
*
338+
* Use `ScopesOption` to restrict the authentication scope for the obtained
339+
* credentials.
340+
*
341+
* @ingroup guac
342+
*
343+
* @note While JSON file formats are supported for both REST and gRPC transport,
344+
* PKCS#12 is only supported for REST transport.
345+
*
346+
* @param file_path path to file containing the service account key
347+
* Typically applications read this from a file, or download the contents from
348+
* something like Google's secret manager service.
349+
* @param opts optional configuration values. Note that the effect of these
350+
* parameters depends on the underlying transport. For example,
351+
* `LoggingComponentsOption` is ignored by gRPC-based services.
352+
*/
353+
std::shared_ptr<Credentials> MakeServiceAccountCredentialsFromFile(
354+
std::string const& file_path, Options opts = {});
355+
304356
/**
305357
* Creates credentials based on external accounts.
306358
*

google/cloud/internal/credentials_impl.cc

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,11 @@ std::vector<std::string> const& ImpersonateServiceAccountConfig::delegates()
8787
return options_.get<DelegatesOption>();
8888
}
8989

90-
ServiceAccountConfig::ServiceAccountConfig(std::string json_object,
91-
Options opts)
90+
ServiceAccountConfig::ServiceAccountConfig(
91+
absl::optional<std::string> json_object,
92+
absl::optional<std::string> file_path, Options opts)
9293
: json_object_(std::move(json_object)),
94+
file_path_(std::move(file_path)),
9395
options_(PopulateAuthOptions(std::move(opts))) {}
9496

9597
ExternalAccountConfig::ExternalAccountConfig(std::string json_object,

google/cloud/internal/credentials_impl.h

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,15 +144,23 @@ class ImpersonateServiceAccountConfig : public Credentials {
144144

145145
class ServiceAccountConfig : public Credentials {
146146
public:
147-
ServiceAccountConfig(std::string json_object, Options opts);
148-
149-
std::string const& json_object() const { return json_object_; }
147+
// Only one of json_object or file_path should have a value.
148+
// TODO(#15886): Use the C++ type system to make better constructors that
149+
// enforces this comment.
150+
ServiceAccountConfig(absl::optional<std::string> json_object,
151+
absl::optional<std::string> file_path, Options opts);
152+
153+
absl::optional<std::string> const& json_object() const {
154+
return json_object_;
155+
}
156+
absl::optional<std::string> const& file_path() const { return file_path_; }
150157
Options const& options() const { return options_; }
151158

152159
private:
153160
void dispatch(CredentialsVisitor& v) const override { v.visit(*this); }
154161

155-
std::string json_object_;
162+
absl::optional<std::string> json_object_;
163+
absl::optional<std::string> file_path_;
156164
Options options_;
157165
};
158166

google/cloud/internal/oauth2_service_account_credentials.cc

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,18 @@
1313
// limitations under the License.
1414

1515
#include "google/cloud/internal/oauth2_service_account_credentials.h"
16+
#include "google/cloud/credentials.h"
1617
#include "google/cloud/internal/absl_str_join_quiet.h"
1718
#include "google/cloud/internal/getenv.h"
1819
#include "google/cloud/internal/make_jwt_assertion.h"
1920
#include "google/cloud/internal/make_status.h"
2021
#include "google/cloud/internal/oauth2_google_credentials.h"
2122
#include "google/cloud/internal/oauth2_universe_domain.h"
23+
#include "google/cloud/internal/parse_service_account_p12_file.h"
2224
#include "google/cloud/internal/rest_response.h"
2325
#include "google/cloud/internal/sign_using_sha256.h"
2426
#include <nlohmann/json.hpp>
27+
#include <fstream>
2528
#include <functional>
2629

2730
namespace google {
@@ -240,6 +243,74 @@ StatusOr<std::string> MakeSelfSignedJWT(
240243
info.private_key);
241244
}
242245

246+
StatusOr<std::shared_ptr<Credentials>>
247+
CreateServiceAccountCredentialsFromJsonContents(
248+
std::string const& contents, Options const& options,
249+
HttpClientFactory client_factory) {
250+
auto info = ParseServiceAccountCredentials(contents, "memory");
251+
if (!info) return info.status();
252+
253+
if (options.has<ScopesOption>()) {
254+
auto const& s = options.get<ScopesOption>();
255+
std::set<std::string> scopes{s.begin(), s.end()};
256+
info->scopes = std::move(scopes);
257+
}
258+
259+
// Verify this is usable before returning it.
260+
auto const tp = std::chrono::system_clock::time_point{};
261+
auto const components = AssertionComponentsFromInfo(*info, tp);
262+
auto jwt = MakeJWTAssertionNoThrow(components.first, components.second,
263+
info->private_key);
264+
if (!jwt) return jwt.status();
265+
return StatusOr<std::shared_ptr<Credentials>>(
266+
std::make_shared<ServiceAccountCredentials>(*info, options,
267+
std::move(client_factory)));
268+
}
269+
270+
StatusOr<std::shared_ptr<Credentials>>
271+
CreateServiceAccountCredentialsFromJsonFilePath(
272+
std::string const& path, Options const& options,
273+
HttpClientFactory client_factory) {
274+
std::ifstream is(path);
275+
if (!is.is_open()) {
276+
// We use kUnknown here because we don't know if the file does not exist, or
277+
// if we were unable to open it for some other reason.
278+
return internal::UnknownError("Cannot open credentials file " + path,
279+
GCP_ERROR_INFO());
280+
}
281+
std::string contents(std::istreambuf_iterator<char>{is}, {});
282+
return CreateServiceAccountCredentialsFromJsonContents(
283+
std::move(contents), options, std::move(client_factory));
284+
}
285+
286+
StatusOr<std::shared_ptr<Credentials>>
287+
CreateServiceAccountCredentialsFromP12FilePath(
288+
std::string const& path, Options const& options,
289+
HttpClientFactory client_factory) {
290+
auto info = ParseServiceAccountP12File(path);
291+
if (!info) return std::move(info).status();
292+
293+
if (options.has<ScopesOption>()) {
294+
auto const& s = options.get<ScopesOption>();
295+
std::set<std::string> scopes{s.begin(), s.end()};
296+
info->scopes = std::move(scopes);
297+
}
298+
return StatusOr<std::shared_ptr<Credentials>>(
299+
std::make_shared<ServiceAccountCredentials>(*info, options,
300+
std::move(client_factory)));
301+
}
302+
303+
StatusOr<std::shared_ptr<Credentials>>
304+
CreateServiceAccountCredentialsFromFilePath(std::string const& path,
305+
Options const& options,
306+
HttpClientFactory client_factory) {
307+
auto credentials = CreateServiceAccountCredentialsFromJsonFilePath(
308+
path, options, client_factory);
309+
if (credentials) return *credentials;
310+
return CreateServiceAccountCredentialsFromP12FilePath(
311+
path, options, std::move(client_factory));
312+
}
313+
243314
ServiceAccountCredentials::ServiceAccountCredentials(
244315
ServiceAccountCredentialsInfo info, Options options,
245316
HttpClientFactory client_factory)
@@ -313,7 +384,8 @@ bool ServiceAccountUseOAuth(ServiceAccountCredentialsInfo const& info) {
313384
}
314385

315386
bool ServiceAccountCredentials::UseOAuth() {
316-
return ServiceAccountUseOAuth(info_);
387+
return options_.has<DisableSelfSignedJWTOption>() ||
388+
ServiceAccountUseOAuth(info_);
317389
}
318390

319391
StatusOr<AccessToken> ServiceAccountCredentials::GetTokenOAuth(

google/cloud/internal/oauth2_service_account_credentials.h

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#include "absl/types/optional.h"
2525
#include <chrono>
2626
#include <string>
27+
#include <variant>
2728
#include <vector>
2829

2930
namespace google {
@@ -127,6 +128,58 @@ StatusOr<std::string> MakeSelfSignedJWT(
127128
ServiceAccountCredentialsInfo const& info,
128129
std::chrono::system_clock::time_point tp);
129130

131+
/**
132+
* Creates a ServiceAccountCredentials from a JSON string.
133+
*/
134+
StatusOr<std::shared_ptr<Credentials>>
135+
CreateServiceAccountCredentialsFromJsonContents(
136+
std::string const& contents, Options const& options,
137+
HttpClientFactory client_factory);
138+
139+
/**
140+
* Creates a ServiceAccountCredentials from a JSON file at the specified path.
141+
*/
142+
StatusOr<std::shared_ptr<Credentials>>
143+
CreateServiceAccountCredentialsFromJsonFilePath(
144+
std::string const& path, Options const& options,
145+
HttpClientFactory client_factory);
146+
147+
/**
148+
* Creates a ServiceAccountCredentials from a P12 file at the specified path.
149+
*/
150+
StatusOr<std::shared_ptr<Credentials>>
151+
CreateServiceAccountCredentialsFromP12FilePath(
152+
std::string const& path, Options const& options,
153+
HttpClientFactory client_factory);
154+
155+
/**
156+
* Creates a ServiceAccountCredentials from a file at the specified path.
157+
*
158+
* @note This function automatically detects if the file is a JSON or P12 (aka
159+
* PFX aka PKCS#12) file and tries to load the file as a service account
160+
* credential. We strongly recommend that applications use JSON files for
161+
* service account key files.
162+
*
163+
* These credentials use the cloud-platform OAuth 2.0 scope, defined by
164+
* `GoogleOAuthScopeCloudPlatform()`. To specify alternate scopes, use the
165+
* `google::cloud::ScopesOption`.
166+
*/
167+
168+
StatusOr<std::shared_ptr<Credentials>>
169+
CreateServiceAccountCredentialsFromFilePath(std::string const& path,
170+
Options const& options,
171+
HttpClientFactory client_factory);
172+
173+
/**
174+
* Specifying this Option prevents self-signed JWTs from being used.
175+
*
176+
* Some services, namely storage, have more stringent requirements w.r.t.
177+
* self-signed JWTs.
178+
*/
179+
struct DisableSelfSignedJWTOption {
180+
using Type = std::monostate;
181+
};
182+
130183
/**
131184
* Implements service account credentials for REST clients.
132185
*

google/cloud/internal/unified_grpc_credentials.cc

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,28 @@ std::shared_ptr<GrpcAuthenticationStrategy> CreateAuthenticationStrategy(
115115
std::move(options));
116116
}
117117
void visit(ServiceAccountConfig const& cfg) override {
118-
result = std::make_unique<GrpcServiceAccountAuthentication>(
119-
cfg.json_object(), std::move(options));
118+
if (cfg.file_path().has_value()) {
119+
std::ifstream is(*cfg.file_path());
120+
if (!is.is_open()) {
121+
// We use kUnknown here because we don't know if the file does not
122+
// exist, or if we were unable to open it for some other reason.
123+
result = std::make_unique<GrpcErrorCredentialsAuthentication>(
124+
ErrorCredentialsConfig{UnknownError(
125+
"Cannot open credentials file " + *cfg.file_path(),
126+
GCP_ERROR_INFO())});
127+
}
128+
std::string contents(std::istreambuf_iterator<char>{is}, {});
129+
result = std::make_unique<GrpcServiceAccountAuthentication>(
130+
std::move(contents), std::move(options));
131+
} else if (cfg.json_object().has_value()) {
132+
result = std::make_unique<GrpcServiceAccountAuthentication>(
133+
*cfg.json_object(), std::move(options));
134+
} else {
135+
result = std::make_unique<GrpcErrorCredentialsAuthentication>(
136+
ErrorCredentialsConfig{InternalError(
137+
"ServiceAccountConfig has neither json_object nor file_path",
138+
GCP_ERROR_INFO())});
139+
}
120140
}
121141
void visit(ExternalAccountConfig const& cfg) override {
122142
grpc::SslCredentialsOptions ssl_options;

google/cloud/internal/unified_rest_credentials.cc

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include "google/cloud/internal/unified_rest_credentials.h"
1616
#include "google/cloud/common_options.h"
1717
#include "google/cloud/internal/make_jwt_assertion.h"
18+
#include "google/cloud/internal/make_status.h"
1819
#include "google/cloud/internal/oauth2_access_token_credentials.h"
1920
#include "google/cloud/internal/oauth2_anonymous_credentials.h"
2021
#include "google/cloud/internal/oauth2_api_key_credentials.h"
@@ -49,25 +50,6 @@ std::shared_ptr<oauth2_internal::Credentials> MakeErrorCredentials(
4950
return std::make_shared<oauth2_internal::ErrorCredentials>(std::move(status));
5051
}
5152

52-
std::shared_ptr<oauth2_internal::Credentials>
53-
CreateServiceAccountCredentialsFromJsonContents(
54-
std::string const& contents, Options const& options,
55-
oauth2_internal::HttpClientFactory client_factory) {
56-
auto info =
57-
oauth2_internal::ParseServiceAccountCredentials(contents, "memory");
58-
if (!info) return MakeErrorCredentials(std::move(info).status());
59-
60-
// Verify this is usable before returning it.
61-
auto const tp = std::chrono::system_clock::time_point{};
62-
auto const components = AssertionComponentsFromInfo(*info, tp);
63-
auto jwt = internal::MakeJWTAssertionNoThrow(
64-
components.first, components.second, info->private_key);
65-
if (!jwt) return MakeErrorCredentials(std::move(jwt).status());
66-
67-
return std::make_shared<oauth2_internal::ServiceAccountCredentials>(
68-
*info, options, std::move(client_factory));
69-
}
70-
7153
} // namespace
7254

7355
std::shared_ptr<oauth2_internal::Credentials> MapCredentials(
@@ -120,10 +102,29 @@ std::shared_ptr<oauth2_internal::Credentials> MapCredentials(
120102
}
121103

122104
void visit(ServiceAccountConfig const& cfg) override {
123-
result = Decorate(
124-
CreateServiceAccountCredentialsFromJsonContents(
125-
cfg.json_object(), cfg.options(), std::move(client_factory_)),
126-
cfg.options());
105+
if (cfg.file_path().has_value()) {
106+
auto creds =
107+
oauth2_internal::CreateServiceAccountCredentialsFromFilePath(
108+
*cfg.file_path(), cfg.options(), std::move(client_factory_));
109+
if (creds) {
110+
result = Decorate(*creds, cfg.options());
111+
} else {
112+
result = MakeErrorCredentials(std::move(creds).status());
113+
}
114+
} else if (cfg.json_object().has_value()) {
115+
auto creds =
116+
oauth2_internal::CreateServiceAccountCredentialsFromJsonContents(
117+
*cfg.json_object(), cfg.options(), std::move(client_factory_));
118+
if (creds) {
119+
result = Decorate(std::move(*creds), cfg.options());
120+
return;
121+
}
122+
result = MakeErrorCredentials(std::move(creds).status());
123+
} else {
124+
result = MakeErrorCredentials(internal::InternalError(
125+
"ServiceAccountConfig has neither json_object nor file_path",
126+
GCP_ERROR_INFO()));
127+
}
127128
}
128129

129130
void visit(ExternalAccountConfig const& cfg) override {

google/cloud/internal/unified_rest_credentials_test.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,7 @@ TEST(UnifiedRestCredentialsTest, ServiceAccount) {
405405
EXPECT_CALL(client_factory, Call).Times(0);
406406

407407
auto const config =
408-
internal::ServiceAccountConfig(contents.dump(), Options{});
408+
internal::ServiceAccountConfig(contents.dump(), absl::nullopt, Options{});
409409
auto credentials = MapCredentials(config, client_factory.AsStdFunction());
410410
auto access_token = credentials->GetToken(now);
411411
ASSERT_STATUS_OK(access_token);

0 commit comments

Comments
 (0)