Member Login Using GitHub in Umbraco 9

Umbraco 9.3 shipped with improved OAuth support, to enable member and backoffice user login using external providers. Let us see how to use your GitHub account to enable member login for your Umbraco 9 website.

The first thing we need is this Nuget package installed AspNet.Security.OAuth.GitHub

Next we need a custom named configuration of the MemberExternalLoginProviderOptions class. This class provides the options for configuring the external login provider. My class is called GitHubMemberExternalLoginProviderOptions and is as shown below. This is primarily for configuring Auto Linking. In the scenario where member login is using an external provider, the external login provider will be the source of truth and the login will be based on the Single Sign-On approach and a new member will be created or an existing member linked and updated based on the member data from the external login provider. You can read more about the feature in the official Umbraco docs.

public class GitHubMemberExternalLoginProviderOptions : IConfigureNamedOptions<MemberExternalLoginProviderOptions>
{
	public const string SchemeName = "GitHub";

	public void Configure(string name, MemberExternalLoginProviderOptions options)
	{
		if (name != Constants.Security.MemberExternalAuthenticationTypePrefix + SchemeName)
		{
			return;
		}

		Configure(options);
	}

	public void Configure(MemberExternalLoginProviderOptions options) =>
		options.AutoLinkOptions = new MemberExternalSignInAutoLinkOptions(
			
			// Must be true for auto-linking to be enabled
			autoLinkExternalAccount: true,

			// Optionally specify the default culture to create
			// the user as. If null it will use the default
			// culture defined in the web.config, or it can
			// be dynamically assigned in the OnAutoLinking
			// callback.
			defaultCulture: null,

			// Optionally specify the default "IsApprove" status. Must be true for auto-linking.
			defaultIsApproved: true,

			// Optionally specify the member type alias. Default is "Member"
			defaultMemberTypeAlias: "Member",

			// Optionally specify the member groups names to add the auto-linking user to.
			defaultMemberGroups: Array.Empty<string>()
		)
		{
			// Optional callback
			OnAutoLinking = (autoLinkUser, loginInfo) =>
			{
				// You can customize the member before it's linked.
				// i.e. Modify the member's groups based on the Claims returned
				// in the externalLogin info
			},
			OnExternalLogin = (user, loginInfo) =>
			{
				// You can customize the member before it's saved whenever they have
				// logged in with the external provider.
				// i.e. Sync the member's name based on the Claims returned
				// in the externalLogin info

				return true; //returns a boolean indicating if sign in should continue or not.
			}
		};
}

The next thing we need is a static extension class for the external login provider. 

public static class GitHubMemberAuthenticationExtensions
{
	public static IUmbracoBuilder AddGitHubMemberAuthentication(this IUmbracoBuilder builder)
	{
		builder.AddMemberExternalLogins(logins =>
		{
			logins.AddMemberLogin(
				memberAuthenticationBuilder =>
				{
					memberAuthenticationBuilder.AddGitHub(
						
						memberAuthenticationBuilder.SchemeForMembers(GitHubMemberExternalLoginProviderOptions.SchemeName),
						options =>
						{
							options.ClientId = "client-id-for-your-app";
							options.ClientSecret = "client-secret-for-the-app";
						});
				});
		});
		return builder;
	}
}

This method should be then called in the ConfigureServices method in Startup.cs

Note that there is also a client id and client secret which is the details for the GitHub app(OAuth app) I have created. GitHub apps can be created at the url Developer applications (github.com).

We now need some markup and a corresponding controller action.

 <a class="nav-link px-lg-3 py-3 py-lg-4" asp-controller="AccountSurface" asp-action="GithubLogin" asp-route-returnUrl="@Context.Request.Path">Login with GitHub</a>
public IActionResult GithubLogin(string returnUrl)
{
	return Challenge(
	  new AuthenticationProperties
	  {
		  RedirectUri = Url.Action(nameof(GithubLoginCallback)),
		  Items = { { "returnUrl", returnUrl } }
	  }, Constants.Security.MemberExternalAuthenticationTypePrefix + GitHubMemberExternalLoginProviderOptions.SchemeName);
}

We now need to create the members in the Umbraco back office and assign roles based on the claims once the login is successful. For this, we need another controller action. This is also the url called back by the Login action above.

[HttpGet]
public async Task<IActionResult> GithubLoginCallback()
{
	// load & validate the temporary cookie
	var result = await HttpContext.AuthenticateAsync("Identity.External");
	if (!result.Succeeded) throw new Exception("Missing external cookie");

	// auto-create account using email address
	var email = result.Principal.FindFirstValue(ClaimTypes.Email)
				?? result.Principal.FindFirstValue("email")
				?? throw new Exception("Missing email claim");

	var user = await _memberManager.FindByEmailAsync(email);
	if (user == null)
	{
		_memberService.CreateMemberWithIdentity(email, email, email, Constants.Security.DefaultMemberTypeAlias);

		user = await _memberManager.FindByNameAsync(email);
		await _memberManager.AddToRolesAsync(user, new[] { "User" });
	}

	// create the full membership session and cleanup the temporary cookie
	await HttpContext.SignOutAsync("Identity.External");
	await _memberSignInManager.SignInAsync(user, false);

	// basic open redirect defense
	var returnUrl = result.Properties?.Items["returnUrl"];
	if (returnUrl == null || !Url.IsLocalUrl(returnUrl)) returnUrl = "~/";

	return new RedirectResult(returnUrl);
}

Build and run the application, you should be able to login with your GitHub account.

A big #h5yr to Scott Brady for this wonderful article which helped me a lot when I was trying to get this working!