SAFE-Stack / SAFE-template

dotnet CLI template for SAFE project
MIT License
282 stars 88 forks source link

CORS error when attempting Authorization on Azure deployed app #423

Closed EspenBrun closed 3 years ago

EspenBrun commented 3 years ago

Hello, I have tried for days to get Authentication to work in the SAFE-template, asking both on the F# slack and stack overflow, and following what I could find from guides. There is a lot of info on a blog post from compositional it about how to do it. It also states that there will be CORS issues for local development, but that the issue will disappear when the solution is deployed. I can not get this to work. I tried the approach in the blog post, and I have also tried to explicitly allow any origin. I am posting an issue here, since I feel my code should be working, and I wonder if it could be an issue with the SAFE-stack.

My Server.fs

module Server

open FSharp.Control.Tasks
open Fable.Remoting.Server
open Fable.Remoting.Giraffe
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Cors.Infrastructure
open Microsoft.Extensions.Configuration
open Microsoft.Extensions.DependencyInjection
open Microsoft.Identity.Web
open Saturn
open Giraffe
open Microsoft.AspNetCore.Http

open Shared

type Storage () =
    let todos = ResizeArray<_>()

    member __.GetMessageRequiringLoggedIn() = LoggedInMessage "Logged in message from server"

    member __.GetMessageRequiringAuthChallenge() = AuthChallengeMessage "Auth challenge message from server"

    member __.GetTodos () =
        List.ofSeq todos

    member __.AddTodo (todo: Todo) =
        if Todo.isValid todo.Description then
            todos.Add todo
            Ok ()
        else Error "Invalid todo"

let storage = Storage()

storage.AddTodo(Todo.create "Create new SAFE project") |> ignore

let todosApi ctx =
    { getTodos = fun () -> async { return storage.GetTodos() }
      addTodo =
        fun todo -> async {
            match storage.AddTodo todo with
            | Ok () -> return todo
            | Error e -> return failwith e
        } }

let messageRequiringLoggedInApi ctx =
    { getMessageRequiringLoggedIn = fun () -> async { return storage.GetMessageRequiringLoggedIn() } }

let messageRequiringAuthChallengeApi ctx =
    { getMessageRequiringAuthChallenge = fun () -> async { return storage.GetMessageRequiringAuthChallenge() } }

let configureApp (app : IApplicationBuilder) =
    app.UseAuthentication()

let configureServices (services : IServiceCollection) =
    let config = services.BuildServiceProvider().GetService<IConfiguration>()

    services
        .AddMicrosoftIdentityWebAppAuthentication(config, openIdConnectScheme = "AzureAD")
        |> ignore

    services

let buildRemotingApi api next ctx = task {
    let handler =
        Remoting.createApi()
        |> Remoting.withRouteBuilder Route.builder
        |> Remoting.fromValue (api ctx)
        |> Remoting.buildHttpHandler
    return! handler next ctx }

let authScheme = "AzureAD"

let httpContextFuncFunc = requiresAuthentication (RequestErrors.UNAUTHORIZED authScheme "My Application" "You must be logged in.")

let requireLoggedIn : HttpFunc -> HttpContext -> HttpFuncResult =
    httpContextFuncFunc

let authChallenge : HttpFunc -> HttpContext -> HttpFuncResult =
    requiresAuthentication (Auth.challenge authScheme)

let routes =
    choose [
        authChallenge >=> buildRemotingApi messageRequiringAuthChallengeApi 
        requireLoggedIn >=> buildRemotingApi messageRequiringLoggedInApi 
    ]

let configureCors (builder : CorsPolicyBuilder) =
    builder
        .AllowAnyOrigin()
        .AllowAnyMethod()
        .AllowAnyHeader()
    |> ignore

let app =
    application {
        url "http://0.0.0.0:8085"
        service_config configureServices
        app_config configureApp
        use_router routes
        memory_cache
        use_static "public"
        use_gzip
        use_cors "CORS policy" configureCors
    }

Application.run app

My Dockerfile for deployment

FROM mcr.microsoft.com/dotnet/core/sdk:3.1 as build

