-
Notifications
You must be signed in to change notification settings - Fork 3.9k
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
feat: add user_id in the default scopes if no scopes are requested from payload #34300
feat: add user_id in the default scopes if no scopes are requested from payload #34300
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, this is a good idea! It looks good to me, might be worth running by the arch-bom team, too.
client credentials, it should use available scopes as default. | ||
""" | ||
if request.grant_type == 'client_credentials' and not request.scopes: | ||
return get_scopes_backend().get_available_scopes(application=request.client, request=request) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. Some high-level thoughts:
- I don't know how this will interact with the email/profile defaults implemented in the JWT code? Whatever change we might make, we need to ensure we have good test coverage.
- See feat: Add scope user_id to JWT payload #33455 for related background around the user_id claim. I wish we had a cleaner simpler story for where user_id should not be added. It also includes default scope code, which seems to exist in other locations, so I'm not clear on the best implementation.
- It feels like adding all scopes as default would make things less-secure. I'm wondering if we should keep this scoped (pun-intended) down to just
user_id
? I think we are moving to a place where we are trying to makeuser_id
a default ifuser_id
is an allowable scope.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here are some specific requests. Please let me know if you have any questions or concerns. I don't think any of this will take very long.
- Instead of adding all allowable scopes, only add
user_id
if it is in the allowable scopes list. (Note: we can always grow the default list in the future if need be, but it will be difficult to impossible to ever shrink it.) - Update the ADR in feat: Add scope user_id to JWT payload #33455 with this addition. You can add a Change History section, like is used in OEPs.
- Add a temporary rollout toggle setting. It is easy to get auth stuff wrong, or have unintended consequences, and this will make it simple to turn on/off. We can remove the toggle after all has proven ok.
- Add or update unit tests. This includes tests around the JWTs. Having the rollout toggle should make it simple to test that scopes are working as we expect before and after.
@iloveagent57: You can also let me know if you think any of this seems unreasonable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That all sounds extremely reasonable! Thanks for reviewing this.
For my own clarity on (1) - what are the reasons to not return all scopes to the requesting application when the corresponding ApplicationAccess
record says it's ok for that application to have those scopes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For my own clarity on (1) - what are the reasons to not return all scopes to the requesting application when the corresponding
ApplicationAccess
record says it's ok for that application to have those scopes?
a. For (1), it's about the principle of least privilege. One could ask why users have to request a scope at all when we could always return a token that can do anything the user can do? That's basically what this proposal would be. Although this is perfectly valid, it would mean that tokens may have more access than a user needs.
b. Related, we don't have a lot of scopes at this time. Sorry although it seemingly isn't a big deal, because this decision would be difficult to reverse, I'd rather add as little as possible as a default.
c. The user_id
claim and our needs around it are unique. I'm not sure, but I'm guessing that the system or person requesting the token doesn't care about user_id
, and only we care about it. So, we don't want the user to have to think about requesting this scope. Actually, when the user does request a specific scope, I think other defaults are not included unless requested. This could be an issue if we really want user_id
on all of these JWTs. Maybe scope isn't the right way to go around this at all? That said, we can iterate on improvements, as long as you get user_id
where you need it for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be nice if we could add tests for this newly added logic.
If the request payload does not have `scopes` attribute for a grant_type of | ||
client credentials, it should use available scopes as default. | ||
""" | ||
if request.grant_type == 'client_credentials' and not request.scopes: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@moeez96: Once this lands, would it be possible to consolidate the password grant code in this same location, or are there reasons why the user_id
default code needs to be all over the place? If it can't be consolidated, I wonder where we could best comment about this to help our future selves? In the ADR? In _get_updated_scopes? Other?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@robrap please have a look at the updated logic and let me know if there's anything out of logic.
147c5cd
to
f832f91
Compare
f832f91
to
51de2c6
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally looks good. Thank you. Any "nit" comments you can ignore or follow as you wish.
Additionally, there are these remaining comments from the past:
- Update the ADR in feat: Add scope user_id to JWT payload #33455 with this addition. You can add a Change History section, like is used in OEPs.
- Add a temporary rollout toggle setting. It is easy to get auth stuff wrong, or have unintended consequences, and this will make it simple to turn on/off. We can remove the toggle after all has proven ok.
If the request payload does not have `scopes` attribute for a grant_type of | ||
client credentials, it should add `user_id` in the default scopes. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the request payload does not have `scopes` attribute for a grant_type of | |
client credentials, it should add `user_id` in the default scopes. | |
Returns the default scopes. | |
If the request payload does not have `scopes` attribute for a grant_type of | |
client credentials, add `user_id` as a default scope if it is an allowed scope. |
'openedx.core.djangoapps.oauth_dispatch.scopes.ApplicationModelScopes.has_user_id_in_application_scopes' | ||
) | ||
@mock.patch('oauth2_provider.oauth2_validators.OAuth2Validator.get_default_scopes') | ||
def test_get_updated_default_scopes(self, mock_get_default_scopes, mock_has_user_id_in_application_scopes): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit:
def test_get_updated_default_scopes(self, mock_get_default_scopes, mock_has_user_id_in_application_scopes): | |
def test_get_default_scopes_with_user_id(self, mock_get_default_scopes, mock_has_user_id_in_application_scopes): |
default_scopes = ['profile', 'email'] | ||
mock_get_default_scopes.return_value = default_scopes.copy() | ||
mock_has_user_id_in_application_scopes.return_value = True |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit:
default_scopes = ['profile', 'email'] | |
mock_get_default_scopes.return_value = default_scopes.copy() | |
mock_has_user_id_in_application_scopes.return_value = True | |
expected_default_scopes = ['profile', 'email', 'user_id'] | |
mock_get_default_scopes.return_value = ['profile', 'email'] | |
mock_has_user_id_in_application_scopes.return_value = True |
default_scopes = ['profile', 'email'] | ||
mock_get_default_scopes.return_value = default_scopes.copy() | ||
mock_has_user_id_in_application_scopes.return_value = True |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a simple way to do this with a real application, without mocking?
""" | ||
default_scopes = ['profile', 'email'] | ||
mock_get_default_scopes.return_value = default_scopes.copy() | ||
mock_has_user_id_in_application_scopes.return_value = False |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't this case just work without mocking?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. It's looking good.
...angoapps/oauth_dispatch/docs/decisions/0016-add-scope-user-id-for-jwt-client-credentials.rst
Outdated
Show resolved
Hide resolved
|
||
This ADR builds upon ADR 0015: Add Scope user_id for JWT Token. It inherits the context from that ADR, including the initial addition of the user_id claim for analytics and the rationale for using a scope to control its inclusion. | ||
|
||
However, this ADR focuses specifically on the enterprise context highlighted in the consequences of ADR 0015. External organizations often utilize the LMS API with the grant_type: client_credentials to request tokens on behalf of their users. These tokens are crucial for enterprise integrations and require access to the user's user_id for proper functionality. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe something like the following addition?: The current implementation requires clients to remember to request using the scope, which is not a trustworthy solution.
This ADR builds upon ADR 0015: Add Scope user_id for JWT Token. It inherits the context from that ADR, including the initial addition of the user_id claim for analytics and the rationale for using a scope to control its inclusion. | ||
|
||
However, this ADR focuses specifically on the enterprise context highlighted in the consequences of ADR 0015. External organizations often utilize the LMS API with the grant_type: client_credentials to request tokens on behalf of their users. These tokens are crucial for enterprise integrations and require access to the user's user_id for proper functionality. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could add something like the following:
Based on ADR 5 and ADR 6, it may be that the original purpose of the user_id scope was to protect against leakage of the user_id in the case of some combination of Authorization Grant and Restricted Applications. More investigation would be required to refactor based on this context, but it may be useful for future iterations.
- You could replace
ADR 5
(and 6) with a more complete title and links, which could even be to readthedocs (see edx-platform README). - https://github.com/openedx/edx-platform/blob/49dcb68a5e6978e29b4256e65c23fe5e2b9f3f6e/openedx/core/djangoapps/oauth_dispatch/docs/decisions/0005-restricted-application-for-SSO.rst
- https://github.com/openedx/edx-platform/blob/master/openedx/core/djangoapps/oauth_dispatch/docs/decisions/0006-enforce-scopes-in-LMS-APIs.rst#3-restricted-applications-receive-unexpired-jwts-signed-with-a-new-key
...angoapps/oauth_dispatch/docs/decisions/0016-add-scope-user-id-for-jwt-client-credentials.rst
Outdated
Show resolved
Hide resolved
...angoapps/oauth_dispatch/docs/decisions/0016-add-scope-user-id-for-jwt-client-credentials.rst
Show resolved
Hide resolved
client credentials, add `user_id` as a default scope if it is an allowed scope. | ||
""" | ||
default_scopes = super().get_default_scopes(client_id, request, *args, **kwargs) | ||
if settings.FEATURES.get('ENABLE_USER_ID_SCOPE', True): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- This doesn't need to live in FEATURES, and could be top-level, but not a huge deal for a temporary toggle.
- I would rename to something more specific like ENABLE_DEFAULT_ALLOWABLE_USER_ID_SCOPE.
- You could use a
SettingToggle
. Again, your choice on updating, but that is the best practice. - The toggle should be documented via annotations. See other SettingToggles in edx-platform and https://edx.readthedocs.io/projects/edx-toggles/en/latest/how_to/documenting_new_feature_toggles.html.
@@ -77,6 +80,28 @@ def test_inactive_user_validates(self): | |||
request = self.request_factory.get('/') | |||
assert self.validator.validate_user('darkhelmet', self.TEST_PASSWORD, client=None, request=request) | |||
|
|||
@mock.patch.dict(settings.FEATURES, ENABLE_USER_ID_SCOPE=True) | |||
def test_get_default_scopes_with_user_id(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add tests for the False version as well, to make this really clear.
2ff78b5
to
5be6ed6
Compare
6909468
to
070582a
Compare
2U Release Notice: This PR has been deployed to the edX staging environment in preparation for a release to production. |
2U Release Notice: This PR has been deployed to the edX production environment. |
1 similar comment
2U Release Notice: This PR has been deployed to the edX production environment. |
Description
client_credentials
.scope
attribute. Only default scopes were being used.user_id
will be included in the default scopes if no scopes are requested anduser_id
is an allowed scope.Supporting information
Ticket: ENT-8426: Let API users get a user_id in their JWTs
Testing instructions