Externl Login
Some checks failed
Dotnet build and test / log-the-inputs (push) Successful in 6s
Dotnet build and test / build (push) Failing after 1m26s

This commit is contained in:
Paul Schneider
2025-07-10 15:19:28 +01:00
parent 19a3ba6f87
commit 984b76b170
12 changed files with 346 additions and 35 deletions

5
.vscode/launch.json vendored
View File

@ -109,6 +109,11 @@
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
},
"presentation": {
"hidden": false,
"group": "run",
"order": 1
}
},
{

View File

@ -16,7 +16,7 @@ namespace Yavsc.Helpers
public static string GetUserName(this ClaimsPrincipal user)
{
return user.FindFirstValue(ClaimTypes.Name);
return user.FindFirstValue("name");
}
public static bool IsSignedIn(this ClaimsPrincipal user)

View File

@ -36,6 +36,7 @@ namespace Yavsc.Models
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Yavsc.Abstract.Models.Messaging;
using Org.BouncyCastle.Asn1.Crmf;
using Microsoft.AspNetCore.Identity;
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
@ -85,8 +86,7 @@ namespace Yavsc.Models
}
builder.Entity<Activity>().Property(a => a.ParentCode).IsRequired(false);
//builder.Entity<BlogPost>().HasOne(p => p.Author).WithMany(a => a.Posts);
builder.Entity<IdentityUserLogin<String>>().HasKey(i=> new { i.LoginProvider, i.UserId, i.ProviderKey });
}
// this is not a failback procedure.
@ -301,6 +301,6 @@ namespace Yavsc.Models
public DbSet<Scope> Scopes { get; set; }
public DbSet<BlogSpotPublication> blogSpotPublications{ get; set; }
public DbSet<IdentityUserLogin<String>> AspNetUserLogins { get; set; }
}
}

View File

@ -0,0 +1,33 @@
namespace Yavsc.Models.Auth
{
using Microsoft.AspNetCore.Identity;
using System.ComponentModel.DataAnnotations.Schema;
public class YaIdentityUserLogin
{
/// <summary>
/// Gets or sets the login provider for the login (e.g. facebook, google)
/// </summary>
public virtual string LoginProvider { get; set; } = default!;
/// <summary>
/// Gets or sets the unique provider identifier for this login.
/// </summary>
public virtual string ProviderKey { get; set; } = default!;
/// <summary>
/// Gets or sets the friendly name used in a UI for this login.
/// </summary>
public virtual string? ProviderDisplayName { get; set; }
/// <summary>
/// Gets or sets the primary key of the user associated with this login.
/// </summary>
public String UserId { get; set; } = default!;
[ForeignKey("UserId")]
public virtual ApplicationUser User { get; set; }
}
}

View File