# Install node
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash
RUN apt-get update && apt-get install -y nodejs

WORKDIR /workspace
COPY . .
RUN dotnet tool restore

RUN dotnet fake build -t Bundle

FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-alpine
COPY --from=build /workspace/deploy /app
WORKDIR /app
EXPOSE 8085
ENTRYPOINT [ "dotnet", "Server.dll" ]
teknikal-wizard commented 3 years ago

Hey there :) I just replied on Twitter as well. The issue I mention in the blog isn't actually CORS related, it is SameSite cookie restrictions as explained here : https://blog.heroku.com/chrome-changes-samesite-cookie.

Maybe the issue is that you have enabled CORS in your app but not in Azure? Unless you need it for something, I wouldn't turn it on.

EspenBrun commented 3 years ago

Thanks for replying :D

I have tried disabling same-site cookie restrictions, but I still get the same error, which I forgot to include in my original post (with ids etc removed):

Access to XMLHttpRequest at 'https://login.microsoftonline.com/tenantId/oauth2/v2.0/authorize?client_id=clientId&redirect_uri=http%3A%2F%2Fsite.azurewebsites.net%2Fsignin-oidc&response_type=id_token&scope=openid%20profile&response_mode=form_post&nonce=637491545169072248.NWM1OWIwZmYtOGVjZS00YWU0LWE3OTItNmZjZTdjZDY2MGM2OWFmNTdiYTgtY2Q0NC00ZmVkLWE5YzEtZmU1MGI4NTg2NmMz&client_info=1&x-client-brkrver=IDWeb.1.5.1.0&state=CfDJ8EOo0UleOJVClxYTloqK1IF02qCg0RMI-Y5I9TE5doDyIcfYXGrOlvbyAZG0Q8GhFBM86hgb9DXo3pOtGPKabxTSELazCL8zLoSGbYFjLgRtSrmSU1rUIDse6dVuET6CNfSyi9w3eyUnUY4n5yV03qo6bLToPuT4KK9vRKGtpsiODEtl2IFgc7d8s44EAOIUA2zWBYIZ8ZQzPZcGomAz2rA1H2VtftrPSI4JCgrKkzj8ECG5-A0GaFfCEVdR1p5YljAfEDo_Wp_5tkgB2y5KaUrhiyNKWwTXTJbn6vC0O6_fnBQcukTI049an8Fid48C2MyoVzxvoDXSocB5f328u6-ofcKc788M4yfdqjjXoBs8TcHXfHjqnxPfRQPwV93BUElmaESckcd3rCXPzdKSctw&x-client-SKU=ID_NETSTANDARD2_0&x-client-ver=6.8.0.0' (redirected from 'https://site.azurewebsites.net/api/IMessageRequiringAuthChallengeApi/getMessageRequiringAuthChallenge') from origin 'https://site.azurewebsites.net' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

I do not get this error on Firefox if also using a plugin called "CORS everywhere", so it does seem to be a cors issue?

Did you not have the same problems when you created the blog post, when deploying to azure? I feel like I have done exactly the same, except checking for development environment, but that part should not matter for the deployed solution

teknikal-wizard commented 3 years ago

Do you see that error even if you remove the configureCORS bit of your setup?

Also, I would add

        .UseHsts() // See https://docs.microsoft.com/en-us/aspnet/core/security/enforcing-ssl?view=aspnetcore-3.1&tabs=visual-studio
        .UseHttpsRedirection() // As above

to your app builder as well.

Did you not have the same problems when you created the blog post,

Nope, I'm using it in a new SAFE app right now and it is working as expected. Sure we can get you up and running.

teknikal-wizard commented 3 years ago

Another thought - have you added all the required config to your appSettings? https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-web-app-sign-user-app-configuration?tabs=aspnetcore#configuration-files

EspenBrun commented 3 years ago

Nope, I'm using it in a new SAFE app right now and it is working as expected. Sure we can get you up and running.

Awesome, that makes me hopeful as well.

I tried removing the configureCors part, and deployed. Still cors error

I then changed my app configuration to include hsts and https redirection,

