Navigation Menu

Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrating Auth API to the new Identity Toolkit endpoint #256

Merged
merged 5 commits into from Feb 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,7 @@
# Unreleased

- [added] Migrated the `auth` user management API to the
new Identity Toolkit endpoint.
- [fixed] Extending HTTP retries to more HTTP methods like POST and PATCH.

# v2.15.1
Expand Down
2 changes: 1 addition & 1 deletion firebase_admin/_token_gen.py
Expand Up @@ -206,7 +206,7 @@ def create_session_cookie(self, id_token, expires_in):
'validDuration': expires_in,
}
try:
response = self.client.body('post', 'createSessionCookie', json=payload)
response = self.client.body('post', ':createSessionCookie', json=payload)
except requests.exceptions.RequestException as error:
self._handle_http_error(COOKIE_CREATE_ERROR, 'Failed to create session cookie', error)
else:
Expand Down
12 changes: 6 additions & 6 deletions firebase_admin/_user_mgt.py
Expand Up @@ -394,7 +394,7 @@ def get_user(self, **kwargs):
raise TypeError('Unsupported keyword arguments: {0}.'.format(kwargs))

try:
response = self._client.body('post', 'getAccountInfo', json=payload)
response = self._client.body('post', '/accounts:lookup', json=payload)
except requests.exceptions.RequestException as error:
msg = 'Failed to get user by {0}: {1}.'.format(key_type, key)
self._handle_http_error(INTERNAL_ERROR, msg, error)
Expand All @@ -421,7 +421,7 @@ def list_users(self, page_token=None, max_results=MAX_LIST_USERS_RESULTS):
if page_token:
payload['nextPageToken'] = page_token
try:
return self._client.body('post', 'downloadAccount', json=payload)
return self._client.body('get', '/accounts:batchGet', params=payload)
except requests.exceptions.RequestException as error:
self._handle_http_error(USER_DOWNLOAD_ERROR, 'Failed to download user accounts.', error)

