flutter_appauth
flutter_appauth copied to clipboard
Web support
Since there is an AppAuth JS SDK it should be possible to add web support for this plugin.
Not sure what would be involved on the web side. Looks like the JS SDK is typically consumed as a node package and unless I've missed it, I haven't seen an example of using a node package within a web plugin. I can add the platform interface so someone can look at a web implementation.
The platform interface is now available as a separate package so it can be used to implement the web version of the plugin. Let me know if you plan to work on this @athornz as otherwise I'll be closing this as it's not something I'll be looking to work on. Feel free to submit a PR or create another repository for the web support version and when published I can take a look and add it to the pubspec of this plugin as the endorsed web implementation
Can we get flutter web to work with this version so we have one plugin for auth on mobile and web?
@Jeremywhiteley in case there's a misunderstanding, that would still be possible what what I mentioned in my previous post. If you want details about this then you can read the doc at flutter.dev/go/federated-plugins. For reference, you can look at some of the first-party plugins like the url_launcher one. The web version is a separate plugin that is pulled in as a dependency and used by url_launcher when run on the web.
could you mention the plugin's url please
@ir2pid sorry missed your message but don't know if that was directed to me and what plugin you're referring to but all flutter plugins can be searched on pub.dev
Also I'm going to add a needs help label for this in case someone can tackle the problem
I think that this might be doable using typescript to compile the js code that consumes the node_module or use webpack or rollup to do the same if that helps.
Here's some code for contribution that allows the use of appAuth.js for Dart: appAuthJavascript Interop
What it does is export the entire appAuth.js library to appAuth.xxx so that it can be used directly and only depends on the appAuth.js file that it generates when you run npx webpack.
In theory from here it should be straight forward to then hook up the various parts of appAuth.js using the dart js interop.
I'm very not familiar with doing flutter plugins or even writing the interop so someone else might be better for this, but if you give me directions on how you want this to be structured I'm happy to do a pull request and give it a go if there's no one better suited.
Here's some code for contribution that allows the use of appAuth.js for Dart:
Looks as though you may have forgotten to add something after this sentence so don't know what you were trying to show for myself or someone else to comment on.
There are a number of plugins in the Flutter ecosystem that have a web implementation that can be used as a reference if needed. Some of those can be found in the repo for the first-party Flutter plugins https://github.com/flutter/plugins
@MaikuB Sorry, got markdowned. Updated now with the link.
What I'm thinking is that the only way to make this work with the current structure is that appAuth.js will have to do a popup. Without the popup there's no way to do an await on the result like you can with ios/android because the browser navigates away from the app.
This can be a plus or a minus, it's hard to say per person. I personally think that it's better that way because your app doesn't have to reload one redirect but that's me.
I am not sure if this will help, but Microsoft is having a session on MSAL.js tomorrow.
Authenticate users in JavaScript apps with MSAL.js June 25, 2020 | 2:00 PM ET / 11:00 AM PT
https://www.microsoftevents.com/profile/form/index.cfm?PKformID=0x10958403abcd&wt.mc_id=AID3012905_QSG_EML_436897
@jhancock4d haven't used it but looking at the code for firebase_auth that seems to be the way they've done it though if possible I'd say better not to force it or have it as an option. Having said that, I'm wondering if there is even an issue. From what I saw in the docs, an authorization request is done synchronously and an event listener is required to capture the response. I assume the request can open another page so would imply that the request handler has a way of maintaining state even when that is done. Haven't had the chance to do this myself but might be worth running this example to confirm
@MaikuB The listener is supposed to be setup when the app starts, and then intercepts the first request and then notifies. From what I'm reading in the current interface plugin, it expects a response back and when I tested, I couldn't just call it again because of timing issues between the 2 so it still thought it wasn't ready and tried to execute code that it shouldn't.
I'll take a look at firebase_auth to see what they're doing. From what I remember, they were handling it differently for web, so they had an initialize call that you put at the top of main.dart that intercepted everything there and completed the login and then notified everything else.
@Jeremywhiteley I investigated the msal.js stuff but it's very strongly tied to Microsoft. I'm trying to find a path that uses appAuth.js but even then they seem to be focused on electron more than real web spas. Hence the problems.
@jhancock4d ignoring the full page transition, is part of the problem about how to fit the listener with the asynchronous methods that return a Future? If so, perhaps this could be solved by
- Having the listener is setup in the constructor of the web implementation of the plugin
- When the listener fires, responses then get added to a stream kept internally
- When the the authorisation methods for this plugin is invoked, it makes the authorisation request via AppAuth.js and then use
await foron the stream to return the response
@MaikuB It's more that if I don't use a popup window to do the authentication, that the flutter app is torn down entirely, and your current plugin won't even know what an auth request occured and won't know what to do.
It has to:
- Request authentication
- Leave the app
- Come back and load the app
- Process the authentication (and store it)
- Tell the app that it's now authenticated.
The current interface it appears works like this:
- Request Authentication
- Wait until it's done.
- Return success/failure.
Because web requires a tear down if not done in a popup window (and you can't use iframes) they're incompatible from what I can tell. The only hack would be to call the authenticate again on rebuild assuming it went right back to where it went and then have it check if it happened and return immediately in that case. This might be viable but it requires a lot of assumptions in the app outside of the plugin.
Here are some more resources from Microsoft that might be helpful here.
You can email and ask questions. [email protected]
Resources: https://msusdev.github.io/microsoft_identity_platform_dev/ https://github.com/MSUSDEV/microsoft_identity_platform_dev
So I've implemented this with a popup window (works in all browsers without warning) so that it fits with the current process. It requires an HTML file that I don't know how to get to then be deployed to the site that consumes it. If anyone knows how to do that, please let me know or I'll include instructions on how to add the callback.html that is needed to handle the response back from the IdP.
This does not use appAuth because it would just add weight for a few simple calls. It just uses dart:html and then a few simple http.posts. the appAuth.js is designed to work with electron etc. That doesn't flow for web properly.
I'm happy to do a pull request to add it to this repo like the flutter web plugins are done as a separate federated plugin, or I can host it somewhere else separately. It's up to you.
The other thing I'd like to do is make the token request if it is the grant type for refresh token to fire up an iframe and do a silent renew in the iframe, then pull it down out of the root document and return the updated values in the token response.
If it's desirable to not use a popup window, then we need a new interface item that would be a static called at startup of the app that would do the validation of the URL and store the login information for a redirect loop. I'm fine with what I have using the popup window, so if someone wants to take that and run with it, that would be great.
Is there plans to create a complete library that consumes these and handles authentication, automatically refreshing of credentials, storage, and parsing of the id_token etc into properties? It seems to me that this is a great low-level package, but needs the next level done that abstracts all of the details away.
Also note that it would be nice if there was a nullable checkSessionIFrameEndpoint on the AuthorizationServiceConfiguration so that that could be passed in/persisted.
Actually I was thinking about the platform-specific properties could be taken out of the platform interface. I know there are some there right now. This avoids having a monolithic plugin that platform interfaces are meant to solve.
I did this for another plugin before. Platform-specific implementations of a plugin would extend the platform interface first. It can then override a method and have additional named parameters that aren't in the original method signature. For example this adds a named notificationDetails parameter that isn't in the method defined in the platform interface here.
Ok, well if someone wants to point me to how this would be called once done, I'm happy to make it work that way but ultimately the end user has to:
put in a silent_renew.html file in their web project. Specify if they want a browser popup window (doesn't work in firefox) or a redirect loop In their main method await authentication processing on startup. (for the redirect loop) I haven't a clue how to put in line by line conditionals directly into dart code like you can with compiler directives in say C# and C++, so I need direction on how to make it so that this can work. Otherwise it's done with the exception of the silent renew auth request that I'm trying to figure out what I'm doing wrong for the request itself. Once that's solved it's ready to go (sans unit tests at the moment) for someone to take a look at if they want.
What kind of conditions need to be checked though? If it's to check if the app is running on the web then use https://api.flutter.dev/flutter/foundation/kIsWeb-constant.html
Conditional imports are available too if needed https://dart.dev/guides/libraries/create-library-packages#conditionally-importing-and-exporting-library-files
It's a matter that with web, the app has to check auth state information in the URL when it's redirected back. So there has to be an initialize function that is called in main()
And then there is refresh tokens/silent renew that isn't considered in this project that is a must have for anyone using this in the real world.
Can't both issues solved with a platform-specific method? I gave links to another plugin of mine that has platform-specific properties and methods. token method can already be used for refreshing tokens. That certain methods don't exist in the platform interface doesn't prevent platform implementations to have extra methods in them.
The link that was provided shows a hacking way of getting those in with dummy stubs for other implementations that do nothing. My point is that the interface should provide an init method in the interface that is already dummied out for the others with a no-op. This is by far the best developer exerience.
And while yes you can request tokens manually right now, the refresh token experience is one on a timer that the developer would implement. That might be fine to leave to the developer on mobile, but on web that isn't how it works. On startup of your app, it has to create an invisible iframe that calls the checksession endpoint and acts to get a new login token when there is a change to authentication based on the message sent back to the parent page. So formalizing setting up the timer for refreshing would be a good thing because it benefits mobile by having it be setup properly so that people don't make mistakes, and it's basically required (it's phenomenally complex and badly documented) on web as part of your auth cycle.
The link that was provided shows a hacking way of getting those in with dummy stubs for other implementations that do nothing. My point is that the interface should provide an init method in the interface that is already dummied out for the others with a no-op. This is by far the best developer exerience.
Which part/examples are you looking at when you're referring to dummy stubs here? It sounds like, and I may be wrong about this, what you're saying is actually going against the federated plugin approach used by plugins now to help scale to different platforms. Here's the official design doc on this https://docs.google.com/document/d/1LD7QjmzJZLCopUrFAAE98wOUQpjmguyGTN2wd_89Srs/edit?usp=drivesdk
Here's the model, tell me how you'd approach it, because the docs don't at all address this without the end user code having full knowledge of the plugin which is obviously not desirable.
Login => redirect to auth server => redirect back to login => app processes redrect back by looking at href of page.
The way you're wanting it, the end developer has to know that it's web and have custom code to handle it's web.
My way, the end developer calls Platform.Init() and it's done for him if necessary and if not, no-op occurs.
Having custom code per platform in your end code is exactly what you don't want to have to deal with. I should NEVER have to concern myself with the platform in the end code. That's the job of the plugin itself that knows the details.
Your way I have to do if (kWeb) => call code that I've subbed out a dummy for because I still can't do a conditional import, only a conditional export and thus I can import an export that handles this with a dummy stub and fake out the import do what should be in the platform.
Even if I was using C# or similar I'd still have to do a compiler directive and import the code based on the platform and call it. Again, this makes the end developer have to understand the platform specifics of the implementation, which defeats the entire model of plugins.
This is the same issue of refresh. The end developer should be able to use this effectively without having to know any details of openidconnect or at least have bare minimum understanding of it yet still get a successful result that is both secure and standards compliant.
As written this is bare metal. Which is fine persay because another plugin can just consume this and put in all of the stuff that you should have for success. However, without the init() call, this won't work even in that method on web without a ton of custom known code so you're just shifting using this correctly up a level for a plugin and making the web implementation of this useless in the process because it's incomplete and absolutely will not allow a complete login cycle without init()
BTW, this nowhere outlines that I can find any conditional imports as part of the definition. The only thing that I can find anywhere, even if you dismiss the above is conditional exports with a bait and switch approach, which is obviously a hack.
https://docs.google.com/document/d/1LD7QjmzJZLCopUrFAAE98wOUQpjmguyGTN2wd_89Srs/edit
Here's the model, tell me how you'd approach it, because the docs don't at all address this without the end user code having full knowledge of the plugin which is obviously not desirable.
Whilst there are no code samples, it does talk about this
It is somewhat common for a plugin to have features that are available only on a single platform. In the monolithic world this has been typically handled either by including a separate platform-specific API in the same package, or by making a method a no-op or throw on platforms it isn’t supported on.
For federated plugins, it feels natural to keep a platform-specific API in the platform implementation package. In order to use this API the app developer will directly import a library from the platform package, and will add a direct dependency on the platform package.
Based on what i'm reading here it means having platform-specific code. When it comes to cross-platform plugins that have methods that perform the same functionality on all platforms, I'd agree with you. The abstraction is akin to refactoring all the code that would've needed to be done for all platforms. In this case, there's only one method that it doesn't need a "refactor".
Having code that only works on specific platforms in the cross-platform facing API can confuse developers as there's a mix of methods that cross-platform and platform-specific. The inconsistency isn't great IMO and the only way to know which one is platform-specific is to know about the finer details. If this keeps happening (i.e. if we think about this as a general approach to go with), the cross-platform facing API ends up growing more than needed that the inconsistency sticks out more. They'd need to sift through more docs to filter out the "noise" as there are platforms they may not care about. It has an impact on the suggestions made by the IDE where more methods could be shown than necessary as well. Adding platform-specific features could end up being blocked waiting on the maintainer of the cross-platform facing API to update it as well and this is one of the points raised in the doc as they may not be the same group of people.
Unless I've missed it, I haven't seen the approach you suggest adopted at scale either so for me, I'd question if it does provide the best developer experience if it's not a commonly used approach by those with experience in building plugins. Even with Xamarin.Forms, what I'd seen were cross-platform abstractions. Sometimes this might not have been sufficient that one would need to write their own abstraction or leverage a plugin and then have custom code for the cases that weren't covered.
The approach I'm suggesting is keep everything that works the same way in the cross-platform facing API (and in the platform interface). Anything that is platform-specific extends the platform interface but has extra methods specific to that platform. There's a cleaner separation IMO and allows for platform-specific implementations to add more features at their own pace. As an example of how this would look in this scenario, this could be to check if the user is running on the web and then create a instance of the web implementation of the plugin (e.g. a class called WebAppAuth) and then call init() on it.
Alternatively, can use the approach for what I've done for the flutter_local_notifications plugin. Here, the cross-platform facing plugin holds a reference to the platform-specific implementation,. The cross-platform facing API has a method to return the a reference to this for developers to get access to larger API surface specific to the platform. It returns null when the type doesn't match the platform the app is running on. This means check for the platform that the app is running from can be omitted by using the ?. operator when calling methods. The way this is working now could possibly be refined more but so far has worked pretty well.
BTW, this nowhere outlines that I can find any conditional imports as part of the definition.
Do you mean as part of the federated plugin? If so, then I would've thought it's not needed as it's an implementation detail as a platform-specific plugin would only have code specific to the plugin that conditional imports aren't needed by the developer. They'd only need to import the plugin library as they normally would i.e. importing flutter_appauth.dart grants access to the classes for the cross-platform plugin (the existing FlutterAppAuth class) and web-specific version (WebAppAuth). WebAppAuth here would wrap around the web implementation of the official AppAuth library and call the appropriate APIs. I'm wondering if part of the confusion here is if you thought I meant for developers to check if they're running on the web and using the the official web AppAuth library directly
@MaikuB @jhancock4d I've taken a stab at adding web support today and made some progress but I'm not sure the approach is going to pan out:
https://github.com/josh-burton/flutter_appauth/tree/web
I've used js_facade_gen to generate dart bindings for AppAuth JS, and started a basic web plugin.
If you run the sample, 'Sign in with auto code exchange' will trigger AppAuth to navigate to the identity server for login, but returning the result is where I'm stuck.
I'm not sure what to use for the redirect url - I guess this should be a Flutter route - and I'm not sure that this redirect based flow is going to work at all since it loses any state in the Flutter app.
From what I can see there isn't a way to ask AppAuth to login in a new tab instead.
@josh-burton I looked at using appauth.js but it explicitly isn't designed for this usage. The only way that it works is with a popup window which Firefox actively blocks.
I have already got a pull request in that fully enables web and all of the supported flows that this library supports. You can grab it by going to pull requests. It should just work and gives you the option of using a popup or redirect and return as is supported on all browsers.
At the very least you can see how the code works and what actually gets posted and returned. My way is a significantly smaller download in the browser too.
@jhancock4d I can't seem to find your PR. Do you have a link?
@josh-burton Odd. Here it is as a zip for you to take a look at. flutter_appauth_web.zip
Hey @jhancock4d just trying to find your fork so I can test out the web implementation but can't see it on the list of available forks in GitHub. Did you happen to have a link to it at all?
@lewcianci The code is above in a zip file. The reason why I haven't put up the fork is because it requires a new platform interface method be added for initialization that has to occur on startup of the application. The other platforms simply need to do a no-op in that case but web requires it so that the token on the redirect back can be processed. You can use the code in the zip above, you just have to do a fake out wrapper that calls the init only on web right now. As soon as the platform interface has an init function then I can do a full pull request and get this in. I'm happy to work with whomever to get that done.
Any updates on the init method for the interface?
@jhancock4d I made a plugin based on your code but still can't manage to get response back on redirect, any hints?
Also i don't quite get which is the method i should call on a wrpaeer for web because the only one that makes sense is processStartup but that one needs a AuthorizationTokenRequest and the window containing the values, can you guide me?
Thanks in advance!
@stvoidmain In your main() function you'd call the processStartup.
It will take the redirection back that went through the callback.html file that should be in your /web folder (and your redirect URL always points to it for web)
That should result in it processing the code information from the redirect and then it should request the auth/id tokens automatically as part of the exchange from there.
Oh, i see, thanks! I will try it and I'll try another approach which, if works, could be potentially better, based on the code i saw on openid_client but i have a problem with awaiters not being honored at some point and didn't catch it yet, i'll be back at here if i made some progress, thanks again!
Here's how mine looks:
Future
authClient is a package that I've created until this is supported in the root package that does a bait and switch like this:
export './src/client/client.dart' if (dart.library.io) './src/mobile/client_mobile.dart' if (dart.library.js) './src/web/client_web.dart';
This allows the method to be available and do a no-op on mobile and call the code necessary to complete the login on web. (it just calls processStartup in the web plugin)
Oh, nice, that was one of my first approachs i just want to have something more "standard" but yeah, that works!
Hopefully we can get the call added in this package and the mobile clients no oping, and then this could be fully integrated and be trivial. Pretty minor change and that enables all platforms to be supported. (i.e. adding Windows support to this is trivial)
Yeah, I like to have desktop added as well, being able to write fully cross platform code has been my dream since I can remember!
Hey All,
I need this for web too, using Azure AD B2C. Please let me know where I can find the progress and potentially contribute.
Thanks!
Leaving a +1, @jhancock4d is your approach working?
Need this for auth on keycloak. Thank you very much guys.
@Memo99 Yes, my way works completely. If the platformInterface had an init method added to it, this would just work and we'd be done and I could push the pr.
While no other platforms specifically require this right now, they could in the future because it's a pretty standard thing for plugins to have to do initialization code. (i.e. updating refresh tokens or some such)
I'd love to get this wrapped up and in the library one way or another. If someone wants to take my code and put it in the way they want this done, that's fine too since it's all working well, I would just encourage whoever does it to consider that the end coder should be able to write code once that works in a project with all platforms and shouldn't have to write custom code for web versus everything else if at all possible. Adding one interface member and adding no-ops for ios/android/windows/linux when implementing isn't a big deal and it's the ONLY thing that this library requires to be fully supported across all Flutter platforms.
@jhancock4d can you please share your callback.html file?
<!DOCTYPE html>
<html>
<head>
<script>
window.onload = function() {
var parent = window.opener ?? window.parent;
if (window.top == window.self) {
//this is a redirect
const desitionation = sessionStorage.getItem("auth_destination_url") || "/";
sessionStorage.removeItem("auth_destination_url");
sessionStorage.setItem("auth_info", window.location);
location.assign(desitionation);
} else {
parent.postMessage(location.href, "*");
}
}
</script>
</head>
<body>
</body>
</html>
(I had to paste directly because HTML isn't allowed as an upload type)
This should be included in the documentation for the project to be added if this gets merged.
Hey @jhancock4d - I'm having trouble getting this implementation working, would you be willing to post a more complete code sample showing the authClient package details and how it can be integrated with the rest of the flutter app?
@mdrideout When I get a minute I'll do a full pull request that does the entire implementation.
In short:
-
Pull the code
-
Reference it in your pubspec.yaml (and the interface and the android/ios) dependencies: flutter_appauth: ^0.9.2+6 flutter_appauth_web: path: ../flutter_appauth_web
-
Add the html file to your web folder of the project.
-
Then, for right now you're going to need to create a fake out that checks if it's html and exports the right stuff based on that.
Here's how mine works:
export './src/client/client.dart' if (dart.library.io) './src/mobile/client_mobile.dart' if (dart.library.js) './src/web/client_web.dart';
client.dart is a base class that has all of the endpoints I need. Then based on the dart.library stuff it pulls in the specific implementation of the abstract base class that wraps everything.
Reference your wrapper dart file that has the export, not any of the specific files.
Once I do a full pull request to update everything, you'll just have to add the pubspec reference and the html file to your web folder and you'll be done.
I'll likely wait until we have an alpha of the UWP version of flutter that has package support and then I'll add the UWP code per #171 as well which will just leave Linux and MacOs.
@mdrideout When I get a minute I'll do a full pull request that does the entire implementation.
🙏 Awesome, thank you @jhancock4d !
I finally got this working. The Zip'd plugin from @jhancock4d worked great, ~~except I had to correct a typo for a JSON decode to change jsonResponse["expires_in"] to jsonResponse["refresh_token_expires_in"].~~
EDIT: I am using Azure AD B2C (which I am learning has several non-standard conventions vs. the oauth specification), which requires you to add your client_id to the scope list in order to return an access token. This is hidden in the Microsoft documentation here
| Parameter | Required? | Description |
|---|---|---|
| scope | Required | A space-separated list of scopes. A single scope value indicates to Azure Active Directory (Azure AD) both of the permissions that are being requested. Using the client ID as the scope indicates that your app needs an access token that can be used against your own service or web API, represented by the same client ID. The offline_access scope indicates that your app needs a refresh token for long-lived access to resources. You also can use the openid scope to request an ID token from Azure AD B2C. |
My file structure for the Step 4 from the above post (my syntax is a little different too)
lib |-- authclient |---- client_interface.dart |---- src |------ client_stub.dart |------ client_mobile.dart |------ client_web.dart
client_interface.dart
Allows for interacting with the conditionally used subclasses based on the abstract class (keeps IDE happy)
import 'package:flutter_appauth_platform_interface/flutter_appauth_platform_interface.dart';
import './src/client_stub.dart' // Stub implementation
if (dart.library.io) './src/client_mobile.dart' // dart:io implementation
if (dart.library.html) './src/client_web.dart'; // dart:html implementation
abstract class B2CAuth {
// Process startup
Future<AuthorizationTokenResponse> processStartup() {}
// Sign In Function
void signIn() {}
// Return the correct implementation
factory B2CAuth() => getB2CAuthImplementation();
}
client_mobile.dart
Uses mobile specific configuration variables
import 'dart:io';
import 'package:b2cdemo/authclient/client_interface.dart';
import 'package:flutter_appauth/flutter_appauth.dart';
class B2CAuthMobile implements B2CAuth {
/// Azure AD B2C Attributes (AppAuth Web And Mobile Demo)
final clientId = 'xxxxxxxxxxxxxx';
final redirectUrl = 'xxx.xxx.xxxxxxx://oauthredirect/';
final policyNameSignIn = 'B2C_1_xxxxx';
final List<String> scopes = ['openid', 'profile', 'offline_access', 'client_id_here'];
@override
void processStartup() {
// does nothing on mobile
return;
}
@override
void signIn() async {
FlutterAppAuth _appAuth = FlutterAppAuth();
print("Starting authentication on MOBILE");
try {
final AuthorizationTokenResponse result = await _appAuth.authorizeAndExchangeCode(
AuthorizationTokenRequest(
clientId, redirectUrl,
promptValues: ['login'],
serviceConfiguration: AuthorizationServiceConfiguration(
"https://xxx.b2clogin.com/xxx.onmicrosoft.com/$policyNameSignIn/oauth2/v2.0/authorize",
"https://xxx.b2clogin.com/xxx.onmicrosoft.com/$policyNameSignIn/oauth2/v2.0/token",
),
scopes: scopes,
preferEphemeralSession: Platform.isIOS, // iOS requires this to support the private browser (iOS 13 and newer)
),
);
print("AppAuth Sign In Response ID Token: " + result.idToken);
} catch (e) {
print(e.toString());
}
}
}
B2CAuth getB2CAuthImplementation() => B2CAuthMobile();
client_web.dart
Utilizes web specific configuration variables
import 'dart:html';
import 'package:b2cdemo/authclient/client_interface.dart';
import 'package:flutter_appauth_web/flutter_appauth_web.dart';
import 'package:flutter_appauth_platform_interface/flutter_appauth_platform_interface.dart';
class B2CAuthWeb implements B2CAuth {
/// Azure AD B2C Attributes (AppAuth Web And Mobile Demo)
final discoveryUrl =
'https://xxx.b2clogin.com/xxx.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=B2C_1_xxx';
final issuer = 'https://xxx.b2clogin.com/xxxxxxxxxxxx/v2.0/';
final clientId = 'xxxxxxxx';
final redirectUrl = 'http://localhost:60664/callback.html';
final policyNameSignIn = 'B2C_1_xxx';
final additionalParameters = null;
final List<String> scopes = ['openid', 'profile'];
@override
Future<AuthorizationTokenResponse> processStartup() async {
AppAuthWebPlugin _appAuth = AppAuthWebPlugin();
print("Running b2c web startup process...");
print("Session storage: " + window.sessionStorage.toString());
AuthorizationTokenRequest _request = AuthorizationTokenRequest(
clientId,
redirectUrl,
discoveryUrl: discoveryUrl,
scopes: scopes,
promptValues: ['login'],
serviceConfiguration: AuthorizationServiceConfiguration(
"https://xxx.b2clogin.com/xxx.onmicrosoft.com/$policyNameSignIn/oauth2/v2.0/authorize",
"https://xxx.b2clogin.com/xxx.onmicrosoft.com/$policyNameSignIn/oauth2/v2.0/token",
),
additionalParameters: additionalParameters,
issuer: issuer,
);
try {
AuthorizationTokenResponse _authTokenResponse = await AppAuthWebPlugin.processStartup(_request);
print("Startup response: " + _authTokenResponse.toString());
return _authTokenResponse;
} catch (e, s) {
print("B2C Process Startup Error.");
print(e);
print(s);
return null;
}
}
@override
void signIn() async {
AppAuthWebPlugin _appAuth = AppAuthWebPlugin();
print("Starting authentication on WEB");
try {
final AuthorizationTokenResponse result = await _appAuth.authorizeAndExchangeCode(
AuthorizationTokenRequest(
clientId,
redirectUrl,
promptValues: ['login'],
serviceConfiguration: AuthorizationServiceConfiguration(
"https://xxx.b2clogin.com/xxx.onmicrosoft.com/$policyNameSignIn/oauth2/v2.0/authorize",
"https://xxx.b2clogin.com/xxx.onmicrosoft.com/$policyNameSignIn/oauth2/v2.0/token",
),
scopes: scopes,
),
);
print("AppAuth Sign In Response ID Token: " + result.idToken);
} catch (e, s) {
print("B2C Web Sign In Error");
print(e);
print(s);
}
}
}
B2CAuth getB2CAuthImplementation() => B2CAuthWeb();
main.dart
Incorporates the processStartup() function before runApp()
import 'package:b2cdemo/authclient/client_interface.dart';
import 'package:flutter/material.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await B2CAuth().processStartup();
runApp(MyApp());
}
@mdrideout Thank you for posting the details. I'm sure this will help others until I can get this in.
Note that missing "refresh_token_expires_in" isn't really a typo. It's intentional because that's what the spec says it should be "expires_in" for all token responses. (There is no concept of refresh_token_expires_in as a standard as far as I can tell. Although a few companies use it.
There is a significant difference even with those companies that support this, as to what expires_in versus refresh_token_expires_in does. The former is in relation to the access token itself, the later how long you can use the refresh token to get new access token. Most clients would never use refresh_token_expires_in because they would simply make a request to refresh the token if the access_token was expired and if it was rejected because of this, they'd prompt for login again.
The current futter_appauth doesn't provide this as a parameter so there is nowhere to actually put it and even with Azure, expires_in is still passed by the refresh token response and refers to the expiration of the client access token per about 2/5ths of the way down here under "Successful Response": https://docs.microsoft.com/en-us/windows-server/identity/ad-fs/overview/ad-fs-openid-connect-oauth-flows-scenarios
If you need the refresh_token_expires_in I pass it in the tokenAdditionalParameters map in the TokenResponse so you can dig it out if you need it.
Note that the ios and android implementations do the same.
flutter_auth_client.zip BTW, here's my full implementation of a fully working client wrapper that does the majority of what you'd expect it to do. This also handles doing l/p credential login with refresh tokens as well so that you can create a hybred login that prompts for email address and click next or allows the user to also pick a standard sso provider, and then on next, test if the user is an sso corporate user and login through their sso or go to a password screen if they're not so that you get a much more native feel in all cases other than SSO which users expect to shell out to a browser window.
It also stores tokens etc. for you, handles refresh, with an event sink that you can subscribe to, and does it's best to keep the identity information safe. Of course on web that isn't really possible so you're trusting control of the browser, and you should never allow refresh tokens to be reused on your server side (which is an explicit setting on OpenIddict, IdentityServer, Ping, and others that you should be configuring)
Note that this now has null safety enabled (ish since http isn't null safe yet). I've included the null safeish version of the code in question above too. flutter_appauth_web.zip
Edit
Fixed the comment above to show that the client_id must be included in the scope list to get an access token.
Original
Note that missing "refresh_token_expires_in" isn't really a typo. It's intentional because that's what the spec says it should be "expires_in" for all token responses. (There is no concept of refresh_token_expires_in as a standard as far as I can tell. Although a few companies use it.
OK makes sense. This is my first time using Azure AD B2C and because the response after login does not contain an expires_in value, I was getting an error preventing the whole thing from working. It appears to be due to the Date / Duration function potentially not working with a null value.
Line ~150 and ~247 - you can see where in the DateTime line, I changed it to the available refresh_token_expires_in key.
return TokenResponse(
jsonResponse["access_token"].toString(),
jsonResponse["refresh_token"] == null ? null : jsonResponse["refresh_token"].toString(),
DateTime.now().add(new Duration(seconds: jsonResponse["refresh_token_expires_in"])),
jsonResponse["id_token"].toString(),
jsonResponse["token_type"].toString(),
jsonResponse,
);
This is the error I was getting after returning back to the web app (FWIW, this works fine on iOS - only had this issue on Web), which was resolved by updating to refresh_token_expires_in, specifically calling out that line in the stack trace
dart-sdk/lib/core/duration.dart 138:35 new
packages/flutter_appauth_web/flutter_appauth_web.dart 153:30 requestToken
NoSuchMethodError: '<Unexpected Null Value>'
method not found
Receiver: null
Arguments: []
B2C's reference
Successful Response Example (this mirrors what I am currently getting)
{
"not_before": "1442340812",
"token_type": "Bearer",
"id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ik5HVEZ2ZEstZnl0aEV1Q...",
"scope": "openid offline_access",
"id_token_expires_in": "3600",
"profile_info": "eyJ2ZXIiOiIxLjAiLCJ0aWQiOiI3NzU1MjdmZi05YTM3LTQzMDctOGIzZC1jY...",
"refresh_token": "AAQfQmvuDy8WtUv-sd0TBwWVQs1rC-Lfxa_NDkLqpg50Cxp5Dxj0VPF1mx2Z...",
"refresh_token_expires_in": "1209600"
}
However, I'm seeing a different response from this documentation here, so maybe there is an issue with my request format which would allow me to get this response. https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ik5HVEZ2ZEstZnl0aEV1Q...",
"token_type": "Bearer",
"expires_in": 3599,
"scope": "https%3A%2F%2Fgraph.microsoft.com%2Fmail.read",
"refresh_token": "AwABAAAAvPM1KaPlrEqdFSBzjqfTGAMxZGUTdM0t4B4...",
"id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJhdWQiOiIyZDRkMTFhMi1mODE0LTQ2YTctOD...",
}
@mdrideout The problem is that you're using 2 values for the same field and they are NOT the same thing. expires_in is when the access_token expires.
refresh_token_expires_in is what the refresh token can no longer be used to refresh your access token.
So you're creating a major bug in your code doing what you're doing. I believe the problem is in your setup of your Azure app.
https://docs.microsoft.com/en-us/answers/questions/135912/azure-ad-b2c-access-token-missing.html
You should be getting expires_in back if you're getting an access token back. If you're not it's either configuration or a bug that you need to reach out to Azure about.
The ios code is simply setting expires_in to DateTime.Now() if it isn't set in the response, but that isn't really valid either because that isn't when it expires and if you trusted that you'd be refreshing before every requrest. (I've updated my null safe code that I'll do a pull request on later to follow this behavior but it isn't really right)
According to the spec docs if you receive an access_token you will ALWAYS receive an expires_in. Full stop.
Thanks @jhancock4d - I found the issue and corrected the above comments for future searchers. The B2C request needs to include the client_id in the scope list in order to return an access token. Adding that to the scope allowed my code to work with your originally coded package.
@mdrideout Glad you found it. I think alternatively, you can check off "access tokens" and "id tokens" in the UI and you'll get the same result.
@mdrideout Glad you found it. I think alternatively, you can check off "access tokens" and "id tokens" in the UI and you'll get the same result.
I actually had these checked off already when I was experiencing the issue.
@mdrideout Odd. Then that is absolutely a bug in Azure. With the rest that is sent, they should have errored instead of just not returning it.
Hi guys, great work on this! I found this thread testing out my sample flutter app on android and web. So, I'm not sure where this PR is with the web support. I see you guys did great work on platform interface but I'm not sure how far that is from having something to test. Here's the error I get when I try to authenticate in the web platform,
login error: MissingPluginException(No implementation found for method authorizeAndExchangeCode on channel crossingthestreams.io/flutter_appauth) - stack: C:/b/s/w/ir/cache/builder/src/out/host_debug/dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 236:49 throw_
So I'm wondering is this related to the redirect url having different schemes? For web, does it have to be something based on https? I am kinda hoping that we could have a 100% portable package for the platforms.
I'm not sure how I can help - maybe test something if anyone is looking into the web support. Is the issue with the sdk or just implementing the pattern? thanks
@MartyBolton same error here, looks like the web implementation is a work in progess (PR is still Open). I see that @jhancock4d published a zip file, have you tried it?
@MartyBolton same error here, looks like the web implementation is a work in progess (PR is still Open). I see that @jhancock4d published a zip file, have you tried it?
I probably looked at things a while ago but I moved onto the app features. I think I'm going with the lit_auth_firebase - it kinda has everything I need, albeit, still lots of features missing. I decided to come back to oauth after I get my app built because I was spending too much time in auth all platforms.
Thank you @jhancock4d for your Web example, @MaikuB for the lib and everyone else for contributing too. I managed to get this working on my web project, finally 🙏
@MaikuB is there anything we can do to make @jhancock4d's web code part of your lib?
@MaikuB I'm considering forking this into a new library that has Web + iOS + Android and enlisting others to add macOS, Linux and Windows since you appear uncomfortable with merging this in because it doesn't use their js library which isn't designed to work for flutter.
Thoughts?
@jhancock4d was about to suggest the same and makes sense if the JS library isn't used. Avoids confusion/misleading others with the names since this plugin mentions AppAuth and that it's a wrapper for said SDKs.
Edit: I'm not an expert in this area but one thing to be mindful of with the fork is around the license. My understanding is you'd need to retain the original license details
@jhancock4d thank you very much for providing the example zip file for the updated library. I am trying to piece together all the comments so that I can build a working Flutter app, but I can't get it working. I am new to Flutter, so apologies if somethings are obvious and I'm not following correctly. I want to use Flutter for my single code base for iOS, Android & Web, and integrate with Auth0.
Would you please provide a sample app which uses your library?
any progress on that @stephengibson83 ? I am basically about to do the very same while having a working "App-only" flow.
@Mereep @stephengibson83 The Dart team has released a full oauth2 package for all platforms. The issue is how it pops the web login (i.e. it doesn't and that's your problem)
In a perfect world the code from here for ios and android to pop the proper authentication window would be done as a pull request to the oauth2 package from the dart team, and then someone else would right the C++ project reunion code for Windows and formalize the browser pop on Linux.
Or a wrapper could be made that does it and uses the oauth2 package to manage it. I'm considering the later.
@JohnGalt1717 I actually saw this package today. But for the first glimpse I wasn't too sure on how it should actually show a proper login page (guess thats also what you refering to) on web and actually redirect back correctly (guess this is the main issue). I am right now really considering if I should dump time into it, without being certain it will do what I want.
I basically just want Login -> Get Access Token -> Bye. I don't want the App-side to do anything else. Is this something that can be done using the oauth2-plugin for web AND app as is? (I don't need any other fancy flows since my existing backend will handle further authorization).
@Mereep The app has to do something else.
Password flow: This just works using an http post/get that passes the login and password and gets json back with all of the items in the payload.
Code flow: Using PKCE which you should use ore you're insecure, you have to redirect to a page, have the user login/password, then that redirects back to the URL provided and the URL contains the code, then on your side you have to do a http request out of band using the code provided to get the token payload back.
Implicit flow: same as code flow but the redirect includes the tokens in the URL.
Other flows: Similar to Code Flow just more complex.
Code flow is the only one that should be used for 3rd party authentication or any environment where you don't have complete control of the page. Password flow can be used for people logging in directly to you.
In the case of code flow/implicit you have to launch a web browser. On iOS, Android, Mac, and Windows you MUST use the authentication browser window features of that platform to launch the web browser. If you don't, you will get your app rejected, (they might miss it originally if you have password flow as well, but eventually they'll catch you and block new app updates until you fix it. Especially if you just pop a web browser in the app, they hate that). I'm not aware of any formalized mechanism for linux, so you can indeed just use urllauncher for it.
The flutter/dart oAuth2 library tells you to use urllauncher with app redirects and then the app redirect watcher to do this which is incorrect and will get you blocked from the stores. I have a ticket in to have them add this functionality (in which case you could just use theirs) but you could use their oauth2 library and wrap it so that it handles all of the above automatically.
Anything less is both not secure, and will get you blocked from publication from the stores.
@JohnGalt1717 thanks for this input. I guess it will help people that stumble around here.
At the moment I had a solution which basically just uses authorizeAndExchangeCode (to the auth0 provider). This redirects back to my app (correctly on iOS and Android) providing me a bearer token granting me access to the users' email address. This is basically all I need.
This is all I use the plugin for, althought I don't know which oauth flow (looks like code flow) for what I read from your text. This is what flutter_appauth is doing and frankly I do not want to be concerned into every niche detail to every protocol detail (that's why people use plugins at the first place).
Since none of the solutions seem to be working as they should for all platforms atm, I am currently working on a own registration / login procedure on my server side, which is a bit a pitty, since its just a waste of time but will work platform-independent.
As soon as some plugin works without hassle I might switch it back and have two authentications schemes in place then. Not so bad after all.
Thanks again for that dive into the details and the risks on app refusal :)
@Mereep thar of course is your choice but that’s a really great way to have massive security issues in your app. Parler found that out the hard way.
Strongly suggest you learn about openidconnect as it’s foundational to all programming at this point and a required skill.
There are 2 main flows you need to understand: code and password. The other 2 you see (credentials and device) are less used in apps.
Password is standard here’s a login and password give me a token. You can do that now on flutter trivially without anything special. In fact the current library works on all platforms for password flow. If you don’t need login with Facebook/Apple etc use this and be done. It’s safe as long as you’re in control of your apps and no one else tries using this.
Code flow requires a browser pop that redirects to a third party site and then back. (This can also be used internally to force complete control over login but your apps get an ugly web browser pop). Basically it opens the browser, logs in and the. Redirects to where you told it to go. This is complex is flutter because web has to redirect away, redirects back, load flutter and the. Process the response. And on apps iOS,android and windows have special browser pops to protect faking pages. (Linux doesn’t). Pkce fixes a vulnerability in redirect trust.
Refresh tokens is even worse because on browsers you can’t trust them so you can’t use refresh tokens but apps you can.
by now you should be getting the hint about the can of worms you’re walking Into if you try and roll your own. Everything in openidconnect is there for a reason. If you don’t do it in yours you’re screwed.
which is to say that flutter needs experts to implement this properly and maintain it all or you’re going to either not have a platform or you’re going to do it yourself and make a disaster of it. I guarantee you can’t do it yourself securely. Every company that has tried has failed.
@JohnGalt1717
Password is standard here’s a login and password give me a token. You can do that now on flutter trivially without anything special. In fact the current library works on all platforms for password flow. If you don’t need login with Facebook/Apple etc use this and be done. It’s safe as long as you’re in control of your apps and no one else tries using this.`
this is exactly what will happen when I am finished. I have a Django / DRF which is already configured to return JWTs which all endpoints use as authorization. Pretty standard and works flawlessly. The only thing changing will be that users cannot register / login anymore using social logins for now. Which is totally fine, since this solution is also just a in-development-step (the end product may use decentralized identifiers (i.e., DIDs) for authentication) and also the reason why I cannot dump massive time into it. If there is no plugin supporting web and native alike its ok I move on for now.
Code flow requires a browser pop that redirects to a third party site and then back. (This can also be used internally to force complete control over login but your apps get an ugly web browser pop). Basically it opens the browser, logs in and the. Redirects to where you told it to go. This is complex is flutter because web has to redirect away, redirects back, load flutter and the. Process the response. And on apps iOS,android and windows have special browser pops to protect faking pages. (Linux doesn’t). Pkce fixes a vulnerability in redirect trust.
I got that. But since "open browser and redirect back" is a concept which is very platform specific, it just doesn't just work at the time of writing having the same code base (and this is why this thread exists).
@MaikuB Is there a plan to include @jhancock4d solution into the package?
Since there doesn't seem to be movement on this (rightfully so I'd say) I've created a library that has the intent of supporting all platforms and currently supports ios, android and web (using similar code to the above)
You can find it here: https://github.com/Concerti-IO/openidconnect_flutter
It is not published to pub.dev yet. Working with our legal team to get it there as quickly as possible. The readme in /openidconnect/readme.md has details about the other platforms and (very) basic instructions for use for now. There is also an example that uses the direct calls. More documentation etc. coming especially on how to use the OpenIdConnect client in a project effectively. (it manages your entire login state and raises events for you to respond to globally)

Open since 2020
AppAuth web plugin https://github.com/CarlosPacheco/flutter_appauth_web
thks everyone here.
hello ! I followed this intervention on the forum at this link https://github.com/MaikuB/flutter_appauth/issues/103 concerning URL redirection works well on Android but not on iOS. In fact I have the same problem but with the only difference that the B2C login page opens well on android but not on IOS. I really ask for your help if you can help me solve this problem. my project is develop with flutter and i use flutter_appauth to establish azure ad b2c connection