ThexXTURBOXx / flutter_web_auth_2

Flutter plugin for authenticating a user with a web service
https://pub.dev/packages/flutter_web_auth_2
MIT License
50 stars 49 forks source link

window.opener is null in auth.html #44

Closed c-seeger closed 1 year ago

c-seeger commented 1 year ago

Describe the bug

when trying to authenticate against twitter using twitter_oauth2_pkce which just uses flutter_web_auth_2 in the backend I managed to let it open the popup with correct redirect but when auth.html is called the window.opener.postMessage does not work since window.opener is null.

Expected behavior

window.opener should not be null or any other way to send the data back to it's parrent

Device (please complete the following information!)

ThexXTURBOXx commented 1 year ago

Can you try running the example of flutter_web_auth_2 and verify that your setup kind of resembles that one?

c-seeger commented 1 year ago

Yes I can reproduce the example when using pure flutter_web_auth_2 implementation against twitter. If I use the example it works, I think the main difference here is that the popup/new tab first opens twitter and then redirects back the window seems to loose its opener value.

I think its similar to this issue: window-opener-is-null-after-redirect

example code I use:

final String codeVerifier = _generateSecureAlphaNumeric(80);
final String codeChallenge = _generateCodeChallenge(codeVerifier);

final String state = _generateSecureAlphaNumeric(25);

final url = Uri.https('twitter.com', '/i/oauth2/authorize', {
  'response_type': 'code',
  'client_id': Environment().config.twitter.clientID,
  'redirect_uri': Environment().config.twitter.redirectUri,
  'scope': 'tweet.read',
  'state': state,
  'code_challenge': codeChallenge,
  'code_challenge_method': 'S256',
});

final result = await FlutterWebAuth2.authenticate(
    url: url.toString(),
    callbackUrlScheme: Environment().config.twitter.customUriScheme,
    contextArgs: ['twitter_oauth2_pkce::authenticateWindow']);
print(result);

utility functions

  Map<String, String> _buildAuthorizationHeader({
    required String clientId,
    required String clientSecret,
  }) {
    final credentials = base64.encode(utf8.encode('$clientId:$clientSecret'));

    return {'Authorization': 'Basic $credentials'};
  }

  String _generateSecureAlphaNumeric(final int length) {
    final random = Random.secure();
    final values = List<int>.generate(length, (i) => random.nextInt(255));

    return base64UrlEncode(values);
  }

  String _generateCodeChallenge(String codeVerifier) {
    final digest = sha256.convert(utf8.encode(codeVerifier));
    final codeChallenge = base64UrlEncode(digest.bytes);

    if (codeChallenge.endsWith('=')) {
      //! Since code challenge must contain only chars in the range
      //! ALPHA | DIGIT | "-" | "." | "_" | "~"
      //! (see https://tools.ietf.org/html/rfc7636#section-4.2)
      return codeChallenge.substring(0, codeChallenge.length - 1);
    }

    return codeChallenge;
  }

the code works and i can authorize my app but once i reach the auth.html it errors

modified auth.html

<!DOCTYPE html>
<title>Authentication complete</title>
<p>Authentication is complete. If this does not happen automatically, please close the window.</p>
<script>
  try{
  window.opener.postMessage({
    'flutter-web-auth-2': window.location.href
  }, window.location.origin);
  }catch(e) {
    alert(e)
  }
  window.close();
</script>

Error: TypeError: Cannot read properties of null (reading 'postMessage')

which refers to window.opener.postMessage where window.opener is null

c-seeger commented 1 year ago

from the stacktrace issue:

I just put another redirecting page in between. Now the popup goes first: To a page on my domain, second: To PayPal and Third: Back to my domain. This way it doesnt loose the parent information. Great

Maybe this could help since for now I open new tab with twitter, then twitter redirects back to auth.html

c-seeger commented 1 year ago

Just for reference, I was able to solve the issue a bit differently.

when pressing the button I fire and forget FlutterWebAuth2.authenticate.

I'm currently using qlevar_router which allows me to do QR.params['code'] and QR.params['state'] to get current url parameters. Instead of auth.html I just redirect back to the same page.

some notes here: I need to use '_parent' as contextArgs and configure usePathUrlStrategy(); for this to work since twitter does not like redirect urls with #

However, this solution depends on an external package so if you can find a working solution with native support it would be great.

ThexXTURBOXx commented 1 year ago

I am a bit confused about your contextArgs: ['twitter_oauth2_pkce::authenticateWindow']. I added that named parameter in order to be able to pass, e.g., popup to the url launcher. Are you sure that you need to pass that parameter there? See #40 for more info on why contextArgs was added

c-seeger commented 1 year ago

no not really i just copied it form twitter-oauth2-pkce, I guess it's not needed

c-seeger commented 1 year ago

Yeah tested it and it can be removed

joseph-skyclover commented 1 year ago

I am having the same issue. I've seen some talk online on how this could be accomplished. some suggested putting the values into local storage and then periodically querying it for a time. Considering how each oauth implementation is slightly different it would make sense to not use window.opener as it may not always be opened from the same domain. Is this a solution that might be welcome in this package?

ThexXTURBOXx commented 1 year ago

That also sounds like a good solution. If someone is willing to create a PR for this, I would gladly merge it after a short review. Alas, I am currently on vacation, so I cannot work on this myself

joseph-skyclover commented 1 year ago

Ill take a crack at it on my personal account.

poster983 commented 1 year ago

Hey it's me on my personal account. A couple of things:

  1. I got it working with the local storage method 🎉

  2. I noticed that windows and linux both have 90 second timeouts. I set the web timeout to be 5 mins (checking every second). Should the authenticate method have a argument called timeout that would adjust the default timeout value on web, windows, and linux? Or was that 90 second timeout there for another reason?

  3. When it does timeout, what would be the best practice for throwing an error? Currently I have it doing the following:

    PlatformException(
    code: 'error',
    message: 'Timeout waiting for callback value',
    ),);

    But is there a better way to indicate that the call has timed out so it can be caught? (I'm new to contributing to dart packages so I may be unaware of a best practice)

  4. I'm going to ignore the apple edge case for now. Not sure if this issue applies to apple and I have no way to test it currently.