Auth is the most critical part of the modern application stack. The abundance of data breaches signals that it is also challenging to get right. I find the idea of managing identities and secrets scary. I don’t want to be the cause of a worldwide data breach, and I doubt anybody does. Luckily for most developers, they can leverage the identity providers of popular web companies to bring ideas to market faster by focusing on the product’s core value rather than the plumbing of auth infrastructure.

This post will walk through the steps needed to configure an ASP.NET Core application to leverage the GitHub identity provider to add auth to a web application. As a bonus, you’ll see how to use your GitHub identity to make additional calls to the GitHub API.

OpenID All The Things

OAuth is the security mechanism for many web services, but it lacks the sufficient means to do both authentication and authorization. OpenID builds on OAuth to allow users to use existing accounts on their favorite websites to log into other websites without creating new logins. The OpenID technology is called OpenID Connect, which defines a specification for identity providers to follow. The specification allows interoperability between all vendors and clients in the space.

Since 2014, OpenID Connect has been widely adopted by most software companies as the approach to handle user identities, authentication, and authorization. The proliferation of OpenID Connect means it’s easier than ever to integrate the technology into any client.

If you’re interested in knowing more about OpenID Connect, I suggest visiting the IdentityServer documentation site. The folks behind the project are very knowledgeable and do an excellent job explaining the benefits of the technology.

ASP.NET Core with GitHub Auth

There will be a few steps in this process, but I hope they’ll be straightforward. I’ll also have a sample of the final solution available on GitHub at the end of this post.

Setting Application Secrets

The first step is to generate a clientId and clientSecret from your GitHub account. Next, visit your settings page and create a new OAuth App. Be sure to save these two values, as you’ll need to use them when creating the ASP.NET Core application.

Important: The values when setting up the OAuth app should be:

  • Homepage URL: https://localhost:5001/
  • Authorization callback URL: https://localhost:5001/signin-github

Let’s start by creating a brand new ASP.NET Core application. Now, create a new solution using the Web App template from your favorite IDE or a terminal. Once created, you’ll want to store the clientId and clientSecret in the project. Using the user secrets feature of .NET will ensure you don’t accidentally leak sensitive values. From a terminal window, navigate to your solution directory. Next, you’ll initialize your project to retrieve user secrets.

dotnet user-secrets init --project <projectname>

The command adds a UserSecretsId element to your project file. The key is used to retrieve your secrets at startup.

<PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
    <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
    <UserSecretsId>138d7a04-d730-411f-a665-9f44f863f688</UserSecretsId>
</PropertyGroup>

Be aware that these secrets will only be available during development. Accordingly, managing secrets in a production environment will depend on your setup, and the topic is beyond the scope of this post.

Now, let’s add your secrets to your user secrets store. You want to run the following commands from the same terminal used in the previous step, making sure to replace the <projectname> with the name of your project.

‣ dotnet user-secrets set github:clientId "<from github>" --project <projectname>
‣ dotnet user-secrets set github:clientSecret "<from github>" --project <projectname>
‣ dotnet user-secrets list
   github:clientSecret = <secret>
   github:clientId = <secret>      

Now you’re ready to start writing some code.

Configure GitHub Auth Provider

Before you start writing code, you’ll need to add a reference to a NuGet package to pull in GitHub auth handlers. While you’re here, also add the OctoKit package.

dotnet add package AspNet.AspNet.Security.OAuth.GitHub
dotnet add package OctoKit

You could also alter the project file.

<ItemGroup>
    <PackageReference Include="AspNet.Security.OAuth.GitHub" Version="5.0.8"/>
    <PackageReference Include="Octokit" Version="0.50.0"/>
</ItemGroup>

In your Startup file, you’ll want to add the Authentication middleware and set up our Cookie auth mechanism. While you’re using GitHub, you want to store our successful login information in a cookie for a set time. Writing to a cookie reduces the round trips to GitHub or any other identity provider.

In your ConfigureServices method in the Startup class, let’s add the necessary changes.

