Authenticate with an OAuth provider in your Giraffe application (.Net Core 2.1)

Introduction (rambling)

When I started my F# learning journey some months ago, I planned to go full functional F#, to use what the community used. Suave, Code, FAKE, SQL Type Providers, Paket. But I found out that made it not easier and with the limited spare time to invest in F#, that plan has changed a bit. I realized that when I wrote my previous blog  about Using Entity Framework Core with F#. I’m now gonna use technologies closer to home and that’s more Microsoft stackish, so Entity Framework and now also Giraffe on Asp.net Core. I hope this will make learning all this easier and I hope I’ll pickup the other technologies later on my path. You can say these technologies will be my gateway drugs to F#, my.. my gateway stack!  I would love to introduce F# at places I work; these are mostly Microsoft shops and I think it will be easier like this to get clients interested to try all this F# goodness out.

Setup the Giraffe solution

You can do dotnet new giraffe to create a complete giraffe web sample project and that’s what I did to play around and find out stuff for this blog. When I wrote code sample for this blog I created everything from scratch because I want to learn & understand this stuff. So first we start by creating the project. The complete sample code for this blog is on GitHub.

To create the project, go to the command line and enter the following commands:

dotnet new web -lang F#
dotnet add package Giraffe --version 1.2.0-preview-1
dotnet add package AspNet.Security.OAuth.GitHub --version 2.0.0-rc2-final
dotnet restore

I removed Startup.fs and added some files to organize the code I mostly copied from the Giraffe sample template.

Setup Https on local machine

When you develop and test your application you’ll do this locally and because OAuth providers like Facebook requires https you also need to run your local application on https. To do this you can follow these next steps.

First you need a certificate. I followed these instructions: Configuring HTTPS in ASP.NET Core across different platforms. You can export the certificate as a pfx file and place this in your web root for example.

From that same link I ported and simplified the example code to configure Kestrel to use only https and assign the correct certificate. Warning! I only aimed at using this locally and tested accordingly! First the code load the certificate, you need to change the settings in the appsetting.json. If you omit the file path and the password settings, the function will try to retrieve the certificate from the store.

let loadCertificate (configuration: IConfiguration)  = 
    let filepath = configuration.GetValue<string> "CertificateFilename"
    let password = configuration.GetValue<string> "CertificatePassword"
    let storename = configuration.GetValue<string> "CertificateStoreName"
    let storelocation = configuration.GetValue<StoreLocation> "CertificateStoreLocation"

    let loadFromStore =
        (
            use store = new X509Store(storename, storelocation)
            store.Open(OpenFlags.ReadOnly)
            let certificate = store.Certificates.Find(X509FindType.FindBySubjectName, "localhost", false) // TODO
            certificate.[0]
        )

    let loadFromFile = 
        new X509Certificate2(filepath, password)

    match String.IsNullOrWhiteSpace filepath, String.IsNullOrWhiteSpace password with
    | false, false -> loadFromFile
    | _ -> loadFromStore

The following code configures Kestrel to use this certificate on port 443.

let configureKestrel (options : KestrelServerOptions) = 
    let configuration = options.ApplicationServices.GetRequiredService<IConfiguration>()
    let certificate = loadCertificate configuration

    options.Listen(IPAddress.Loopback, 443, 
        fun (listenOptions) -> listenOptions.UseHttps(certificate) |> ignore
    )

Then you point the WebHostBuilder.UseKestrel method to the configureKestrel function.

WebHostBuilder()
    .UseKestrel(configureKestrel)
    .UseContentRoot(contentRoot)
    .UseIISIntegration()
    .UseWebRoot(webRoot)
    .Configure(Action<IApplicationBuilder> configureApp)
    .ConfigureServices(configureServices)
    .ConfigureLogging(configureLogging)
    .Build()
    .Run()

Configure OAuth Providers

Configure GitHub App

Well it took some time to get this working, even with this great blog Authenticate with OAuth 2.0 in ASP.NET Core 2.0. I ported this to the Giraffe example but got in a redirect loop. With some help from someone in the functional programming slack channel (thanks damukles!) that was fixed and as it turned out I needed only half of the code. A newer version of Core might have also come in play there, with the current version it is just super easy!

