Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions DocIntel.AdminConsole/DocIntel.AdminConsole.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="HumanDateParser" Version="1.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.0" />
<PackageReference Include="Sharprompt" Version="2.4.5" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.0" />
</ItemGroup>
Expand Down
1 change: 1 addition & 0 deletions DocIntel.Core/DocIntel.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<PackageReference Include="JsonSchema.Net.Generation" Version="3.0.3" />
<PackageReference Include="MailKit" Version="3.4.2" />
<PackageReference Include="Markdig" Version="0.30.4" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="7.0.0" />

Expand Down
1 change: 1 addition & 0 deletions DocIntel.Tests/DocIntel.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.0" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
Expand Down
183 changes: 183 additions & 0 deletions DocIntel.WebApp/Controllers/AccountController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Claims;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading;
using System.Threading.Tasks;
using DocIntel.Core.Authentication;
using DocIntel.Core.Authorization;
Expand All @@ -39,6 +43,7 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;

using SixLabors.ImageSharp;
Expand Down Expand Up @@ -113,9 +118,12 @@ public async Task<IActionResult> Login(string returnUrl = null)
{
// Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync();
var externalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();

ViewData["ReturnUrl"] = returnUrl;
return View(new SigninViewModel
{
ExternalLogins = externalLogins,
RememberMe = true
});
}
Expand Down Expand Up @@ -149,6 +157,7 @@ public async Task<IActionResult> Login(SigninViewModel model,
LogEvent.Formatter);

ViewData["ReturnUrl"] = returnUrl;
model.ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();

if (ModelState.IsValid)
try
Expand Down Expand Up @@ -1201,5 +1210,179 @@ public async Task<IActionResult> VerifyAuthenticator(ConfigureTwoFactorAuthentic
ModelState.AddModelError(nameof(model.Code), "Your code appears to be invalid.");
return View(model);
}

[HttpPost]
[ValidateAntiForgeryToken]
[AllowAnonymous]
public async Task<IActionResult> LoginExternal(string provider, string returnUrl = null)
{
// Request a redirect to the external login provider.
var redirectUrl = Url.Action("ExternalLogin", "Account", new { ReturnUrl = returnUrl });
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
return new ChallengeResult(provider, properties);
}

[AllowAnonymous]
public async Task<IActionResult> ExternalLogin(string returnUrl = null, string remoteError = null)
{
returnUrl = returnUrl ?? Url.Content("~/");

if (remoteError != null)
{
TempData["ErrorMessage"] = $"Error from external provider: {remoteError}";
return RedirectToLocal(returnUrl);
}
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
TempData["ErrorMessage"] = "Error loading external login information.";
return RedirectToLocal(returnUrl);
}

// Sign in the user with this external login provider if the user already has a login.
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true);
if (result.Succeeded)
{
var username = info.Principal.Identity.Name;
if (info.Principal.HasClaim(c => c.Type == "preferred_username"))
{
username = info.Principal.FindFirstValue("preferred_username");
}
var currentUser = await _userManager.FindByNameAsync(username);
if (currentUser != null)
{
currentUser.LastLogin = DateTime.UtcNow;
await _userManager.UpdateAsync(currentUser);
}
_logger.LogInformation("{Name} logged in with {LoginProvider} provider.", username, info.LoginProvider);

return RedirectToLocal(returnUrl);
}
// if (result.IsLockedOut)
// {
// return RedirectToPage("./Lockout");
// }
else
{
// If the user does not have an account, then ask the user to create an account.
ViewData["ReturnUrl"] = returnUrl;
var externalLoginViewModel = new ExternalLoginViewModel {
ProviderDisplayName = info.ProviderDisplayName,
};
if (info.Principal.HasClaim(c => c.Type == ClaimTypes.Email))
{
externalLoginViewModel.Email = info.Principal.FindFirstValue(ClaimTypes.Email);
}
if (info.Principal.HasClaim(c => c.Type == "preferred_username"))
{
externalLoginViewModel.UserName = info.Principal.FindFirstValue("preferred_username");
}
return View(externalLoginViewModel);
}
}