public void ConfigureServices(IServiceCollection services)
{
    // Step 1. Add Authentication Mechanisms
    services
        .AddAuthentication(o =>
        {
            o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        })
        .AddCookie(o =>
        {
            // set the path for the authentication challenge
            o.LoginPath = "/signin";
            // set the path for the sign out
            o.LogoutPath = "/signout";
        })
        .AddGitHub(o =>
        {
            o.ClientId = Configuration["github:clientId"];
            o.ClientSecret = Configuration["github:clientSecret"];
            o.CallbackPath = "/signin-github";
            
            // Grants access to read a user's profile data.
            // https://docs.github.com/en/developers/apps/building-oauth-apps/scopes-for-oauth-apps
            o.Scope.Add("read:user");

            // Optional
            // if you need an access token to call GitHub Apis
            o.Events.OnCreatingTicket += context =>
            {
                if (context.AccessToken is { })
                {
                    context.Identity?.AddClaim(new Claim("access_token", context.AccessToken));
                }
                
                return Task.CompletedTask;
            };
        });

    services.AddRazorPages();
}

Let’s walk through this code from top to bottom and explain what each step accomplishes.

  • The call to AddAuthentication adds the authentication services into the ASP.NET Core application service collection. The authentication services allows the request pipeline to challenge, sign in, and sign out users.
  • The call to AddCookie sets the LoginPath and LogoutPath for user actions.
  • The call to AddGitHub will allow you to set the clientId and clientSecret from the configuration object (remember the values come from user secrets). You also need to setup the callback path and the GitHub scope. A scope is a set of actions the app is allowed to perform on behalf of a user. Finally, when authenticating successfully, the code will store the access_token claim on the auth cookie for later use.

Moving to the Configure method in Startup, you need to add two middleware.

app.UseAuthentication();
app.UseAuthorization();

I usually place these after the call to routing and before endpoints, but you need to determine where it makes sense for your application.

Login Pages and Signout Endpoints

While you’re still in the Startup file, let’s handle the sign-out process. In the call to UseEndpoints you’ll set up a new endpoint for signout. I’ve included the entire call for UseEndpoints for clarity. This endpoint will handle any requests to sign out of the current session.

app.UseEndpoints(endpoints =>
{
    endpoints.MapRazorPages();
    
    // 3. Sign out Endpoint
    // go to ./Pages/Signin.cshtml for 4.
    endpoints.MapGet("/signout", async ctx =>
    {
        await ctx.SignOutAsync(
            CookieAuthenticationDefaults.AuthenticationScheme,
            new AuthenticationProperties
            {
                RedirectUri = "/"
            });
    });
});

Let’s move on to handling the logging-in process. First, in the web application, create a new Signin page under the Pages directory. Then, within the view, add the following HTML.

<div class="jumbotron">
    <h1>Authentication</h1>
    <p class="lead text-left">Sign in using one of these external providers:</p>

    @foreach (var scheme in Model.Schemes.OrderBy(p => p.DisplayName))
    {
        <form asp-page="SignIn" method="post">
            <input type="hidden" name="Provider" value="@scheme.Name" />
            <input type="hidden" name="ReturnUrl" value="@Model.ReturnUrl" />

            <button class="btn btn-lg btn-success m-1" type="submit">
                Sign in using @scheme.DisplayName
            </button>
        </form>
    }
</div>

The HTML will scan your application’s authentication providers and generate log-in buttons for your users to click. In the pages model, you’ll need to handle the form submission from the HTML page.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.DependencyInjection;

namespace SocialLogins.Pages
{
    public class SignIn : PageModel
    {
        public IEnumerable<AuthenticationScheme> Schemes { get; set; }
        
        [BindProperty(SupportsGet = true)]
        public string ReturnUrl { get; set; }

        public async Task OnGet()
        {
            Schemes = await GetExternalProvidersAsync(HttpContext);
        }

        public async Task<IActionResult> OnPost([FromForm] string provider)
        {
            if (string.IsNullOrWhiteSpace(provider))
            {
                return BadRequest();
            }

            return await IsProviderSupportedAsync(HttpContext, provider) is false
                ? BadRequest()
                : Challenge(new AuthenticationProperties
                {
                    RedirectUri = Url.IsLocalUrl(ReturnUrl) ? ReturnUrl : "/"
                }, provider);
        }

        private static async Task<AuthenticationScheme[]> GetExternalProvidersAsync(HttpContext context)
        {
            var schemes = context.RequestServices.GetRequiredService<IAuthenticationSchemeProvider>();
            return (await schemes.GetAllSchemesAsync())
                .Where(scheme => !string.IsNullOrEmpty(scheme.DisplayName))
                .ToArray();
        }

