20
20
import cachecontrol
21
21
import requests
22
22
import six
23
+ from google .auth import credentials
24
+ from google .auth import exceptions
25
+ from google .auth import iam
23
26
from google .auth import jwt
24
27
from google .auth import transport
25
28
import google .oauth2 .id_token
26
-
27
- from firebase_admin import credentials
29
+ import google .oauth2 .service_account
28
30
29
31
30
32
# ID token constants
46
48
'acr' , 'amr' , 'at_hash' , 'aud' , 'auth_time' , 'azp' , 'cnf' , 'c_hash' ,
47
49
'exp' , 'firebase' , 'iat' , 'iss' , 'jti' , 'nbf' , 'nonce' , 'sub'
48
50
])
51
+ METADATA_SERVICE_URL = ('http://metadata/computeMetadata/v1/instance/service-accounts/'
52
+ 'default/email' )
49
53
50
54
# Error codes
51
55
COOKIE_CREATE_ERROR = 'COOKIE_CREATE_ERROR'
56
+ TOKEN_SIGN_ERROR = 'TOKEN_SIGN_ERROR'
52
57
53
58
54
59
class ApiCallError (Exception ):
@@ -60,20 +65,81 @@ def __init__(self, code, message, error=None):
60
65
self .detail = error
61
66
62
67
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
+
63
93
class TokenGenerator (object ):
64
94
"""Generates custom tokens and session cookies."""
65
95
66
96
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
69
140
70
141
def create_custom_token (self , uid , developer_claims = None ):
71
142
"""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
-
77
143
if developer_claims is not None :
78
144
if not isinstance (developer_claims , dict ):
79
145
raise ValueError ('developer_claims must be a dictionary' )
@@ -93,10 +159,11 @@ def create_custom_token(self, uid, developer_claims=None):
93
159
if not uid or not isinstance (uid , six .string_types ) or len (uid ) > 128 :
94
160
raise ValueError ('uid must be a string between 1 and 128 characters.' )
95
161
162
+ signing_provider = self .signing_provider
96
163
now = int (time .time ())
97
164
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 ,
100
167
'aud' : FIREBASE_AUDIENCE ,
101
168
'uid' : uid ,
102
169
'iat' : now ,
@@ -105,7 +172,12 @@ def create_custom_token(self, uid, developer_claims=None):
105
172
106
173
if developer_claims is not None :
107
174
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
+
109
181
110
182
def create_session_cookie (self , id_token , expires_in ):
111
183
"""Creates a session cookie from the provided ID token."""
@@ -131,7 +203,7 @@ def create_session_cookie(self, id_token, expires_in):
131
203
'validDuration' : expires_in ,
132
204
}
133
205
try :
134
- response = self ._client .request ('post' , 'createSessionCookie' , json = payload )
206
+ response = self .client .request ('post' , 'createSessionCookie' , json = payload )
135
207
except requests .exceptions .RequestException as error :
136
208
self ._handle_http_error (COOKIE_CREATE_ERROR , 'Failed to create session cookie' , error )
137
209
else :
0 commit comments