Skip to content
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

[CIS-1185] Token provider #2031

Merged
merged 3 commits into from Jun 2, 2022
Merged

[CIS-1185] Token provider #2031

merged 3 commits into from Jun 2, 2022

Conversation

evsaev
Copy link
Contributor

@evsaev evsaev commented May 27, 2022

🔗 Issue Links

CIS-1185

🎯 Goal

Make it possible to pass a tokenProvider when connecting the chat-client.

📝 Summary

  1. chat client has connect method that takes a token provider as an argument (the same as on other platforms)
  2. chat client still has connect method that requires a token but with the note that a token must not expire
  3. chat client no longer have tokenProvider exposed

⚠️ The 3rd change is a breaking one but it might be OK to ship it:

  • customers who is using non-expiring tokens will not be affected by this change
  • customers who is using expiring and a tokenProvider that was removed would be glad to hear that the whole flow is simplified:

Before:

// 1. Get a user
let user = 

// 2. Create a token provider
let tokenProvider: TokenProvider = { completion in
   authService.fetchToken(for: user, completion: completion)
}

// 3. Call the token provider to receive the 1st token
tokenProvider { tokenResult in
 guard case .success(let token) = tokenResult else { return }

 // 4. Create and connect the chat-client
 let chatClient: ChatClient = ...
 chatClient.tokenProvider = tokenProvider

 OR

 let chatClient = ChatClient(config, tokenProvider: tokenProvider)

 // 5. Connect the chat client with received token
 chatClient.connect(userInfo: user, token: token, completion: { /*handle connection error*/ })
}

After:

// 1. Get a user
let user: UserInfo = 

// 2. Create a token provider
let tokenProvider: TokenProvider = { completion in
   authService.fetchToken(for: user, completion: completion)
}

// 3. Create and connect chat-client with a token provider
let chatClient: ChatClient = ...
chatClient.connect(userInfo: user, tokenProvider: tokenProvider, completion: { /*handle connection error*/ })

🧪 Manual Testing Notes

Sign in to the DemoApp as a normal/guest/anon user and make sure the connection flow works as before.

☑️ Contributor Checklist

  • I have signed the Stream CLA (required)
  • Changelog is updated with client-facing changes
  • New code is covered by unit tests
  • This change follows zero ⚠️ policy (required)
  • Comparison screenshots added for visual changes
  • Affected documentation updated (docusaurus, tutorial, CMS)

🎁 Meme

@evsaev evsaev added 🔧 WIP A PR that is Work in Progress 🌐 SDK: StreamChat (LLC) Tasks related to the StreamChat LLC SDK labels May 27, 2022
@evsaev evsaev requested a review from a team as a code owner May 27, 2022 13:58
@evsaev evsaev self-assigned this May 27, 2022
@evsaev evsaev marked this pull request as draft May 27, 2022 13:58
@bielikb
Copy link
Contributor

bielikb commented May 30, 2022

This is much needed change! The approach is basically the same I took. Great minds think alike :)

Copy link
Member

@nuno-vieira nuno-vieira left a comment

Choose a reason for hiding this comment

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

The change LGTM ✅ I would prefer to deprecate the old API instead of removing it though. Is this possible?

Copy link
Contributor

@polqf polqf left a comment

Choose a reason for hiding this comment

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

If I were to be the client, I would not be happy with either the previous nor the proposed approaches. Chances are that I want to configure all the things in an initial point in the execution of the app, and only call connect whenever needed, without the need of knowing more about it.

Is this something we should maybe consider? connect should know nothing of tokens IMO

@evsaev
Copy link
Contributor Author

evsaev commented May 30, 2022

If I were to be the client, I would not be happy with either the previous nor the proposed approaches. Chances are that I want to configure all the things in an initial point in the execution of the app, and only call connect whenever needed, without the need of knowing more about it.

Is this something we should maybe consider? connect should know nothing of tokens IMO

