Skip to content

Commit 498e230

Browse files
author
Simon S. Pedersen
committed
feat: added security groups
1 parent 74217ea commit 498e230

File tree

3 files changed

+66
-29
lines changed

3 files changed

+66
-29
lines changed

src/EAVFW.Extensions.EasyAuth.MicrosoftEntraId/IEntraIDSecurityGroup.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@ namespace EAVFW.Extensions.EasyAuth.MicrosoftEntraId
88
[EntityInterface(EntityKey = "Security Group")]
99
public interface IEntraIDSecurityGroup
1010
{
11-
11+
public Guid Id { get; set; }
1212
public Guid? EntraIdGroupId { get; set; }
1313
}
14-
15-
1614
}

src/EAVFW.Extensions.EasyAuth.MicrosoftEntraId/MicrosoftEntraEasyAuthProvider.cs

Lines changed: 62 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,31 @@
11
using EAVFramework;
22
using EAVFramework.Authentication;
33
using EAVFramework.Endpoints;
4-
using EAVFramework.Extensions;
54
using EAVFW.Extensions.SecurityModel;
6-
using IdentityModel;
75
using IdentityModel.Client;
86
using Microsoft.AspNetCore.Http;
9-
using Microsoft.AspNetCore.Mvc;
10-
using Microsoft.AspNetCore.WebUtilities;
7+
using Microsoft.EntityFrameworkCore;
8+
using Microsoft.Extensions.DependencyInjection;
119
using Microsoft.Extensions.Options;
12-
using Newtonsoft.Json;
13-
using Newtonsoft.Json.Linq;
1410
using System;
1511
using System.Collections.Generic;
1612
using System.IdentityModel.Tokens.Jwt;
1713
using System.IO;
1814
using System.Linq;
1915
using System.Net.Http;
20-
using System.Net.Sockets;
2116
using System.Security.Claims;
22-
using System.Text;
2317
using System.Threading.Tasks;
24-
using System.Web;
2518
using static IdentityModel.OidcConstants;
26-
using static System.Net.WebRequestMethods;
2719

