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

Firebase auth Email Action Links API #258

Merged
merged 7 commits into from
Feb 13, 2019
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Unreleased

- [added] Added `generate_password_reset_link()`,
`generate_email_verification_link()` and `generate_sign_in_with_email_link()`
methods to the `auth` API.
- [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.
Expand Down
7 changes: 7 additions & 0 deletions firebase_admin/_auth_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
'acr', 'amr', 'at_hash', 'aud', 'auth_time', 'azp', 'cnf', 'c_hash', 'exp', 'iat',
'iss', 'jti', 'nbf', 'nonce', 'sub', 'firebase',
])
VALID_EMAIL_ACTION_TYPES = set(['VERIFY_EMAIL', 'EMAIL_SIGNIN', 'PASSWORD_RESET'])


def validate_uid(uid, required=False):
Expand Down Expand Up @@ -181,3 +182,9 @@ def validate_custom_claims(custom_claims, required=False):
raise ValueError(
'Claim "{0}" is reserved, and must not be set.'.format(invalid_claims.pop()))
return claims_str

def validate_action_type(action_type):
if action_type not in VALID_EMAIL_ACTION_TYPES:
raise ValueError('Invalid action type provided action_type: {0}. \
Valid values are {1}'.format(action_type, ', '.join(VALID_EMAIL_ACTION_TYPES)))
return action_type
118 changes: 118 additions & 0 deletions firebase_admin/_user_mgt.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import requests
import six
from six.moves import urllib

from firebase_admin import _auth_utils
from firebase_admin import _user_import
Expand All @@ -30,6 +31,7 @@
USER_DELETE_ERROR = 'USER_DELETE_ERROR'
USER_IMPORT_ERROR = 'USER_IMPORT_ERROR'
USER_DOWNLOAD_ERROR = 'LIST_USERS_ERROR'
GENERATE_EMAIL_ACTION_LINK_ERROR = 'GENERATE_EMAIL_ACTION_LINK_ERROR'

MAX_LIST_USERS_RESULTS = 1000
MAX_IMPORT_USERS_SIZE = 1000
Expand Down Expand Up @@ -372,6 +374,87 @@ def photo_url(self):
def provider_id(self):
return self._data.get('providerId')

class ActionCodeSettings(object):
"""Contains required continue/state URL with optional Android and iOS settings.
Used when invoking the email action link generation APIs.
"""

def __init__(self, url, handle_code_in_app=None, dynamic_link_domain=None, ios_bundle_id=None,
android_package_name=None, android_install_app=None, android_minimum_version=None):
self.url = url
self.handle_code_in_app = handle_code_in_app
self.dynamic_link_domain = dynamic_link_domain
self.ios_bundle_id = ios_bundle_id
self.android_package_name = android_package_name
self.android_install_app = android_install_app
self.android_minimum_version = android_minimum_version

def encode_action_code_settings(settings):
""" Validates the provided action code settings for email link generation and
populates the REST api parameters.

settings - ``ActionCodeSettings`` object provided to be encoded
returns - dict of parameters to be passed for link gereration.
"""

parameters = {}
# url
if not settings.url:
raise ValueError("Dynamic action links url is mandatory")

try:
parsed = urllib.parse.urlparse(settings.url)
if not parsed.netloc:
raise ValueError('Malformed dynamic action links url: "{0}".'.format(settings.url))
parameters['continueUrl'] = settings.url
except Exception:
raise ValueError('Malformed dynamic action links url: "{0}".'.format(settings.url))

# handle_code_in_app
if settings.handle_code_in_app is not None:
if not isinstance(settings.handle_code_in_app, bool):
raise ValueError('Invalid value provided for handle_code_in_app: {0}'
.format(settings.handle_code_in_app))
parameters['canHandleCodeInApp'] = settings.handle_code_in_app

# dynamic_link_domain
if settings.dynamic_link_domain is not None:
if not isinstance(settings.dynamic_link_domain, six.string_types):
raise ValueError('Invalid value provided for dynamic_link_domain: {0}'
.format(settings.dynamic_link_domain))
parameters['dynamicLinkDomain'] = settings.dynamic_link_domain

# ios_bundle_id
if settings.ios_bundle_id is not None:
if not isinstance(settings.ios_bundle_id, six.string_types):
raise ValueError('Invalid value provided for ios_bundle_id: {0}'
.format(settings.ios_bundle_id))
parameters['iosBundleId'] = settings.ios_bundle_id

# android_* attributes
if (settings.android_minimum_version or settings.android_install_app) \
and not settings.android_package_name:
raise ValueError("Android package name is required when specifying other Android settings")

if settings.android_package_name is not None:
if not isinstance(settings.android_package_name, six.string_types):
raise ValueError('Invalid value provided for android_package_name: {0}'
.format(settings.android_package_name))
parameters['androidPackageName'] = settings.android_package_name

if settings.android_minimum_version is not None:
if not isinstance(settings.android_minimum_version, six.string_types):
raise ValueError('Invalid value provided for android_minimum_version: {0}'
.format(settings.android_minimum_version))
parameters['androidMinimumVersion'] = settings.android_minimum_version

if settings.android_install_app is not None:
if not isinstance(settings.android_install_app, bool):
raise ValueError('Invalid value provided for android_install_app: {0}'
.format(settings.android_install_app))
parameters['androidInstallApp'] = settings.android_install_app

