ASP.Net Core 3.0 authenticate using Github OAuth

Go to the settings page and create a new OAuth app, https://github.com/settings/developers

Enter a name of the application, a homepage url and a callback url. The home page url is not used to anything more than information about the app. The callback url is the most important and I will get back to this one.

When the application is created, all needed to authenticate using Github using the OAuth flow is here. The ClientId and ClientSecret should be used in the app to authenticate and they should also be kept secret. Now it is time to integrate Github's oauth flow to authenticate.

$ dotnet --list-sdks
1.1.13 [C:\Program Files\dotnet\sdk]
1.1.14 [C:\Program Files\dotnet\sdk]
2.1.202 [C:\Program Files\dotnet\sdk]
2.1.302 [C:\Program Files\dotnet\sdk]
2.1.500 [C:\Program Files\dotnet\sdk]
2.1.503 [C:\Program Files\dotnet\sdk]
2.1.505 [C:\Program Files\dotnet\sdk]
2.1.600-preview-009426 [C:\Program Files\dotnet\sdk]
2.1.604 [C:\Program Files\dotnet\sdk]
2.1.700 [C:\Program Files\dotnet\sdk]
2.2.104 [C:\Program Files\dotnet\sdk]
3.0.100 [C:\Program Files\dotnet\sdk]

First the .Net Core 3 SDK need to be downloaded and installed from dot.net. To check if tge SDK is installed, the command above list all available and installed SDK on the machine. The .Net Core 3.0 SDK is 3.0.100.

$ mkdir TestWebApp && cd TestWebApp
$ dotnet new webapp

When the SDK is in place, it is time to create a folder for the test application and create a new project from the webapp template. Then open the folder in the code editor of your choice. Open the project file and add the UserSecretsId property and then set the user secrets from the command line.

<PropertyGroup>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <UserSecretsId>YOUR-USERSECRET-ID-HERE</UserSecretsId>
  </PropertyGroup>
dotnet user-secrets set Github:ClientId <your clientid here>
dotnet user-secrets set Github:ClientSecret <your clientsecret here>
$ dotnet user-secrets list -v

The user secrets is just a set of key valye pairs that is stored outside of the project itself. Running the last command above, with the -v argument will show the path to where the user secrets are located on disk.

  • On Windows user secrets are stores in this location C:\Users\%USERPROFILE%\AppData\Roaming\Microsoft\UserSecrets
{
	"login": "teilin",
	"id": 311637,
	"node_id": "MDQ6VXNlcjMxMTYzNw==",
	"avatar_url": "https://avatars1.githubusercontent.com/u/311637?v=4",
	"gravatar_id": "",
	"url": "https://api.github.com/users/teilin",
	"html_url": "https://github.com/teilin",
	"followers_url": "https://api.github.com/users/teilin/followers",
	"following_url": "https://api.github.com/users/teilin/following{/other_user}",
	"gists_url": "https://api.github.com/users/teilin/gists{/gist_id}",
	"starred_url": "https://api.github.com/users/teilin/starred{/owner}{/repo}",
	"subscriptions_url": "https://api.github.com/users/teilin/subscriptions",
	"organizations_url": "https://api.github.com/users/teilin/orgs",
	"repos_url": "https://api.github.com/users/teilin/repos",
	"events_url": "https://api.github.com/users/teilin/events{/privacy}",
	"received_events_url": "https://api.github.com/users/teilin/received_events",
	"type": "User",
	"site_admin": false,
	"name": "Teis Lindemark",
	"company": "DIPS Front, DIPS",
	"blog": "https://www.teilin.net",
	"location": "Bergen, Norway",
	"email": "teis.lindemark@gmail.com",
	"hireable": null,
	"bio": null,
	"public_repos": 55,
	"public_gists": 5,
	"followers": 12,
	"following": 33,
	"created_at": "2010-06-22T11:29:36Z",
	"updated_at": "2019-10-26T15:43:14Z"
}

The json above is a set of claims that I got from Github. I did not need all the information from my profile for the website I created. In the code below, I am mapping JsonKey. What is going on here is that the claims from in this case Github is mapped to a claim that is in the authentication cookie by the name that is given as the first argument.

.AddOAuth("github", "Github", options =>
{
    options.ClientId = Configuration["GitHub:ClientId"];
    options.ClientSecret = Configuration["GitHub: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";
    //options.ClaimsIssuer = "OAuth2-Github";
    options.SaveTokens = true;
    options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
    options.ClaimActions.MapJsonKey(ClaimTypes.Name, "name");
    options.ClaimActions.MapJsonKey("urn:github:login", "login");
    options.ClaimActions.MapJsonKey("urn:github:url", "html_url");
    options.ClaimActions.MapJsonKey("urn:github:avatar", "avatar_url");

    options.Events = new OAuthEvents
    {
        //OnRemoteFailure = HandleOnRemoteFailure,
        OnCreatingTicket = async context =>
        {
            var request = new HttpRequestMessage(HttpMethod.Get,          					context.Options.UserInformationEndpoint);
            request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);

            var response = await context.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted);
            response.EnsureSuccessStatusCode();

            var user = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());

                        context.HttpContext.Response.Cookies.Append("token", context.AccessToken);

                        context.RunClaimActions(user.RootElement);
         }
     };
});

For my application, I am setting up a policy to authenticate my Razor Pages against. I am creating a policy "Admin" that is valid when the github authentication scheme is used and the claim urn:github:login is present. This claim is the Github username, so if this is not present, there is no way that you are logged in to Github either.

services.AddAuthorization(options =>
{
    options.AddPolicy("Admin", policyBuilder =>
    {
        policyBuilder.AddAuthenticationSchemes("github");
        policyBuilder.RequireClaim("urn:github:login",                     Configuration["Authorization:AdminUsers"].Split(','));
    });
});

I like to configure the authentication of my razor pages in the configuration of the website and not at each razor page. At one point, I will forget to add authentication if it is at each page. In the configuration below, I am authentication the /Admin folder with the Admin policy.

services.AddRazorPages()
    .AddRazorPagesOptions(options =>
    {
        options.Conventions.AuthorizeFolder("/Admin", "Admin");
        //options.Conventions.AuthorizePage("/Day", "FacebookUser");
        //options.Conventions.AuthorizePage("/ThankYou", "FacebookUser");
        options.Conventions.AllowAnonymousToPage("/Index");
    });

Happy coding!

Teis Lindemark

Read more posts by this author.