Expand All @@ -440,7 +440,7 @@ def create_user(self, uid=None, display_name=None, email=None, phone_number=None
}
payload = {k: v for k, v in payload.items() if v is not None}
try:
response = self._client.body('post', 'signupNewUser', json=payload)
response = self._client.body('post', '/accounts', json=payload)
except requests.exceptions.RequestException as error:
self._handle_http_error(USER_CREATE_ERROR, 'Failed to create new user.', error)
else:
Expand Down Expand Up @@ -490,7 +490,7 @@ def update_user(self, uid, display_name=_UNSPECIFIED, email=None, phone_number=_

payload = {k: v for k, v in payload.items() if v is not None}
try:
response = self._client.body('post', 'setAccountInfo', json=payload)
response = self._client.body('post', '/accounts:update', json=payload)
except requests.exceptions.RequestException as error:
self._handle_http_error(
USER_UPDATE_ERROR, 'Failed to update user: {0}.'.format(uid), error)
Expand All @@ -503,7 +503,7 @@ def delete_user(self, uid):
"""Deletes the user identified by the specified user ID."""
_auth_utils.validate_uid(uid, required=True)
try:
response = self._client.body('post', 'deleteAccount', json={'localId' : uid})
response = self._client.body('post', '/accounts:delete', json={'localId' : uid})
except requests.exceptions.RequestException as error:
self._handle_http_error(
USER_DELETE_ERROR, 'Failed to delete user: {0}.'.format(uid), error)
Expand All @@ -529,7 +529,7 @@ def import_users(self, users, hash_alg=None):
raise ValueError('A UserImportHash is required to import users with passwords.')
payload.update(hash_alg.to_dict())
try:
response = self._client.body('post', 'uploadAccount', json=payload)
response = self._client.body('post', '/accounts:batchCreate', json=payload)
except requests.exceptions.RequestException as error:
self._handle_http_error(USER_IMPORT_ERROR, 'Failed to import users.', error)
else:
Expand Down
11 changes: 9 additions & 2 deletions firebase_admin/auth.py
Expand Up @@ -466,13 +466,20 @@ def __init__(self, code, message, error=None):
class _AuthService(object):
"""Firebase Authentication service."""

ID_TOOLKIT_URL = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/'
ID_TOOLKIT_URL = 'https://identitytoolkit.googleapis.com/v1/projects/'

def __init__(self, app):
credential = app.credential.get_credential()
version_header = 'Python/Admin/{0}'.format(firebase_admin.__version__)

if not app.project_id:
raise ValueError("""Project ID is required to access the auth service.
1. Use a service account credential, or
2. set the project ID explicitly via Firebase App options, or
3. set the project ID via the GOOGLE_CLOUD_PROJECT environment variable.""")

client = _http_client.JsonHttpClient(
credential=credential, base_url=self.ID_TOOLKIT_URL,
credential=credential, base_url=self.ID_TOOLKIT_URL + app.project_id,
headers={'X-Client-Version': version_header})
self._token_generator = _token_gen.TokenGenerator(app, client)
self._token_verifier = _token_gen.TokenVerifier(app)
Expand Down
1 change: 1 addition & 0 deletions integration/test_auth.py
Expand Up @@ -65,6 +65,7 @@ def test_custom_token_without_service_account(api_key):
cred = CredentialWrapper.from_existing_credential(google_cred)
custom_app = firebase_admin.initialize_app(cred, {
'serviceAccountId': google_cred.service_account_email,
'projectId': firebase_admin.get_app().project_id
}, 'temp-app')
try:
custom_token = auth.create_custom_token('user1', app=custom_app)
Expand Down
24 changes: 8 additions & 16 deletions tests/test_token_gen.py
Expand Up @@ -199,7 +199,7 @@ def test_noncert_credential(self, user_mgt_app):
auth.create_custom_token(MOCK_UID, app=user_mgt_app)

def test_sign_with_iam(self):
options = {'serviceAccountId': 'test-service-account'}
options = {'serviceAccountId': 'test-service-account', 'projectId': 'mock-project-id'}
app = firebase_admin.initialize_app(
testutils.MockCredential(), name='iam-signer-app', options=options)
try:
Expand All @@ -213,7 +213,7 @@ def test_sign_with_iam(self):
firebase_admin.delete_app(app)

def test_sign_with_iam_error(self):
options = {'serviceAccountId': 'test-service-account'}
options = {'serviceAccountId': 'test-service-account', 'projectId': 'mock-project-id'}
app = firebase_admin.initialize_app(
testutils.MockCredential(), name='iam-signer-app', options=options)
try:
Expand All @@ -228,7 +228,9 @@ def test_sign_with_iam_error(self):

def test_sign_with_discovered_service_account(self):
request = testutils.MockRequest(200, 'discovered-service-account')
app = firebase_admin.initialize_app(testutils.MockCredential(), name='iam-signer-app')
options = {'projectId': 'mock-project-id'}
app = firebase_admin.initialize_app(testutils.MockCredential(), name='iam-signer-app',
options=options)
try:
_overwrite_iam_request(app, request)
# Force initialization of the signing provider. This will invoke the Metadata service.
Expand All @@ -248,7 +250,9 @@ def test_sign_with_discovered_service_account(self):

def test_sign_with_discovery_failure(self):
request = testutils.MockFailedRequest(Exception('test error'))
app = firebase_admin.initialize_app(testutils.MockCredential(), name='iam-signer-app')
options = {'projectId': 'mock-project-id'}
app = firebase_admin.initialize_app(testutils.MockCredential(), name='iam-signer-app',
options=options)
try:
_overwrite_iam_request(app, request)
with pytest.raises(ValueError) as excinfo:
Expand Down Expand Up @@ -409,12 +413,6 @@ def test_project_id_env_var(self, env_var_app):
claims = auth.verify_id_token(TEST_ID_TOKEN, env_var_app)
assert claims['admin'] is True

@pytest.mark.parametrize('env_var_app', [{}], indirect=True)
def test_no_project_id(self, env_var_app):
_overwrite_cert_request(env_var_app, MOCK_REQUEST)
with pytest.raises(ValueError):
auth.verify_id_token(TEST_ID_TOKEN, env_var_app)

def test_custom_token(self, auth_app):
id_token = auth.create_custom_token(MOCK_UID, app=auth_app)
_overwrite_cert_request(auth_app, MOCK_REQUEST)
Expand Down Expand Up @@ -515,12 +513,6 @@ def test_project_id_env_var(self, env_var_app):
claims = auth.verify_session_cookie(TEST_SESSION_COOKIE, app=env_var_app)
assert claims['admin'] is True

@pytest.mark.parametrize('env_var_app', [{}], indirect=True)
def test_no_project_id(self, env_var_app):
_overwrite_cert_request(env_var_app, MOCK_REQUEST)
with pytest.raises(ValueError):
auth.verify_session_cookie(TEST_SESSION_COOKIE, app=env_var_app)

def test_custom_token(self, auth_app):
custom_token = auth.create_custom_token(MOCK_UID, app=auth_app)
_overwrite_cert_request(auth_app, MOCK_REQUEST)
Expand Down
22 changes: 16 additions & 6 deletions tests/test_user_mgt.py
Expand Up @@ -26,6 +26,8 @@
from firebase_admin import _user_mgt
from tests import testutils

from six.moves import urllib


INVALID_STRINGS = [None, '', 0, 1, True, False, list(), tuple(), dict()]
INVALID_DICTS = [None, 'foo', 0, 1, True, False, list(), tuple()]
Expand Down Expand Up @@ -87,6 +89,14 @@ def _check_user_record(user, expected_uid='testuser'):
assert provider.provider_id == 'phone'


class TestAuthServiceInitialization(object):

def test_fail_on_no_project_id(self):
app = firebase_admin.initialize_app(testutils.MockCredential(), name='userMgt2')
with pytest.raises(ValueError):
auth._get_auth_service(app)
firebase_admin.delete_app(app)

class TestUserRecord(object):

# Input dict must be non-empty, and must not contain unsupported keys.
Expand Down Expand Up @@ -511,7 +521,7 @@ def test_list_multiple_pages(self, user_mgt_app):
assert page.next_page_token == ''
assert page.has_next_page is False
assert page.get_next_page() is None
self._check_rpc_calls(recorder, {'maxResults': 1000, 'nextPageToken': 'token'})
self._check_rpc_calls(recorder, {'maxResults': '1000', 'nextPageToken': 'token'})

def test_list_users_paged_iteration(self, user_mgt_app):
# Page 1
Expand All @@ -537,7 +547,7 @@ def test_list_users_paged_iteration(self, user_mgt_app):
assert user.uid == 'user4'
with pytest.raises(StopIteration):
next(iterator)
self._check_rpc_calls(recorder, {'maxResults': 1000, 'nextPageToken': 'token'})
self._check_rpc_calls(recorder, {'maxResults': '1000', 'nextPageToken': 'token'})

def test_list_users_iterator_state(self, user_mgt_app):
response = {
Expand Down Expand Up @@ -590,13 +600,13 @@ def test_list_users_with_max_results(self, user_mgt_app):
_, recorder = _instrument_user_manager(user_mgt_app, 200, MOCK_LIST_USERS_RESPONSE)
page = auth.list_users(max_results=500, app=user_mgt_app)
self._check_page(page)
self._check_rpc_calls(recorder, {'maxResults' : 500})
self._check_rpc_calls(recorder, {'maxResults' : '500'})

def test_list_users_with_all_args(self, user_mgt_app):
_, recorder = _instrument_user_manager(user_mgt_app, 200, MOCK_LIST_USERS_RESPONSE)
page = auth.list_users(page_token='foo', max_results=500, app=user_mgt_app)
self._check_page(page)
self._check_rpc_calls(recorder, {'nextPageToken' : 'foo', 'maxResults' : 500})
self._check_rpc_calls(recorder, {'nextPageToken' : 'foo', 'maxResults' : '500'})

def test_list_users_error(self, user_mgt_app):
_instrument_user_manager(user_mgt_app, 500, '{"error":"test"}')
Expand All @@ -618,9 +628,9 @@ def _check_page(self, page):

def _check_rpc_calls(self, recorder, expected=None):
if expected is None:
expected = {'maxResults' : 1000}
expected = {'maxResults' : '1000'}
assert len(recorder) == 1
request = json.loads(recorder[0].body.decode())
request = dict(urllib.parse.parse_qsl(urllib.parse.urlsplit(recorder[0].url).query))
assert request == expected


Expand Down