From 669deea54af6c2af78e5e70a471022fbf627262a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Gjesse?= Date: Thu, 5 Feb 2015 13:46:52 +0100 Subject: [PATCH] Add support for impersonating a user when using a service account Allow `ServiceAccountCredentials` constructors to take an optional `user` argument to specify a user to impersonate. BUG=https://github.com/dart-lang/googleapis_auth/issues/15 R=kustermann@google.com Review URL: https://codereview.chromium.org/893423003 --- CHANGELOG.md | 7 ++++++- README.md | 12 +++++++++++ lib/auth.dart | 37 ++++++++++++++++++++++++++------- lib/auth_io.dart | 2 ++ lib/src/oauth2_flows/jwt.dart | 8 +++++-- pubspec.yaml | 2 +- test/oauth2_flows/jwt_test.dart | 18 +++++++++++++--- test/oauth2_test.dart | 35 +++++++++++++++++++++++++++++++ 8 files changed, 106 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a55d33..a3cb3a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,15 @@ +## 0.2.3 + +- Allow `ServiceAccountCredentials` constructors to take an optional + `user` argument to specify a user to impersonate. + ## 0.2.2 - Allow `ServiceAccountCredentials.fromJson` to accept a `Map`. - Cleaned up `README.md` ## 0.2.1 -- Added optinoal `force` and `immediate` arguments to `runHybridFlow`. +- Added optional `force` and `immediate` arguments to `runHybridFlow`. ## 0.2.0 - Renamed `forceUserConsent` parameter to `immediate`. diff --git a/README.md b/README.md index 06cd3e9..19d9b43 100644 --- a/README.md +++ b/README.md @@ -219,6 +219,18 @@ clientViaServiceAccount(accountCredentials, scopes).then((AuthClient client) { The authenticated HTTP client can now access APIs. +##### Impersonation + +For some APIs the use of a service account also requires to impersonate a +user. To support that the `ServiceAccountCredentials` constructors have an +optional argument `user` to specify the user to impersonate. + +One example of this are the Google Apps APIs. See [Perform Google Apps +Domain-Wide Delegation of Authority] +(https://developers.google.com/admin-sdk/directory/v1/guides/delegation) +for information on the additional security configuration required to +enable this for a service account. + #### Autonomous Application / Compute Engine using metadata service diff --git a/lib/auth.dart b/lib/auth.dart index 2d4b24a..1f83142 100644 --- a/lib/auth.dart +++ b/lib/auth.dart @@ -91,22 +91,28 @@ class ClientId { /// Represents credentials for a service account. class ServiceAccountCredentials { - /// The email addres of this service account + /// The email address of this service account. final String email; - /// The clientId + /// The clientId. final ClientId clientId; - /// Private key + /// Private key. final String privateKey; + /// Impersonated user, if any. If not impersonating any user this is `null`. + final String impersonatedUser; + /// Private key as an [RSAPrivateKey]. final RSAPrivateKey privateRSAKey; /// Creates a new [ServiceAccountCredentials] from JSON. /// /// [json] can be either a [Map] or a JSON map encoded as a [String]. - factory ServiceAccountCredentials.fromJson(json) { + /// + /// The optional named argument [impersonatedUser] is used to set the user + /// to impersonate if impersonating a user. + factory ServiceAccountCredentials.fromJson(json, {String impersonatedUser}) { if (json is String) { json = JSON.decode(json); } @@ -124,15 +130,30 @@ class ServiceAccountCredentials { } if (identifier == null || privateKey == null || email == null) { - throw new ArgumentError('The given credentials do not contain a' - 'identifier, privateKey or email field.'); + throw new ArgumentError('The given credentials do not contain all the ' + 'fields: client_id, private_key and client_email.'); } var clientId = new ClientId(identifier, null); - return new ServiceAccountCredentials(email, clientId, privateKey); + return new ServiceAccountCredentials( + email, clientId, privateKey, impersonatedUser: impersonatedUser); } - ServiceAccountCredentials(this.email, this.clientId, String privateKey) + /// Creates a new [ServiceAccountCredentials]. + /// + /// [email] is the e-mail address of the service account. + /// + /// [clientId] is the client ID for the service account. + /// + /// [privateKey] is the base 64 encoded, unencrypted private key, including + /// the '-----BEGIN PRIVATE KEY-----' and '-----END PRIVATE KEY-----' + /// boundaries. + /// + /// The optional named argument [impersonatedUser] is used to set the user + /// to impersonate if impersonating a user is needed. + ServiceAccountCredentials( + this.email, this.clientId, String privateKey, + {String this.impersonatedUser}) : privateKey = privateKey, privateRSAKey = keyFromString(privateKey) { if (email == null || clientId == null || privateKey == null) { diff --git a/lib/auth_io.dart b/lib/auth_io.dart index defd0de..f8c4920 100644 --- a/lib/auth_io.dart +++ b/lib/auth_io.dart @@ -140,6 +140,7 @@ Future clientViaServiceAccount( var flow = new JwtFlow(clientCredentials.email, clientCredentials.privateRSAKey, + clientCredentials.impersonatedUser, scopes, baseClient); return flow.run().catchError((error, stack) { @@ -266,6 +267,7 @@ Future obtainAccessCredentialsViaServiceAccount( List scopes, Client baseClient) { return new JwtFlow(clientCredentials.email, clientCredentials.privateRSAKey, + clientCredentials.impersonatedUser, scopes, baseClient).run(); } diff --git a/lib/src/oauth2_flows/jwt.dart b/lib/src/oauth2_flows/jwt.dart index f8bb66f..35036c5 100644 --- a/lib/src/oauth2_flows/jwt.dart +++ b/lib/src/oauth2_flows/jwt.dart @@ -27,9 +27,11 @@ class JwtFlow { final String _clientEmail; final RS256Signer _signer; final List _scopes; + final String _user; final http.Client _client; - JwtFlow(this._clientEmail, RSAPrivateKey key, this._scopes, this._client) + JwtFlow(this._clientEmail, RSAPrivateKey key, this._user, + this._scopes, this._client) : _signer = new RS256Signer(key); Future run() { @@ -39,13 +41,15 @@ class JwtFlow { jwtHeader() => {"alg": "RS256", "typ": "JWT"}; jwtClaimSet() { - return { + var claimSet = { 'iss' : _clientEmail, 'scope' : _scopes.join(' '), 'aud' : GOOGLE_OAUTH2_TOKEN_URL, 'exp' : timestamp + 3600 , 'iat' : timestamp, }; + if (_user != null) claimSet['sub'] = _user; + return claimSet; } var jwtHeaderBase64 = _base64url(ASCII.encode(JSON.encode(jwtHeader()))); diff --git a/pubspec.yaml b/pubspec.yaml index 92f1066..764f266 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: googleapis_auth -version: 0.2.2 +version: 0.2.3 author: Dart Team description: Obtain Access credentials for Google services using OAuth 2.0 homepage: https://github.com/dart-lang/googleapis_auth diff --git a/test/oauth2_flows/jwt_test.dart b/test/oauth2_flows/jwt_test.dart index 6019608..6c967ec 100644 --- a/test/oauth2_flows/jwt_test.dart +++ b/test/oauth2_flows/jwt_test.dart @@ -48,7 +48,7 @@ main() { var scopes = ['s1', 's2']; test('successfull', () { - var flow = new JwtFlow(clientEmail, TestPrivateKey, scopes, + var flow = new JwtFlow(clientEmail, TestPrivateKey, null, scopes, mockClient(expectAsync(successfullSignRequest), expectClose: false)); flow.run().then(expectAsync((AccessCredentials credentials) { @@ -59,15 +59,27 @@ main() { })); }); + test('successfull-with-user', () { + var flow = new JwtFlow(clientEmail, TestPrivateKey, 'x@y.com', scopes, + mockClient(expectAsync(successfullSignRequest), expectClose: false)); + + flow.run().then(expectAsync((AccessCredentials credentials) { + expect(credentials.accessToken.data, equals('atok')); + expect(credentials.accessToken.type, equals('Bearer')); + expect(credentials.scopes, equals(['s1', 's2'])); + expectExpiryOneHourFromNow(credentials.accessToken); + })); + }); + test('invalid-server-response', () { - var flow = new JwtFlow(clientEmail, TestPrivateKey, scopes, + var flow = new JwtFlow(clientEmail, TestPrivateKey, null, scopes, mockClient(expectAsync(invalidAccessToken), expectClose: false)); expect(flow.run(), throwsA(isException)); }); test('transport-failure', () { - var flow = new JwtFlow(clientEmail, TestPrivateKey, scopes, + var flow = new JwtFlow(clientEmail, TestPrivateKey, null, scopes, transportFailure); expect(flow.run(), throwsA(isTransportException)); diff --git a/test/oauth2_test.dart b/test/oauth2_test.dart index efe00e4..e5f9f6a 100644 --- a/test/oauth2_test.dart +++ b/test/oauth2_test.dart @@ -86,6 +86,17 @@ main() { expect(credentials.email, equals('email')); expect(credentials.clientId, equals(clientId)); expect(credentials.privateKey, equals(TestPrivateKeyString)); + expect(credentials.impersonatedUser, isNull); + }); + + test('from-valid-individual-params-with-user', () { + var credentials = new ServiceAccountCredentials( + 'email', clientId, TestPrivateKeyString, + impersonatedUser: 'x@y.com'); + expect(credentials.email, equals('email')); + expect(credentials.clientId, equals(clientId)); + expect(credentials.privateKey, equals(TestPrivateKeyString)); + expect(credentials.impersonatedUser, equals('x@y.com')); }); test('from-json-string', () { @@ -95,6 +106,18 @@ main() { expect(credentialsFromJson.clientId.identifier, equals('myid')); expect(credentialsFromJson.clientId.secret, isNull); expect(credentialsFromJson.privateKey, equals(TestPrivateKeyString)); + expect(credentialsFromJson.impersonatedUser, isNull); + }); + + test('from-json-string-with-user', () { + var credentialsFromJson = + new ServiceAccountCredentials.fromJson( + JSON.encode(credentials), impersonatedUser: 'x@y.com'); + expect(credentialsFromJson.email, equals('a@b.com')); + expect(credentialsFromJson.clientId.identifier, equals('myid')); + expect(credentialsFromJson.clientId.secret, isNull); + expect(credentialsFromJson.privateKey, equals(TestPrivateKeyString)); + expect(credentialsFromJson.impersonatedUser, equals('x@y.com')); }); test('from-json-map', () { @@ -104,6 +127,18 @@ main() { expect(credentialsFromJson.clientId.identifier, equals('myid')); expect(credentialsFromJson.clientId.secret, isNull); expect(credentialsFromJson.privateKey, equals(TestPrivateKeyString)); + expect(credentialsFromJson.impersonatedUser, isNull); + }); + + test('from-json-map-with-user', () { + var credentialsFromJson = + new ServiceAccountCredentials.fromJson( + credentials, impersonatedUser: 'x@y.com'); + expect(credentialsFromJson.email, equals('a@b.com')); + expect(credentialsFromJson.clientId.identifier, equals('myid')); + expect(credentialsFromJson.clientId.secret, isNull); + expect(credentialsFromJson.privateKey, equals(TestPrivateKeyString)); + expect(credentialsFromJson.impersonatedUser, equals('x@y.com')); }); });