diff --git a/Directory.Build.props b/Directory.Build.props
index fd4b3e2..16a7850 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,8 +1,8 @@
- 10.0
- 4.2.8
+ 12.0
+ 4.5.0-dev.2
true
$(MSBuildThisFileDirectory)/external/EAVFramework
$(MSBuildThisFileDirectory)/external
diff --git a/src/EAVFW.Extensions.EasyAuth.MicrosoftEntraId/EAVFW.Extensions.EasyAuth.MicrosoftEntraId.csproj b/src/EAVFW.Extensions.EasyAuth.MicrosoftEntraId/EAVFW.Extensions.EasyAuth.MicrosoftEntraId.csproj
index 0c87b58..870a3c9 100644
--- a/src/EAVFW.Extensions.EasyAuth.MicrosoftEntraId/EAVFW.Extensions.EasyAuth.MicrosoftEntraId.csproj
+++ b/src/EAVFW.Extensions.EasyAuth.MicrosoftEntraId/EAVFW.Extensions.EasyAuth.MicrosoftEntraId.csproj
@@ -18,7 +18,7 @@
-
+
@@ -28,5 +28,7 @@
+
+
diff --git a/src/EAVFW.Extensions.EasyAuth.MicrosoftEntraId/MicrosoftEntraEasyAuthProvider.cs b/src/EAVFW.Extensions.EasyAuth.MicrosoftEntraId/MicrosoftEntraEasyAuthProvider.cs
index a6145c2..825d76d 100644
--- a/src/EAVFW.Extensions.EasyAuth.MicrosoftEntraId/MicrosoftEntraEasyAuthProvider.cs
+++ b/src/EAVFW.Extensions.EasyAuth.MicrosoftEntraId/MicrosoftEntraEasyAuthProvider.cs
@@ -1,14 +1,22 @@
using EAVFramework;
using EAVFramework.Authentication;
+using EAVFramework.Authentication.Passwordless;
+using EAVFramework.Configuration;
using EAVFramework.Endpoints;
+using EAVFramework.Extensions;
using EAVFW.Extensions.SecurityModel;
using IdentityModel.Client;
using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.WebUtilities;
using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Primitives;
+using Microsoft.IdentityModel.JsonWebTokens;
using System;
using System.Collections.Generic;
+using System.Collections.ObjectModel;
using System.IdentityModel.Tokens.Jwt;
using System.IO;
using System.Linq;
@@ -20,99 +28,148 @@
namespace EAVFW.Extensions.EasyAuth.MicrosoftEntraId
{
- public class MicrosoftEntraEasyAuthProvider : IEasyAuthProvider
+ public class MicrosoftEntraEasyAuthProvider : DefaultAuthProvider
+ where TContext : DynamicContext
where TSecurityGroup : DynamicEntity, IEntraIDSecurityGroup
where TSecurityGroupMember : DynamicEntity, ISecurityGroupMember, new()
+ where TIdentity : DynamicEntity, IIdentity
{
private readonly IOptions _options;
private readonly IHttpClientFactory _clientFactory;
+
- public string AuthenticationName => "MicrosoftEntraId";
-
- public HttpMethod CallbackHttpMethod => HttpMethod.Post;
-
- public bool AutoGenerateRoutes { get; set; } = true;
-
- public MicrosoftEntraEasyAuthProvider() { }
+ public MicrosoftEntraEasyAuthProvider() :base("MicrosoftEntraId", HttpMethod.Post) { }
public MicrosoftEntraEasyAuthProvider(
IOptions options,
- IHttpClientFactory clientFactory)
+ IHttpClientFactory clientFactory) : this()
{
_options = options ?? throw new System.ArgumentNullException(nameof(options));
_clientFactory = clientFactory ?? throw new ArgumentNullException(nameof(clientFactory));
}
- public async Task OnAuthenticate(HttpContext httpcontext, string handleId, string redirectUrl)
+ public override async Task OnAuthenticate(OnAuthenticateRequest authenticateRequest)
{
- var email = httpcontext.Request.Query["email"].FirstOrDefault();
- var redirectUri = httpcontext.Request.Query["redirectUri"].FirstOrDefault();
- var callbackUri = $"{httpcontext.Request.Scheme}://{httpcontext.Request.Host}{httpcontext.Request.Path}/callback";
+
- var ru = new RequestUrl(_options.Value.GetMicrosoftAuthorizationUrl(httpcontext));
+ var callbackurl = new Uri(authenticateRequest.CallbackUrl);
+
+ var ru = new RequestUrl(_options.Value.GetMicrosoftAuthorizationUrl(authenticateRequest.HttpContext));
var authUri = ru.CreateAuthorizeUrl(
clientId: _options.Value.ClientId,
- redirectUri: callbackUri,
+ redirectUri: callbackurl.GetLeftPart(UriPartial.Path),
responseType: ResponseTypes.Code,
responseMode: ResponseModes.FormPost,
scope: _options.Value.Scope,
- loginHint: String.IsNullOrEmpty(email) || email == "undefined" ? null : email,
- state: handleId + "&" + redirectUri);
- httpcontext.Response.Redirect(authUri);
+ loginHint: authenticateRequest.IdentityId.HasValue ?
+ await authenticateRequest.Options.FindEmailFromIdentity(
+ new EmailDiscoveryRequest
+ {
+ HttpContext = authenticateRequest.HttpContext,
+ IdentityId = authenticateRequest.IdentityId.Value,
+ ServiceProvider = authenticateRequest.ServiceProvider
+ }):null,
+ state: callbackurl.GetLeftPart(UriPartial.Path));
+
+ authenticateRequest.HttpContext.Response.Redirect(authUri);
+
+ return new OnAuthenticateResult { Success = true };
+ }
+
+ private async Task ValidateMicrosoftEntraIdUser(OnCallbackRequest request, Guid handleid, JsonWebToken jwtSecurityToken)
+ {
+
+
+
+
+
+ var user = await _options.Value.FindIdentityAsync(request, jwtSecurityToken.Claims);
+
+
+
+ var identity = new ClaimsIdentity(new[]
+ {
+
+ new Claim(IdentityModel.JwtClaimTypes.Subject, user.ToString()),
+ }, "MicrosoftEntraId");
+
+ return new ClaimsPrincipal(identity);
+
+
}
- public async Task<(ClaimsPrincipal, string, string)> OnCallback(HttpContext httpcontext)
+ public override async Task PopulateCallbackRequest(OnCallbackRequest request)
{
- var m = new IdentityModel.Client.AuthorizeResponse(await new StreamReader(httpcontext.Request.Body).ReadToEndAsync());
- var state = m.State.Split(new char[] { '&' }, 2);
- var handleId = state[0];
- var redirectUri = state[1];
- var callbackUri = $"{httpcontext.Request.Scheme}://{httpcontext.Request.Host}{httpcontext.Request.Path}";
+ var m = new IdentityModel.Client.AuthorizeResponse(await new StreamReader(request.HttpContext.Request.Body).ReadToEndAsync());
+ var query = QueryHelpers.ParseNullableQuery(m.State);
+
+ if (query.TryGetValue("token", out var handleid))
+ {
+ request.HandleId = new Guid(handleid);
+ }
+ request.Props.Add("code", m.Code);
+ request.Props.Add("state", m.State);
+
+
+ }
+ public override async Task OnCallback(OnCallbackRequest callbackRequest)
+ {
+ var httpcontext= callbackRequest.HttpContext;
+
+
+
var http = _clientFactory.CreateClient();
var response = await http.RequestAuthorizationCodeTokenAsync(new AuthorizationCodeTokenRequest
{
Address = _options.Value.GetMicrosoftTokenEndpoint(httpcontext),
ClientId = _options.Value.ClientId,
ClientSecret = _options.Value.ClientSecret,
- Code = m.Code,
- RedirectUri = callbackUri,
+ Code = callbackRequest.Props["code"],
+ RedirectUri = callbackRequest.Props["state"],
});
- ClaimsPrincipal identity = await _options.Value.ValidateUserAsync(httpcontext, handleId, response);
+ var handler = new JsonWebTokenHandler();
+
+ var jwtSecurityToken = handler.ReadJsonWebToken(response.IdentityToken);
+
+
+ ClaimsPrincipal identity = await ValidateMicrosoftEntraIdUser(callbackRequest, callbackRequest.HandleId, jwtSecurityToken);
+
if (identity == null)
{
- httpcontext.Response.Redirect($"{httpcontext.Request.Scheme}://{httpcontext.Request.Host}callback?error=access_denied&error_subcode=user_not_found");
- //return;
+ return new OnCallBackResult { ErrorCode = "access_denied", ErrorSubCode = "user_validation_failed", ErrorMessage = "User could not be validated", Success = false };
+
}
- var handler = new JwtSecurityTokenHandler();
- var jwtSecurityToken = handler.ReadJwtToken(response.IdentityToken);
-
- // Get string of group claims from the token
+
var groupClaims = jwtSecurityToken.Claims.Where(c => c.Type == "groups");
- if (!groupClaims.Any())
+
+ if (!string.IsNullOrEmpty(_options.Value.GroupId))
{
- httpcontext.Response.Redirect($"{httpcontext.Request.Scheme}://{httpcontext.Request.Host}callback?error=access_denied&error_subcode=group_not_found");
- //return;
+
+ if (!groupClaims.Any(x=>x.Value == _options.Value.GroupId))
+ {
+ return new OnCallBackResult { ErrorCode = "access_denied", ErrorSubCode = "user_access_group_missing", ErrorMessage = "User does not have access", Success = false };
+
+ }
}
- // Get the group ids from the claims
+
var groupIds = groupClaims.Select(c => new Guid(c.Value)).ToList();
var db = httpcontext.RequestServices.GetRequiredService>();
await SyncUserGroup(identity, groupIds, db);
- return (identity, redirectUri, handleId);
+ return new OnCallBackResult { Principal = identity, Success = true };
}
private async Task SyncUserGroup(ClaimsPrincipal identity, List groupIds, EAVDBContext db)
{
- var claimDict = identity.Claims.ToDictionary(c => c.Type, c => c.Value);
- var userId = new Guid(claimDict["sub"]);
+ var identityId = Guid.Parse(identity.FindFirstValue("sub"));
// Fetch all security group members for user
var groupMembersQuery = db.Set()
- .Where(sgm => sgm.IdentityId == userId);
+ .Where(sgm => sgm.IdentityId == identityId);
// Fetch in memory
var groupMembersDict = await groupMembersQuery.ToDictionaryAsync(sgm => sgm.Id);
@@ -134,7 +191,7 @@ private async Task SyncUserGroup(ClaimsPrincipal identity, List groupIds,
if (!sgmGroupSpecific.Any(sgm => sgm.SecurityGroupId == sg.Id))
{
var sgm = new TSecurityGroupMember();
- sgm.IdentityId = userId;
+ sgm.IdentityId = identityId;
sgm.SecurityGroupId = sg.Id;
db.Add(sgm);
diff --git a/src/EAVFW.Extensions.EasyAuth.MicrosoftEntraId/MicrosoftEntraIdEasyAuthExtensions.cs b/src/EAVFW.Extensions.EasyAuth.MicrosoftEntraId/MicrosoftEntraIdEasyAuthExtensions.cs
index bdae08a..06417bd 100644
--- a/src/EAVFW.Extensions.EasyAuth.MicrosoftEntraId/MicrosoftEntraIdEasyAuthExtensions.cs
+++ b/src/EAVFW.Extensions.EasyAuth.MicrosoftEntraId/MicrosoftEntraIdEasyAuthExtensions.cs
@@ -1,5 +1,6 @@
using EAVFramework;
using EAVFramework.Configuration;
+using EAVFramework.Extensions;
using EAVFW.Extensions.SecurityModel;
using IdentityModel.Client;
using Microsoft.AspNetCore.Builder;
@@ -8,6 +9,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using System;
+using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
@@ -23,17 +25,20 @@ public static class MicrosoftEntraIdEasyAuthExtensions
{
- public static AuthenticatedEAVFrameworkBuilder AddMicrosoftEntraIdEasyAuth(
+ public static AuthenticatedEAVFrameworkBuilder AddMicrosoftEntraIdEasyAuth(
this AuthenticatedEAVFrameworkBuilder builder,
- Func> validateUserAsync,
+ Func, Task> findIdentityAsync,
Func getMicrosoftAuthorizationUrl , Func getMicrosoftTokenEndpoint)
+ where TContext : DynamicContext
+ where TIdentity: DynamicEntity,IIdentity
where TSecurityGroup : DynamicEntity, IEntraIDSecurityGroup
where TSecurityGroupMemeber : DynamicEntity, ISecurityGroupMember, new()
{
- builder.AddAuthenticationProvider, MicrosoftEntraIdEasyAuthOptions,IConfiguration>((options, config) =>
+ builder.AddAuthenticationProvider,
+ MicrosoftEntraIdEasyAuthOptions,IConfiguration>((options, config) =>
{
config.GetSection("EAVEasyAuth:MicrosoftEntraId").Bind(options);
- options.ValidateUserAsync = validateUserAsync;
+ options.FindIdentityAsync = findIdentityAsync;
options.GetMicrosoftAuthorizationUrl = getMicrosoftAuthorizationUrl;
options.GetMicrosoftTokenEndpoint = getMicrosoftTokenEndpoint;
@@ -43,16 +48,18 @@ public static AuthenticatedEAVFrameworkBuilder AddMicrosoftEntraIdEasyAuth(
+ public static AuthenticatedEAVFrameworkBuilder AddMicrosoftEntraIdEasyAuth(
this AuthenticatedEAVFrameworkBuilder builder,
- Func> validateUserAsync)
+ Func, Task> findIdentityAsync)
+ where TContext : DynamicContext
+ where TIdentity : DynamicEntity, IIdentity
where TSecurityGroup : DynamicEntity, IEntraIDSecurityGroup
where TSecurityGroupMemeber : DynamicEntity, ISecurityGroupMember,new()
{
- builder.AddAuthenticationProvider, MicrosoftEntraIdEasyAuthOptions, IConfiguration>((options, config) =>
+ builder.AddAuthenticationProvider, MicrosoftEntraIdEasyAuthOptions, IConfiguration>((options, config) =>
{
config.GetSection("EAVEasyAuth:MicrosoftEntraId").Bind(options);
- options.ValidateUserAsync = validateUserAsync;
+ options.FindIdentityAsync = findIdentityAsync;
});
builder.Services.AddScoped>();
diff --git a/src/EAVFW.Extensions.EasyAuth.MicrosoftEntraId/MicrosoftEntraIdEasyAuthOptions.cs b/src/EAVFW.Extensions.EasyAuth.MicrosoftEntraId/MicrosoftEntraIdEasyAuthOptions.cs
index f5df8a5..15b3f73 100644
--- a/src/EAVFW.Extensions.EasyAuth.MicrosoftEntraId/MicrosoftEntraIdEasyAuthOptions.cs
+++ b/src/EAVFW.Extensions.EasyAuth.MicrosoftEntraId/MicrosoftEntraIdEasyAuthOptions.cs
@@ -1,24 +1,46 @@
+using EAVFramework;
+using EAVFramework.Endpoints;
+using EAVFramework.Extensions;
+using EAVFW.Extensions.SecurityModel;
using IdentityModel.Client;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using System;
+using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
namespace EAVFW.Extensions.EasyAuth.MicrosoftEntraId
{
public class MicrosoftEntraIdEasyAuthOptions
+
{
+ ///
+ /// The Entra Client ID (applicationid) used to authenticate with EntraID
+ ///
public string ClientId { get; set; }
+ ///
+ /// The Entra Client Secret used to authenticate with EntraID
+ ///
public string ClientSecret { get; set; }
+ ///
+ /// The Entra Tenant ID used to authenticate with EntraID, if not provided the common tenant is used (multitenant signin)
+ ///
public string TenantId { get; set; }= "common";
+ ///
+ /// If provided the user should be part of this groupid to be given access
+ ///
public string GroupId { get; set; }
- public string Scope { get; set; } = "openid email";
+
+
+ public string Scope { get; set; } = "openid email profile";
public Func GetMicrosoftAuthorizationUrl { get; set; } = DefaultGetMicrosoftAuthorizationUrl;
public Func GetMicrosoftTokenEndpoint { get; set; } = DefaultGetMicrosoftTokenEndpoint;
- public Func> ValidateUserAsync { get; set; }
+ public Func, Task> FindIdentityAsync { get; set; }
+
+ // public Func> ValidateUserAsync { get; set; }
private static string DefaultGetMicrosoftAuthorizationUrl(HttpContext context)
{
@@ -32,5 +54,7 @@ private static string DefaultGetMicrosoftTokenEndpoint(HttpContext context)
if (options.Value.TenantId == null) throw new Exception("TenantId is not configured");
return $"https://login.microsoftonline.com/{options.Value.TenantId}/oauth2/v2.0/token";
}
+
+
}
}
\ No newline at end of file