@ -0,0 +1,228 @@
/*
Copyright (c) 2024 HigginsSoft, Alexander Higgins - https://github.com/alexhiggins732/
Copyright (c) 2018, Brock Allen & Dominick Baier. All rights reserved.
Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
Source code and license this software can be found
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
using System.Security.Claims;
using IdentityModel;
using IdentityServer8;
using IdentityServer8.Events;
using IdentityServer8.Services;
using IdentityServer8.Stores;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Yavsc;
using Yavsc.Extensions;
using Yavsc.Interfaces;
using Yavsc.Models;
namespace IdentityServerHost.Quickstart.UI;
[SecurityHeaders]
[AllowAnonymous]
public class ExternalController : Controller
{
private readonly IIdentityServerInteractionService _interaction;
private readonly IClientStore _clientStore;
private readonly ILogger<ExternalController> _logger;
private readonly IEventService _events;
private IExternalIdentityManager _users;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly RoleManager<IdentityRole> _roleManager;
private readonly ApplicationDbContext _dbContext;
public ExternalController(
IIdentityServerInteractionService interaction,
IClientStore clientStore,
IEventService events,
ILogger<ExternalController> logger,
IExternalIdentityManager externalIdentityProviderManager,
SignInManager<ApplicationUser> signInManager,
ApplicationDbContext dbContext,
RoleManager<IdentityRole> roleManager
)
{
// if the TestUserStore is not in DI, then we'll just use the global users collection
// this is where you would plug in your own custom identity management library (e.g. ASP.NET Identity)
_users = externalIdentityProviderManager;
_interaction = interaction;
_clientStore = clientStore;
_logger = logger;
_events = events;
_signInManager = signInManager;
_roleManager = roleManager;
_dbContext = dbContext;
}
/// <summary>
/// initiate roundtrip to external authentication provider
/// </summary>
[HttpGet]
public IActionResult Challenge(string scheme, string returnUrl)
{
if (string.IsNullOrEmpty(returnUrl)) returnUrl = "~/";
// validate returnUrl - either it is a valid OIDC URL or back to a local page
if (Url.IsLocalUrl(returnUrl) == false && _interaction.IsValidReturnUrl(returnUrl) == false)
{
// user might have clicked on a malicious link - should be logged
throw new Exception("invalid return URL");
}
// start challenge and roundtrip the return URL and scheme
var props = new AuthenticationProperties
{
RedirectUri = Url.Action(nameof(Callback)),
Items =
{
{ "returnUrl", returnUrl },
{ "scheme", scheme },
}
};
return Challenge(props, scheme);
}
/// <summary>
/// Post processing of external authentication
/// </summary>
[HttpGet]
public async Task<IActionResult> Callback()
{
// read external identity from the temporary cookie
var result = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
if (result?.Succeeded != true)
{
throw new Exception("External authentication error");
}
if (_logger.IsEnabled(LogLevel.Debug))
{
var externalClaims = result.Principal.Claims.Select(c => $"{c.Type}: {c.Value}");
_logger.LogDebug("External claims: {@claims}", externalClaims);
}
// lookup our user and external provider info
var (user, provider, providerUserId, claims) = await FindUserFromExternalProvider(result);
if (user == null)
{
// this might be where you might initiate a custom workflow for user registration
// in this sample we don't show how that would be done, as our sample implementation
// simply auto-provisions new external user
user = AutoProvisionUser(provider, providerUserId, claims);
}
// this allows us to collect any additional claims or properties
// for the specific protocols used and store them in the local auth cookie.
// this is typically used to store data needed for signout from those protocols.
var additionalLocalClaims = new List<Claim>();
var localSignInProps = new AuthenticationProperties();
ProcessLoginCallback(result, additionalLocalClaims, localSignInProps);
// issue authentication cookie for user
var isuser = new IdentityServerUser(user.Id)
{
DisplayName = user.UserName,
IdentityProvider = provider,
AdditionalClaims = additionalLocalClaims
};
await HttpContext.SignInAsync(isuser, localSignInProps);
//await HttpContext.SignInAsync(user, _roleManager, false, _dbContext);
// delete temporary cookie used during external authentication
await HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
// retrieve return URL
var returnUrl = result.Properties.Items["returnUrl"] ?? "~/";
// check if external login is in the context of an OIDC request
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
await _events.RaiseAsync(new UserLoginSuccessEvent(provider, providerUserId, user.Id, user.UserName, true, context?.Client.ClientId));
if (context != null)
{
if (context.IsNativeClient())
{
// The client is native, so this change in how to
// return the response is for better UX for the end user.
return this.LoadingPage("Redirect", returnUrl);
}
}
return Redirect(returnUrl);
}
private async Task<(ApplicationUser user,
string provider,
string providerUserId,
IEnumerable<Claim> claims)>
FindUserFromExternalProvider(AuthenticateResult result)
{
var externalUser = result.Principal;
// try to determine the unique id of the external user (issued by the provider)
// the most common claim type for that are the sub claim and the NameIdentifier
// depending on the external provider, some other claim type might be used
var userIdClaim = externalUser.FindFirst(JwtClaimTypes.Subject) ??
externalUser.FindFirst(ClaimTypes.NameIdentifier) ??
throw new Exception("Unknown userid");
// remove the user id claim so we don't include it as an extra claim if/when we provision the user
var claims = externalUser.Claims.ToList();
claims.Remove(userIdClaim);
var provider = result.Properties.Items["scheme"];
var providerUserId = userIdClaim.Value;
// find external user
ApplicationUser? user = await _users.FindByExternaleProviderAsync (provider, providerUserId);
return (user, provider, providerUserId, claims);
}
/// <summary>
/// Register a new user by external id
/// </summary>
/// <param name="provider"></param>
/// <param name="providerUserId"></param>
/// <param name="claims"></param>
/// <returns></returns>
private ApplicationUser AutoProvisionUser(string provider, string providerUserId, IEnumerable<Claim> claims)
{
var user = _users.AutoProvisionUser(provider, providerUserId, claims.ToList());
return user;
}
// if the external login is OIDC-based, there are certain things we need to preserve to make logout work
// this will be different for WS-Fed, SAML2p or other protocols
private void ProcessLoginCallback(AuthenticateResult externalResult, List<Claim> localClaims, AuthenticationProperties localSignInProps)
{
// if the external system sent a session id claim, copy it over
// so we can use it for single sign-out
var sid = externalResult.Principal.Claims.FirstOrDefault(x => x.Type == JwtClaimTypes.SessionId);
if (sid != null)
{
localClaims.Add(new Claim(JwtClaimTypes.SessionId, sid.Value));
}
// if the external provider issued an id_token, we'll keep it for signout
var idToken = externalResult.Properties.GetTokenValue("id_token");
if (idToken != null)
{
localSignInProps.StoreTokens(new[] { new AuthenticationToken { Name = "id_token", Value = idToken } });
}
}
}

View File

@ -0,0 +1,22 @@
/*
Copyright (c) 2024 HigginsSoft, Alexander Higgins - https://github.com/alexhiggins732/
Copyright (c) 2018, Brock Allen & Dominick Baier. All rights reserved.
Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
Source code and license this software can be found
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
using System.Security.Claims;
using Yavsc.Models;
namespace Yavsc.Interfaces;
public interface IExternalIdentityManager
{
ApplicationUser AutoProvisionUser(string provider, string providerUserId, List<Claim> claims);
Task<ApplicationUser?> FindByExternaleProviderAsync(string provider, string providerUserId);
}

View File

@ -36,6 +36,7 @@ using Microsoft.IdentityModel.Protocols.Configuration;
using IdentityModel;
using System.Security.Claims;
using IdentityServer8.Security;
using Yavsc.Interfaces;
namespace Yavsc.Extensions;
@ -112,6 +113,7 @@ public static class HostingExtensions
AddYavscPolicies(services);
services.AddScoped<IAuthorizationHandler, PermissionHandler>();
services.AddTransient<IExternalIdentityManager, ExternalIdentityManager>();
AddAuthentication(builder);
@ -215,11 +217,12 @@ public static class HostingExtensions
// set the redirect URI to https://localhost:5001/signin-google
options.ClientId = googleClientId;
options.ClientSecret = googleClientSecret;
});
}
private static IIdentityServerBuilder AddIdentityServer(WebApplicationBuilder builder)
{
builder.Services.AddTransient<IProfileService,ProfileService>();
//builder.Services.AddTransient<IProfileService,ProfileService>();
var identityServerBuilder = builder.Services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
@ -234,7 +237,7 @@ public static class HostingExtensions
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddInMemoryClients(Config.Clients)
.AddInMemoryApiScopes(Config.ApiScopes)
.AddProfileService<ProfileService>()
// .AddProfileService<ProfileService>()
.AddAspNetIdentity<ApplicationUser>()
;
if (builder.Environment.IsDevelopment())

View File

@ -0,0 +1,28 @@
using System.Security.Claims;
using Microsoft.EntityFrameworkCore;
using Yavsc.Interfaces;
using Yavsc.Models;
public class ExternalIdentityManager : IExternalIdentityManager
{
private ApplicationDbContext _applicationDbContext;
public ExternalIdentityManager(ApplicationDbContext applicationDbContext)
{
_applicationDbContext = applicationDbContext;
}
public ApplicationUser AutoProvisionUser(string provider, string providerUserId, List<Claim> claims)
{
throw new NotImplementedException();
}
public async Task<ApplicationUser?> FindByExternaleProviderAsync(string provider, string providerUserId)
{
var user = await _applicationDbContext.AspNetUserLogins
.FirstOrDefaultAsync(
i => (i.LoginProvider == provider) && (i.ProviderKey == providerUserId)
);
if (user == null) return null;
return await _applicationDbContext.Users.FirstOrDefaultAsync(u=>u.Id == user.UserId);
}
}

View File

@ -2,17 +2,10 @@
@inject SignInManager<ApplicationUser> SignInManager
@inject UserManager<ApplicationUser> UserManager
@{
#nullable enable
string? name = null;
if (Context.User!=null)
{
name = Context.User.GetUserName();
}
}
@if (name!=null)
@if (Context.User?.Identity?.IsAuthenticated ?? false)
{
string userName = User.GetUserName();
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="dropdown04" data-bs-toggle="dropdown" aria-expanded="false">Plateforme</a>
<ul class="dropdown-menu" aria-labelledby="dropdown04">
@ -37,7 +30,7 @@
</ul>
</li>}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="dropdown04" data-bs-toggle="dropdown" aria-expanded="false">Hello @UserManager.GetUserName(User)!</a>
<a class="nav-link dropdown-toggle" href="#" id="dropdown04" data-bs-toggle="dropdown" aria-expanded="false">Hello @userName!</a>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" asp-controller="Manage" asp-action="Index" title="Manage">
@ -61,3 +54,5 @@ else
<a class="nav-link" asp-controller="Account" asp-action="Login" asp-route-ReturnUrl="~/" >Login</a>
</li>
}
using IdentityServer8.Extensions;
#line default

View File

@ -17,30 +17,31 @@
<PackageReference Include="HigginsSoft.IdentityServer8.AspNetIdentity" Version="8.0.5-preview-net9" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="9.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="9.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="9.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.6">
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="9.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="9.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="9.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.7">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.6">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.7" />
<PackageReference Include="Google.Apis.Compute.v1" Version="1.70.0.3829" />
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Razor" Version="2.3.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="9.0.0" />
<PackageReference Include="System.Security.Cryptography.Pkcs" Version="9.0.6" />
<PackageReference Include="System.Security.Cryptography.Pkcs" Version="9.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Antiforgery" Version="2.3.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3" />
<PackageReference Include="AsciiDocNet" Version="1.0.0-alpha6" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.10" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="9.0.7" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../Yavsc.Server/Yavsc.Server.csproj" />

View File

@ -13,9 +13,7 @@
using System.IdentityModel.Tokens.Jwt;
using Microsoft.AspNetCore.Authentication;
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
JwtSecurityTokenHandler.DefaultMapInboundClaims = true;
var builder = WebApplication.CreateBuilder(args);
@ -38,13 +36,11 @@ builder.Services
options.Scope.Add("profile");
options.Scope.Add("scope2");
options.MapInboundClaims = true;
options.ClaimActions.MapUniqueJsonKey("preferred_username","preferred_username");
options.ClaimActions.MapUniqueJsonKey("preferred_username", "preferred_username");
options.ClaimActions.MapUniqueJsonKey("gender", "gender");
options.SaveTokens = true;
});
using (var app = builder.Build())
{

View File

@ -8,7 +8,7 @@
<ItemGroup>
<ProjectReference Include="..\Yavsc.Abstract\Yavsc.Abstract.csproj" />
<PackageReference Include="IdentityModel.AspNetCore" Version="4.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.7" />
</ItemGroup>
<ItemGroup>