def authorize_code(self, sessionOrAssertion, scope=None, client_id=None, code_challenge=None, code_challenge_method=None): """Trade an identity assertion for an oauth authorization code. This method takes an identity assertion for a user and uses it to generate an oauth authentication code. This code can in turn be traded for a full-blown oauth token. Note that the authorize_token() method does the same thing but skips the intermediate step of using a short-lived code. You should prefer that method if the registered OAuth client_id has `canGrant` permission. :param sessionOrAssertion: an identity assertion for the target user, or an auth session to use to make one. :param scope: optional scope to be provided by the token. :param client_id: the string generated during FxA client registration. :param code_challenge: optional PKCE code challenge. """ if client_id is None: client_id = self.client_id assertion = self._get_identity_assertion(sessionOrAssertion, client_id) url = "/authorization" body = { "client_id": client_id, "assertion": assertion, "state": "x", # state is required, but we don't use it } if scope is not None: body["scope"] = scope if code_challenge is not None: body["code_challenge"] = code_challenge body["code_challenge_method"] = code_challenge_method or "S256" resp = self.apiclient.post(url, body) if "redirect" not in resp: error_msg = "redirect missing in OAuth response" raise OutOfProtocolError(error_msg) # This flow is designed for web-based redirects. # In order to get the code we must parse it from the redirect url. query_params = parse_qs(urlparse(resp["redirect"]).query) try: return query_params["code"][0] except (KeyError, IndexError, ValueError): error_msg = "code missing in OAuth redirect url" raise OutOfProtocolError(error_msg)
def trade_code(self, code, client_id=None, client_secret=None, code_verifier=None, ttl=None): """Trade the authentication code for a longer lived token. :param code: the authentication code from the oauth redirect dance. :param client_id: the string generated during FxA client registration. :param client_secret: the related secret string. :param code_verifier: optional PKCE code verifier. :param ttl: optional ttl in seconds, the access token is valid for. :returns: a dict with user id and authorized scopes for this token. """ if client_id is None: client_id = self.client_id if client_secret is None: client_secret = self.client_secret url = '/token' body = { 'code': code, 'client_id': client_id, } if client_secret is not None: body["client_secret"] = client_secret if code_verifier is not None: body["code_verifier"] = code_verifier if ttl is not None: body["ttl"] = ttl resp = self.apiclient.post(url, body) if 'access_token' not in resp: error_msg = 'access_token missing in OAuth response' raise OutOfProtocolError(error_msg) return resp
def authorize_token(self, sessionOrAssertion, scope=None, client_id=None): """Trade an identity assertion for an oauth token. This method takes an identity assertion for a user and uses it to generate an oauth token. The client_id must have implicit grant privileges. :param sessionOrAssertion: an identity assertion for the target user, or an auth session to use to make one. :param scope: optional scope to be provided by the token. :param client_id: the string generated during FxA client registration. """ if client_id is None: client_id = self.client_id assertion = self._get_identity_assertion(sessionOrAssertion, client_id) url = "/authorization" body = { "client_id": client_id, "assertion": assertion, "response_type": "token", "state": "x", # state is required, but we don't use it } if scope is not None: body["scope"] = scope resp = self.apiclient.post(url, body) if 'access_token' not in resp: error_msg = 'access_token missing in OAuth response' raise OutOfProtocolError(error_msg) return resp['access_token']
def trade_code(self, code, client_id=None, client_secret=None): """Trade the authentication code for a longer lived token. :param code: the authentication code from the oauth redirect dance. :param client_id: the string generated during FxA client registration. :param client_secret: the related secret string. :returns: a dict with user id and authorized scopes for this token. """ if client_id is None: client_id = self.client_id if client_secret is None: client_secret = self.client_secret url = '/token' body = { 'code': code, 'client_id': client_id, 'client_secret': client_secret } resp = self.apiclient.post(url, body) if 'access_token' not in resp: error_msg = 'access_token missing in OAuth response' raise OutOfProtocolError(error_msg) return resp['access_token']
def verify_token(self, token, scope=None): """Verify an OAuth token, and retrieve user id and scopes. :param token: the string to verify. :param scope: optional scope expected to be provided for this token. :returns: a dict with user id and authorized scopes for this token. :raises fxa.errors.ClientError: if the provided token is invalid. :raises fxa.errors.TrustError: if the token scopes do not match. """ url = '/v1/verify' body = {'token': token} resp = self.apiclient.post(url, body) missing_attrs = ", ".join( [k for k in ('user', 'scope', 'client_id') if k not in resp]) if missing_attrs: error_msg = '{0} missing in OAuth response'.format(missing_attrs) raise OutOfProtocolError(error_msg) if scope is not None: authorized_scope = resp['scope'] if not scope_matches(authorized_scope, scope): raise ScopeMismatchError(authorized_scope, scope) return resp
def get_avatar_url(self, token): """Get the url for a user's avatar picture.""" url = '/avatar' resp = self.apiclient.get(url, auth=BearerTokenAuth(token)) try: return resp["url"] except KeyError: error_msg = "url missing in profile response" raise OutOfProtocolError(error_msg)
def get_uid(self, token): """Get the account uid for the user associated with this token.""" url = '/uid' resp = self.apiclient.get(url, auth=BearerTokenAuth(token)) try: return resp["uid"] except KeyError: error_msg = "uid missing in profile response" raise OutOfProtocolError(error_msg)
def get_email(self, token): """Get the email address for the user associated with this token.""" url = '/email' resp = self.apiclient.get(url, auth=BearerTokenAuth(token)) try: return resp["email"] except KeyError: error_msg = "email missing in profile response" raise OutOfProtocolError(error_msg)
def authorize_code(self, assertion, scope=None, client_id=None): """Trade an identity assertion for an oauth authorization code. This method takes an identity assertion for a user and uses it to generate an oauth authentication code. This code can in turn be traded for a full-blown oauth token. Note that the authorize_token() method does the same thing but skips the intermediate step of using a short-lived code, and hence this method is likely only useful for testing purposes. :param assertion: an identity assertion for the target user. :param scope: optional scope to be provided by the token. :param client_id: the string generated during FxA client registration. """ if client_id is None: client_id = self.client_id url = "/authorization" body = { "client_id": client_id, "assertion": assertion, "state": "x", # state is required, but we don't use it } if scope is not None: body["scope"] = scope resp = self.apiclient.post(url, body) if "redirect" not in resp: error_msg = "redirect missing in OAuth response" raise OutOfProtocolError(error_msg) # This flow is designed for web-based redirects. # In order to get the code we must parse it from the redirect url. query_params = parse_qs(urlparse(resp["redirect"]).query) try: return query_params["code"][0] except (KeyError, IndexError, ValueError): error_msg = "code missing in OAuth redirect url" raise OutOfProtocolError(error_msg)
def verify_token(self, token, scope=None): """Verify an OAuth token, and retrieve user id and scopes. :param token: the string to verify. :param scope: optional scope expected to be provided for this token. :returns: a dict with user id and authorized scopes for this token. :raises fxa.errors.ClientError: if the provided token is invalid. :raises fxa.errors.TrustError: if the token scopes do not match. """ key = 'fxa.oauth.verify_token:%s:%s' % (get_hmac( token, TOKEN_HMAC_SECRET), scope) if self.cache is not None: resp = self.cache.get(key) else: resp = None if resp is None: url = '/verify' body = {'token': token} resp = self.apiclient.post(url, body) missing_attrs = ", ".join( [k for k in ('user', 'scope', 'client_id') if k not in resp]) if missing_attrs: error_msg = '{0} missing in OAuth response'.format( missing_attrs) raise OutOfProtocolError(error_msg) if scope is not None: authorized_scope = resp['scope'] if not scope_matches(authorized_scope, scope): raise ScopeMismatchError(authorized_scope, scope) if self.cache is not None: self.cache.set(key, json.dumps(resp)) else: resp = json.loads(resp) return resp
def verify_token(self, token, scope=None): """Verify an OAuth token, and retrieve user id and scopes. :param token: the string to verify. :param scope: optional scope expected to be provided for this token. :returns: a dict with user id and authorized scopes for this token. :raises fxa.errors.ClientError: if the provided token is invalid. :raises fxa.errors.TrustError: if the token scopes do not match. """ key = 'fxa.oauth.verify_token:%s:%s' % ( get_hmac(token, TOKEN_HMAC_SECRET), scope) if self.cache is not None: resp = self.cache.get(key) else: resp = None if resp is None: # We want to fetch # https://oauth.accounts.firefox.com/.well-known/openid-configuration # and then get the jwks_uri key to get the /jwks url, but we'll # just hardcodes it like this for now; our /jwks url will never # change. # https://github.com/mozilla/PyFxA/issues/81 is an issue about # getting the jwks url out of the openid-configuration. keys = self.apiclient.get('/jwks').get('keys', []) resp = None try: for k in keys: try: resp = self._verify_jwt_token(json.dumps(k), token) break except jwt.exceptions.InvalidSignatureError: # It's only worth trying other keys in the event of # `InvalidSignature`; if it was invalid for other reasons # (e.g. it's expired) then using a different key won't # help. continue else: # It's a well-formed JWT, but not signed by any of the advertized keys. # We can immediately surface this as an error. if len(keys) > 0: raise TrustError({"error": "invalid signature"}) except (jwt.exceptions.DecodeError, jwt.exceptions.InvalidKeyError): # It wasn't a JWT at all, or it was signed using a key type we # don't support. Fall back to asking the FxA server to verify. pass except jwt.exceptions.PyJWTError as e: # Any other JWT-related failure (e.g. expired token) can # immediately surface as a trust error. raise TrustError({"error": str(e)}) if resp is None: resp = self.apiclient.post('/verify', {'token': token}) missing_attrs = ", ".join([ k for k in ('user', 'scope', 'client_id') if resp.get(k) is None ]) if missing_attrs: error_msg = '{0} missing in OAuth response'.format( missing_attrs) raise OutOfProtocolError(error_msg) if scope is not None: authorized_scope = resp['scope'] if not scope_matches(authorized_scope, scope): raise ScopeMismatchError(authorized_scope, scope) if self.cache is not None: self.cache.set(key, json.dumps(resp)) else: resp = json.loads(resp) return resp
def authorize_code(self, sessionOrAssertion, scope=None, client_id=None, code_challenge=None, code_challenge_method=None): """Trade an identity assertion for an oauth authorization code. This method takes an identity assertion for a user and uses it to generate an oauth authentication code. This code can in turn be traded for a full-blown oauth token. Note that the authorize_token() method does the same thing but skips the intermediate step of using a short-lived code. You should prefer that method if the registered OAuth client_id has `canGrant` permission. :param sessionOrAssertion: an identity assertion for the target user, or an auth session to use to make one. :param scope: optional scope to be provided by the token. :param client_id: the string generated during FxA client registration. :param code_challenge: optional PKCE code challenge. """ if client_id is None: client_id = self.client_id assertion = self._get_identity_assertion(sessionOrAssertion, client_id) url = "/authorization" # Although not relevant in this scenario from a security perspective, # we generate a random 'state' and check the returned redirect URL # for completeness. state = base64.urlsafe_b64encode(os.urandom(24)).decode('utf-8') body = { "client_id": client_id, "assertion": assertion, "state": state } if scope is not None: body["scope"] = scope if code_challenge is not None: body["code_challenge"] = code_challenge body["code_challenge_method"] = code_challenge_method or "S256" resp = self.apiclient.post(url, body) if "redirect" not in resp: error_msg = "redirect missing in OAuth response" raise OutOfProtocolError(error_msg) # This flow is designed for web-based redirects. # In order to get the code we must parse it from the redirect url. query_params = parse_qs(urlparse(resp["redirect"]).query) # Check that the 'state' parameter is present and the same we provided if "state" not in query_params: error_msg = "state missing in OAuth response" raise OutOfProtocolError(error_msg) if state != query_params["state"][0]: error_msg = "state mismatch in OAuth response (wanted: '{}', got: '{}')".format( state, query_params["state"][0]) raise OutOfProtocolError(error_msg) try: return query_params["code"][0] except (KeyError, IndexError, ValueError): error_msg = "code missing in OAuth redirect url" raise OutOfProtocolError(error_msg)