2820
namespace EAVFW.Extensions.EasyAuth.MicrosoftEntraId
2921
{
3022

31-
public class MicrosoftEntraEasyAuthProvider<TSecurityGroup,TSecurityGroupMember> : IEasyAuthProvider
23+
public class MicrosoftEntraEasyAuthProvider<TSecurityGroup, TSecurityGroupMember> : IEasyAuthProvider
3224
where TSecurityGroup : DynamicEntity, IEntraIDSecurityGroup
33-
where TSecurityGroupMember : DynamicEntity, ISecurityGroupMember
25+
where TSecurityGroupMember : DynamicEntity, ISecurityGroupMember, new()
3426
{
3527
private readonly IOptions<MicrosoftEntraIdEasyAuthOptions> _options;
3628
private readonly IHttpClientFactory _clientFactory;
37-
private readonly EAVDBContext<DynamicContext> _db;
3829

3930
public string AuthenticationName => "MicrosoftEntraId";
4031

@@ -44,12 +35,12 @@ public class MicrosoftEntraEasyAuthProvider<TSecurityGroup,TSecurityGroupMember>
4435

4536
public MicrosoftEntraEasyAuthProvider() { }
4637

47-
public MicrosoftEntraEasyAuthProvider(IOptions<MicrosoftEntraIdEasyAuthOptions> options,
48-
IHttpClientFactory clientFactory, EAVDBContext<DynamicContext> db)
38+
public MicrosoftEntraEasyAuthProvider(
39+
IOptions<MicrosoftEntraIdEasyAuthOptions> options,
40+
IHttpClientFactory clientFactory)
4941
{
5042
_options = options ?? throw new System.ArgumentNullException(nameof(options));
5143
_clientFactory = clientFactory ?? throw new ArgumentNullException(nameof(clientFactory));
52-
_db = db;
5344
}
5445

5546
public async Task OnAuthenticate(HttpContext httpcontext, string handleId, string redirectUrl)
@@ -98,24 +89,71 @@ public async Task OnAuthenticate(HttpContext httpcontext, string handleId, strin
9889
var handler = new JwtSecurityTokenHandler();
9990
var jwtSecurityToken = handler.ReadJwtToken(response.IdentityToken);
10091

101-
var groupId = jwtSecurityToken.Claims.First(claim => claim.Type == "groups").Value;
92+
// Get string of group claims from the token
93+
var groupClaims = jwtSecurityToken.Claims.Where(c => c.Type == "groups");
94+
if (!groupClaims.Any())
95+
{
96+
httpcontext.Response.Redirect($"{httpcontext.Request.Scheme}://{httpcontext.Request.Host}callback?error=access_denied&error_subcode=group_not_found");
97+
//return;
98+
}
99+
// Get the group ids from the claims
100+
var groupIds = groupClaims.Select(c => new Guid(c.Value)).ToList();
101+
var db = httpcontext.RequestServices.GetRequiredService<EAVDBContext<DynamicContext>>();
102102

103-
var groupids = new string[] { "", "" };
103+
await SyncUserGroup(identity, groupIds, db);
104104

105-
var sgs = _db.Set<TSecurityGroup>();
106-
var assignments = _db.Set<TSecurityGroupMember>();
105+
return (identity, redirectUri, handleId);
106+
}
107107

108+
private async Task SyncUserGroup(ClaimsPrincipal identity, List<Guid> groupIds, EAVDBContext<DynamicContext> db)
109+
{
110+
var claimDict = identity.Claims.ToDictionary(c => c.Type, c => c.Value);
111+
var userId = new Guid(claimDict["sub"]);
108112

109-
//Add securityMember for the group ids where found
113+
// Fetch all security group members for user
114+
var groupMembersQuery = db.Set<TSecurityGroupMember>()
115+
.Where(sgm => sgm.IdentityId == userId);
116+
// Fetch in memory
117+
var groupMembersDict = await groupMembersQuery.ToDictionaryAsync(sgm => sgm.Id);
110118

119+
// Fetch all security groups
120+
var groupsDict = await db.Set<TSecurityGroup>()
121+
.Where(sg => groupMembersQuery.Any(sgm => sgm.SecurityGroupId == sg.Id) ||
122+
(sg.EntraIdGroupId != null && groupIds.Contains(sg.EntraIdGroupId.Value)))
123+
.ToDictionaryAsync(sg => sg.Id);
111124

112-
//Remove securirymember from existing that was not given in groupids.
113-
114125

126+
// Fetch specific security group and group members
127+
var sgGroupSpecific = groupsDict.Values.Where(sg => sg.EntraIdGroupId != null && groupIds.Contains(sg.EntraIdGroupId.Value)).ToDictionary(sg => sg.Id);
128+
var sgmGroupSpecific = groupMembersDict.Values.Where(sgm => sgm.SecurityGroupId != null && sgGroupSpecific.ContainsKey((Guid) sgm.SecurityGroupId));
115129

130+
// Check if member group exists else add it
131+
bool isDirty = false;
132+
foreach (var sg in sgGroupSpecific.Values)
133+
{
134+
if (!sgmGroupSpecific.Any(sgm => sgm.SecurityGroupId == sg.Id))
135+
{
136+
var sgm = new TSecurityGroupMember();
137+
sgm.IdentityId = userId;
138+
sgm.SecurityGroupId = sg.Id;
139+
db.Add(sgm);
140+
141+
isDirty = true;
142+
}
143+
}
116144

145+
// Fecth expired group members by comparing the "historical" group members with that of the current based on the group ids
146+
var expiredGroupMembers = groupMembersDict.Values.Where(sgm =>
147+
!sgmGroupSpecific.Any(x => x.Id == sgm.Id) &&
148+
sgm.SecurityGroupId != null &&
149+
groupsDict[(Guid) sgm.SecurityGroupId].EntraIdGroupId != null); // Groups of higher aurthority has no EntraGroupId and should not be removed
150+
foreach (var sgm in expiredGroupMembers)
151+
{
152+
db.Remove(sgm);
153+
isDirty = true;
154+
}
117155

118-
return await Task.FromResult((new ClaimsPrincipal(identity), redirectUri, handleId));
156+
if (isDirty) await db.SaveChangesAsync(identity);
119157
}
120158

121159
public RequestDelegate OnSignedOut()

src/EAVFW.Extensions.EasyAuth.MicrosoftEntraId/MicrosoftEntraIdEasyAuthExtensions.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,15 @@ public class GroupMatcherService<TSecurityGroup>
1717
{
1818

1919
}
20+
2021
public static class MicrosoftEntraIdEasyAuthExtensions
2122
{
22-
public static AuthenticatedEAVFrameworkBuilder AddMicrosoftEntraIdEasyAuth<TSecurityGroup,TSecurityGroupMemeber>(
23+
public static AuthenticatedEAVFrameworkBuilder AddMicrosoftEntraIdEasyAuth<TSecurityGroup, TSecurityGroupMemeber>(
2324
this AuthenticatedEAVFrameworkBuilder builder,
2425
Func<HttpContext, string, TokenResponse, Task<ClaimsPrincipal>> validateUserAsync,
2526
Func<HttpContext, string> getMicrosoftAuthorizationUrl, Func<HttpContext, string> getMicrosoftTokenEndpoint)
2627
where TSecurityGroup : DynamicEntity, IEntraIDSecurityGroup
27-
where TSecurityGroupMemeber : DynamicEntity, ISecurityGroupMember
28+
where TSecurityGroupMemeber : DynamicEntity, ISecurityGroupMember, new()
2829
{
2930
builder.AddAuthenticationProvider<MicrosoftEntraEasyAuthProvider<TSecurityGroup,TSecurityGroupMemeber>, MicrosoftEntraIdEasyAuthOptions,IConfiguration>((options, config) =>
3031
{

0 commit comments

Comments
 (0)