def new_token(self, refresh_token, client_id=None, client_secret=None): if not refresh_token: raise OAuthError('refresh_token required') # If these aren't set on the Account object, use the values from # config so that the dev version of the sync engine continues to work. client_id = client_id or self.OAUTH_CLIENT_ID client_secret = client_secret or self.OAUTH_CLIENT_SECRET access_token_url = self.OAUTH_ACCESS_TOKEN_URL data = urllib.urlencode({ 'refresh_token': refresh_token, 'client_id': client_id, 'client_secret': client_secret, 'grant_type': 'refresh_token' }) headers = { 'Content-type': 'application/x-www-form-urlencoded', 'Accept': 'text/plain' } try: response = requests.post(access_token_url, data=data, headers=headers) except requests.exceptions.ConnectionError as e: log.error('Network error renewing access token', error=e) raise ConnectionError() try: session_dict = response.json() except JSONDecodeError: log.error('Invalid JSON renewing on renewing token', response=response.text) raise ConnectionError('Invalid JSON response on renewing token') if 'error' in session_dict: if session_dict['error'] == 'invalid_grant': # This is raised if the user has revoked access to the # application (or if the refresh token is otherwise invalid). raise OAuthError('invalid_grant') elif session_dict['error'] == 'deleted_client': # If the developer has outright deleted their Google OAuth app # ID. We treat this too as a case of 'invalid credentials'. raise OAuthError('deleted_client') else: # You can also get e.g. {"error": "internal_failure"} log.error('Error renewing access token', session_dict=session_dict) raise ConnectionError('Server error renewing access token') return session_dict['access_token'], session_dict['expires_in']
def _new_access_token_from_refresh_token(self, account): refresh_token = account.refresh_token if not refresh_token: raise OAuthError("refresh_token required") client_id, client_secret = account.get_client_info() access_token_url = self.OAUTH_ACCESS_TOKEN_URL data = urllib.parse.urlencode({ "refresh_token": refresh_token, "client_id": client_id, "client_secret": client_secret, "grant_type": "refresh_token", }) headers = { "Content-type": "application/x-www-form-urlencoded", "Accept": "text/plain", } try: response = requests.post(access_token_url, data=data, headers=headers) except requests.exceptions.ConnectionError as e: log.error("Network error renewing access token", error=e) raise ConnectionError() try: session_dict = response.json() except ValueError: log.error("Invalid JSON renewing on renewing token", response=response.text) raise ConnectionError("Invalid JSON response on renewing token") if "error" in session_dict: if session_dict["error"] == "invalid_grant": # This is raised if the user has revoked access to the # application (or if the refresh token is otherwise invalid). raise OAuthError("invalid_grant") elif session_dict["error"] == "deleted_client": # If the developer has outright deleted their Google OAuth app # ID. We treat this too as a case of 'invalid credentials'. raise OAuthError("deleted_client") else: # You can also get e.g. {"error": "internal_failure"} log.error("Error renewing access token", session_dict=session_dict) raise ConnectionError("Server error renewing access token") return session_dict["access_token"], session_dict["expires_in"]
def _get_authenticated_user(self, authorization_code): args = { 'client_id': self.OAUTH_CLIENT_ID, 'client_secret': self.OAUTH_CLIENT_SECRET, 'redirect_uri': self.OAUTH_REDIRECT_URI, 'code': authorization_code, 'grant_type': 'authorization_code' } headers = {'Content-type': 'application/x-www-form-urlencoded', 'Accept': 'text/plain'} data = urllib.urlencode(args) resp = requests.post(self.OAUTH_ACCESS_TOKEN_URL, data=data, headers=headers) session_dict = resp.json() if u'error' in session_dict: raise OAuthError(session_dict['error']) access_token = session_dict['access_token'] validation_dict = self.validate_token(access_token) userinfo_dict = self._get_user_info(access_token) z = session_dict.copy() z.update(validation_dict) z.update(userinfo_dict) return z
def new_token(self, refresh_token, client_id=None, client_secret=None): if not refresh_token: raise OAuthError('refresh_token required') # If these aren't set on the Account object, use the values from # config so that the dev version of the sync engine continues to work. client_id = client_id or self.OAUTH_CLIENT_ID client_secret = client_secret or self.OAUTH_CLIENT_SECRET access_token_url = self.OAUTH_ACCESS_TOKEN_URL args = { 'refresh_token': refresh_token, 'client_id': client_id, 'client_secret': client_secret, 'grant_type': 'refresh_token' } try: headers = { 'Content-type': 'application/x-www-form-urlencoded', 'Accept': 'text/plain' } data = urllib.urlencode(args) response = requests.post(access_token_url, data=data, headers=headers) except (requests.exceptions.HTTPError, requests.exceptions.ConnectionError), e: log.error(e) raise ConnectionError()
def _get_authenticated_user(self, authorization_code): args = { "client_id": self.OAUTH_CLIENT_ID, "client_secret": self.OAUTH_CLIENT_SECRET, "redirect_uri": self.OAUTH_REDIRECT_URI, "code": authorization_code, "grant_type": "authorization_code", } headers = { "Content-type": "application/x-www-form-urlencoded", "Accept": "text/plain", } data = urllib.parse.urlencode(args) resp = requests.post(self.OAUTH_ACCESS_TOKEN_URL, data=data, headers=headers) session_dict = resp.json() if u"error" in session_dict: raise OAuthError(session_dict["error"]) userinfo_dict = self._get_user_info(session_dict) z = session_dict.copy() z.update(userinfo_dict) return z
def new_token(self, refresh_token, client_id=None, client_secret=None): if refresh_token in self.connection_error_tokens: raise ConnectionError("Invalid connection!") if refresh_token in self.revoked_refresh_tokens: raise OAuthError("Invalid token") expires_in = 10000 return ACCESS_TOKEN, expires_in
def new_token(self, scope, client_ids=None): """ Retrieves a new access token w/ access to the given scope. Returns a GToken namedtuple. If this comes across any invalid refresh_tokens, it'll set the auth_credentials' is_valid flag to False. If no valid auth tokens are available, throws an OAuthError. If client_ids is given, only looks at auth credentials with a client id in client_ids. """ non_oauth_error = None possible_credentials = [ auth_creds for auth_creds in self.valid_auth_credentials if scope in auth_creds.scopes and ( client_ids is None or auth_creds.client_id in client_ids) ] # If more than one set of credentials is present, we don't want to # just use the same one each time. shuffle(possible_credentials) for auth_creds in possible_credentials: try: token, expires_in = self.auth_handler.new_token( auth_creds.refresh_token, auth_creds.client_id, auth_creds.client_secret) expires_in -= 10 expiration = (datetime.utcnow() + timedelta(seconds=expires_in)) return GToken(token, expiration, auth_creds.scopes, auth_creds.client_id, auth_creds.id) except OAuthError as e: log.error('Error validating', account_id=self.id, auth_creds_id=auth_creds.id, logstash_tag='mark_invalid') auth_creds.is_valid = False except Exception as e: log.error('Error while getting access token: {}'.format(e), account_id=self.id, auth_creds_id=auth_creds.id, exc_info=True) non_oauth_error = e if non_oauth_error: # Some auth credential might still be valid! raise non_oauth_error else: raise OAuthError("No valid tokens")
def validate_token(self, access_token): response = requests.get(self.OAUTH_TOKEN_VALIDATION_URL, params={'access_token': access_token}) validation_dict = response.json() if 'error' in validation_dict: raise OAuthError(validation_dict['error']) return validation_dict
def _new_access_token_from_authalligator(self, account, force_refresh): """ Return the access token based on an account created in AuthAlligator. """ assert account.secret.type == SecretType.AuthAlligator.value assert self.AUTHALLIGATOR_AUTH_KEY assert self.AUTHALLIGATOR_SERVICE_URL aa_client = AuthAlligatorApiClient( token=self.AUTHALLIGATOR_AUTH_KEY, service_url=self.AUTHALLIGATOR_SERVICE_URL, ) aa_data = json.loads(account.secret.secret) provider = ProviderType(aa_data["provider"]) username = aa_data["username"] account_key = aa_data["account_key"] try: if force_refresh: aa_response = aa_client.verify_account( provider=provider, username=username, account_key=account_key, ) aa_account = aa_response.account else: aa_response = aa_client.query_account( provider=provider, username=username, account_key=account_key, ) aa_account = aa_response except AccountError as exc: log.warn( "AccountError during AuthAlligator account query", account_id=account.id, error_code=exc.code and exc.code.value, error_message=exc.message, retry_in=exc.retry_in, ) if exc.code in ( AccountErrorCode.AUTHORIZATION_ERROR, AccountErrorCode.CONFIGURATION_ERROR, AccountErrorCode.DOES_NOT_EXIST, ): raise OAuthError( "Could not obtain access token from AuthAlligator") else: raise ConnectionError( "Temporary error while obtaining access token from AuthAlligator" ) else: now = datetime.datetime.now(pytz.UTC) expires_in = int( (aa_account.access_token_expires_at - now).total_seconds()) assert expires_in > 0 return (aa_account.access_token, expires_in)
def create_account(self, db_session, email_address, response): email_address = response.get('email') # See if the account exists in db, otherwise create it try: account = db_session.query(GmailAccount) \ .filter_by(email_address=email_address).one() except NoResultFound: namespace = Namespace() account = GmailAccount(namespace=namespace) # We only get refresh tokens on initial login (or failed credentials) # otherwise, we don't force the login screen and therefore don't get a # refresh token back from google. new_refresh_token = response.get('refresh_token') if new_refresh_token: account.refresh_token = new_refresh_token else: if not account.refresh_token or account.sync_state == 'invalid': # We got a new auth without a refresh token, so we need to back # out and force the auth flow, since we don't already have # a refresh (or the one we have doesn't work.) raise OAuthError("Missing refresh token") tok = response.get('access_token') expires_in = response.get('expires_in') token_manager.cache_token(account, tok, expires_in) account.scope = response.get('scope') account.email_address = email_address account.family_name = response.get('family_name') account.given_name = response.get('given_name') account.name = response.get('name') account.gender = response.get('gender') account.g_id = response.get('id') account.g_user_id = response.get('user_id') account.g_id_token = response.get('id_token') account.link = response.get('link') account.locale = response.get('locale') account.picture = response.get('picture') account.home_domain = response.get('hd') account.client_id = response.get('client_id') account.client_secret = response.get('client_secret') account.sync_contacts = response.get('contacts', True) account.sync_events = response.get('events', True) try: self.verify_config(account) except GmailSettingError as e: raise UserRecoverableConfigError(e) # Hack to ensure that account syncs get restarted if they were stopped # because of e.g. invalid credentials and the user re-auths. # TODO(emfree): remove after status overhaul. if account.sync_state != 'running': account.sync_state = None return account
def get_client_info(self): """ Obtain the client ID and secret for this OAuth account. Return: Tuple with (client_id, client_secret). """ if not self.client_id or self.client_id == self.OAUTH_CLIENT_ID: return (self.OAUTH_CLIENT_ID, self.OAUTH_CLIENT_SECRET) else: raise OAuthError("No valid tokens.")
def update_account(self, account, account_data): account.email_address = account_data.email if account_data.secret_type: account.set_secret(account_data.secret_type, account_data.secret_value) if not account.secret: raise OAuthError("No valid auth info.") account.sync_email = account_data.sync_email account.client_id = account_data.client_id account.scope = account_data.scope return account
def new_token(self, scope): """ Retrieves a new access token w/ access to the given scope. Returns a GToken namedtuple. If this comes across any invalid refresh_tokens, it'll set the auth_credentials' is_valid flag to False. If no valid auth tokens are available, throws an OAuthError. """ non_oauth_error = None for auth_creds in self.valid_auth_credentials: if scope in auth_creds.scopes: try: token, expires_in = self.auth_handler.new_token( auth_creds.refresh_token, auth_creds.client_id, auth_creds.client_secret) expires_in -= 10 expiration = (datetime.utcnow() + timedelta(seconds=expires_in)) return GToken(token, expiration, auth_creds.scopes, auth_creds.id) except OAuthError as e: log.error('Error validating', account_id=self.id, auth_creds_id=auth_creds.id, logstash_tag='mark_invalid') auth_creds.is_valid = False except Exception as e: log.error('Error while getting access token: {}'.format(e), account_id=self.id, auth_creds_id=auth_creds.id, exc_info=True) non_oauth_error = e if non_oauth_error: # Some auth credential might still be valid! raise non_oauth_error else: raise OAuthError("No valid tokens")
def _get_user_info(self, session_dict): access_token = session_dict["access_token"] request = urllib.request.Request( self.OAUTH_USER_INFO_URL, headers={"Authorization": "Bearer {}".format(access_token)}, ) try: response = urllib.request.urlopen(request) except urllib.error.HTTPError as e: if e.code == 401: raise OAuthError("Could not retrieve user info.") log.error("user_info_fetch_failed", error_code=e.code, error=e) raise ConnectionError() except urllib.error.URLError as e: log.error("user_info_fetch_failed", error=e) raise ConnectionError() userinfo_dict = json.loads(response.read()) return {"email": userinfo_dict["EmailAddress"]}
def _get_user_info(self, access_token): try: response = requests.get(self.OAUTH_USER_INFO_URL, params={'access_token': access_token}) except requests.exceptions.ConnectionError as e: log.error('user_info_fetch_failed', error=e) raise ConnectionError() userinfo_dict = response.json() if 'error' in userinfo_dict: assert userinfo_dict['error'] == 'invalid_token' log.error('user_info_fetch_failed', error=userinfo_dict['error'], error_description=userinfo_dict['error_description']) log.error('%s - %s' % (userinfo_dict['error'], userinfo_dict['error_description'])) raise OAuthError() return userinfo_dict
def acquire_access_token(self, account, force_refresh=False): """ Acquire a new access token for the given account. Args: force_refresh (bool): Whether a token refresh should be forced when requesting it from an external token service (AuthAlligator) Raises: OAuthError: If the token is no longer valid and syncing should stop. ConnectionError: If there was a temporary/connection error renewing the auth token. """ if account.secret.type == SecretType.AuthAlligator.value: return self._new_access_token_from_authalligator( account, force_refresh) elif account.secret.type == SecretType.Token.value: # Any token requested from the refresh token is refreshed already. return self._new_access_token_from_refresh_token(account) else: raise OAuthError("No supported secret found.")
class OAuthAuthHandler(AuthHandler): def connect_account(self, email, pw, imap_endpoint, account_id=None): """Provide a connection to a IMAP account. Raises ------ socket.error If we cannot connect to the IMAP host. IMAPClient.error If the credentials are invalid. """ host, port = imap_endpoint try: conn = IMAPClient(host, port=port, use_uid=True, ssl=True) except IMAPClient.AbortError as e: log.error('account_connect_failed', account_id=account_id, email=email, host=host, port=port, error="[ALERT] Can't connect to host - may be transient") raise TransientConnectionError(str(e)) except (IMAPClient.Error, gaierror, socket_error) as e: log.error('account_connect_failed', account_id=account_id, email=email, host=host, port=port, error='[ALERT] (Failure): {0}'.format(str(e))) raise ConnectionError(str(e)) conn.debug = False try: conn.oauth2_login(email, pw) except IMAPClient.AbortError as e: log.error('account_verify_failed', account_id=account_id, email=email, host=host, port=port, error="[ALERT] Can't connect to host - may be transient") raise TransientConnectionError(str(e)) except IMAPClient.Error as e: log.error('IMAP Login error during connection. ' 'Account: {}, error: {}'.format(email, e), account_id=account_id) if (str(e) == '[ALERT] Invalid credentials (Failure)' or str(e).startswith('[AUTHENTICATIONFAILED]')): raise ValidationError(str(e)) else: raise ConnectionError(str(e)) except SSLError as e: log.error('account_verify_failed', account_id=account_id, email=email, host=host, port=port, error='[ALERT] (Failure) SSL Connection error') raise ConnectionError(str(e)) return conn def verify_account(self, account): """Verifies a IMAP account by logging in.""" try: access_token = token_manager.get_token(account) conn = self.connect_account(account.email_address, access_token, account.imap_endpoint, account.id) conn.logout() except ValidationError: # Access token could've expired, refresh and try again. access_token = token_manager.get_token(account, force_refresh=True) conn = self.connect_account(account.email_address, access_token, account.imap_endpoint, account.id) conn.logout() return True def validate_token(self, access_token): """Implemented by subclasses.""" raise NotImplementedError def new_token(self, refresh_token, client_id=None, client_secret=None): if not refresh_token: raise OAuthError('refresh_token required') # If these aren't set on the Account object, use the values from # config so that the dev version of the sync engine continues to work. client_id = client_id or self.OAUTH_CLIENT_ID client_secret = client_secret or self.OAUTH_CLIENT_SECRET access_token_url = self.OAUTH_ACCESS_TOKEN_URL args = { 'refresh_token': refresh_token, 'client_id': client_id, 'client_secret': client_secret, 'grant_type': 'refresh_token' } try: headers = { 'Content-type': 'application/x-www-form-urlencoded', 'Accept': 'text/plain' } data = urllib.urlencode(args) response = requests.post(access_token_url, data=data, headers=headers) except (requests.exceptions.HTTPError, requests.exceptions.ConnectionError), e: log.error(e) raise ConnectionError() try: session_dict = response.json() except JSONDecodeError: raise ConnectionError("Invalid json: " + response.text) if u'error' in session_dict: raise OAuthError(session_dict['error']) return session_dict['access_token'], session_dict['expires_in']
def get_token_for_email(self, account, force_refresh=False): if self.allow_auth: return 'foo' raise OAuthError()
def raise_oauth_error(e): raise OAuthError(e)
def get_token(self, account, force_refresh=True): if self.allow_auth: # return a fake token. return "foo" raise OAuthError()
def create_account(self, db_session, email_address, response): email_address = response.get('email') # See if the account exists in db, otherwise create it try: account = db_session.query(GmailAccount) \ .filter_by(email_address=email_address).one() except NoResultFound: namespace = Namespace() account = GmailAccount(namespace=namespace) # We only get refresh tokens on initial login (or failed credentials) # otherwise, we don't force the login screen and therefore don't get a # refresh token back from google. new_refresh_token = response.get('refresh_token') if new_refresh_token: account.refresh_token = new_refresh_token else: if not account.refresh_token or account.sync_state == 'invalid': # We got a new auth without a refresh token, so we need to back # out and force the auth flow, since we don't already have # a refresh (or the one we have doesn't work.) raise OAuthError("Missing refresh token") tok = response.get('access_token') expires_in = response.get('expires_in') token_manager.cache_token(account, tok, expires_in) account.scope = response.get('scope') account.email_address = email_address account.family_name = response.get('family_name') account.given_name = response.get('given_name') account.name = response.get('name') account.gender = response.get('gender') account.g_id = response.get('id') account.g_user_id = response.get('user_id') account.g_id_token = response.get('id_token') account.link = response.get('link') account.locale = response.get('locale') account.picture = response.get('picture') account.home_domain = response.get('hd') account.client_id = response.get('client_id') account.client_secret = response.get('client_secret') account.sync_contacts = response.get('contacts', True) account.sync_events = response.get('events', True) try: self.verify_config(account) except GmailSettingError as e: raise UserRecoverableConfigError(e) # Ensure account has sync enabled. account.enable_sync() # See if we've already stored this refresh token match = [ auth_creds for auth_creds in account.auth_credentials if auth_creds.refresh_token == new_refresh_token ] # For new refresh_tokens, create new GmailAuthCredentials entry if new_refresh_token and len(match) == 0: auth_creds = GmailAuthCredentials() auth_creds.gmailaccount = account auth_creds.scopes = response.get('scope') auth_creds.g_id_token = response.get('id_token') auth_creds.client_id = response.get('client_id') auth_creds.client_secret = response.get('client_secret') auth_creds.refresh_token = new_refresh_token return account
def raise_401(*args): raise OAuthError()
def create_account(self, db_session, email_address, response): email_address = response.get('email') # See if the account exists in db, otherwise create it try: account = db_session.query(GmailAccount) \ .filter_by(email_address=email_address).one() except NoResultFound: namespace = Namespace() account = GmailAccount(namespace=namespace) # We only get refresh tokens on initial login (or failed credentials) # otherwise, we don't force the login screen and therefore don't get a # refresh token back from google. new_refresh_token = response.get('refresh_token') if new_refresh_token: account.refresh_token = new_refresh_token else: if (len(account.valid_auth_credentials) == 0 or account.sync_state == 'invalid'): # We got a new auth without a refresh token, so we need to back # out and force the auth flow, since we don't already have # a refresh (or the ones we have don't work.) raise OAuthError("No valid refresh tokens") account.email_address = email_address account.family_name = response.get('family_name') account.given_name = response.get('given_name') account.name = response.get('name') account.gender = response.get('gender') account.g_id = response.get('id') account.g_user_id = response.get('user_id') account.link = response.get('link') account.locale = response.get('locale') account.picture = response.get('picture') account.home_domain = response.get('hd') account.sync_contacts = (account.sync_contacts or response.get('contacts', True)) account.sync_events = (account.sync_events or response.get('events', True)) # These values are deprecated and should not be used, along # with the account's refresh_token. Access all these values # through the GmailAuthCredentials objects instead. account.client_id = response.get('client_id') account.client_secret = response.get('client_secret') account.scope = response.get('scope') account.g_id_token = response.get('id_token') # Don't need to actually save these now # tok = response.get('access_token') # expires_in = response.get('expires_in') client_id = response.get('client_id') or OAUTH_CLIENT_ID client_secret = response.get('client_secret') or OAUTH_CLIENT_SECRET if new_refresh_token: # See if we already have credentials for this client_id/secret # pair. If those don't exist, make a new GmailAuthCredentials auth_creds = next( (auth_creds for auth_creds in account.auth_credentials if (auth_creds.client_id == client_id and auth_creds.client_secret == client_secret)), GmailAuthCredentials()) auth_creds.gmailaccount = account auth_creds.scopes = response.get('scope') auth_creds.g_id_token = response.get('id_token') auth_creds.client_id = client_id auth_creds.client_secret = client_secret auth_creds.refresh_token = new_refresh_token auth_creds.is_valid = True db_session.add(auth_creds) self.verify_config(account) # Ensure account has sync enabled. account.enable_sync() return account