781flyingdutchman / background_downloader

Flutter plugin for file downloads and uploads
Other
162 stars 76 forks source link

OAuth Support for refreshing tokens given an access and refresh token #311

Closed jeffosmith closed 5 months ago

jeffosmith commented 6 months ago

We have an application that using background downloader to upload images in the background, as part of completing a large process based submission. The service is using OAuth with JWTs to manage the Authentication and Authorization of the upload requests. We are seeing instances of the access token expiring between when we create the task (and set the Auth Header) and when the task happens.

Would love to be able to provide, access token, refresh token and token refresh endpoint to allow the token to be refreshed if the current access token expires.

Have attempted to update the access token, just before creating the task, and although this reduces the error, it doesn't eliminate it.

I'm willing to put together a PR across both platforms, although mindful that I don't want to complicate your API too much, and question whether we can make it generic enough to update the token for a wide enough audience to make it worth while.

781flyingdutchman commented 6 months ago

Hi, thanks for raising this issue and for offering to help solve for this use case. As you mention, it is a bit niche and difficult to generalize, so I propose the following conditions before committing to including this: 1) It needs to apply to a reasonably large group of OAuth/JWT use cases, This means that the authentication needs to be configurable (e.g. different end points, different names of the header or url structure). I think this is doable, but it is more work than just solving for your specific use case 2) It needs to work on Android, iOS and Desktop (desktop is pure Dart code), and because I am not familiar with OAuth you will have to implement on all three platforms, as it needs to be a feature that is consistent across 3) No new dependencies, especially on iOS and Android, as that code is not tree-shaken so lives with every use of the package

If you think you can meet these conditions, then I would love for you to implement this, and, knowing the downloader code base, have a few suggestions for how to approach it: 1) Create a new Auth class in models.dart that contains the basic data for the auth/token request, e.g. the auth and refresh tokens themselves, as well as the string representation of the urls for re-authentication and for adding authentication to the url or headers. This class must have a toJson and fromJson (manual, not using JsonSerializable). You will also need to create this class in iOS and Android, where serialization is automated 2) In this Auth class, use templates to allow the user to put the token in the tight place, e.g. allow a string to contain {token} which will be replaced by the actual token upon execution. See how this is done for notifications, where you can for example insert the filename in the notification text. This bit requires some thought, as there are likely many different ways the token can be included in the request, and it is important that the structure you propose can represent those different ways 3) Add an auth field to the Request class: this is the base class for Task and ideally this would work also for Request (it's the same logic), and modify toJson and fromJson so that this field is passed to the native/desktop download code. on iOS and Android you need to add that same auth field to the Task struct and class 4) On desktop, iOS and Android there is a point in the code where the actual HTTP request is constructed (e.g. adding headers etc). Just before this happens, you should check if the task has an auth field, and if so, this is where you do your special handling, that should result in a proper request being constructed (with the auth headers as needed, but in such a way that all the other options are also still possible, e.g. the user may supply other headers and may set WiFi requirements etc - you should not duplicate that code, just create a starting point that already has the right authentication things included, then let the rest of the code that's already there do the other stuff). That ensures that all of the following code (for all task types) uses that request, now guaranteed to have a valid token 5) In your special handling, check for expiration of the auth token and refresh if needed, then construct the request (or just the url and headers, you'll have to determine what is easiest) based in the information in the task's auth object. This is where you would substitute {token} etc.

Let me know what you think.

kuhnroyal commented 6 months ago

I have the exact same use-case and was gonna create a ticket for this :)

Wondering if this would be easier by providing some kind of token resolver on the Dart side, not sure if the plugin could launch a Dart VM and execute the resolver even if the app is suspended.

CC @lyio

781flyingdutchman commented 6 months ago

I believe it's possible to launch a Dart VM from the background, but it is complicated and resource intense, so I believe this should be handled on the native side so that a background task can remain fully in the background.

jeffosmith commented 6 months ago

I think the approach outlined looks solid, let me have a review of some other OAuth libraries, to get a feel for the different approaches for refreshing tokens, and how those requests are formatted, query param, post body etc.

I'll add some notes here, once I'm clear on a generic approach for refreshing tokens

781flyingdutchman commented 5 months ago

I'm going to convert this to a discussion (instead of an issue) so it doesn't get labeled as stale by the bot.