diff --git a/src/Squidex/Config/Identity/GithubHandler.cs b/src/Squidex/Config/Identity/GithubHandler.cs new file mode 100644 index 000000000..509b38cab --- /dev/null +++ b/src/Squidex/Config/Identity/GithubHandler.cs @@ -0,0 +1,42 @@ +// ========================================================================== +// GitHubHandler.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.OAuth; +using Squidex.Core.Identity; + +namespace Squidex.Config.Identity +{ + public sealed class GitHubHandler : OAuthEvents + { + public override Task CreatingTicket(OAuthCreatingTicketContext context) + { + var userLogin = context.User.Value("login"); + var userName = context.User.Value("name"); + + if (!string.IsNullOrEmpty(userName)) + { + context.Identity.AddClaim(new Claim(SquidexClaimTypes.SquidexDisplayName, userName)); + } + else if (!string.IsNullOrWhiteSpace(userLogin)) + { + context.Identity.AddClaim(new Claim(SquidexClaimTypes.SquidexDisplayName, userName)); + } + + var pictureUrl = context.User.Value("avatar_url"); + + if (!string.IsNullOrEmpty(pictureUrl)) + { + context.Identity.AddClaim(new Claim(SquidexClaimTypes.SquidexPictureUrl, pictureUrl)); + } + + return base.CreatingTicket(context); + } + } +} diff --git a/src/Squidex/Config/Identity/GithubIdentityUsage.cs b/src/Squidex/Config/Identity/GithubIdentityUsage.cs new file mode 100644 index 000000000..e13098bb7 --- /dev/null +++ b/src/Squidex/Config/Identity/GithubIdentityUsage.cs @@ -0,0 +1,39 @@ +// ========================================================================== +// GithubIdentityUsage.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using AspNet.Security.OAuth.GitHub; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +// ReSharper disable InvertIf + +namespace Squidex.Config.Identity +{ + public static class GitHubIdentityUsage + { + public static IApplicationBuilder UseMyGithubAuthentication(this IApplicationBuilder app) + { + var options = app.ApplicationServices.GetService>().Value; + + if (options.IsGithubAuthConfigured()) + { + var githubOptions = + new GitHubAuthenticationOptions + { + ClientId = options.GithubClient, + ClientSecret = options.GithubSecret + }; + + app.UseGitHubAuthentication(githubOptions); + } + + return app; + } + } +} diff --git a/src/Squidex/Config/Identity/GoogleHandler.cs b/src/Squidex/Config/Identity/GoogleHandler.cs new file mode 100644 index 000000000..9f1516e09 --- /dev/null +++ b/src/Squidex/Config/Identity/GoogleHandler.cs @@ -0,0 +1,47 @@ +// ========================================================================== +// GoogleHandler.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.OAuth; +using Squidex.Core.Identity; +using Squidex.Infrastructure.Tasks; + +// ReSharper disable InvertIf + +namespace Squidex.Config.Identity +{ + public sealed class GoogleHandler : OAuthEvents + { + public override Task RedirectToAuthorizationEndpoint(OAuthRedirectToAuthorizationContext context) + { + context.Response.Redirect(context.RedirectUri + "&prompt=select_account"); + + return TaskHelper.Done; + } + + public override Task CreatingTicket(OAuthCreatingTicketContext context) + { + var displayNameClaim = context.Identity.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Name); + if (displayNameClaim != null) + { + context.Identity.AddClaim(new Claim(SquidexClaimTypes.SquidexDisplayName, displayNameClaim.Value)); + } + + var pictureUrl = context.User?.Value("picture"); + + if (!string.IsNullOrWhiteSpace(pictureUrl)) + { + context.Identity.AddClaim(new Claim(SquidexClaimTypes.SquidexPictureUrl, pictureUrl)); + } + + return base.CreatingTicket(context); + } + } +} diff --git a/src/Squidex/Config/Identity/GoogleIdentityUsage.cs b/src/Squidex/Config/Identity/GoogleIdentityUsage.cs new file mode 100644 index 000000000..8d668ccc7 --- /dev/null +++ b/src/Squidex/Config/Identity/GoogleIdentityUsage.cs @@ -0,0 +1,39 @@ +// ========================================================================== +// GoogleIdentityUsage.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +// ReSharper disable InvertIf + +namespace Squidex.Config.Identity +{ + public static class GoogleIdentityUsage + { + public static IApplicationBuilder UseMyGoogleAuthentication(this IApplicationBuilder app) + { + var options = app.ApplicationServices.GetService>().Value; + + if (options.IsGoogleAuthConfigured()) + { + var googleOptions = + new GoogleOptions + { + ClientId = options.GoogleClient, + ClientSecret = options.GoogleSecret, + Events = new GoogleHandler() + }; + + app.UseGoogleAuthentication(googleOptions); + } + + return app; + } + } +} diff --git a/src/Squidex/Config/Identity/IdentityServices.cs b/src/Squidex/Config/Identity/IdentityServices.cs index 6fca53cb3..1295ddde0 100644 --- a/src/Squidex/Config/Identity/IdentityServices.cs +++ b/src/Squidex/Config/Identity/IdentityServices.cs @@ -110,8 +110,7 @@ namespace Squidex.Config.Identity public static IServiceCollection AddMyIdentity(this IServiceCollection services) { - services.AddIdentity() - .AddDefaultTokenProviders(); + services.AddIdentity().AddDefaultTokenProviders(); return services; } diff --git a/src/Squidex/Config/Identity/IdentityUsage.cs b/src/Squidex/Config/Identity/IdentityUsage.cs index 1fef70487..0819f9355 100644 --- a/src/Squidex/Config/Identity/IdentityUsage.cs +++ b/src/Squidex/Config/Identity/IdentityUsage.cs @@ -6,20 +6,14 @@ // All rights reserved. // ========================================================================== -using System; using System.Linq; -using System.Net.Http; -using System.Security.Claims; using System.Threading.Tasks; -using Microsoft.AspNetCore.Authentication.OAuth; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.MongoDB; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using Newtonsoft.Json.Linq; using Squidex.Core.Identity; -using Squidex.Infrastructure.Tasks; // ReSharper disable InvertIf @@ -41,56 +35,53 @@ namespace Squidex.Config.Identity return app; } - public static IApplicationBuilder UseMyDefaultUser(this IApplicationBuilder app) + public static IApplicationBuilder UseAdminRole(this IApplicationBuilder app) { - var options = app.ApplicationServices.GetService>().Value; - - var username = options.DefaultUsername; - var userManager = app.ApplicationServices.GetService>(); - - if (!string.IsNullOrWhiteSpace(options.DefaultUsername) && - !string.IsNullOrWhiteSpace(options.DefaultPassword)) - { - Task.Run(async () => - { - if (userManager.SupportsQueryableUsers && !userManager.Users.Any()) - { - var user = new IdentityUser { UserName = username, Email = username, EmailConfirmed = true }; + var roleManager = app.ApplicationServices.GetRequiredService>(); - await userManager.CreateAsync(user, options.DefaultPassword); - } - }).Wait(); - } + roleManager.CreateAsync(new IdentityRole { Name = SquidexRoles.Administrator, NormalizedName = SquidexRoles.Administrator }).Wait(); return app; } - public static IApplicationBuilder UseMyGoogleAuthentication(this IApplicationBuilder app) + public static IApplicationBuilder UseMyAdmin(this IApplicationBuilder app) { var options = app.ApplicationServices.GetService>().Value; - if (!string.IsNullOrWhiteSpace(options.GoogleClient) && - !string.IsNullOrWhiteSpace(options.GoogleSecret)) + var userManager = app.ApplicationServices.GetService>(); + + if (options.IsAdminConfigured()) { - var googleOptions = - new GoogleOptions - { - Events = new GoogleHandler(), - ClientId = options.GoogleClient, - ClientSecret = options.GoogleSecret - }; + var adminEmail = options.AdminEmail; + var adminPass = options.AdminPassword; - app.UseGoogleAuthentication(googleOptions); - } + Task.Run(async () => + { + var user = await userManager.FindByEmailAsync(adminPass); - return app; - } + async Task userInitAsync(IdentityUser theUser) + { + await userManager.RemovePasswordAsync(theUser); + await userManager.ChangePasswordAsync(theUser, null, adminEmail); + await userManager.AddToRoleAsync(theUser, SquidexRoles.Administrator); + } - public static IApplicationBuilder UseAdminRole(this IApplicationBuilder app) - { - var roleManager = app.ApplicationServices.GetRequiredService>(); + if (user != null) + { + if (options.EnforceAdmin) + { + await userInitAsync(user); + } + } + else if ((userManager.SupportsQueryableUsers && !userManager.Users.Any()) || options.EnforceAdmin) + { + user = new IdentityUser { UserName = adminEmail, Email = adminEmail, EmailConfirmed = true }; - roleManager.CreateAsync(new IdentityRole { Name = SquidexRoles.Administrator, NormalizedName = SquidexRoles.Administrator }).Wait(); + await userManager.CreateAsync(user); + await userInitAsync(user); + } + }).Wait(); + } return app; } @@ -118,51 +109,5 @@ namespace Squidex.Config.Identity return app; } - - private class RetrieveClaimsHandler : OAuthEvents - { - public override Task CreatingTicket(OAuthCreatingTicketContext context) - { - var displayNameClaim = context.Identity.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Name); - if (displayNameClaim != null) - { - context.Identity.AddClaim(new Claim(SquidexClaimTypes.SquidexDisplayName, displayNameClaim.Value)); - } - - return base.CreatingTicket(context); - } - } - - private sealed class GoogleHandler : RetrieveClaimsHandler - { - private static readonly HttpClient HttpClient = new HttpClient(); - - public override Task RedirectToAuthorizationEndpoint(OAuthRedirectToAuthorizationContext context) - { - context.Response.Redirect(context.RedirectUri + "&prompt=select_account"); - - return TaskHelper.Done; - } - - public override async Task CreatingTicket(OAuthCreatingTicketContext context) - { - if (!string.IsNullOrWhiteSpace(context.AccessToken)) - { - var apiRequestUri = new Uri($"https://www.googleapis.com/oauth2/v2/userinfo?access_token={context.AccessToken}"); - - var jsonReponseString = await HttpClient.GetStringAsync(apiRequestUri); - var jsonResponse = JToken.Parse(jsonReponseString); - - var pictureUrl = jsonResponse["picture"]?.Value(); - - if (!string.IsNullOrWhiteSpace(pictureUrl)) - { - context.Identity.AddClaim(new Claim(SquidexClaimTypes.SquidexPictureUrl, pictureUrl)); - } - } - - await base.CreatingTicket(context); - } - } } } diff --git a/src/Squidex/Config/Identity/MyIdentityOptions.cs b/src/Squidex/Config/Identity/MyIdentityOptions.cs index 88ee5aa31..9268e9ddf 100644 --- a/src/Squidex/Config/Identity/MyIdentityOptions.cs +++ b/src/Squidex/Config/Identity/MyIdentityOptions.cs @@ -10,16 +10,37 @@ namespace Squidex.Config.Identity { public sealed class MyIdentityOptions { - public string DefaultUsername { get; set; } + public string AdminEmail { get; set; } - public string DefaultPassword { get; set; } + public string AdminPassword { get; set; } public string GoogleClient { get; set; } public string GoogleSecret { get; set; } + public string GithubClient { get; set; } + + public string GithubSecret { get; set; } + + public bool EnforceAdmin { get; set; } + public bool RequiresHttps { get; set; } public bool LockAutomatically { get; set; } + + public bool IsAdminConfigured() + { + return !string.IsNullOrWhiteSpace(AdminEmail) && !string.IsNullOrWhiteSpace(AdminPassword); + } + + public bool IsGithubAuthConfigured() + { + return !string.IsNullOrWhiteSpace(GithubClient) && !string.IsNullOrWhiteSpace(GithubSecret); + } + + public bool IsGoogleAuthConfigured() + { + return !string.IsNullOrWhiteSpace(GoogleClient) && !string.IsNullOrWhiteSpace(GoogleSecret); + } } } diff --git a/src/Squidex/Controllers/UI/Account/AccountController.cs b/src/Squidex/Controllers/UI/Account/AccountController.cs index d53a4eca2..bbf97b9ba 100644 --- a/src/Squidex/Controllers/UI/Account/AccountController.cs +++ b/src/Squidex/Controllers/UI/Account/AccountController.cs @@ -169,16 +169,29 @@ namespace Squidex.Controllers.UI.Account if (!isLoggedIn) { - var user = CreateUser(externalLogin); + var email = externalLogin.Principal.FindFirst(ClaimTypes.Email).Value; - var isFirst = userManager.Users.LongCount() == 0; + var user = await userManager.FindByEmailAsync(email); - isLoggedIn = - await AddUserAsync(user) && - await AddLoginAsync(user, externalLogin) && - await MakeAdminAsync(user, isFirst) && - await LockAsync(user, isFirst) && - await LoginAsync(externalLogin); + if (user != null) + { + isLoggedIn = + await AddLoginAsync(user, externalLogin) && + await LoginAsync(externalLogin); + } + else + { + user = CreateUser(externalLogin, email); + + var isFirst = userManager.Users.LongCount() == 0; + + isLoggedIn = + await AddUserAsync(user) && + await AddLoginAsync(user, externalLogin) && + await MakeAdminAsync(user, isFirst) && + await LockAsync(user, isFirst) && + await LoginAsync(externalLogin); + } } if (!isLoggedIn) @@ -232,11 +245,9 @@ namespace Squidex.Controllers.UI.Account return MakeIdentityOperation(() => userManager.AddToRoleAsync(user, SquidexRoles.Administrator)); } - private static IdentityUser CreateUser(ExternalLoginInfo externalLogin) + private static IdentityUser CreateUser(ExternalLoginInfo externalLogin, string email) { - var mail = externalLogin.Principal.FindFirst(ClaimTypes.Email).Value; - - var user = new IdentityUser { Email = mail, UserName = mail }; + var user = new IdentityUser { Email = email, UserName = email }; foreach (var squidexClaim in externalLogin.Principal.Claims.Where(c => c.Type.StartsWith(SquidexClaimTypes.Prefix))) { diff --git a/src/Squidex/Squidex.csproj b/src/Squidex/Squidex.csproj index 8d5e7bd00..25aa87a68 100644 --- a/src/Squidex/Squidex.csproj +++ b/src/Squidex/Squidex.csproj @@ -38,6 +38,7 @@ + diff --git a/src/Squidex/Startup.cs b/src/Squidex/Startup.cs index 1d2bcac16..cd072adf6 100644 --- a/src/Squidex/Startup.cs +++ b/src/Squidex/Startup.cs @@ -143,6 +143,7 @@ namespace Squidex identityApp.UseAdminRole(); identityApp.UseMyApiProtection(); identityApp.UseMyGoogleAuthentication(); + identityApp.UseMyGithubAuthentication(); identityApp.UseStaticFiles(); identityApp.MapWhen(x => IsIdentityRequest(x), mvcApp => diff --git a/src/Squidex/Views/Account/Login.cshtml b/src/Squidex/Views/Account/Login.cshtml index abf2c3d88..2e5be9b77 100644 --- a/src/Squidex/Views/Account/Login.cshtml +++ b/src/Squidex/Views/Account/Login.cshtml @@ -12,12 +12,17 @@

