Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to handle redirects to auth provider from the backend in Fable Elmish SPA

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.

like image 346
Balinth Avatar asked Apr 12 '20 19:04

Balinth


1 Answers

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).

like image 180
Jordan Marr Avatar answered Oct 04 '22 07:10

Jordan Marr