Skip to content

Commit

Permalink
Add GitHub auth (#3493)
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaelPetrinolis authored and sebastienros committed May 2, 2019
1 parent ac27de6 commit 7046bd7
Show file tree
Hide file tree
Showing 23 changed files with 668 additions and 1 deletion.
9 changes: 8 additions & 1 deletion OrchardCore.sln
Expand Up @@ -367,7 +367,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.ResponseCompres
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Google", "src\OrchardCore.Modules\OrchardCore.Google\OrchardCore.Google.csproj", "{E7CB5F02-2E90-4ADD-A851-1C49CDA184D6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrchardCore.HealthChecks", "src\OrchardCore.Modules\OrchardCore.HealthChecks\OrchardCore.HealthChecks.csproj", "{76023624-8D28-4666-9CA6-5978ACC2571D}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.HealthChecks", "src\OrchardCore.Modules\OrchardCore.HealthChecks\OrchardCore.HealthChecks.csproj", "{76023624-8D28-4666-9CA6-5978ACC2571D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Github", "src\OrchardCore.Modules\OrchardCore.Github\OrchardCore.Github.csproj", "{E8594DF8-5E41-4D24-B7DE-3AD0C1D035BA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -975,6 +977,10 @@ Global
{76023624-8D28-4666-9CA6-5978ACC2571D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{76023624-8D28-4666-9CA6-5978ACC2571D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{76023624-8D28-4666-9CA6-5978ACC2571D}.Release|Any CPU.Build.0 = Release|Any CPU
{E8594DF8-5E41-4D24-B7DE-3AD0C1D035BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E8594DF8-5E41-4D24-B7DE-3AD0C1D035BA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E8594DF8-5E41-4D24-B7DE-3AD0C1D035BA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E8594DF8-5E41-4D24-B7DE-3AD0C1D035BA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -1147,6 +1153,7 @@ Global
{C6EEBB52-183F-418F-A5C0-458D146C10A2} = {A066395F-6F73-45DC-B5A6-B4E306110DCE}
{E7CB5F02-2E90-4ADD-A851-1C49CDA184D6} = {A066395F-6F73-45DC-B5A6-B4E306110DCE}
{76023624-8D28-4666-9CA6-5978ACC2571D} = {A066395F-6F73-45DC-B5A6-B4E306110DCE}
{E8594DF8-5E41-4D24-B7DE-3AD0C1D035BA} = {A066395F-6F73-45DC-B5A6-B4E306110DCE}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {46A1D25A-78D1-4476-9CBF-25B75E296341}
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Expand Up @@ -88,4 +88,5 @@ nav:
- Microsoft: OrchardCore.Modules/OrchardCore.Microsoft.Authentication/README.md
- Facebook: OrchardCore.Modules/OrchardCore.Facebook/README.md
- Twitter: OrchardCore.Modules/OrchardCore.Twitter/README.md
- Github: OrchardCore.Modules/OrchardCore.Github/README.md
- Google: OrchardCore.Modules/OrchardCore.Google/README.md
41 changes: 41 additions & 0 deletions src/OrchardCore.Modules/OrchardCore.Github/AdminMenu.cs
@@ -0,0 +1,41 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Localization;
using OrchardCore.Environment.Shell.Descriptor.Models;
using OrchardCore.Modules;
using OrchardCore.Navigation;

namespace OrchardCore.Github
{
[Feature(GithubConstants.Features.GithubAuthentication)]
public class AdminMenuGithubLogin : INavigationProvider
{
private readonly ShellDescriptor _shellDescriptor;

public AdminMenuGithubLogin(
IStringLocalizer<AdminMenuGithubLogin> localizer,
ShellDescriptor shellDescriptor)
{
T = localizer;
_shellDescriptor = shellDescriptor;
}

public IStringLocalizer T { get; set; }

public Task BuildNavigationAsync(string name, NavigationBuilder builder)
{
if (String.Equals(name, "admin", StringComparison.OrdinalIgnoreCase))
{
builder.Add(T["Github"], "15", settings => settings
.AddClass("github").Id("github")
.Add(T["Github Authentication"], "10", client => client
.Action("Index", "Admin", new { area = "OrchardCore.Settings", groupId = GithubConstants.Features.GithubAuthentication })
.Permission(Permissions.ManageGithubAuthentication)
.LocalNav())
);
}
return Task.CompletedTask;
}
}
}
@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace OrchardCore.Github.Configuration
{
public class GithubDefaults
{
public const string AuthenticationScheme = "Github";
public static readonly string DisplayName = "Github";
public static readonly string AuthorizationEndpoint = "https://github.com/login/oauth/authorize";
public static readonly string TokenEndpoint = "https://github.com/login/oauth/access_token";
public static readonly string UserInformationEndpoint = "https://api.github.com/user";
}
}
@@ -0,0 +1,41 @@
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace OrchardCore.Github.Configuration
{
public class GithubHandler : OAuthHandler<GithubOptions>
{
public GithubHandler(IOptionsMonitor<GithubOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{ }

protected override async Task<AuthenticationTicket> CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens)
{
var request = new HttpRequestMessage(HttpMethod.Get, Options.UserInformationEndpoint);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);

var response = await Backchannel.SendAsync(request, Context.RequestAborted);
if (!response.IsSuccessStatusCode)
{
throw new HttpRequestException($"An error occurred when retrieving Github user information ({response.StatusCode}). Please check if the authentication information is correct in the corresponding Github Application.");
}

var payload = JObject.Parse(await response.Content.ReadAsStringAsync());

var context = new OAuthCreatingTicketContext(new ClaimsPrincipal(identity), properties, Context, Scheme, Options, Backchannel, tokens, payload);
context.RunClaimActions();

await Events.CreatingTicket(context);
return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name);
}
}
}
@@ -0,0 +1,30 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.Http;

namespace OrchardCore.Github.Configuration
{
/// <summary>
/// Configuration options for <see cref="MicrosoftAccountHandler"/>.
/// </summary>
public class GithubOptions : OAuthOptions
{
/// <summary>
/// Initializes a new <see cref="MicrosoftAccountOptions"/>.
/// </summary>
public GithubOptions()
{
CallbackPath = new PathString("/signin-github");
AuthorizationEndpoint = GithubDefaults.AuthorizationEndpoint;
TokenEndpoint = GithubDefaults.TokenEndpoint;
UserInformationEndpoint = GithubDefaults.UserInformationEndpoint;


ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
ClaimActions.MapJsonKey("name", "login");
ClaimActions.MapJsonKey(ClaimTypes.Email, "email", ClaimValueTypes.Email);
ClaimActions.MapJsonKey("url", "url");
}
}
}
@@ -0,0 +1,94 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OrchardCore.Modules;
using OrchardCore.Github.Services;
using OrchardCore.Github.Settings;

namespace OrchardCore.Github.Configuration
{
public class GithubOptionsConfiguration :
IConfigureOptions<AuthenticationOptions>,
IConfigureNamedOptions<GithubOptions>
{
private readonly IGithubAuthenticationService _githubAuthenticationService;
private readonly IDataProtectionProvider _dataProtectionProvider;
private readonly ILogger<GithubOptionsConfiguration> _logger;

public GithubOptionsConfiguration(
IGithubAuthenticationService githubAuthenticationService,
IDataProtectionProvider dataProtectionProvider,
ILogger<GithubOptionsConfiguration> logger)
{
_githubAuthenticationService = githubAuthenticationService;
_dataProtectionProvider = dataProtectionProvider;
_logger = logger;
}

public void Configure(AuthenticationOptions options)
{
var settings = GetGithubAuthenticationSettingsAsync().GetAwaiter().GetResult();
if (settings == null)
{
return;
}

if (_githubAuthenticationService.ValidateSettings(settings).Any())
return;

// Register the OpenID Connect client handler in the authentication handlers collection.
options.AddScheme(GithubDefaults.AuthenticationScheme, builder =>
{
builder.DisplayName = "Github";
builder.HandlerType = typeof(GithubHandler);
});
}

public void Configure(string name, GithubOptions options)
{
// Ignore OpenID Connect client handler instances that don't correspond to the instance managed by the OpenID module.
if (!string.Equals(name, GithubDefaults.AuthenticationScheme, StringComparison.Ordinal))
{
return;
}

var loginSettings = GetGithubAuthenticationSettingsAsync().GetAwaiter().GetResult();

options.ClientId = loginSettings?.ClientID ?? string.Empty;

try
{
options.ClientSecret = _dataProtectionProvider.CreateProtector(GithubConstants.Features.GithubAuthentication).Unprotect(loginSettings.ClientSecret);
}
catch
{
_logger.LogError("The Microsoft Account secret key could not be decrypted. It may have been encrypted using a different key.");
}

if (loginSettings.CallbackPath.HasValue)
{
options.CallbackPath = loginSettings.CallbackPath;
}
}

public void Configure(GithubOptions options) => Debug.Fail("This infrastructure method shouldn't be called.");

private async Task<GithubAuthenticationSettings> GetGithubAuthenticationSettingsAsync()
{
var settings = await _githubAuthenticationService.GetSettingsAsync();
if ((_githubAuthenticationService.ValidateSettings(settings)).Any(result => result != ValidationResult.Success))
{
_logger.LogWarning("The Microsoft Account Authentication is not correctly configured.");

return null;
}
return settings;
}
}
}
@@ -0,0 +1,90 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
using OrchardCore.DisplayManagement.Entities;
using OrchardCore.DisplayManagement.Handlers;
using OrchardCore.DisplayManagement.Views;
using OrchardCore.Environment.Shell;
using OrchardCore.Github.Settings;
using OrchardCore.Github.ViewModels;
using OrchardCore.Settings;

namespace OrchardCore.Github.Drivers
{
public class GithubAuthenticationSettingsDisplayDriver : SectionDisplayDriver<ISite, GithubAuthenticationSettings>
{
private readonly IAuthorizationService _authorizationService;
private readonly IDataProtectionProvider _dataProtectionProvider;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IShellHost _shellHost;
private readonly ShellSettings _shellSettings;

public GithubAuthenticationSettingsDisplayDriver(
IAuthorizationService authorizationService,
IDataProtectionProvider dataProtectionProvider,
IHttpContextAccessor httpContextAccessor,
IShellHost shellHost,
ShellSettings shellSettings)
{
_authorizationService = authorizationService;
_dataProtectionProvider = dataProtectionProvider;
_httpContextAccessor = httpContextAccessor;
_shellHost = shellHost;
_shellSettings = shellSettings;
}

public override async Task<IDisplayResult> EditAsync(GithubAuthenticationSettings settings, BuildEditorContext context)
{
var user = _httpContextAccessor.HttpContext?.User;
if (user == null || !await _authorizationService.AuthorizeAsync(user, Permissions.ManageGithubAuthentication))
{
return null;
}

return Initialize<GithubAuthenticationSettingsViewModel>("GithubAuthenticationSettings_Edit", model =>
{
model.ClientID = settings.ClientID;
if (!string.IsNullOrWhiteSpace(settings.ClientSecret))
{
var protector = _dataProtectionProvider.CreateProtector(GithubConstants.Features.GithubAuthentication);
model.ClientSecret = protector.Unprotect(settings.ClientSecret);
}
else
{
model.ClientSecret = string.Empty;
}
if (settings.CallbackPath.HasValue)
{
model.CallbackUrl = settings.CallbackPath;
}
}).Location("Content:5").OnGroup(GithubConstants.Features.GithubAuthentication);
}

public override async Task<IDisplayResult> UpdateAsync(GithubAuthenticationSettings settings, BuildEditorContext context)
{
if (context.GroupId == GithubConstants.Features.GithubAuthentication)
{
var user = _httpContextAccessor.HttpContext?.User;
if (user == null || !await _authorizationService.AuthorizeAsync(user, Permissions.ManageGithubAuthentication))
{
return null;
}

var model = new GithubAuthenticationSettingsViewModel();
await context.Updater.TryUpdateModelAsync(model, Prefix);

if (context.Updater.ModelState.IsValid)
{
var protector = _dataProtectionProvider.CreateProtector(GithubConstants.Features.GithubAuthentication);

settings.ClientID = model.ClientID;
settings.ClientSecret = protector.Protect(model.ClientSecret);
settings.CallbackPath = model.CallbackUrl;
await _shellHost.ReloadShellContextAsync(_shellSettings);
}
}
return await EditAsync(settings, context);
}
}
}
10 changes: 10 additions & 0 deletions src/OrchardCore.Modules/OrchardCore.Github/GithubConstants.cs
@@ -0,0 +1,10 @@
namespace OrchardCore.Github
{
public static class GithubConstants
{
public static class Features
{
public const string GithubAuthentication = "OrchardCore.Github.Authentication";
}
}
}
17 changes: 17 additions & 0 deletions src/OrchardCore.Modules/OrchardCore.Github/Manifest.cs
@@ -0,0 +1,17 @@
using OrchardCore.Modules.Manifest;
using OrchardCore.Github;

[assembly: Module(
Name = "Github",
Author = "The Orchard Team",
Website = "https://orchardproject.net",
Version = "2.0.0",
Category = "Github"
)]

[assembly: Feature(
Id = GithubConstants.Features.GithubAuthentication,
Name = "Github Authentication",
Category = "Github",
Description = "Authenticates users with their Github Account."
)]

0 comments on commit 7046bd7

Please sign in to comment.