return parameters

class UserManager(object):
"""Provides methods for interacting with the Google Identity Toolkit."""
Expand Down Expand Up @@ -537,6 +620,41 @@ def import_users(self, users, hash_alg=None):
raise ApiCallError(USER_IMPORT_ERROR, 'Failed to import users.')
return response

def generate_email_action_link(self, action_type, email, action_code_settings=None):
"""Fetches the email action links for types

Args:
action_type: String. Valid values ['VERIFY_EMAIL', 'EMAIL_SIGNIN', 'PASSWORD_RESET']
email: Email of the user for which the action is performed
action_code_settings: ``ActionCodeSettings`` object or dict (optional). Defines whether
the link is to be handled by a mobile app and the additional state information to be
passed in the deep link, etc.
Returns:
link_url: action url to be emailed to the user

Raises:
ApiCallError: If an error occurs while generating the link
ValueError: If the provided arguments are invalid
"""
payload = {
'requestType': _auth_utils.validate_action_type(action_type),
'email': _auth_utils.validate_email(email),
'returnOobLink': True
}

if action_code_settings:
payload.update(encode_action_code_settings(action_code_settings))

try:
response = self._client.body('post', '/accounts:sendOobCode', json=payload)
except requests.exceptions.RequestException as error:
self._handle_http_error(GENERATE_EMAIL_ACTION_LINK_ERROR, 'Failed to generate link.',
error)
else:
if not response or not response.get('oobLink'):
raise ApiCallError(GENERATE_EMAIL_ACTION_LINK_ERROR, 'Failed to generate link.')
return response.get('oobLink')

def _handle_http_error(self, code, msg, error):
if error.response is not None:
msg += '\nServer response: {0}'.format(error.response.content.decode())
Expand Down
77 changes: 77 additions & 0 deletions firebase_admin/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@


__all__ = [
'ActionCodeSettings',
'AuthError',
'ErrorInfo',
'ExportedUserRecord',
Expand All @@ -51,6 +52,9 @@
'create_session_cookie',
'create_user',
'delete_user',
'generate_password_reset_link',
'generate_email_verification_link',
'generate_sign_in_with_email_link',
'get_user',
'get_user_by_email',
'get_user_by_phone_number',
Expand All @@ -63,6 +67,7 @@
'verify_session_cookie',
]

ActionCodeSettings = _user_mgt.ActionCodeSettings
ErrorInfo = _user_import.ErrorInfo
ExportedUserRecord = _user_mgt.ExportedUserRecord
ListUsersPage = _user_mgt.ListUsersPage
Expand Down Expand Up @@ -448,6 +453,78 @@ def import_users(users, hash_alg=None, app=None):
except _user_mgt.ApiCallError as error:
raise AuthError(error.code, str(error), error.detail)

def generate_password_reset_link(email, action_code_settings=None, app=None):
"""Generates the out-of-band email action link for password reset flows for the specified email
address.

Args:
email: The email of the user whose password is to be reset.
action_code_settings: ``ActionCodeSettings`` instance (optional). Defines whether
the link is to be handled by a mobile app and the additional state information to be
passed in the deep link.
app: An App instance (optional).
Returns:
link: The password reset link created by API

Raises:
ValueError: If the provided arguments are invalid
AuthError: If an error occurs while generating the link
"""
user_manager = _get_auth_service(app).user_manager
try:
return user_manager.generate_email_action_link('PASSWORD_RESET', email,
action_code_settings=action_code_settings)
except _user_mgt.ApiCallError as error:
raise AuthError(error.code, str(error), error.detail)

def generate_email_verification_link(email, action_code_settings=None, app=None):
"""Generates the out-of-band email action link for email verification flows for the specified
email address.

Args:
email: The email of the user to be verified.
action_code_settings: ``ActionCodeSettings`` instance (optional). Defines whether
the link is to be handled by a mobile app and the additional state information to be
passed in the deep link.
app: An App instance (optional).
Returns:
link: The email verification link created by API

Raises:
ValueError: If the provided arguments are invalid
AuthError: If an error occurs while generating the link
"""
user_manager = _get_auth_service(app).user_manager
try:
return user_manager.generate_email_action_link('VERIFY_EMAIL', email,
action_code_settings=action_code_settings)
except _user_mgt.ApiCallError as error:
raise AuthError(error.code, str(error), error.detail)

def generate_sign_in_with_email_link(email, action_code_settings, app=None):
"""Generates the out-of-band email action link for email link sign-in flows, using the action
code settings provided.

Args:
email: The email of the user signing in.
action_code_settings: ``ActionCodeSettings`` instance. Defines whether
the link is to be handled by a mobile app and the additional state information to be
passed in the deep link.
app: An App instance (optional).
Returns:
link: The email sign in link created by API

Raises:
ValueError: If the provided arguments are invalid
AuthError: If an error occurs while generating the link
"""
user_manager = _get_auth_service(app).user_manager
try:
return user_manager.generate_email_action_link('EMAIL_SIGNIN', email,
action_code_settings=action_code_settings)
except _user_mgt.ApiCallError as error:
raise AuthError(error.code, str(error), error.detail)

def _check_jwt_revoked(verified_claims, error_code, label, app):
user = get_user(verified_claims.get('uid'), app=app)
if verified_claims.get('iat') * 1000 < user.tokens_valid_after_timestamp:
Expand Down
Loading