Azure / static-web-apps

Azure Static Web Apps. For bugs and feature requests, please create an issue in this repo. For community discussions, latest updates, kindly refer to the Discussions Tab. To know what's new in Static Web Apps, visit https://aka.ms/swa/ThisMonth
https://aka.ms/swa
MIT License
320 stars 53 forks source link

post_login_redirect_uri supports neither query string parameters nor passing of state #435

Open JonSilver opened 3 years ago

JonSilver commented 3 years ago

So I guess I'm going to have to do something elaborate to save my own state to local storage or cookie before the login is performed, and recover it afterwards?

Please confirm the expected implementation pattern for this scenario.

Thanks!

JonSilver commented 3 years ago

In case it helps anyone else, here's my solution which temporarily persists the query string to local storage and then recovers it when the app is reloaded. I call loginWithSavedState to allow the user to login, and recoverSavedState from a useEffect hook in my App.js.

const SAVED_STATE_KEY = "MyAppSavedState";

export const prepareLoginUrl = () => {
    const redirectUrl = `${window.location.origin}${window.location.pathname}`;
    return `/.auth/login/aad?post_login_redirect_uri=${redirectUrl}`;
};

export const loginWithSavedState = () => {
    localStorage.setItem(SAVED_STATE_KEY, window.location.search);
    window.location.assign(prepareLoginUrl());
};

export const recoverSavedState = () => {
    const queryString = localStorage.getItem(SAVED_STATE_KEY);
    if (queryString) {
        window.location.assign(`${window.location.href}${queryString}`);
        localStorage.removeItem(SAVED_STATE_KEY);
    }
};

So I can set an href to a simple login URL from prepareLoginUrl(), or execute a login with saved state using loginWithSavedState().

Then in my App component:

    useEffect(() => {
        recoverSavedState();
    }, []);

It'd be nice if the platform took care of all that for us though. There are precedents elsewhere.

Of course I'm just using this to take some initialisation state contained in the query string parameters and persist those through the login process. You could use the same technique to persist any sort of state content through the destructive cross-site, round-tripping login process that effectively relaunches your client side app in an uninitialised state.

mishapos commented 2 years ago

Post_login_redirect_uri and post_logout_redirect_uri should now support query parameters. Make sure to encode them before appending to the route rule if using a redirect. For example:

{
  "route": "/login",
  "redirect": "/.auth/login/aad?post_login_redirect_uri=%2findex.html%3fq1%3dp1%26q2%3dp2"
},
{
  "route": "/logout",
  "rewrite": "/.auth/logout?post_logout_redirect_uri=/index.html?q1=p1&q2=p2"
}
svdoever commented 2 years ago

Post_login_redirect_uri and post_logout_redirect_uri should now support query parameters. Make sure to encode them before appending to the route rule if using a redirect. For example:

{
  "route": "/login",
  "redirect": "/.auth/login/aad?post_login_redirect_uri=%2findex.html%3fq1%3dp1%26q2%3dp2"
},
{
  "route": "/logout",
  "rewrite": "/.auth/logout?post_logout_redirect_uri=/index.html?q1=p1&q2=p2"
}

The parameters needs to be passed from code - in above example - it is hardcoded in staticwebappconfig.json. Atleast with local development (swa start v0.8.0), it still does not work.

But this makes no sense, why do we have query parameters? For dynamic values! They are parameters... how do we specify that the dynamic value should be used within staticwebappconfig.json? For example something like this would be nice:

{
    "route": "/login",
    "redirect": "/.auth/login/aad?post_login_redirect_uri=/index.html?q1={q1}&q2={q2}"
},

No problem if the special characters need to be encoded, but this for clarity...

johnnyreilly commented 1 year ago

Heya,

I've just bumped on this too. I thought I'd share my use case. Consider the following URL:

https://my.app/my-screen?time=hours-4&selectedService=thing&selectedId=a52f89ae-c522-4b45-ba96-fa3a703a11c7

A support engineer is using my.app and finds something of interest, they send the above URL to their colleague. The colleague clicks on the link, expecting to see what their colleague sees. Instead, they are authenticated and then presented with an empty screen.

