Skip to content

Commit

Permalink
Add support for imersonated_credentials.Sign, IDToken (#348)
Browse files Browse the repository at this point in the history
  • Loading branch information
salrashid123 authored and busunkim96 committed Aug 7, 2019
1 parent 1322d89 commit 7a8641a
Show file tree
Hide file tree
Showing 4 changed files with 423 additions and 6 deletions.
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ also provides integration with several HTTP libraries.

- Support for Google :func:`Application Default Credentials <google.auth.default>`.
- Support for signing and verifying :mod:`JWTs <google.auth.jwt>`.
- Support for creating `Google ID Tokens <user-guide.html#identity-tokens>`__.
- Support for verifying and decoding :mod:`ID Tokens <google.oauth2.id_token>`.
- Support for Google :mod:`Service Account credentials <google.oauth2.service_account>`.
- Support for Google :mod:`Impersonated Credentials <google.auth.impersonated_credentials>`.
Expand Down
80 changes: 79 additions & 1 deletion docs/user-guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -241,13 +241,91 @@ the "Service Account Token Creator" IAM role. ::
client = storage.Client(credentials=target_credentials)
buckets = client.list_buckets(project='your_project')
for bucket in buckets:
print bucket.name
print(bucket.name)


In the example above `source_credentials` does not have direct access to list buckets
in the target project. Using `ImpersonatedCredentials` will allow the source_credentials
to assume the identity of a target_principal that does have access.

Identity Tokens
+++++++++++++++

`Google OpenID Connect`_ tokens are avaiable through :mod:`Service Account <google.oauth2.service_account>`,
:mod:`Impersonated <google.auth.impersonated_credentials>`,
and :mod:`Compute Engine <google.auth.compute_engine>`. These tokens can be used to
authenticate against `Cloud Functions`_, `Cloud Run`_, a user service behind
`Identity Aware Proxy`_ or any other service capable of verifying a `Google ID Token`_.

ServiceAccount ::

from google.oauth2 import service_account

target_audience = 'https://example.com'

creds = service_account.IDTokenCredentials.from_service_account_file(
'/path/to/svc.json',
target_audience=target_audience)


Compute ::

from google.auth import compute_engine
import google.auth.transport.requests

target_audience = 'https://example.com'

request = google.auth.transport.requests.Request()
creds = compute_engine.IDTokenCredentials(request,
target_audience=target_audience)

Impersonated ::

from google.auth import impersonated_credentials

# get target_credentials from a source_credential

target_audience = 'https://example.com'

creds = impersonated_credentials.IDTokenCredentials(
target_credentials,
target_audience=target_audience)

IDToken verification can be done for various type of IDTokens using the :class:`google.oauth2.id_token` module

A sample end-to-end flow using an ID Token against a Cloud Run endpoint maybe ::

from google.oauth2 import id_token
from google.oauth2 import service_account
import google.auth
import google.auth.transport.requests
from google.auth.transport.requests import AuthorizedSession

target_audience = 'https://your-cloud-run-app.a.run.app'
url = 'https://your-cloud-run-app.a.run.app'

creds = service_account.IDTokenCredentials.from_service_account_file(
'/path/to/svc.json', target_audience=target_audience)

authed_session = AuthorizedSession(creds)

# make authenticated request and print the response, status_code
resp = authed_session.get(url)
print(resp.status_code)
print(resp.text)

# to verify an ID Token
request = google.auth.transport.requests.Request()
token = creds.token
print(token)
print(id_token.verify_token(token,request))

.. _Cloud Functions: https://cloud.google.com/functions/
.. _Cloud Run: https://cloud.google.com/run/
.. _Identity Aware Proxy: https://cloud.google.com/iap/
.. _Google OpenID Connect: https://developers.google.com/identity/protocols/OpenIDConnect
.. _Google ID Token: https://developers.google.com/identity/protocols/OpenIDConnect#validatinganidtoken

Making authenticated requests
-----------------------------

Expand Down
125 changes: 122 additions & 3 deletions google/auth/impersonated_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
https://cloud.google.com/iam/credentials/reference/rest/
"""

import base64
import copy
from datetime import datetime
import json
Expand All @@ -35,6 +36,8 @@
from google.auth import _helpers
from google.auth import credentials
from google.auth import exceptions
from google.auth import jwt
from google.auth.transport.requests import AuthorizedSession

_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds

Expand All @@ -43,8 +46,18 @@
_IAM_ENDPOINT = ('https://iamcredentials.googleapis.com/v1/projects/-' +
'/serviceAccounts/{}:generateAccessToken')

_IAM_SIGN_ENDPOINT = ('https://iamcredentials.googleapis.com/v1/projects/-' +
'/serviceAccounts/{}:signBlob')

_IAM_IDTOKEN_ENDPOINT = ('https://iamcredentials.googleapis.com/v1/' +
'projects/-/serviceAccounts/{}:generateIdToken')

_REFRESH_ERROR = 'Unable to acquire impersonated credentials'

_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds

_DEFAULT_TOKEN_URI = 'https://oauth2.googleapis.com/token'


def _make_iam_token_request(request, principal, headers, body):
"""Makes a request to the Google Cloud IAM service for an access token.
Expand Down Expand Up @@ -94,7 +107,7 @@ def _make_iam_token_request(request, principal, headers, body):
six.raise_from(new_exc, caught_exc)


class Credentials(credentials.Credentials):
class Credentials(credentials.Credentials, credentials.Signing):
"""This module defines impersonated credentials which are essentially
impersonated identities.
Expand Down Expand Up @@ -153,7 +166,7 @@ class Credentials(credentials.Credentials):
client = storage.Client(credentials=target_credentials)
buckets = client.list_buckets(project='your_project')
for bucket in buckets:
print bucket.name
print(bucket.name)
"""

def __init__(self, source_credentials, target_principal,
Expand All @@ -172,7 +185,8 @@ def __init__(self, source_credentials, target_principal,
granted to the prceeding identity. For example, if set to
[serviceAccountB, serviceAccountC], the source_credential
must have the Token Creator role on serviceAccountB.
serviceAccountB must have the Token Creator on serviceAccountC.
serviceAccountB must have the Token Creator on
serviceAccountC.
Finally, C must have Token Creator on target_principal.
If left unset, source_credential must have that role on
target_principal.
Expand Down Expand Up @@ -229,3 +243,108 @@ def _update_token(self, request):
principal=self._target_principal,
headers=headers,
body=body)

def sign_bytes(self, message):

iam_sign_endpoint = _IAM_SIGN_ENDPOINT.format(self._target_principal)

body = {
"payload": base64.b64encode(message),
"delegates": self._delegates
}

headers = {
'Content-Type': 'application/json',
}

authed_session = AuthorizedSession(self._source_credentials)

response = authed_session.post(
url=iam_sign_endpoint,
headers=headers,
json=body)

return base64.b64decode(response.json()['signedBlob'])

@property
def signer_email(self):
return self._target_principal

@property
def service_account_email(self):
return self._target_principal

@property
def signer(self):
return self


class IDTokenCredentials(credentials.Credentials):
"""Open ID Connect ID Token-based service account credentials.
"""
def __init__(self, target_credentials,
target_audience=None, include_email=False):
"""
Args:
target_credentials (google.auth.Credentials): The target
credential used as to acquire the id tokens for.
target_audience (string): Audience to issue the token for.
include_email (bool): Include email in IdToken
"""
super(IDTokenCredentials, self).__init__()

if not isinstance(target_credentials,
Credentials):
raise exceptions.GoogleAuthError("Provided Credential must be "
"impersonated_credentials")
self._target_credentials = target_credentials
self._target_audience = target_audience
self._include_email = include_email

def from_credentials(self, target_credentials,
target_audience=None):
return self.__class__(
target_credentials=self._target_credentials,
target_audience=target_audience)

def with_target_audience(self, target_audience):
return self.__class__(
target_credentials=self._target_credentials,
target_audience=target_audience)

def with_include_email(self, include_email):
return self.__class__(
target_credentials=self._target_credentials,
target_audience=self._target_audience,
include_email=include_email)

@_helpers.copy_docstring(credentials.Credentials)
def refresh(self, request):

iam_sign_endpoint = _IAM_IDTOKEN_ENDPOINT.format(self.
_target_credentials.
signer_email)

body = {
"audience": self._target_audience,
"delegates": self._target_credentials._delegates,
"includeEmail": self._include_email
}

headers = {
'Content-Type': 'application/json',
}

authed_session = AuthorizedSession(self._target_credentials.
_source_credentials)

response = authed_session.post(
url=iam_sign_endpoint,
headers=headers,
data=json.dumps(body).encode('utf-8'))

id_token = response.json()['token']
self.token = id_token
self.expiry = datetime.fromtimestamp(jwt.decode(id_token,
verify=False)['exp'])
Loading

0 comments on commit 7a8641a

Please sign in to comment.