Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/develop' into feat/414-large-num…
Browse files Browse the repository at this point in the history
…bers-of-projects-on-the-home-page-are-difficult-to-navigate
  • Loading branch information
myieye committed Dec 7, 2023
2 parents bbf649d + f450959 commit 819fffa
Show file tree
Hide file tree
Showing 48 changed files with 260 additions and 160 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/release-pipeline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,5 @@ jobs:
version: ${{ needs.set-version.outputs.version }}
image: 'ghcr.io/sillsdev/lexbox-*'
k8s-environment: production
deploy-domain: prod.languagedepot.org
deploy-domain: languagedepot.org

27 changes: 24 additions & 3 deletions .idea/.idea.LexBox/.idea/dataSources.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/.idea.LexBox/.idea/indexLayout.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 0 additions & 6 deletions .idea/.idea.LexBox/.idea/kubernetes-settings.xml

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace LexBoxApi.Auth;
namespace LexBoxApi.Auth.Attributes;

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AdminRequiredAttribute : LexboxAuthAttribute
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace LexBoxApi.Auth;
namespace LexBoxApi.Auth.Attributes;

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CreateProjectRequiredAttribute: LexboxAuthAttribute
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
using Microsoft.AspNetCore.Authorization;
using AuthorizeAttribute = HotChocolate.Authorization.AuthorizeAttribute;

namespace LexBoxApi.Auth;
namespace LexBoxApi.Auth.Attributes;