        private static async Task<bool> IsProviderSupportedAsync(HttpContext context, string provider) =>
            (await GetExternalProvidersAsync(context))
            .Any(scheme => string.Equals(scheme.Name, provider, StringComparison.OrdinalIgnoreCase));
    }
}

There you have it! Your application is now ready to log in your users. In the next section, you’ll see how to use the GitHub access token to make calls to the GitHub API.

Calling GitHub API using an AccessToken

In the previous section, you stored the GitHub access token as part of the identity claim. To retrieve the claim, you can use the User property found in the context of a Razor Page. Let’s add some helper extension methods before continuing.

using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;

namespace SocialLogins.Models
{
    public static class ClaimsExtensions
    {
        public static string? FirstClaim(this IEnumerable<Claim>? claims, string type)
        {
            return claims?
                .Where(c => c.Type == type)
                .Select(c => c.Value)
                .FirstOrDefault();
        }

        public static string? AccessToken(this ClaimsPrincipal principal) => 
            principal.Claims.FirstClaim("access_token");
    }
}

In the same project, create a new Razor page named Account. For this blog post, let’s look exclusively at the code behind the page.

[Authorize]
public class Account : PageModel
{
    public async Task OnGet()
    {
        // 6. We are reading claims that were
        //    supplied from our OpenID provider
        Claims = User.Claims.ToList();
        
        // 6. From GitHub
        if (User.AccessToken() is { } accessToken)
        {
            var client = new GitHubClient(new ProductHeaderValue("test")) {
                Credentials = new Credentials(accessToken)
            };
            GitHubUser = await client.User.Get(User.Identity?.Name);
        }
    }

    public User GitHubUser { get; set; }
    public List<Claim> Claims { get; set; }
}

Wow! It couldn’t be any easier. First, you pull the access token from the User property and then create a new GitHubClient, which calls the GitHub API. Next, let’s update the Razor page to display our results.

@page
@model SocialLogins.Pages.Account

@{
    var Uri = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/";
}

<h2>User Claims</h2>
<table class="table table-bordered table-striped">
    <thead>
    <tr>
        <th>Type</th>
        <th>Value</th>
    </tr>
    </thead>
    <tbody>
    @foreach (var claim in Model.Claims)
    {
        <tr>
            <td>@claim.Type.Replace(Uri, string.Empty)</td>
            <td>
                @if (claim.Type == "access_token")
                {
                    <div class="input-group mb-3">
                        <div class="input-group-prepend">
                            <span class="input-group-text" id="access-token">
                                &#128065;
                            </span>
                        </div>
                        <input id="access-token" 
                               type="password" value="@claim.Value" 
                               onclick="toggleVisibility(this)" 
                               class="form-control" 
                               title="click me to reveal password" />
                    </div>
                }
                else
                {
                    @claim.Value                    
                }
            </td>
        </tr>
    }
    </tbody>
</table>

@if (Model.GitHubUser is {})
{
    <h2>GitHub Octokit Response</h2>
    <table class="table table-bordered table-striped">
        <thead>
        <tr>
            <th>Type</th>
            <th>Value</th>
        </tr>
        </thead>
        <tbody>
        <tr>
            <td>@nameof(Model.GitHubUser.Bio)</td>
            <td>@Model.GitHubUser.Bio</td>
        </tr>
        <tr>
            <td>@nameof(Model.GitHubUser.Location)</td>
            <td>@Model.GitHubUser.Location</td>
        </tr>
        <tr>
            <td>@nameof(Model.GitHubUser.AvatarUrl)</td>
            <td>
                <img src="@Model.GitHubUser.AvatarUrl" alt="avatar"/>
            </td>
        </tr>
        </tbody>
    </table>
}

Now, starting up the application, you can navigate to /account and trigger the log-in process. The resulting page should look something like the following.

github auth with octokit call

There is nothing left for you to do now but code your application.

Conclusion

Setting up OpenID connect authentication in ASP.NET Core is relatively straightforward. Much of the consideration needs to be put into storing secrets, saving access tokens, and utilizing it all. In addition, there are 100s of OpenID providers associated with the AspNet.Security.OAuth library, which is fantastic for .NET developers. This post should help

To get a working sample of this post, head over to my GitHub repository and try it out for yourself. Remember to set up your OAuth application in GitHub first to get access to the necessary secrets.

As always, thanks for reading.