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

Flutter Web: OIDC Web Flow - state Not Found After Redirect* #122

Open
CuriousDev21 opened this issue Sep 4, 2024 · 2 comments
Open

Flutter Web: OIDC Web Flow - state Not Found After Redirect* #122

CuriousDev21 opened this issue Sep 4, 2024 · 2 comments
Labels
bug Something isn't working question Further information is requested

Comments

@CuriousDev21
Copy link

Inquiry: OIDC Web Flow - state Not Found After Redirect

Description:

I'm working on a Flutter web app using the OIDC Flutter package for authentication. After logging in, the app redirects to the redirect.html page, which displays the message:

"Operation Successful! Please close this page."

However, when I close this tab and return to the original login page, the login flow does not complete, and I see the following error message in the browser console:

handleRedirect    - state not found, key: 944ef68e-0b29-4fe0-8054-f56e8832cfca

It seems that the state parameter is not being saved or retrieved correctly from localStorage, which is causing the authentication process to break after redirection.


Setup Details:

  • OIDC Provider: Keycloak
  • Platform: Flutter Web
  • Browser: Chrome
  • Redirect URIs:
    • Development: http://localhost:3000/redirect.html

OIDC manager setup:

  late final OidcUserManager _manager = OidcUserManager.lazy(
    discoveryDocumentUri: _oidcDiscoveryDocumentUri,
    clientCredentials: const OidcClientAuthentication.none(clientId: _clientId),
    store: OidcDefaultStore(
      secureStorageInstance: DataSource.secureStorage.secureStorage,
      sharedPreferences: DataSource.sharedPreferences.sharedPreferences,
    ),
    settings: OidcUserManagerSettings(
      refreshBefore: null,
      redirectUri: _redirectUrl,
      postLogoutRedirectUri: _postLogoutRedirectUrl,
      scope: ['openid', 'roles', 'offline_access'],
      options: const OidcPlatformSpecificOptions(
        ios: OidcPlatformSpecificOptions_AppAuth_IosMacos(preferEphemeralSession: true),
        web: OidcPlatformSpecificOptions_Web(
          broadcastChannel: 'oidc_flutter_web/redirect',
        ),
      ),
    ),
  );

Here is the content of the redirect.html file:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <title>Flutter Oidc Redirect</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script type="text/javascript">
        const stateNamespace = 'state';
        const stateResponseNamespace = 'response.state';
        const requestNamespace = 'request';

        const requestBroadcastChannel = 'oidc_flutter_web/request';
        const redirectBroadcastChannel = 'oidc_flutter_web/redirect';

        if (!handleFrontChannelLogout()) {
            handleRedirect();
        }

        function handleRedirect() {
            var bc = new BroadcastChannel(redirectBroadcastChannel);
            bc.postMessage(window.location.toString());
            bc.close();

            let dataSrc = new URLSearchParams(window.location.search);
            var state = dataSrc.get('state');
            if (!state && window.location.hash) {
                dataSrc = new URLSearchParams(window.location.hash.substring(1));
                state = dataSrc.get('state');
            }

            if (!state) {
                console.error('state not found, key: ' + state);
                return;
            }

            const stateDataRaw = getLocalStorage(stateNamespace, state);
            if (!stateDataRaw) {
                console.error('state data not found, key: ' + state);
                return;
            }

            setLocalStorage(stateResponseNamespace, state, window.location.toString());

            const parsedStateString = JSON.parse(stateDataRaw);
            if (!parsedStateString) {
                console.error('parsed state is null');
                return;
            }

            const webLaunchMode = parsedStateString.options?.webLaunchMode;
            if (!webLaunchMode) {
                console.error('webLaunchMode not found in parsed state.');
                return;
            }

            if (webLaunchMode != 'samePage') {
                return;
            }

            const original_uri = parsedStateString.original_uri;
            if (!original_uri) {
                console.warn("It's preferred that original_uri is used when webLaunchMode is samePage.");
                return;
            }

            window.location.assign(original_uri);
        }

        function handleFrontChannelLogout() {
            const queryParams = new URLSearchParams(window.location.search);
            if (queryParams.get('requestType') == 'front-channel-logout') {
                var bc = new BroadcastChannel(requestBroadcastChannel);
                bc.postMessage(window.location.toString());
                bc.close();

                setLocalStorage(requestNamespace, 'front-channel-logout', window.location.toString());
                return true;
            }
            return false;
        }

        function getLocalStorage(namespace, key) {
            const rawRes = localStorage.getItem('oidc.' + namespace + '.' + key);
            if (!rawRes) {
                return null;
            }
            return rawRes;
        }

        function setLocalStorage(namespace, key, value) {
            const keysEntryKey = 'oidc.keys.' + namespace;
            var keys = localStorage.getItem(keysEntryKey);
            if (!keys) {
                keys = "[]";
            }

            const parsedKeys = JSON.parse(keys);
            if (!(parsedKeys instanceof Array)) {
                console.error('parsedKeys is not an array.', parsedKeys);
            }
            parsedKeys.push(key);
            localStorage.setItem(keysEntryKey, JSON.stringify(parsedKeys));
            localStorage.setItem('oidc.' + namespace + '.' + key, value);
        }
    </script>
</head>

<body>
<h3>Operation Successful! Please close this page.</h3>
</body>

</html>

Steps to Reproduce:

  1. Start the Flutter web app.
  2. Click the login button, which redirects to the identity provider.
  3. After successful login, the redirect.html page is shown with the message: "Operation Successful! Please close this page."
  4. Close the tab, but notice that the app does not update and the login state remains unrecognized.

What I’ve Tried:

  • Verified the redirect.html is being correctly served.
  • Checked that BroadcastChannel is working properly.
  • Ensured that the same broadcast channel name is used across the Flutter app and redirect.html.

Any advice or help on how to resolve this issue would be greatly appreciated.

@CuriousDev21 CuriousDev21 added the bug Something isn't working label Sep 4, 2024
@ahmednfwela
Copy link
Member

ahmednfwela commented Sep 6, 2024

hi @CuriousDev21, will check it out now

@ahmednfwela ahmednfwela added the question Further information is requested label Sep 6, 2024
@ahmednfwela
Copy link
Member

@CuriousDev21 when the Operation Successful! Please close this page is displayed, can you check the existing local storage values ? you can see them in F12 -> Application tab > Local Storage

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants