Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User SecurityStamp validation added #99

Merged
merged 3 commits into from
Jun 29, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,27 @@ namespace OneBeyond.Studio.Obelisk.Authentication.Application.CommandHandlers;
internal sealed class SignOutHandler : IRequestHandler<SignOut>
{
private readonly SignInManager<AuthUser> _signInManager;
private readonly UserManager<AuthUser> _userManager;

public SignOutHandler(
UserManager<AuthUser> userManager,
SignInManager<AuthUser> signInManager
)
{
EnsureArg.IsNotNull(userManager, nameof(userManager));
EnsureArg.IsNotNull(signInManager, nameof(signInManager));

_userManager = userManager;
_signInManager = signInManager;
}

public async Task Handle(SignOut command, CancellationToken cancellationToken)
{
EnsureArg.IsNotNull(command, nameof(command));

var identityUser = await _userManager.FindByIdAsync(command.LoginId).ConfigureAwait(false);
//We use SecurityStampValidator to invalidate all existing auth cookies for this user
await _userManager.UpdateSecurityStampAsync(identityUser!).ConfigureAwait(false);
await _signInManager.SignOutAsync().ConfigureAwait(false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ public static IdentityBuilder AddApplicationAuthentication(
services.AddSingleton((_) =>
configuration.GetOptions<CookieAuthNOptions>("CookieAuthN"));

services.Configure<SecurityStampValidatorOptions>(options =>
{
//Every time we logout we update the user security stamp to make sure that all existing auth cookies are invalidated
andriikaplanovskyi marked this conversation as resolved.
Show resolved Hide resolved
options.ValidationInterval = TimeSpan.FromSeconds(10);
fabiomaistro marked this conversation as resolved.
Show resolved Hide resolved
});

//NOTE: Order is important
//App auth uses Identity which will override any configured options for forwarding
//If using JWT and Identity combined, UseCombinedScheme must come last
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using OneBeyond.Studio.Crosscuts.Exceptions;
using OneBeyond.Studio.Crosscuts.Logging;
Expand Down Expand Up @@ -44,6 +45,7 @@ public override async Task ValidatePrincipal(CookieValidatePrincipalContext cont

try
{
await SecurityStampValidator.ValidatePrincipalAsync(context); //We validate security stamp to make sure that after a user is logged out, all existing cookies are invalid.
await _authFlowHandler.OnValidatingLoginAsync(loginId, CancellationToken.None);
await base.ValidatePrincipal(context);
Logger.LogInformation(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
using EnsureThat;
using MediatR;

namespace OneBeyond.Studio.Obelisk.Authentication.Domain.Commands;

public sealed record SignOut : IRequest
{
public SignOut(
string loginId)
{
EnsureArg.IsNotNullOrWhiteSpace(loginId, nameof(loginId));

LoginId = loginId;
}

public string LoginId { get; }
}
20 changes: 13 additions & 7 deletions src/OneBeyond.Studio.Obelisk.WebApi/Controllers/AuthController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,21 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OneBeyond.Studio.Application.SharedKernel.AmbientContexts;
using OneBeyond.Studio.Crosscuts.Logging;
using OneBeyond.Studio.Crosscuts.Utilities.Identities;
using OneBeyond.Studio.Obelisk.Application.Exceptions;
using OneBeyond.Studio.Obelisk.Application.Features.Users.Dto;
using OneBeyond.Studio.Obelisk.Application.Features.Users.Queries;
using OneBeyond.Studio.Obelisk.Application.Services.AmbientContexts;
using OneBeyond.Studio.Obelisk.Authentication.Domain;
using OneBeyond.Studio.Obelisk.Authentication.Domain.Commands;
using OneBeyond.Studio.Obelisk.Authentication.Domain.TfaAuthentication;
using OneBeyond.Studio.Obelisk.Authentication.Domain.TfaAuthentication.Commands;
using OneBeyond.Studio.Obelisk.Domain.Features.Users.Commands;
using OneBeyond.Studio.Obelisk.WebApi.Helpers;
using OneBeyond.Studio.Obelisk.WebApi.Requests.Auth;
using AmbientContext = OneBeyond.Studio.Obelisk.Application.Services.AmbientContexts.AmbientContext;
using SignInResult = OneBeyond.Studio.Obelisk.Authentication.Domain.SignInResult;

namespace OneBeyond.Studio.Obelisk.WebApi.Controllers;
Expand All @@ -28,21 +29,26 @@ namespace OneBeyond.Studio.Obelisk.WebApi.Controllers;
[ApiVersion("1.0")]
public sealed class AuthController : ControllerBase
{
private static readonly ILogger Logger = LogManager.CreateLogger<AuthController>();

private readonly IMediator _mediator;
private readonly ClientApplicationLinkGenerator _clientApplicationLinkGenerator;
private readonly IOptions<IdentityOptions> _identityOptions;
private static readonly ILogger Logger = LogManager.CreateLogger<AuthController>();
private readonly AmbientContext _ambientContext;

public AuthController(
IMediator mediator,
IAmbientContextAccessor<AmbientContext> ambientContextAccessor,
ClientApplicationLinkGenerator clientApplicationLinkGenerator,
IOptions<IdentityOptions> identityOptions)
{
EnsureArg.IsNotNull(mediator, nameof(mediator));
EnsureArg.IsNotNull(ambientContextAccessor, nameof(ambientContextAccessor));
EnsureArg.IsNotNull(clientApplicationLinkGenerator, nameof(clientApplicationLinkGenerator));
EnsureArg.IsNotNull(identityOptions, nameof(identityOptions));

_mediator = mediator;
_ambientContext = ambientContextAccessor.AmbientContext;
_clientApplicationLinkGenerator = clientApplicationLinkGenerator;
_identityOptions = identityOptions;
}
Expand Down Expand Up @@ -73,7 +79,7 @@ public Task<SignInWithRecoveryCodeResult> SignInWithRecoveryCode(
[Authorize]
[HttpPost("SignOut")]
public Task SignOut(CancellationToken cancellationToken)
=> _mediator.Send(new SignOut(), cancellationToken);
=> _mediator.Send(new SignOut(_ambientContext.GetUserContext().UserAuthId), cancellationToken);


[HttpPost("ForgotPassword")]
Expand Down Expand Up @@ -129,8 +135,8 @@ public Task<ChangePasswordResult> ChangePassword(
[FromBody] ChangePasswordRequest changePassword,
CancellationToken cancellationToken)
=> _mediator.Send(new ChangePassword(
HttpContext.User?.Identity?.TryGetLoginId() ?? throw new ObeliskApplicationException("Failed to retrieve the ID of a logged in user"),
changePassword.OldPassword,
_ambientContext.GetUserContext().UserAuthId,
changePassword.OldPassword,
changePassword.NewPassword), cancellationToken);

[HttpGet("PasswordRequirements")]
Expand Down