8

I have an AspNetCore backend api (in F# with Giraffe) that uses AzureAD authentication with Microsoft.AspNetCore.Authentication.AzureAD.UI, with stateful session store, and https only cookies.

The frontend is an Elmish SPA compiled to js with Fable.

If I just type into the url bar a protected endpoint of my backend, everything works correctly, if not already signed in, I get redirected to the login.microsoft endpoint, with the clientID and so on, where upon successful signin, the original request completes and I get the response of my protected endpoint.

But if I try to access the same endpoint from the SPA code, eg.: with fetch, or with Fable.Remoting, if not logged in, the backend still redirects but the redirected request to login.microsoft no longer works. With Fable.Remoting there is a CORS header, that the login endpoint refuses. If I send fetch with nocors, there is a 200 OK response from the login endpoint BUT no response body (eg no html code for the login page) and seemingly nothing happens.

I just have no idea how this should be handled on the SPA side, and could not really find anything about it. Why does the backend include a CORS header in the redirect if initiated from Fable.Remoting vs if initiated from the browser url bar? What is wrong with the fetch-ed response that there is no response body? I can write just js code into my client, but could not even figure out how would this be handled in a pure js SPA.

Also tried the whole thing in production, to remove the webpack devServer proxy from the equation, but everything stays the same.

Balinth
  • 548
  • 4
  • 10
  • Did you ever find a solution to this? I'm currently using a horrible json interop hack to set `window.location`. It causes some weird problems, which I also have to work around. – Rei Miyasaka Jul 17 '20 at 08:40

2 Answers2

1

First, create "signin" and "signout" routes in Giraffe:

        /// Signs a user in via Azure
        GET >=> routeCi "/signin" 
            >=> (fun (next: HttpFunc) (ctx: HttpContext) ->
                if ctx.User.Identity.IsAuthenticated
                then redirectTo false "/" next ctx 
                else challenge AzureADDefaults.AuthenticationScheme next ctx
            )

        /// Signs a user out of Azure
        GET >=> routeCi "/signout" 
            >=> signOut AzureADDefaults.AuthenticationScheme 
            >=> text "You are signed out."

Next, you need to configure the webpack "devServerProxy". This is how my current Fable app is configured:

    // When using webpack-dev-server, you may need to redirect some calls
    // to a external API server. See https://webpack.js.org/configuration/dev-server/#devserver-proxy
    devServerProxy: {
        // delegate the requests prefixed with /api/
        '/api/*': {
          target: "http://localhost:59641",
          changeOrigin: true
        },
        // delegate the requests prefixed with /signin/
        '/signin/*': {
          target: "http://localhost:59641",
          changeOrigin: true
        },
        // delegate the requests prefixed with /signout/
        '/signout/*': {
          target: "http://localhost:59641",
          changeOrigin: true
        }
    },

This will allow you to provide a sign-in link from your SPA:

a [Href "/signin"] [str "Sign in"]

Now when the user loads your app, you can immediately try to pull back some user info (this route should require authentication). If the request (or any other) fails with a 401, you can prompt the user to "Sign in" with your sign-in link.

Lastly, your Azure app registration for your dev environment should point back to the port of your Web Api (which it sounds like yours already does).

Jordan Marr
  • 107
  • 3
0

I had a lot of difficulty with that as well, and the way I handled is by separating the app into the SPA and web API segments. I made a video showing how that could be done. To summarize, you get the JWT token on the client using the MSAL library, and the server's only role is to validate it. I spent a few days trying to find a way to handle redirects, but couldn't find a way to stop the browser from rejecting them.

Marko Grdinić
  • 3,798
  • 3
  • 18
  • 21