public abstract class LexboxAuthAttribute : DescriptorAttribute, IAuthorizeData
{
Expand Down
33 changes: 33 additions & 0 deletions backend/LexBoxApi/Auth/Attributes/RequireAudienceAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using LexCore.Auth;
using Microsoft.AspNetCore.Authorization;

namespace LexBoxApi.Auth.Attributes;

public class RequireAudienceAttribute(params LexboxAudience[] audiences)
: LexboxAuthAttribute(PolicyName), IAuthorizationRequirement, IAuthorizationRequirementData
{
public const string PolicyName = "RequireAudiencePolicy";
/// <param name="audience">audience allowed to access this endpoint</param>
/// <param name="exclusive">when false the default audience is also allowed, when true the default audience is not allowed</param>
public RequireAudienceAttribute(LexboxAudience audience, bool exclusive = false) : this(exclusive
? [audience]
: [audience, LexboxAudience.LexboxApi])
{
}

public LexboxAudience[] ValidAudiences { get; } = audiences;

public IEnumerable<IAuthorizationRequirement> GetRequirements()
{
yield return this;
}
}

public class AllowAnyAudienceAttribute : LexboxAuthAttribute
{
public const string PolicyName = "AllowAnyAudiencePolicy";

public AllowAnyAudienceAttribute() : base(PolicyName)
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Authorization;

namespace LexBoxApi.Auth.Attributes;

/// <summary>
/// validates the updated date of the jwt against the database, should be used to make a jwt expire if the user is updated
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class RequireCurrentUserInfoAttribute: Attribute, IAuthorizationRequirement, IAuthorizationRequirementData
{
public IEnumerable<IAuthorizationRequirement> GetRequirements()
{
yield return this;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace LexBoxApi.Auth;
namespace LexBoxApi.Auth.Attributes;

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class VerifiedEmailRequiredAttribute : LexboxAuthAttribute
Expand Down
24 changes: 7 additions & 17 deletions backend/LexBoxApi/Auth/AuthKernel.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Net.Http.Headers;
using System.Text;
using LexBoxApi.Auth.Attributes;
using LexBoxApi.Auth.Requirements;
using LexCore.Auth;
using Microsoft.AspNetCore.Authentication.Cookies;
Expand All @@ -25,10 +26,12 @@ public static void AddLexBoxAuth(IServiceCollection services,
if (environment.IsDevelopment())
{
IdentityModelEventSource.ShowPII = true;
IdentityModelEventSource.LogCompleteSecurityArtifact = true;
}

services.AddScoped<LexAuthService>();
services.AddSingleton<IAuthorizationHandler, AudienceRequirementHandler>();
services.AddSingleton<IAuthorizationHandler, ValidateUserUpdatedHandler>();
services.AddAuthorization(options =>
{
//fallback policy is used when there's no auth attribute.
Expand All @@ -37,24 +40,11 @@ public static void AddLexBoxAuth(IServiceCollection services,
options.FallbackPolicy = options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireDefaultLexboxAuth()
.Build();
foreach (var audience in Enum.GetValues<LexboxAudience>())
{
if (audience == LexboxAudience.Unknown) continue;
//for exclusive the endpoint is only accessible for the specified audience
options.AddPolicy(audience.PolicyName(true), builder =>
{
builder.RequireAuthenticatedUser();
builder.AddRequirements(new AudienceRequirement(audience));
});
//for non exclusive the endpoint is also accessible for the default audience
options.AddPolicy(audience.PolicyName(false), builder =>
{
builder.RequireAuthenticatedUser();
builder.AddRequirements(new AudienceRequirement(audience, LexboxAudience.LexboxApi));
});
}
//don't use RequireDefaultLexboxAuth here because that only allows the default audience
options.AddPolicy(AllowAnyAudienceAttribute.PolicyName, builder => builder.RequireAuthenticatedUser());
//we still need this policy, without it the default policy is used which requires the default audience
options.AddPolicy(RequireAudienceAttribute.PolicyName, builder => builder.RequireAuthenticatedUser());
options.AddPolicy(AdminRequiredAttribute.PolicyName,
builder => builder.RequireDefaultLexboxAuth()
Expand Down Expand Up @@ -181,7 +171,7 @@ public static void AddLexBoxAuth(IServiceCollection services,
public static AuthorizationPolicyBuilder RequireDefaultLexboxAuth(this AuthorizationPolicyBuilder builder)
{
return builder.RequireAuthenticatedUser()
.AddRequirements(new AudienceRequirement(LexboxAudience.LexboxApi));
.AddRequirements(new RequireAudienceAttribute(LexboxAudience.LexboxApi, true));
}

public static bool IsJwtRequest(this HttpRequest request)
Expand Down
23 changes: 0 additions & 23 deletions backend/LexBoxApi/Auth/RequireAudienceAttribute.cs

This file was deleted.

19 changes: 6 additions & 13 deletions backend/LexBoxApi/Auth/Requirements/AudienceRequirementHandler.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,14 @@
using LexCore.Auth;
using LexBoxApi.Auth.Attributes;
using LexCore.Auth;
using Microsoft.AspNetCore.Authorization;
using Microsoft.IdentityModel.JsonWebTokens;

namespace LexBoxApi.Auth.Requirements;

public class AudienceRequirement : IAuthorizationRequirement
public class AudienceRequirementHandler(ILogger<AudienceRequirementHandler> logger) : AuthorizationHandler<RequireAudienceAttribute>
{
public LexboxAudience[] ValidAudiences { get; }

public AudienceRequirement(params LexboxAudience[] validAudiences)
{
ValidAudiences = validAudiences;
}
}

public class AudienceRequirementHandler : AuthorizationHandler<AudienceRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AudienceRequirement requirement)
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
RequireAudienceAttribute requirement)
{
var claim = context.User.FindFirst(LexAuthConstants.AudienceClaimType);
if (Enum.TryParse<LexboxAudience>(claim?.Value, out var audience) &&
Expand All @@ -26,6 +18,7 @@ protected override Task HandleRequirementAsync(AuthorizationHandlerContext conte
}
else
{
logger.LogInformation("Token does not have the required audience: [{Audience}] not in {ValidAudiences}", claim?.Value, string.Join(',', requirement.ValidAudiences));
context.Fail(new AuthorizationFailureReason(this,
$"Token does not have the required audience: {requirement.ValidAudiences}"));
}
Expand Down
28 changes: 28 additions & 0 deletions backend/LexBoxApi/Auth/Requirements/ValidateUserUpdatedHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using LexBoxApi.Auth.Attributes;
using LexBoxApi.Services;
using LexData;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;

namespace LexBoxApi.Auth.Requirements;

public class ValidateUserUpdatedHandler(IHttpContextAccessor httpContextAccessor, ILogger<ValidateUserUpdatedHandler> logger) : AuthorizationHandler<RequireCurrentUserInfoAttribute>
{
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, RequireCurrentUserInfoAttribute requirement)
{
var httpContext = httpContextAccessor.HttpContext;
var user = httpContext?.RequestServices.GetRequiredService<LoggedInContext>().MaybeUser;
if (user is null) return;
var userService = httpContext!.RequestServices.GetRequiredService<UserService>();
var actualUpdatedDate = await userService.GetUserUpdatedDate(user.Id);
if (actualUpdatedDate != user.UpdatedDate)
{
logger.LogInformation("User has been updated since login, {UpdatedDate} != {ActualUpdatedDate}", user.UpdatedDate, actualUpdatedDate);
context.Fail(new AuthorizationFailureReason(this, "User has been updated since login"));
}
else
{
context.Succeed(requirement);
}
}
}
3 changes: 2 additions & 1 deletion backend/LexBoxApi/Controllers/AdminController.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations;
using LexBoxApi.Auth;
using LexBoxApi.Auth.Attributes;
using LexBoxApi.Services;
using LexCore;
using LexData;
Expand Down Expand Up @@ -33,9 +34,9 @@ public record ResetPasswordAdminRequest([Required(AllowEmptyStrings = false)] st
public async Task<ActionResult> ResetPasswordAdmin(ResetPasswordAdminRequest request)
{
var passwordHash = request.PasswordHash;
var lexAuthUser = _loggedInContext.User;
var user = await _lexBoxDbContext.Users.FirstAsync(u => u.Id == request.userId);
user.PasswordHash = PasswordHashing.HashPassword(passwordHash, user.Salt, true);
user.UpdateUpdatedDate();
await _lexBoxDbContext.SaveChangesAsync();
await _emailService.SendPasswordChangedEmail(user);
return Ok();
Expand Down
1 change: 1 addition & 0 deletions backend/LexBoxApi/Controllers/AuthTestingController.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using LexBoxApi.Auth;
using LexBoxApi.Auth.Attributes;
using LexCore.Auth;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
Expand Down
29 changes: 28 additions & 1 deletion backend/LexBoxApi/Controllers/LoginController.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations;
using LexBoxApi.Auth;
using LexBoxApi.Auth.Attributes;
using LexBoxApi.Models;
using LexBoxApi.Otel;
using LexBoxApi.Services;
Expand Down Expand Up @@ -52,11 +53,23 @@ public async Task<ActionResult> LoginRedirect(
string jwt, // This is required because auth looks for a jwt in the query string
string returnTo)
{
var user = _loggedInContext.User;
var userUpdatedDate = await _userService.GetUserUpdatedDate(user.Id);
if (userUpdatedDate != user.UpdatedDate)
{
return await EmailLinkExpired();
}
await HttpContext.SignInAsync(User,
new AuthenticationProperties { IsPersistent = true });
return Redirect(returnTo);
}

private async Task<ActionResult> EmailLinkExpired()
{
await HttpContext.SignOutAsync();
return Redirect("/login?message=link_expired");
}

[HttpGet("verifyEmail")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
Expand All @@ -73,9 +86,17 @@ public async Task<ActionResult<LexAuthUser>> VerifyEmail(
var userId = _loggedInContext.User.Id;
var user = await _lexBoxDbContext.Users.FindAsync(userId);
if (user == null) return NotFound();
//users can verify their email even if the updated date is out of sync when not changing their email
//this is to prevent some edge cases where changing their name and then using an old verify email link would fail
if (user.Email != _loggedInContext.User.Email &&
user.UpdatedDate.ToUnixTimeSeconds() != _loggedInContext.User.UpdatedDate)
{
return await EmailLinkExpired();
}

user.Email = _loggedInContext.User.Email;
user.EmailVerified = true;
user.UpdateUpdatedDate();
await _lexBoxDbContext.SaveChangesAsync();
await RefreshJwt();
return Redirect(returnTo);
Expand Down Expand Up @@ -136,12 +157,18 @@ public record ResetPasswordRequest([Required(AllowEmptyStrings = false)] string

[HttpPost("resetPassword")]
[RequireAudience(LexboxAudience.ForgotPassword)]
[RequireCurrentUserInfo]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult> ResetPassword(ResetPasswordRequest request)
{
var passwordHash = request.PasswordHash;
var lexAuthUser = _loggedInContext.User;
var user = await _lexBoxDbContext.Users.FirstAsync(u => u.Id == lexAuthUser.Id);
var user = await _lexBoxDbContext.Users.FindAsync(lexAuthUser.Id);
if (user == null) return NotFound();
user.PasswordHash = PasswordHashing.HashPassword(passwordHash, user.Salt, true);
user.UpdateUpdatedDate();
await _lexBoxDbContext.SaveChangesAsync();
await _emailService.SendPasswordChangedEmail(user);
//the old jwt is only valid for calling forgot password endpoints, we need to generate a new one
Expand Down
1 change: 1 addition & 0 deletions backend/LexBoxApi/Controllers/MigrationController.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using LexBoxApi.Auth;
using LexBoxApi.Auth.Attributes;
using LexBoxApi.Services;
using LexCore.Entities;
using LexData;
Expand Down
Loading

0 comments on commit 819fffa

Please sign in to comment.