stream-chat-swift icon indicating copy to clipboard operation
stream-chat-swift copied to clipboard

Expired token not refreshed on startup

Open J-Swift opened this issue 2 years ago • 7 comments

What did you do?

Connect a user, close the app and wait an hour, then restart the app. ChatClient.currentUserId is set correctly from cache, but if I attempt to get channels (client.channelListController(query:)) I get a 401 because of the expired token.

What did you expect to happen?

I thought the tokenProvider would be invoked to refresh the token. Alternatively I would expect to be able to call client.currentUserController.reloadUserIfNeeded but that complains that I haven't called connectUser yet.

What happened instead?

I get a 401 because of the expired token, and I have to manually fetch a token and invoke connectUser.

GetStream Environment

GetStream Chat version: 4.14.0 GetStream Chat frameworks: StreamChat iOS version: 15.5 Swift version: 5 Xcode version: 13.4.1 Device: ipod touch 7th gen, ios 15.5

Additional context

It doesnt look like theres a way to detect an expired token other than manually storing and reading the JWT out of band from the ChatClient?

J-Swift avatar Sep 29 '22 15:09 J-Swift

The specific error I get is

ServerErrorPayload(code: 2, message: "QueryChannels failed with error: "stream-authService-type missing or invalid"", statusCode: 401)).

J-Swift avatar Sep 29 '22 15:09 J-Swift

Hi @J-Swift!

Can you update to the latest version of the SDK? There were a lot of improvements to token refreshing since the 14 version.

Let us know if you can still reproduce it in the latest 4.22.0 version.

Best, Nuno

nuno-vieira avatar Sep 29 '22 16:09 nuno-vieira

OK I'll take a look and let you know.

J-Swift avatar Sep 29 '22 18:09 J-Swift

Well I wouldn't say it fixed the issue so much as it changed the API to force me to use my workaround of manually logging in which avoids the issue I desribed. It looks like it calls the provided tokenProvider on every app start even if the token isnt expired.

J-Swift avatar Sep 29 '22 19:09 J-Swift

Hi @J-Swift!

Can you share a snippet of how you implement the token provider so we can review it?

Thank you!

nuno-vieira avatar Sep 30 '22 09:09 nuno-vieira

Its the most basic it can be I think

public protocol GetStreamTokenFetcher
{
    func getToken() async -> String?
}

public class GetStreamProxy : NSObject {
    enum MyError: Error {
        case runtimeError(String)
    }
    let client: ChatClient;
    let tokenProvider: TokenProvider
    var initialized: Task<Bool, Never>? = nil
    
    public var isLoggedIn: Bool {
        get {
            self.client.currentUserId != nil
        }
    }
    
    public init(apiKey: String, tokenFetcher: GetStreamTokenFetcher) {
        let config = ChatClientConfig(apiKey: APIKey(apiKey))
        tokenProvider = { onComplete in
            Task {
                guard let token = await tokenFetcher.getToken() else {
                    onComplete(.failure(MyError.runtimeError("unable to fetch token")))
                    return
                }
                onComplete(.success(Token.init(stringLiteral: token)))
            }
        }
        
        self.client = ChatClient.init(config: config)
        
        super.init()
        
        initialized = Task {
            guard let userId = self.client.currentUserId else {
                return false
            }
            
            let didLogin = await withCheckedContinuation({(continuation: CheckedContinuation<Bool, Never>) in
                let userInfo = UserInfo.init(id: userId)
                self.client.connectUser(userInfo: userInfo, tokenProvider: tokenProvider) { e in
                    if let e = e {
                        print(e)
                        continuation.resume(returning: false)
                        return
                    }
                    
                    continuation.resume(returning: true)
                }
            })
            
            return didLogin
        }
    }
}

Reading through https://github.com/GetStream/stream-chat-swift/pull/2031 it sounds like the "always refresh on initial connection" is by design.

J-Swift avatar Sep 30 '22 13:09 J-Swift

I feel like theres an API disconnect on the currentUser functionality when viewed against the description of the connectUser API.

For example, if I login and then immediately close the app and reopen then self.client.currentUserId and self.client.currentUserController().currentUser are both non-nil. That heavily implies that I should be able to e.g. call self.client.channelListController(query: query), but that gives the 401 I outlined originally. The docs for currentUserController().currentUser say

The currently logged-in user. nil if the connection hasn't been fully established yet, or the connection wasn't successful.

So I'm not sure if this should be nil on startup, or if my previous token should be passed correctly without needing a separate connectUser call.

J-Swift avatar Sep 30 '22 13:09 J-Swift

Hi @J-Swift!

I would try using the token provider without async/await to make sure there's nothing wrong with the async/await integration. Either way, we are currently planning on improving the token refreshing logic, just like discussed on this ticket: https://github.com/GetStream/stream-chat-swift/issues/2255. We will improve this soon and we will get back to you as soon as possible.

Best, Nuno

nuno-vieira avatar Oct 03 '22 13:10 nuno-vieira

Hi @J-Swift,

There seems to be an error on the documentation. This statement is not true:

if I login and then immediately close the app and reopen then self.client.currentUserId and self.client.currentUserController().currentUser are both non-nil. That heavily implies that I should be able to e.g. call self.client.channelListController(query: query), but that gives the 401 I outlined originally.

Having a currentUser/currentUserId doesn't mean the token is valid, it is just a representation of the user which is cached across sessions. Actually, Stream does not cache the token itself.

What's happening to you is that you might be querying the channels before connect call is completed, which basically means that there is no token, and the API call that is performed will of course return an error due to missing authentication.

We are working on improving that angle, but for now don't assume the token is there when currentUserId is not nil

polqf avatar Oct 31 '22 12:10 polqf

Hi @J-Swift , the documentation has been updated to state what I shared above.

polqf avatar Nov 17 '22 08:11 polqf