Skip to content

Commit d8fe957

Browse files
authored
impl(v3): add support for creating OAuth SA creds from files (#15887)
1 parent c201e56 commit d8fe957

11 files changed

Lines changed: 280 additions & 37 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: 66 additions & 1 deletion
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
*
@@ -406,7 +458,9 @@ struct DelegatesOption {
406458
};
407459

408460
/**
409-
* Configure the scopes for `MakeImpersonateServiceAccountCredentials()`
461+
* Configure the scopes for `MakeImpersonateServiceAccountCredentials()`.
462+
* Override the scopes for `MakeServiceAccountCredentials` and
463+
* `MakeServiceAccountCredentialsFromFile()`.
410464
*
411465
* @ingroup options
412466
* @ingroup guac
@@ -415,6 +469,17 @@ struct ScopesOption {
415469
using Type = std::vector<std::string>;
416470
};
417471

472+
/**
473+
* Overrides the subject for `MakeServiceAccountCredentials` and
474+
* `MakeServiceAccountCredentialsFromFile()`.
475+
*
476+
* @ingroup options
477+
* @ingroup guac
478+
*/
479+
struct SubjectOption {
480+
using Type = std::string;
481+
};
482+
418483
/**
419484
* Configure the access token lifetime
420485
*

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: 82 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,83 @@ 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+
if (options.has<SubjectOption>()) {
260+
info->subject = options.get<SubjectOption>();
261+
}
262+
263+
// Verify this is usable before returning it.
264+
auto const tp = std::chrono::system_clock::time_point{};
265+
auto const components = AssertionComponentsFromInfo(*info, tp);
266+
auto jwt = MakeJWTAssertionNoThrow(components.first, components.second,
267+
info->private_key);
268+
if (!jwt) return jwt.status();
269+
return StatusOr<std::shared_ptr<Credentials>>(
270+
std::make_shared<ServiceAccountCredentials>(*info, options,
271+
std::move(client_factory)));
272+
}
273+
274+
StatusOr<std::shared_ptr<Credentials>>
275+
CreateServiceAccountCredentialsFromJsonFilePath(
276+
std::string const& path, Options const& options,
277+
HttpClientFactory client_factory) {
278+
std::ifstream is(path);
279+
if (!is.is_open()) {
280+
// We use kUnknown here because we don't know if the file does not exist, or
281+
// if we were unable to open it for some other reason.
282+
return internal::UnknownError("Cannot open credentials file " + path,
283+
GCP_ERROR_INFO());
284+
}
285+
std::string contents(std::istreambuf_iterator<char>{is}, {});
286+
return CreateServiceAccountCredentialsFromJsonContents(
287+
std::move(contents), options, std::move(client_factory));
288+
}
289+
290+
StatusOr<std::shared_ptr<Credentials>>
291+
CreateServiceAccountCredentialsFromP12FilePath(
292+
std::string const& path, Options const& options,
293+
HttpClientFactory client_factory) {
294+
auto info = ParseServiceAccountP12File(path);
295+
if (!info) return std::move(info).status();
296+
297+
if (options.has<ScopesOption>()) {
298+
auto const& s = options.get<ScopesOption>();
299+
std::set<std::string> scopes{s.begin(), s.end()};
300+
info->scopes = std::move(scopes);
301+
}
302+
303+
if (options.has<SubjectOption>()) {
304+
info->subject = options.get<SubjectOption>();
305+
}
306+
307+
return StatusOr<std::shared_ptr<Credentials>>(
308+
std::make_shared<ServiceAccountCredentials>(*info, options,
309+
std::move(client_factory)));
310+
}
311+
312+
StatusOr<std::shared_ptr<Credentials>>
313+
CreateServiceAccountCredentialsFromFilePath(std::string const& path,
314+
Options const& options,
315+
HttpClientFactory client_factory) {
316+
auto credentials = CreateServiceAccountCredentialsFromJsonFilePath(
317+
path, options, client_factory);
318+
if (credentials) return *credentials;
319+
return CreateServiceAccountCredentialsFromP12FilePath(
320+
path, options, std::move(client_factory));
321+
}
322+
243323
ServiceAccountCredentials::ServiceAccountCredentials(
244324
ServiceAccountCredentialsInfo info, Options options,
245325
HttpClientFactory client_factory)
@@ -313,7 +393,8 @@ bool ServiceAccountUseOAuth(ServiceAccountCredentialsInfo const& info) {
313393
}
314394

315395
bool ServiceAccountCredentials::UseOAuth() {
316-
return ServiceAccountUseOAuth(info_);
396+
return options_.has<DisableSelfSignedJWTOption>() ||
397+
ServiceAccountUseOAuth(info_);
317398
}
318399

319400
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;

0 commit comments

Comments
 (0)