Skip to content
This repository has been archived by the owner on Jan 26, 2021. It is now read-only.

Commit

Permalink
Add support for impersonating a user when using a service account
Browse files Browse the repository at this point in the history
Allow `ServiceAccountCredentials` constructors to take an optional
`user` argument to specify a user to impersonate.

BUG=#15
[email protected]

Review URL: https://codereview.chromium.org/893423003
  • Loading branch information
sgjesse committed Feb 5, 2015
1 parent af198ed commit 669deea
Show file tree
Hide file tree
Showing 8 changed files with 106 additions and 15 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`.
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
37 changes: 29 additions & 8 deletions lib/auth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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) {
Expand Down
2 changes: 2 additions & 0 deletions lib/auth_io.dart
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ Future<AutoRefreshingAuthClient> clientViaServiceAccount(

var flow = new JwtFlow(clientCredentials.email,
clientCredentials.privateRSAKey,
clientCredentials.impersonatedUser,
scopes,
baseClient);
return flow.run().catchError((error, stack) {
Expand Down Expand Up @@ -266,6 +267,7 @@ Future<AccessCredentials> obtainAccessCredentialsViaServiceAccount(
List<String> scopes, Client baseClient) {
return new JwtFlow(clientCredentials.email,
clientCredentials.privateRSAKey,
clientCredentials.impersonatedUser,
scopes,
baseClient).run();
}
Expand Down
8 changes: 6 additions & 2 deletions lib/src/oauth2_flows/jwt.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@ class JwtFlow {
final String _clientEmail;
final RS256Signer _signer;
final List<String> _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<AccessCredentials> run() {
Expand All @@ -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())));
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: googleapis_auth
version: 0.2.2
version: 0.2.3
author: Dart Team <[email protected]>
description: Obtain Access credentials for Google services using OAuth 2.0
homepage: https://github.com/dart-lang/googleapis_auth
Expand Down
18 changes: 15 additions & 3 deletions test/oauth2_flows/jwt_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -59,15 +59,27 @@ main() {
}));
});

test('successfull-with-user', () {
var flow = new JwtFlow(clientEmail, TestPrivateKey, '[email protected]', 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));
Expand Down
35 changes: 35 additions & 0 deletions test/oauth2_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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: '[email protected]');
expect(credentials.email, equals('email'));
expect(credentials.clientId, equals(clientId));
expect(credentials.privateKey, equals(TestPrivateKeyString));
expect(credentials.impersonatedUser, equals('[email protected]'));
});

test('from-json-string', () {
Expand All @@ -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: '[email protected]');
expect(credentialsFromJson.email, equals('[email protected]'));
expect(credentialsFromJson.clientId.identifier, equals('myid'));
expect(credentialsFromJson.clientId.secret, isNull);
expect(credentialsFromJson.privateKey, equals(TestPrivateKeyString));
expect(credentialsFromJson.impersonatedUser, equals('[email protected]'));
});

test('from-json-map', () {
Expand All @@ -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: '[email protected]');
expect(credentialsFromJson.email, equals('[email protected]'));
expect(credentialsFromJson.clientId.identifier, equals('myid'));
expect(credentialsFromJson.clientId.secret, isNull);
expect(credentialsFromJson.privateKey, equals(TestPrivateKeyString));
expect(credentialsFromJson.impersonatedUser, equals('[email protected]'));
});
});

Expand Down

0 comments on commit 669deea

Please sign in to comment.