If I were to be the client, I would not be happy with either the previous nor the proposed approaches. Chances are that I want to configure all the things in an initial point in the execution of the app, and only call connect whenever needed, without the need of knowing more about it.

Is this something we should maybe consider? connect should know nothing of tokens IMO

@polqf yeah, was also thinking about that.

The tokenProvider could be passed once via init when ChatClient is created but in that case it needs a user to fetch a token for:

(userID: UserId, completion: @escaping (Result<Token, Error>) -> Void) -> Void

Tho it can be done this way, I see 2 potential issues:

  1. it is not aligned with other SDKs
  2. it can be misleading because for guest/anonymous users the tokenProvider won't be triggered but has to be provided

The 2nd is more or less fine and expected but 1st contradicts with API alignment we're aiming for.

@polqf
Copy link
Contributor

polqf commented May 30, 2022

@polqf yeah, was also thinking about that.

The tokenProvider could be passed once via init when ChatClient is created but in that case it needs a user to fetch a token for:

(userID: UserId, completion: @escaping (Result<Token, Error>) -> Void) -> Void
Tho it can be done this way, I see 2 potential issues:

it is not aligned with other SDKs
it can be misleading because for guest/anonymous users the tokenProvider won't be triggered but has to be provided
The 2nd is more or less fine and expected but 1st contradicts with API alignment we're aiming for.

Yeah. I do agree with 1, but tbh I don't thing this should be something we base our decisions off. Otherwise there's always a blocking point towards our ideal "excellence" direction
On the init approach, I always thought it is better to have a configure method that does exactly that. The same way we were discussing before that connect and init could happen at different places/moments, I believe this is the case too.

Maybe I am proposing something that it is too big for this PR, but I think it worths a discussion, at least.

@evsaev
Copy link
Contributor Author

evsaev commented May 30, 2022

Yeah. I do agree with 1, but tbh I don't thing this should be something we base our decisions off. Otherwise there's always a blocking point towards our ideal "excellence" direction On the init approach, I always thought it is better to have a configure method that does exactly that. The same way we were discussing before that connect and init could happen at different places/moments, I believe this is the case too.

Maybe I am proposing something that it is too big for this PR, but I think it worths a discussion, at least.

Ah, I've got you now. Yes, initializing and configuring chat-client in different places is a good point 👍 But if we go with configure, someone can ask why we provide ChatClientConfig via ChatClient.init and not via dedicated configure method? And changing it gonna time because we won't longer be able to create database/apiClient/wsClient without having a config at the client initialization point OR will need to recreate them after configureis called.

I don't thing this should be something we base our decisions off. Otherwise there's always a blocking point towards our ideal "excellence" direction

As mentioned, the approach with configure method is also a good one but since it's connection API we can't immediately introduce it on iOS. This way, we'll never get APIs aligned. But what we can do is to suggest the new approach to other teams and if they like it - plan migration to the new connect API for the next major. And for now, go with what other SDKs have.

@polqf WDYT?

@polqf
Copy link
Contributor

polqf commented May 31, 2022

@evsaev yeah, as long as there is a discussion to improve this, I am happy to let this through for now 😄 Having it aligned with other SDKs is always better than having it as we have it now.

@evsaev evsaev force-pushed the fix/CIS-1185-connect-API branch 3 times, most recently from 130d538 to 7892fe3 Compare May 31, 2022 13:53
@evsaev evsaev marked this pull request as ready for review May 31, 2022 13:53
@evsaev evsaev requested review from polqf, nuno-vieira and a team and removed request for polqf and nuno-vieira May 31, 2022 13:54
@evsaev evsaev added 📋 Docs Work related to docs/wiki and removed 🔧 WIP A PR that is Work in Progress labels May 31, 2022
@evsaev evsaev requested review from nuno-vieira and polqf June 1, 2022 16:05
Copy link
Contributor

@bielikb bielikb left a comment

