2020import cachecontrol
2121import requests
2222import six
23+ from google.auth import credentials
24+ from google.auth import exceptions
25+ from google.auth import iam
2326from google.auth import jwt
2427from google.auth import transport
2528import google.oauth2.id_token
26-
27- from firebase_admin import credentials
29+ import google.oauth2.service_account
2830
2931
3032# ID token constants
4648 'acr', 'amr', 'at_hash', 'aud', 'auth_time', 'azp', 'cnf', 'c_hash',
4749 'exp', 'firebase', 'iat', 'iss', 'jti', 'nbf', 'nonce', 'sub'
4850])
51+ METADATA_SERVICE_URL = ('http://metadata/computeMetadata/v1/instance/service-accounts/'
52+ 'default/email')
4953
5054# Error codes
5155COOKIE_CREATE_ERROR = 'COOKIE_CREATE_ERROR'
56+ TOKEN_SIGN_ERROR = 'TOKEN_SIGN_ERROR'
5257
5358
5459class ApiCallError(Exception):
@@ -60,20 +65,81 @@ def __init__(self, code, message, error=None):
6065 self.detail = error
6166
6267
68+ class _SigningProvider(object):
69+ """Stores a reference to a google.auth.crypto.Signer."""
70+
71+ def __init__(self, signer, signer_email):
72+ self._signer = signer
73+ self._signer_email = signer_email
74+
75+ @property
76+ def signer(self):
77+ return self._signer
78+
79+ @property
80+ def signer_email(self):
81+ return self._signer_email
82+
83+ @classmethod
84+ def from_credential(cls, google_cred):
85+ return _SigningProvider(google_cred.signer, google_cred.signer_email)
86+
87+ @classmethod
88+ def from_iam(cls, request, google_cred, service_account):
89+ signer = iam.Signer(request, google_cred, service_account)
90+ return _SigningProvider(signer, service_account)
91+
92+
6393class TokenGenerator(object):
6494 """Generates custom tokens and session cookies."""
6595
6696 def __init__(self, app, client):
67- self._app = app
68- self._client = client
97+ self.app = app
98+ self.client = client
99+ self.request = transport.requests.Request()
100+ self._signing_provider = None
101+
102+ def _init_signing_provider(self):
103+ """Initializes a signing provider by following the go/firebase-admin-sign protocol."""
104+ # If the SDK was initialized with a service account, use it to sign bytes.
105+ google_cred = self.app.credential.get_credential()
106+ if isinstance(google_cred, google.oauth2.service_account.Credentials):
107+ return _SigningProvider.from_credential(google_cred)
108+
109+ # If the SDK was initialized with a service account email, use it with the IAM service
110+ # to sign bytes.
111+ service_account = self.app.options.get('serviceAccountId')
112+ if service_account:
113+ return _SigningProvider.from_iam(self.request, google_cred, service_account)
114+
115+ # If the SDK was initialized with some other credential type that supports signing
116+ # (e.g. GAE credentials), use it to sign bytes.
117+ if isinstance(google_cred, credentials.Signing):
118+ return _SigningProvider.from_credential(google_cred)
119+
120+ # Attempt to discover a service account email from the local Metadata service. Use it
121+ # with the IAM service to sign bytes.
122+ resp = self.request(url=METADATA_SERVICE_URL, headers={'Metadata-Flavor': 'Google'})
123+ service_account = resp.data.decode()
124+ return _SigningProvider.from_iam(self.request, google_cred, service_account)
125+
126+ @property
127+ def signing_provider(self):
128+ """Initializes and returns the SigningProvider instance to be used."""
129+ if not self._signing_provider:
130+ try:
131+ self._signing_provider = self._init_signing_provider()
132+ except Exception as error:
133+ url = 'https://firebase.google.com/docs/auth/admin/create-custom-tokens'
134+ raise ValueError(
135+ 'Failed to determine service account: {0}. Make sure to initialize the SDK '
136+ 'with service account credentials or specify a service account ID with '
137+ 'iam.serviceAccounts.signBlob permission. Please refer to {1} for more '
138+ 'details on creating custom tokens.'.format(error, url))
139+ return self._signing_provider
69140
70141 def create_custom_token(self, uid, developer_claims=None):
71142 """Builds and signs a Firebase custom auth token."""
72- if not isinstance(self._app.credential, credentials.Certificate):
73- raise ValueError(
74- 'Must initialize Firebase App with a certificate credential '
75- 'to call create_custom_token().')
76-
77143 if developer_claims is not None:
78144 if not isinstance(developer_claims, dict):
79145 raise ValueError('developer_claims must be a dictionary')
@@ -93,10 +159,11 @@ def create_custom_token(self, uid, developer_claims=None):
93159 if not uid or not isinstance(uid, six.string_types) or len(uid) > 128:
94160 raise ValueError('uid must be a string between 1 and 128 characters.')
95161
162+ signing_provider = self.signing_provider
96163 now = int(time.time())
97164 payload = {
98- 'iss': self._app.credential.service_account_email ,
99- 'sub': self._app.credential.service_account_email ,
165+ 'iss': signing_provider.signer_email ,
166+ 'sub': signing_provider.signer_email ,
100167 'aud': FIREBASE_AUDIENCE,
101168 'uid': uid,
102169 'iat': now,
@@ -105,7 +172,12 @@ def create_custom_token(self, uid, developer_claims=None):
105172
106173 if developer_claims is not None:
107174 payload['claims'] = developer_claims
108- return jwt.encode(self._app.credential.signer, payload)
175+ try:
176+ return jwt.encode(signing_provider.signer, payload)
177+ except exceptions.TransportError as error:
178+ msg = 'Failed to sign custom token. {0}'.format(error)
179+ raise ApiCallError(TOKEN_SIGN_ERROR, msg, error)
180+
109181
110182 def create_session_cookie(self, id_token, expires_in):
111183 """Creates a session cookie from the provided ID token."""
@@ -131,7 +203,7 @@ def create_session_cookie(self, id_token, expires_in):
131203 'validDuration': expires_in,
132204 }
133205 try:
134- response = self._client .request('post', 'createSessionCookie', json=payload)
206+ response = self.client .request('post', 'createSessionCookie', json=payload)
135207 except requests.exceptions.RequestException as error:
136208 self._handle_http_error(COOKIE_CREATE_ERROR, 'Failed to create session cookie', error)
137209 else:
0 commit comments