Skip to content

Commit

Permalink
Extend JWT refreshes to role changes and removals
Browse files Browse the repository at this point in the history
  • Loading branch information
myieye committed Dec 11, 2023
1 parent ec41557 commit 7f2cb44
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 17 deletions.
8 changes: 2 additions & 6 deletions backend/LexBoxApi/GraphQL/GraphQlSetupKernel.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
using DataAnnotatedModelValidations;
using HotChocolate.Data.Filters;
using HotChocolate.Data.Filters.Expressions;
using HotChocolate.Data.Projections.Expressions;
using HotChocolate.Diagnostics;
using LexBoxApi.Auth;
using LexBoxApi.Config;
using LexBoxApi.GraphQL.CustomFilters;
using LexBoxApi.Services;
using LexCore.ServiceInterfaces;
using LexData;
using Microsoft.Extensions.Options;

namespace LexBoxApi.GraphQL;

Expand All @@ -22,6 +17,7 @@ public static void AddLexGraphQL(this IServiceCollection services, IHostEnvironm
services.AddHostedService<DevGqlSchemaWriterService>();

services.AddGraphQLServer()
.TryAddTypeInterceptor<RefreshProjectMembershipInterceptor>()
.InitializeOnStartup()
.RegisterDbContext<LexBoxDbContext>()
.RegisterService<IHgService>()
Expand All @@ -41,7 +37,7 @@ public static void AddLexGraphQL(this IServiceCollection services, IHostEnvironm
descriptor.AddDeterministicInvariantContainsFilter();
})
.AddProjections()
.SetPagingOptions(new ()
.SetPagingOptions(new()
{
DefaultPageSize = 100,
MaxPageSize = 1000,
Expand Down
11 changes: 0 additions & 11 deletions backend/LexBoxApi/GraphQL/LexQueries.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@ public async Task<IQueryable<Project>> MyProjects(LoggedInContext loggedInContex
{
var userId = loggedInContext.User.Id;
var projects = context.Projects.Where(p => p.Users.Select(u => u.UserId).Contains(userId));
if (loggedInContext.User.Role != UserRole.admin && !ProjectsMatch(projects, loggedInContext.User.Projects))
{
await lexAuthService.RefreshUser(userId, LexAuthConstants.ProjectsClaimType);
}
return projects;
}

Expand Down Expand Up @@ -64,11 +60,4 @@ public LexAuthUser Me(LoggedInContext loggedInContext)
{
return loggedInContext.User;
}

private static bool ProjectsMatch(IQueryable<Project> dbProjects, AuthUserProject[] jwtProjects)
{
if (dbProjects.Count() != jwtProjects.Length) return false;
var dbProjectIds = dbProjects.Select(p => p.Id).ToHashSet();
return jwtProjects.All(p => dbProjectIds.Contains(p.ProjectId));
}
}
87 changes: 87 additions & 0 deletions backend/LexBoxApi/GraphQL/RefreshProjectMembershipInterceptor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using HotChocolate.Configuration;
using HotChocolate.Resolvers;
using HotChocolate.Types.Descriptors.Definitions;
using HotChocolate.Utilities;
using LexBoxApi.Auth;
using LexCore.Auth;
using LexCore.Entities;

namespace LexBoxApi.GraphQL;

public class RefreshJwtProjectMembershipMiddleware(FieldDelegate next)
{
private readonly FieldDelegate _next = next;

public async Task InvokeAsync(IMiddlewareContext context)
{
await _next(context);
var currUser = context.Service<LoggedInContext>();
if (currUser.User == null || currUser.User.Role == UserRole.admin) return;

var lexAuthService = context.Service<LexAuthService>();
var projectId = context.Parent<Project>().Id;
if (projectId == default)
{
if (context.Result is not Guid projectGuid) return;
if (projectGuid == default) return;
projectId = projectGuid;
} // we know we have a valid project-ID

var currUserMembershipJwt = currUser.User.Projects.FirstOrDefault(projects => projects.ProjectId == projectId);

if (currUserMembershipJwt is null)
{
// The user was probably added to the project and it's not in the token yet
await lexAuthService.RefreshUser(currUser.User.Id, LexAuthConstants.ProjectsClaimType);
}
else
{
if (context.Result is not IEnumerable<ProjectUsers> projectUsers) return;

var sampleProjectUser = projectUsers.FirstOrDefault();
if (sampleProjectUser is not null && sampleProjectUser.UserId == default && (sampleProjectUser.User == null || sampleProjectUser.User.Id == default))
{
// User IDs don't seem to have been loaded from the DB, so we can't do anything
return;
}

var currUserMembershipDb = projectUsers.FirstOrDefault(projectUser => currUser.User.Id == projectUser.UserId || currUser.User.Id == projectUser.User.Id);
if (currUserMembershipDb is null)
{
// The user was probably removed from the project and it's still in the token
await lexAuthService.RefreshUser(currUser.User.Id, LexAuthConstants.ProjectsClaimType);
}
else if (currUserMembershipDb.Role == default)
{
return; // Either the role wasn't loaded by the query (so we can't do anything) or the role is actually Unknown which means it definitely has never been changed
}
else if (currUserMembershipDb.Role != currUserMembershipJwt.Role)
{
// The user's role was changed
await lexAuthService.RefreshUser(currUser.User.Id, LexAuthConstants.ProjectsClaimType);
}
}
}
}

public class RefreshProjectMembershipInterceptor : TypeInterceptor
{
private readonly FieldMiddlewareDefinition refreshProjectMembershipMiddleware = new(
FieldClassMiddlewareFactory.Create<RefreshJwtProjectMembershipMiddleware>(),
false,
"jwt-refresh-middleware");

public override void OnBeforeCompleteType(
ITypeCompletionContext completionContext, DefinitionBase definition)
{
if (definition is ObjectTypeDefinition def && def.Name.Equals(nameof(Project)))
{
var idField = def.Fields.FirstOrDefault(x => x.Name.EqualsInvariantIgnoreCase(nameof(Project.Id)));
if (idField is null) throw new InvalidOperationException("Did not find id field of project type.");
idField.MiddlewareDefinitions.Insert(0, refreshProjectMembershipMiddleware);
var userField = def.Fields.FirstOrDefault(x => x.Name.EqualsInvariantIgnoreCase(nameof(Project.Users)));
if (userField is null) throw new InvalidOperationException("Did not find users field of project type.");
userField.MiddlewareDefinitions.Insert(0, refreshProjectMembershipMiddleware);
}
}
}

0 comments on commit 7f2cb44

Please sign in to comment.