@foreach (var provider in Model.ExternalProviders) { - + }

\ No newline at end of file diff --git a/src/Squidex/app/theme/_static.scss b/src/Squidex/app/theme/_static.scss index 9bd560085..dcfaae418 100644 --- a/src/Squidex/app/theme/_static.scss +++ b/src/Squidex/app/theme/_static.scss @@ -27,8 +27,4 @@ noscript { font-size: 30px; font-weight: lighter; margin-bottom: 20px; -} - -.redirect-button { - display: none; } \ No newline at end of file diff --git a/src/Squidex/appsettings.json b/src/Squidex/appsettings.json index ce1cdc1fd..20619b214 100644 --- a/src/Squidex/appsettings.json +++ b/src/Squidex/appsettings.json @@ -48,6 +48,8 @@ "identity": { "googleClient": "1006817248705-t3lb3ge808m9am4t7upqth79hulk456l.apps.googleusercontent.com", "googleSecret": "QsEi-fHqkGw2_PjJmtNHf2wg", + "githubClient": "211ea00e726baf754c78", + "githubSecret": "d0a0d0fe2c26469ae20987ac265b3a339fd73132", "lockAutomatically": true, "keysStore": { "type": "InMemory", @@ -57,6 +59,6 @@ "folder": { "path": "keys" } - } + } } } \ No newline at end of file