Multiple clients and scopes #52
Conversation
|
Wow! The Speed! |
To be honest I didn't expect it too be wrapped up so quickly but we paired with @leplatrem |
| @@ -45,6 +46,10 @@ def includeme(config): | |||
| settings['fxa-oauth.requested_scope'] = settings['fxa-oauth.scope'] | |||
| settings['fxa-oauth.required_scope'] = settings['fxa-oauth.scope'] | |||
|
|
|||
| resources, scope_routing = parse_resources(settings) | |||
leplatrem
Nov 22, 2017
Contributor
parse_resources() might not be the best name
parse_resources() might not be the best name
| @@ -14,6 +14,8 @@ | |||
|
|
|||
| logger = logging.getLogger(__name__) | |||
|
|
|||
| KEY = 'fxa_verified_token' | |||
leplatrem
Nov 22, 2017
Contributor
REIFY_KEY ?
REIFY_KEY ?
| @@ -49,6 +51,7 @@ class FxAOAuthAuthenticationPolicy(base_auth.CallbackAuthenticationPolicy): | |||
| def __init__(self, realm='Realm'): | |||
| self.realm = realm | |||
| self._cache = None | |||
| self._config = None | |||
leplatrem
Nov 22, 2017
Contributor
unused
unused
| return self._verify_token(token, request) | ||
|
|
||
| user_id, client_name = self._verify_token(token, request) | ||
|
|
leplatrem
Nov 22, 2017
Contributor
Don't add suffix if authentication failed, or no specific client name is configured
Don't add suffix if authentication failed, or no specific client name is configured
Natim
Nov 23, 2017
Author
Member
Is it a comment you want to add?
Is it a comment you want to add?
leplatrem
Nov 24, 2017
Contributor
Yes for the return user_id below
Yes for the return user_id below
| try: | ||
| profile = auth_client.verify_token(token=token, scope=aslist(scope)) | ||
| user_id = profile['user'] | ||
| scopes = profile['scope'] |
leplatrem
Nov 22, 2017
Contributor
nit: plural vs singular
nit: plural vs singular
| # Make sure the bearer token scopes don't match multiple configs. | ||
| if len([x for x in request.registry._fxa_oauth_scope_routing.keys() | ||
| if x and set(x.split()) & set(scopes)]) > 1: | ||
| logger.debug("Invalid FxA token: {} matches multiple config" % scopes) |
leplatrem
Nov 22, 2017
Contributor
We might want to warn no?
We might want to warn no?
| @@ -122,6 +141,11 @@ def _get_cache(self, request): | |||
|
|
|||
| return self._cache | |||
|
|
|||
| def callback(self, userid, request): | |||
| if request.bound_data[KEY][1] != "default": | |||
leplatrem
Nov 22, 2017
Contributor
no safety check here?
no safety check here?
rfk
Nov 23, 2017
Is this adding additional principals, beyond the userid?
Is this adding additional principals, beyond the userid?
Natim
Nov 23, 2017
Author
Member
Yes
Yes
| @@ -122,6 +141,11 @@ def _get_cache(self, request): | |||
|
|
|||
| return self._cache | |||
|
|
|||
| def callback(self, userid, request): | |||
| if request.bound_data[KEY][1] != "default": | |||
| return ["fxa:{}".format(request.bound_data[KEY][0])] | |||
leplatrem
Nov 22, 2017
Contributor
I think this could be clearer with comments or intermediary variables
I think this could be clearer with comments or intermediary variables
| api_mocked.return_value = self.profile_data | ||
| principals = self.policy.effective_principals(self.request) | ||
| self.assertIn("fxa:33", principals) | ||
| self.assertIn("33-notes", principals) |
leplatrem
Nov 22, 2017
Contributor
AssertNotIn("33") ?
AssertNotIn("33") ?
|
|
||
|
|
||
| def parse_resources(settings): | ||
| resources = OrderedDict() |
leplatrem
Nov 22, 2017
Contributor
if settings is not an ordered dict I don't think it makes sense
if settings is not an ordered dict I don't think it makes sense
|
| if client_name is None or client_name == 'default': | ||
| return user_id | ||
|
|
||
| return '{}-{}'.format(user_id, client_name) |
rfk
Nov 23, 2017
The doc at [1] suggests this should have an "fxa:" prefix, like "fxa:5998e2e2e81c40bdad6d08ac2773bf61-lockbox".
[1] https://docs.google.com/document/d/19Tg5AyRdHDP7z_J6kWM-JgiO6tB6FmUuEXUweENYZq8/edit?ts=5a155487#
The doc at [1] suggests this should have an "fxa:" prefix, like "fxa:5998e2e2e81c40bdad6d08ac2773bf61-lockbox".
[1] https://docs.google.com/document/d/19Tg5AyRdHDP7z_J6kWM-JgiO6tB6FmUuEXUweENYZq8/edit?ts=5a155487#
Natim
Nov 23, 2017
Author
Member
Yes this will be added by kinto.
Yes this will be added by kinto.
|
|
||
| # Make sure the bearer token scopes don't match multiple configs. | ||
| if len([x for x in request.registry._fxa_oauth_scope_routing.keys() | ||
| if x and set(x.split()) & set(scopes)]) > 1: |
rfk
Nov 23, 2017
It's not clear to me whether this is doing what you intend. What if the scope routing here was something like:
{
"profile https://identity.mozilla.com/app/notes": "notes",
"profile https://identity.mozilla.com/app/lockbox": "lockbox",
}
Then it seems to me that a token with "profile https://identity.mozilla.com/app/lockbox" would also match the set-intersection check above for "profile https://identity.mozilla.com/app/notes", because they both contain the "profile" scope. Is that the intended behaviour?
Or, if the keys in _fxa_oauth_scope_routing are only ever intended to be a single scope like "https://identity.mozilla.com/app/notes" rather than a list of scopes, maybe we should explicitly error out rather than doing set(x.split()) and so-on here.
On a side note: there's a lot of string splitting and parsing being done here on each request, I wonder if it would make sense to parse the scopes into a set or frozenset during config loading to make the checking here quicker.
It's not clear to me whether this is doing what you intend. What if the scope routing here was something like:
{
"profile https://identity.mozilla.com/app/notes": "notes",
"profile https://identity.mozilla.com/app/lockbox": "lockbox",
}
Then it seems to me that a token with "profile https://identity.mozilla.com/app/lockbox" would also match the set-intersection check above for "profile https://identity.mozilla.com/app/notes", because they both contain the "profile" scope. Is that the intended behaviour?
Or, if the keys in _fxa_oauth_scope_routing are only ever intended to be a single scope like "https://identity.mozilla.com/app/notes" rather than a list of scopes, maybe we should explicitly error out rather than doing set(x.split()) and so-on here.
On a side note: there's a lot of string splitting and parsing being done here on each request, I wonder if it would make sense to parse the scopes into a set or frozenset during config loading to make the checking here quicker.
Natim
Nov 23, 2017
Author
Member
Yes I need to add a few tests for that.
Yes I need to add a few tests for that.
|
|
||
| if client_name in ('relier', 'state', 'webapp'): | ||
| setting_basename = '{}.{}'.format(client_name, setting_basename) | ||
| client_name = 'default' |
rfk
Nov 23, 2017
This mixing of client names with other config parameters seems kinda fragile to me. What if a future version of this library introduces a new config parameter, that happens to share a name with a client in my config?
How would it look if, instead, we used something like "fxa-oauth.clients.lockbox.required_scope" in order to ensure there can't be confusion between the client names and other config names?
This mixing of client names with other config parameters seems kinda fragile to me. What if a future version of this library introduces a new config parameter, that happens to share a name with a client in my config?
How would it look if, instead, we used something like "fxa-oauth.clients.lockbox.required_scope" in order to ensure there can't be confusion between the client names and other config names?
| fxa-oauth.clients.todo.required_scope = profile app-todo | ||
|
|
||
|
|
||
| In that case Kinto will give you two different `user_id`: |
leplatrem
Nov 24, 2017
Contributor
Instead of in that case, I would say something like Depending on the requested scopes, Kinto Fxa will assign a user id or another (using suffix)
Instead of in that case, I would say something like Depending on the requested scopes, Kinto Fxa will assign a user id or another (using suffix)
|
|
||
| In that case Kinto will give you two different `user_id`: | ||
|
|
||
| - `fxa:user_id-notes` for the former |
leplatrem
Nov 24, 2017
Contributor
fxa:{user_id}-notes ?
fxa:{user_id}-notes ?
| - `fxa:user_id-notes` for the former | ||
| - `fxa:user_id-todo` for the later | ||
|
|
||
| Note that you can use `fxa:user_id` to explicitely share data between |
leplatrem
Nov 24, 2017
Contributor
you can still use the fxa:{user_id} principal
you can still use the fxa:{user_id} principal
| access the todo app data. | ||
|
|
||
| The default buckets will also be isolated, one for `notes` and one for | ||
| `todo`. |
leplatrem
Nov 24, 2017
Contributor
👍
| apps for a given FxA user. | ||
|
|
||
| If you don't give any specific permission, it will be impossible for | ||
| someone login in with the `app-notes` scope in their Bearer token to |
leplatrem
Nov 24, 2017
Contributor
logged in?
logged in?
| return self._verify_token(token, request) | ||
|
|
||
| user_id, client_name = self._verify_token(token, request) | ||
|
|
leplatrem
Nov 24, 2017
Contributor
Yes for the return user_id below
Yes for the return user_id below
| scope = profile['scope'] | ||
| client_name = client | ||
|
|
||
| # Make sure the bearer token scope don't match multiple configs. |
leplatrem
Nov 24, 2017
Contributor
scopes
scopes
| client_name = client | ||
|
|
||
| # Make sure the bearer token scope don't match multiple configs. | ||
| if len([x for x in request.registry._fxa_oauth_scope_routing.keys() |
leplatrem
Nov 24, 2017
Contributor
Maybe we could declare routing_scopes outside the loop.
Plus we could iterate on it above instead of request.registry._fxa_oauth_scope_routing.items()
Maybe we could declare routing_scopes outside the loop.
Plus we could iterate on it above instead of request.registry._fxa_oauth_scope_routing.items()
|
|
||
| # Make sure the bearer token scope don't match multiple configs. | ||
| if len([x for x in request.registry._fxa_oauth_scope_routing.keys() | ||
| if x and set(x.split()).issubset(set(scope))]) > 1: |
leplatrem
Nov 24, 2017
Contributor
nit: use intermediary variable to have a something like if len(intersecting_scopes) > 1
nit: use intermediary variable to have a something like if len(intersecting_scopes) > 1
|
Tested and configured in the dev instance: "user": {
"bucket": "159e598f-3c2b-6d93-9df9-74a334657c9e",
"id": "fxa:6ce43a4a28a74cb99b27243035c7dd55-notes",
"principals": [
"fxa:6ce43a4a28a74cb99b27243035c7dd55-notes",
"system.Everyone",
"fxa:6ce43a4a28a74cb99b27243035c7dd55",
"system.Authenticated"
]
} |
f? @rfk @vladikoff