Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Refresh Tokens #5023

Closed
wants to merge 21 commits into from
Closed

Refresh Tokens #5023

wants to merge 21 commits into from

Conversation

langleyd
Copy link
Member

@langleyd langleyd commented Jan 21, 2022

Resolves #4943

What's in the PR

  • Hardcodes refresh_tokens on for the moment
  • Refresh tokens requested during login
  • Refresh tokens and access token expiry parsed in login response
  • Access Token provider synchronises refresh so that we have a leader refresh requests while other requests are blocked and received the new access token when the leader succeeds.
  • Adds refresh task for unauthenticated refresh api.
  • Added interceptor logic may be trigger a refresh before a request if it is deemed the toke is no longer valid(e.g. expired). But also triggers a refresh on the a 401 UNKOWN_TOKEN from the server(the source of truth on token validity).
  • Handles cases of both access token auth/refresh token auth becoming unauthenticated and logs to analytics for monitoring.

#TODO

- Hardcodes refresh_tokens on for the moment
- Refresh tokens requested during login
- Refresh tokens and access token expiry parsed in login response
- Access Token provider synchronises refresh so that we have a leader refresh requests while other requests are blocked and recieve the new access token when the leader succeeds.
- Adds refresh wizard to authentication service for unauthed refresh api.
@langleyd langleyd marked this pull request as draft January 21, 2022 13:52
@github-actions
Copy link

github-actions bot commented Jan 21, 2022

Unit Test Results

  84 files  ±0    84 suites  ±0   1m 0s ⏱️ -1s
157 tests ±0  157 ✔️ ±0  0 💤 ±0  0 ±0 
504 runs  ±0  504 ✔️ ±0  0 💤 ±0  0 ±0 

Results for commit 1d5b19e. ± Comparison against base commit 79b511b.

♻️ This comment has been updated with latest results.

Copy link
Member

@bmarty bmarty left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work. I've made a few remarks.

@github-actions
Copy link

github-actions bot commented Jan 21, 2022

Matrix SDK

Integration Tests Results:

  • [org.matrix.android.sdk.session]
    passed=
  • [org.matrix.android.sdk.account]
    passed=
  • [org.matrix.android.sdk.internal]
    passed=
  • [org.matrix.android.sdk.ordering]
    passed=
  • [org.matrix.android.sdk.PermalinkParserTest]
    passed=

@langleyd langleyd self-assigned this Feb 8, 2022
@langleyd langleyd marked this pull request as ready for review February 9, 2022 13:16
@langleyd langleyd requested a review from bmarty February 9, 2022 13:16
Copy link
Member

@bmarty bmarty left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some more comments, please let me know what you think.
Sorry this is a very sensitive subject, I want to be sure that there is no issue and the implementation is crystal clear.

return responseCredentials
}
// We have received a credential response from the server, estimate the expiry datetime.
return responseCredentials.copy(expiryTs = System.currentTimeMillis() + responseCredentials.expiresInMs)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand reading this code again that expiryTs does not come from the server, do it?
This a bit weird to create a customized Json adapter to handle this.
Not sure how to implement it in another way, I will think more about it.

@langleyd langleyd requested a review from bmarty February 18, 2022 16:21
@github-actions
Copy link

github-actions bot commented Feb 21, 2022

Ktlint Results

👎Failed
matrix-sdk-android 🔸 internal/auth/#/DefaultLoginWizard.kt:20:1: Unused import (no-unused-imports)

Copy link
Member

@bmarty bmarty left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some more remarks.

@ganfra can you have a quick look on this PR please? especially the refresh token management itself.

@Unauthenticated okHttpClient: Lazy<OkHttpClient>,
retrofitFactory: RetrofitFactory,
homeServerConnectionConfig: HomeServerConnectionConfig
): AuthAPI {
): RefreshTokenAPI {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you have retrofit: Retrofit as the only parameter?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The okHttpClient here is unauthenticated. Would I have to provide another unauthenticated version of Retrofit? is there any example of where we do that?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it a problem to send the current (and outdated) access token in the request header to refresh the token?

I think this is not a problem. (but I have done no test)

To me, it should even be mandatory server side to provide the previous access token to be able to get a new token.

If this is not a problem, I would remove this change on the SessionModule

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well according to the spec

This new API should be rate-limited and does not require authentication since only the refresh_token parameter is needed.

So it's not needed but I don't imagine the access token itself would cause a problem.
However the authenticated requests(which /refresh would bcome) run through the AccessTokenInterceptor and HomeserverAccessTokenProvider which is itself making the request to refresh. I think you might have to add additional defensive code there to check that the current request is not /refresh. Seems a more sensible design to keep the /refresh endpoint outside of that code flow.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your (good) point about potential endless loop

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah you can keep it like that it's ok

@bmarty bmarty requested a review from ganfra February 21, 2022 16:42
import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
data class RefreshResult(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be internal

Copy link
Member

@ganfra ganfra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some thoughts

*/
fun getLoginWizard(): LoginWizard
fun getLoginWizard(enableRefreshTokenAuth: Boolean = false): LoginWizard
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this flag enabled on MatrixConfiguration instead? It would avoid passing this everywhere and directly be injected in the few places where it makes sense.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually had it that way initially. @bmarty felt it would be better to be a more explicit part of the api on the AuthenticationService vs a config people might not know about. I don't really have a strong opinion on it. :)

@Unauthenticated okHttpClient: Lazy<OkHttpClient>,
retrofitFactory: RetrofitFactory,
homeServerConnectionConfig: HomeServerConnectionConfig
): AuthAPI {
): RefreshTokenAPI {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah you can keep it like that it's ok

private val sessionParamsStore: SessionParamsStore
private val sessionParamsStore: SessionParamsStore,
private val refreshTokenTask: RefreshTokenTask,
private val globalErrorReceiver: GlobalErrorReceiver
) : AccessTokenProvider {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okhttp has an API to handle accessToken and refreshToken when needed, might be the opportunity to use it instead of a custom interceptor/provider ?
See this article: https://www.lordcodes.com/articles/authorization-of-web-requests-for-okhttp-and-retrofit for example

@bmarty
Copy link
Member

bmarty commented Oct 4, 2022

Will have to be done again, if we want to support this using https://www.lordcodes.com/articles/authorization-of-web-requests-for-okhttp-and-retrofit.

I guess this is probably better to create another PR.

@bmarty bmarty closed this Oct 4, 2022
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implement refresh tokens
3 participants