Make sure you configure the app to use Authentication, this is simply done by the UseAuthentication method on the ApplicationBuilder:

let configureApp (app : IApplicationBuilder) =
    let env = app.ApplicationServices.GetService<IHostingEnvironment>()
    (match env.IsDevelopment() with
    | true  -> app.UseDeveloperExceptionPage()
    | false -> app.UseGiraffeErrorHandler errorHandler)
        .UseCors(configureCors)
        .UseStaticFiles()
        .UseAuthentication()
        .UseGiraffe(webApp)

Create GitHub App

First you need to create an OAuth application in GitHub. Go to OAuth Apps and click New OAuth App.

When you click Register application the application is created and Client Id and Client Secret are generated. You need these later to in your Giraffe application. The Authorization callback URL is also something we’ll need to configure in the application.

Create Facebook App

If you want to use Facebook as provider you also have to make an app. You can then go to Settings > Basic and retrieve the App Id App secrets there.  You’ll also need to add a product  ‘Facebook login’ to this app. Set the Valid OAuth Redirect URIs here, by default this is /signin-facebook.

Configure GitHub & Facebook

As you can see this really easy. There some default settings which meaning I didn’t really dive into, most important settings for now are the client id and secret and make sure the CallbackPath is the same as the one you configured in the GitHub App. For Facebook just add the app id and secret.

let configureServices (services : IServiceCollection) =
    services.AddAuthentication(fun options ->
            options.DefaultAuthenticateScheme <- CookieAuthenticationDefaults.AuthenticationScheme
            options.DefaultSignInScheme <- CookieAuthenticationDefaults.AuthenticationScheme
            options.DefaultChallengeScheme <- "GitHub"
        )
        .AddCookie()
        .AddGitHub(fun options -> 
            options.ClientId <- "<Your ClientId>"
            options.ClientSecret <- "<Your ClientSecret>"
            options.CallbackPath <- new PathString("/signin-github") 

            options.AuthorizationEndpoint <- "https://github.com/login/oauth/authorize"
            options.TokenEndpoint <- "https://github.com/login/oauth/access_token"
            options.UserInformationEndpoint <- "https://api.github.com/user"  
        ) 
        .AddFacebook(fun options ->
            options.AppId <- "<Your AppId>"
            options.AppSecret <- "<Your AppSecret>"
        )
        |> ignore
    services.AddCors()    |> ignore
    services.AddGiraffe() |> ignore

Authenticate!

Now we gonna specify for which parts of the web application you should be authenticated, for this we use the mustBeLoggedIn HttpHandler. If not authenticated it will call the challenge function. This construction is something I still have to wrap my head around, I can use it but understanding it is something else…

let mustBeLoggedIn : HttpHandler =
    requiresAuthentication (challenge "GitHub") 

let webApp : App = 
    choose [
        GET >=> 
            choose [
                route "/" >=>  indexHandler
            ]
        mustBeLoggedIn >=>
            GET >=>
                choose [
                    route "/secured" >=> indexHandler
                ]    
        setStatusCode 404 >=> text "Not Found" ]

We can use the same handler for unauthenticated and authenticated and check if the user is authenticated by checking the IsAuthenticated property of the User in the HttpContext.

let indexHandler (next : HttpFunc) (ctx: HttpContext) =
    let nameClaim = ctx.User.FindFirst (fun c -> c.Type = ClaimTypes.Name)

    if ctx.User.Identity.IsAuthenticated then 
        let model     = { Text = nameClaim.Value }
        let view      = Views.index model    
        htmlView view next ctx
    else
        let view      = Views.loginView
        htmlView view next ctx

That’s it! Not too difficult I think. In next blogs that will appear sooner than this one and then I want to explore creating a user in a database based on the authenticated user. I also would like to know how to apply this to a more front-end talking to api back-end scenario. Again, the sample code for this blog is on GitHub.

Resources

1 thought on “Authenticate with an OAuth provider in your Giraffe application (.Net Core 2.1)

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.