[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> ExternalLoginConfirmation(ExternalLoginViewModel model, string returnUrl = null)
{
returnUrl = returnUrl ?? Url.Content("/");
// Get the information about the user from the external login provider
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
TempData["ErrorMessage"] = "Error loading external login information during confirmation.";
return RedirectToLocal(returnUrl);
}

if (ModelState.IsValid)
{
try
{
var user = new AppUser {
RegistrationDate = DateTime.UtcNow,
Email = model.Email
};
if (info.Principal.HasClaim(c => c.Type == ClaimTypes.GivenName))
{
user.FirstName = info.Principal.FindFirstValue(ClaimTypes.GivenName);
}
if (info.Principal.HasClaim(c => c.Type == ClaimTypes.Surname))
{
user.LastName = info.Principal.FindFirstValue(ClaimTypes.Surname);
}

// set username to supplied value if present, otherwise use email as username
user.UserName = info.Principal.HasClaim(c => c.Type == "preferred_username")
? info.Principal.FindFirstValue("preferred_username")
: model.Email;

var result = await _userManager.CreateAsync(user);
if (result.Succeeded)
{
result = await _userManager.AddLoginAsync(user, info);
if (result.Succeeded)
{
_logger.Log(LogLevel.Information,
EventIDs.RegistrationSuccessful,
new LogEvent($"User '{user.UserName}' created an account using '{info.LoginProvider}' provider.")
.AddUser(user)
.AddHttpContext(_accessor.HttpContext),
null,
LogEvent.Formatter);

if (_userManager.Options.SignIn.RequireConfirmedAccount)
{
var userId = await _userManager.GetUserIdAsync(user);
var tokenConfirmation = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackConfirmationUrl = Url.Action(
"ConfirmEmail", "Account",
new { userId = userId, code = tokenConfirmation },
protocol: Request.Scheme);
await _emailSender.SendEmailConfirmation(user, callbackConfirmationUrl);

return View("EmailConfirmation");
}
await _signInManager.SignInAsync(user, isPersistent: false, info.LoginProvider);
return LocalRedirect(returnUrl);
}
}
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}

_logger.Log(LogLevel.Warning,
EventIDs.RegistrationFailed,
new LogEvent($"Registration failed for user '{model.Email}'.")
.AddHttpContext(_accessor.HttpContext),
null,
LogEvent.Formatter);
}
catch (Exception e)
{
_logger.Log(LogLevel.Error,
EventIDs.RegistrationError,
new LogEvent(
$"An error occured while registering user '{model.Email}' (Exception: {e.Message}).")
.AddException(e)
.AddProperty("user.email", model.Email)
.AddHttpContext(_accessor.HttpContext)
.AddException(e),
e,
LogEvent.Formatter);

TempData["ErrorMessage"] = "Something bad happened while creating your account...";
model.ProviderDisplayName = info.ProviderDisplayName;

return View(model);
}
}

var externalLoginViewModel = new ExternalLoginViewModel {
ProviderDisplayName = info.ProviderDisplayName,
};
ViewData["ReturnUrl"] = returnUrl;
return View(externalLoginViewModel);
}
}
}
2 changes: 2 additions & 0 deletions DocIntel.WebApp/DocIntel.WebApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<PackageReference Include="Bogus" Version="34.0.2" />
<PackageReference Include="GoogleAuthenticator" Version="3.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="7.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Razor.Language" Version="6.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.0">
Expand Down Expand Up @@ -99,6 +100,7 @@

<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
<Exec Command="npm install" />
<Exec Command="npm audit fix" />
<Exec Command="npm run webpack" />
</Target>

Expand Down
9 changes: 9 additions & 0 deletions DocIntel.WebApp/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:5001/"
},
"DocIntelhttps": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7033;http://localhost:5001",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
18 changes: 18 additions & 0 deletions DocIntel.WebApp/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@

using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
Expand All @@ -59,6 +60,7 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;

Expand Down Expand Up @@ -187,6 +189,22 @@ public virtual void ConfigureServices(IServiceCollection services)
options.SlidingExpiration = true;
options.ForwardAuthenticate = "Identity.Application";
});

var openIdSection = Configuration.GetSection("Authentication:OIDC");
if (openIdSection.Exists())
{
services.AddAuthentication()
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme,
openIdSection.GetValue<string>("DisplayName", OpenIdConnectDefaults.DisplayName),
options =>
{
options.Authority = openIdSection["Authority"];
options.ClientId = openIdSection["ClientId"];
options.ClientSecret = openIdSection["ClientSecret"];
options.ResponseType = OpenIdConnectResponseType.Code;
options.SaveTokens = true;
});
}

services.AddAuthorization(options => { options.DefaultPolicy = policy; });
services.AddScoped<IUserClaimsPrincipalFactory<AppUser>, AppUserClaimsPrincipalFactory>();
Expand Down
33 changes: 33 additions & 0 deletions DocIntel.WebApp/ViewModels/Account/ExternalLoginViewModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/* DocIntel
* Copyright (C) 2018-2023 Belgian Defense, Antoine Cailliau
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

using System.ComponentModel.DataAnnotations;

namespace DocIntel.WebApp.ViewModels.Account
{
public class ExternalLoginViewModel
{
public string ProviderDisplayName { get; set; }

[Required]
[EmailAddress]
public string Email { get; set; }

[Display(Name = "User name")]
public string UserName { get; set; }
}
}
4 changes: 4 additions & 0 deletions DocIntel.WebApp/ViewModels/Account/SigninViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Authentication;

namespace DocIntel.WebApp.ViewModels.Account
{
Expand All @@ -31,5 +33,7 @@ public class SigninViewModel
public string Password { get; set; }

[Display(Name = "Remember me?")] public bool RememberMe { get; set; }

public IList<AuthenticationScheme> ExternalLogins { get; set; }
}
}
Loading