let configureApp (app : IApplicationBuilder) =
    app.UseAuthentication()
        .UseHsts()
        .UseHttpsRedirection()

Still got the cors error after deployment.

My appconfig.json:

{
    "AzureAd": {
        "Instance": "https://login.microsoftonline.com/",
        "TenantId": "myTenantId",
        "ClientId": "myClientId",
        "CallbackPath": "/signin-oidc",
        "SignedOutCallbackPath": "/signout-oidc"
    }
}

I think I did that part right, although, even with UseHsts() and UseHttpsRedirection() I can see that the request has http in the redirect url ...&redirect_uri=http.... Not sure if that is related to the cors issue, but thought I should mention it.

EspenBrun commented 3 years ago

I have recreated the issue with a new safe, and followed the guide once more. I have deployed it to https://safe-azure-ad.azurewebsites.net/ and registered the app service for authentication. The whole repository can be found here, https://github.com/EspenBrun/safe-azure-ad. It has only what I believe is minimally necessary to reproduce it, where I have commented out code from the guide that was special for the development environment.

teknikal-wizard commented 3 years ago

I'll have a play with this and get back to you, probably on Friday :)

teknikal-wizard commented 3 years ago

One thing I did just notice - you have your

"CallbackPath": "/signin-oidc",
"SignedOutCallbackPath": "/signout-oidc"

settings set in the standard way. Mine are slightly different as I changed them a bit to make them more readable (to me).

That doesn't really matter anyway, the important bit is how you have the app registered with Active Directory in the portal.

With my app settings like this:

    "CallbackPath": "/api/login-callback",
    "SignedOutCallbackPath": "/api/logout-callback"

my AD app reg looks like this:

image

Note the port number, as the SAFE app runs on 8085.

If your settings don't line up like that, try tweaking them and let me know how you get on :)

EspenBrun commented 3 years ago

I agree, that is more readable. I have changed my appsettings.json and the redirect URIs on azure to match.

The log in itself seems to work: If I click on the link in the web console that is blocked by CORS, I am taken to the azureAD login page, and if I log in, the login-callback is called and the response is the getTodos json.

EspenBrun commented 3 years ago

Do I need a properties\launchSettings.json? As far as I could understand, that is for local development, and would not make a difference when deployed.

teknikal-wizard commented 3 years ago

Ok I have sussed it, I compared your repro to my app and found a couple of differences.

  1. I didn't use authChallenge for my api, I used requireLoggedIn.

  2. I used authChallenge to restrict access to the app base URL.

  3. This required a few tweaks to the SAFE template's out-of-the-box webpack and FAKE configs which I had thought was too much to add to my already-huge blog.

  4. If I enable authChallenge on the api, I get the same CORS issue as you (because of how Fable Remoting works under the hood).

As a result, I will

  1. Update the blog to only show requireLoggedIn.

  2. Write a 'part two' where I show how to restrict access to the app URL and allow login with authChallenge, along with the webpack and FAKE tweaks.

I also found that you need to use the Giraffe subroute command to get requireLoggedIn working for the remoting api like so -

let routes =
    choose [
        subRoute "/api" (requireLoggedIn >=> buildRemotingApi api)
    ]

Sorry for the confusion! I should have tested it more thoroughly when I wrote the blog.

EspenBrun commented 3 years ago

Thanks a lot for looking into this :D Looking forward to part 2!

teknikal-wizard commented 3 years ago

No worries, thanks for your help!

teknikal-wizard commented 3 years ago

Forked and fixed here, and PRd to your repo :) Blog coming soon, hopefully tomorrow. Cheers!

teknikal-wizard commented 3 years ago

Blog now up :) https://www.compositional-it.com/news-blog/safe-stack-authentication-with-active-directory-part-2/

EspenBrun commented 3 years ago

I have tried it out now, and it worked immediately locally. I had to change one line in webpack.config.json to make it work when deployed:

outputDir: isProduction ? './deploy/public' : './src/Server/public'

I have read the blog post as well, awesome stuff :D

Thanks a lot for you help, this whole issue would have been very hard for me to solve on my own.

teknikal-wizard commented 3 years ago

Thanks, I'll update the blog!