The configuration is as follows:

    "responseOverrides": {
        "401": {
            "redirect": "/.auth/login/aad?post_login_redirect_uri=.referrer",
            "statusCode": 302
        }
    },

Ideally, the .referrer would include the full querystring as well as the path. It doesn't at present which impacts our users.

related tangentially: https://github.com/Azure/static-web-apps/issues/785

johnnyreilly commented 1 year ago

@JonSilver , just reading up on your workaround. I'm curious as to what your staticwebappconfig.json looks like. Here's mine:

{
    "auth": {
        "identityProviders": {
            "azureActiveDirectory": {
                "registration": {
                    "openIdIssuer": "https://login.microsoftonline.com/6d6a11bc-469a-48df-a548-d3f353ac1be8/v2.0",
                    "clientIdSettingName": "AAD_CLIENT_ID",
                    "clientSecretSettingName": "AAD_CLIENT_SECRET"
                }
            }
        }
    },
    "navigationFallback": {
        "rewrite": "index.html"
    },
    "routes": [
        {
            "route": "/login",
            "rewrite": "/.auth/login/aad",
            "allowedRoles": ["anonymous", "authenticated"]
        },
        {
            "route": "/.auth/login/github",
            "statusCode": 404
        },
        {
            "route": "/.auth/login/twitter",
            "statusCode": 404
        },
        {
            "route": "/logout",
            "redirect": "/.auth/logout",
            "allowedRoles": ["anonymous", "authenticated"]
        },
        {
            "route": "/*",
            "allowedRoles": ["authenticated"]
        }
    ],
    "responseOverrides": {
        "401": {
            "redirect": "/.auth/login/aad?post_login_redirect_uri=.referrer",
            "statusCode": 302
        }
    },
    "globalHeaders": {
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Methods": "GET, POST, PUT, OPTIONS",
        "content-security-policy": "default-src https: 'unsafe-eval' 'unsafe-inline'; object-src 'none'"
    },
    "mimeTypes": {
        ".json": "text/json",
        ".md": "text/markdown",
        ".xml": "application/xml"
    }
}

We force authentication of all users with this:

        {
            "route": "/*",
            "allowedRoles": ["authenticated"]
        }

I'm guessing this wouldn't work with your approach as you'd not get the chance to save state to localstorage before the redirect. How do you handle this? Do you not use the above approach? Perhaps you allow unauthenticated access in the app and force authentication in the JavaScript? Or do you something else?

johnnyreilly commented 1 year ago

I've taken @JonSilver's approach and run with it. I've come up with a framework agnostic approach to do roughly what Jon suggested. I've blogged about it here and I've created a package to ease consumption called easyauth-deeplink

https://github.com/johnnyreilly/blog.johnnyreilly.com/pull/365

mishapos commented 1 year ago

svdoever johnnyreilly JonSilver We now have support for preserving the query string when using post_login_redirect_uri=.referrer like so:

  "responseOverrides": {
      "401": {
          "redirect": "/.auth/login/<aad or any other provider>?post_login_redirect_uri=.referrer",
          "statusCode": 302
      }
  },

Please let me know if this solves your scenario, and thank you for using Azure Static Web Apps!

johnnyreilly commented 1 year ago

Hi @mishapos - thanks I've used that. The problem is it doesn't preserve querystring / search parameters and so it doesn't solve the problem. It would be amazing if post_login_redirect_uri=.referrer did preserve those values. Is there any chance this behaviour could be added?

mishapos commented 1 year ago

Hi @johnnyreilly, sorry, I should have been clearer. What I am saying is that we have rolled out an update and the behavior you are describing should now be available. The query string should now be preserved after the login flow.

johnnyreilly commented 1 year ago

Oh wow! When did that get rolled out? I'd be happy to give it a go - my workaround seemed to be necessary up to two weeks ago based on systems that I work on. Do you need a particular ARM/bicep template version for it to work!

mishapos commented 1 year ago

It finished just this week. No ARM/bicep template needed. It should just work for existing apps.

johnnyreilly commented 1 year ago

Oh cool! I'll be off work until new year but I'll give it a try then. If it works I'll update my blog post to reflect!

johnnyreilly commented 1 year ago

Apologies for the delay in reporting back @mishapos - it doesn't look like the .referrer functionality works. Let me share my findings:

When the following staticwebapp.config.json is in play:

{
  "auth": {
    "identityProviders": {
      "azureActiveDirectory": {
        "registration": {
          "openIdIssuer": "https://login.microsoftonline.com/6d6a11bc-469a-48df-a548-d3f353ac1be8/v2.0",
          "clientIdSettingName": "AAD_CLIENT_ID",
          "clientSecretSettingName": "AAD_CLIENT_SECRET"
        }
      }
    }
  },
  "navigationFallback": {
    "rewrite": "index.html"
  },
  "routes": [
    {
      "route": "/login",
      "rewrite": "/.auth/login/aad?post_login_redirect_uri=.referrer",
      "allowedRoles": ["anonymous", "authenticated"]
    },
    {
      "route": "/.auth/login/github",
      "statusCode": 404
    },
    {
      "route": "/.auth/login/twitter",
      "statusCode": 404
    },
    {
      "route": "/logout",
      "redirect": "/.auth/logout",
      "allowedRoles": ["anonymous", "authenticated"]
    },
    {
      "route": "/*",
      "allowedRoles": ["authenticated"]
    }
  ],
  "responseOverrides": {
    "401": {
      "redirect": "/login",
      "statusCode": 302
    }
  },
  "globalHeaders": {
    "content-security-policy": "default-src https: 'unsafe-eval' 'unsafe-inline'; object-src 'none'"
  },
  "mimeTypes": {
    ".json": "text/json",
    ".md": "text/markdown",
    ".xml": "application/xml"
  }
}

The .referrer value didn't seem to trigger the desired behaviour:

image

Am I doing anything wrong here? Would be happy to jump on a call and demo this to you.

johnnyreilly commented 1 year ago

I've tried again with this staticwebapp.config.json:

{
  "auth": {
    "identityProviders": {
      "azureActiveDirectory": {
        "registration": {
          "openIdIssuer": "https://login.microsoftonline.com/6d6a11bc-469a-48df-a548-d3f353ac1be8/v2.0",
          "clientIdSettingName": "AAD_CLIENT_ID",
          "clientSecretSettingName": "AAD_CLIENT_SECRET"
        }
      }
    }
  },
  "navigationFallback": {
    "rewrite": "index.html"
  },
  "routes": [
    {
      "route": "/login",
      "rewrite": "/.auth/login/aad",
      "allowedRoles": ["anonymous", "authenticated"]
    },
    {
      "route": "/.auth/login/github",
      "statusCode": 404
    },
    {
      "route": "/.auth/login/twitter",
      "statusCode": 404
    },
    {
      "route": "/logout",
      "redirect": "/.auth/logout",
      "allowedRoles": ["anonymous", "authenticated"]
    },
    {
      "route": "/*",
      "allowedRoles": ["authenticated"]
    }
  ],
  "responseOverrides": {
    "401": {
      "redirect": "/.auth/login/aad?post_login_redirect_uri=.referrer",
      "statusCode": 302
    }
  },
  "globalHeaders": {
    "content-security-policy": "default-src https: 'unsafe-eval' 'unsafe-inline'; object-src 'none'"
  },
  "mimeTypes": {
    ".json": "text/json",
    ".md": "text/markdown",
    ".xml": "application/xml"
  }
}

And it seems to work! I guess the caveat is that it has to be the upfront redirect; rewrite won't handle this for you.

nicolasduerr commented 3 months ago

@johnnyreilly I came across this thread since I run in the very same issue. I have a deep link on a SharePoint page pointing to my static web app.

I have set the following part as well:

"responseOverrides": { "401": { "redirect": "/.auth/login/aad?post_login_redirect_uri=.referrer", "statusCode": 302 } }

But after authenticating I am never redirected to the deep link but to the main page instead.

Any more hints why this occurs?

seidnerj commented 1 month ago

Why doesn't .referrer get populated when redirect to urls that do not start with "./auth/login/"? it is so annoying, I want to redirect to a static page, get some user input then redirect a url to which I pass the referrer, but it just doesn't populated ".referrer" if the url is under "./auth/login"! The amount of time I spent getting Static Web App to work even marginally well is ridiculous!