Choose a reason for hiding this comment

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

Great stuff!

Copy link
Member

@nuno-vieira nuno-vieira left a comment

Choose a reason for hiding this comment

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

LGTM! ✅

docusaurus/docs/iOS/uikit/getting-started.md Show resolved Hide resolved
@sonarcloud
Copy link

sonarcloud bot commented Jun 2, 2022

Kudos, SonarCloud Quality Gate passed!    Quality Gate passed

Bug A 0 Bugs
Vulnerability A 0 Vulnerabilities
Security Hotspot A 0 Security Hotspots
Code Smell A 0 Code Smells

90.0% 90.0% Coverage
0.0% 0.0% Duplication

@evsaev evsaev merged commit a97f434 into develop Jun 2, 2022
@evsaev evsaev deleted the fix/CIS-1185-connect-API branch June 2, 2022 16:57
This was referenced Jun 6, 2022
@bielikb bielikb mentioned this pull request Jun 9, 2022
@bradleyrzeller
Copy link

bradleyrzeller commented Aug 16, 2022

Hi -

If I understand this change correctly, there is now no way for the token provider to differentiate between a token needed for the "initial" connection and one that is needed because the current one expired (a refresh). This distinction is critical for the token provider to determine whether or not a cached token can be attempted or not.

Before, you could provide the initial token in the connect() method and the token provider could be set afterwards. In my testing with this setup, the token provider was only ever invoked upon token invalidation / refreshes. So, I could provide a cached token on the initial connect() call and the token provider was safe to assume it was called as a result of a refresh, and it would always fetch a new one from our app server

There are some workarounds to the new API here but I'm curious what the stream team recommends for this situation.

@nuno-vieira
Copy link
Member

Hi @bradleyrzeller!

You are correct. For now, you can do the workarounds, and maybe add a boolean flag to check if the method is the first time being called. Currently, we don't have plans to change this, especially because it is aligned with the rest of the platforms. But we will discuss this internally and will let you know if we have updates on this in the future 👍

Best,
Nuno

@bradleyrzeller
Copy link

Ok, thanks for the reply @nuno-vieira.

I've been testing my workaround here and I am actually not seeing the token provider be called automatically after the token has expired.

I am generating a token using your token generator with 1 minute expiry. The token works initially as expected. I can send messages up until it expires ... and then the client disconnects. However, as I mentioned, the client is not invoking my token provider when it clearly knows that the token has expired (api calls return 401s etc.).

Can you provide some additional insight here? Thanks!

@nuno-vieira
Copy link
Member

Hi, @bradleyrzeller can you provide a snippet of your workaround?

@bradleyrzeller
Copy link

It's essentially this:

        client.connectUser(
            userInfo: .init(id: "foobar"),
            tokenProvider: { callback in
                if self.allowCachedCredentials {
                    self.allowCachedCredentials = false
                    callback(.success(cachedToken))
                } else {
                    fetchNewToken { tokenResult in
                        callback(tokenResult)
                    }
                }
        )

The token provider is only ever invoked once, upon initial connection. I cannot get it to be invoked again even after my token has expired and the client disconnects.

@bradleyrzeller
Copy link

@nuno-vieira it seems to be because the RequestDecoder is failing to decode an error payload into a ErrorPayload on this line. The actual payload I am seeing is

{
  "code" : 40,
  "message" : "SendEvent failed with error: \"JWTAuth error: token is expired (exp)\"",
  "more_info" : "https:\/\/getstream.io\/chat\/docs\/api_errors_response",
  "StatusCode" : 401,
  "duration" : ""
}

but the ErrorPayload expects a details key.

This throws off downstream logic that detects token expiration and triggers a refresh...

@bradleyrzeller
Copy link

Just filed an issue for visibility.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
📋 Docs Work related to docs/wiki 🌐 SDK: StreamChat (LLC) Tasks related to the StreamChat LLC SDK
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants