Skip to content

Commit

Permalink
Added expiry for refresh tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
jezzsantos committed Feb 10, 2024
1 parent f7c642c commit 22b05c5
Show file tree
Hide file tree
Showing 24 changed files with 197 additions and 110 deletions.
4 changes: 3 additions & 1 deletion src/Application.Resources.Shared/Identity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ public class AuthenticateTokens
{
public required string AccessToken { get; set; }

public required DateTime ExpiresOn { get; set; }
public required DateTime AccessTokenExpiresOn { get; set; }

public required string RefreshToken { get; set; }

public required DateTime RefreshTokenExpiresOn { get; set; }

public required string UserId { get; set; }
}

Expand Down
20 changes: 12 additions & 8 deletions src/IdentityApplication.UnitTests/AuthTokensApplicationSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,21 +56,22 @@ public async Task WhenIssueTokensAsyncAndUserNotExist_ThenReturnsTokens()
var expiresOn = DateTime.UtcNow.AddMinutes(1);
_jwtTokensService.Setup(jts => jts.IssueTokensAsync(It.IsAny<EndUserWithMemberships>()))
.Returns(Task.FromResult<Result<AccessTokens, Error>>(
new AccessTokens("anaccesstoken", "arefreshtoken", expiresOn)));
new AccessTokens("anaccesstoken", expiresOn, "arefreshtoken", expiresOn)));
_repository.Setup(rep => rep.FindByUserIdAsync(It.IsAny<Identifier>(), It.IsAny<CancellationToken>()))
.Returns(Task.FromResult<Result<Optional<AuthTokensRoot>, Error>>(Optional<AuthTokensRoot>.None));

var result = await _application.IssueTokensAsync(_caller.Object, user, CancellationToken.None);

result.Value.AccessToken.Should().Be("anaccesstoken");
result.Value.RefreshToken.Should().Be("arefreshtoken");
result.Value.ExpiresOn.Should().Be(expiresOn);
result.Value.AccessTokenExpiresOn.Should().Be(expiresOn);
_jwtTokensService.Verify(jts => jts.IssueTokensAsync(user));
_repository.Verify(rep => rep.SaveAsync(It.Is<AuthTokensRoot>(at =>
at.Id == "anid"
&& at.AccessToken == "anaccesstoken"
&& at.RefreshToken == "arefreshtoken"
&& at.AccessTokenExpiresOn == expiresOn
&& at.RefreshTokenExpiresOn == expiresOn
), It.IsAny<CancellationToken>()));
}

Expand All @@ -85,21 +86,22 @@ public async Task WhenIssueTokensAsyncAndUserExists_ThenReturnsTokens()
var expiresOn = DateTime.UtcNow.AddMinutes(1);
_jwtTokensService.Setup(jts => jts.IssueTokensAsync(It.IsAny<EndUserWithMemberships>()))
.Returns(Task.FromResult<Result<AccessTokens, Error>>(
new AccessTokens("anaccesstoken", "arefreshtoken", expiresOn)));
new AccessTokens("anaccesstoken", expiresOn, "arefreshtoken", expiresOn)));
_repository.Setup(rep => rep.FindByUserIdAsync(It.IsAny<Identifier>(), It.IsAny<CancellationToken>()))
.Returns(Task.FromResult<Result<Optional<AuthTokensRoot>, Error>>(authTokens.ToOptional()));

var result = await _application.IssueTokensAsync(_caller.Object, user, CancellationToken.None);

result.Value.AccessToken.Should().Be("anaccesstoken");
result.Value.RefreshToken.Should().Be("arefreshtoken");
result.Value.ExpiresOn.Should().Be(expiresOn);
result.Value.AccessTokenExpiresOn.Should().Be(expiresOn);
_jwtTokensService.Verify(jts => jts.IssueTokensAsync(user));
_repository.Verify(rep => rep.SaveAsync(It.Is<AuthTokensRoot>(at =>
at.Id == "anid"
&& at.AccessToken == "anaccesstoken"
&& at.RefreshToken == "arefreshtoken"
&& at.AccessTokenExpiresOn == expiresOn
&& at.RefreshTokenExpiresOn == expiresOn
), It.IsAny<CancellationToken>()));
}

Expand Down Expand Up @@ -127,27 +129,28 @@ public async Task WhenRefreshTokenAsyncAndTokensExist_ThenReturnsRefreshedTokens
var expiresOn1 = DateTime.UtcNow.AddMinutes(1);
var expiresOn2 = DateTime.UtcNow.AddMinutes(2);
var authTokens = AuthTokensRoot.Create(_recorder.Object, _idFactory.Object, "auserid".ToId()).Value;
authTokens.SetTokens("anaccesstoken1", "arefreshtoken1", expiresOn1);
authTokens.SetTokens("anaccesstoken1", "arefreshtoken1", expiresOn1, expiresOn1);
_repository.Setup(rep => rep.FindByRefreshTokenAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.Returns(Task.FromResult<Result<Optional<AuthTokensRoot>, Error>>(authTokens.ToOptional()));
_endUsersService.Setup(eus =>
eus.GetMembershipsAsync(It.IsAny<ICallerContext>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.Returns(Task.FromResult<Result<EndUserWithMemberships, Error>>(user));
_jwtTokensService.Setup(jts => jts.IssueTokensAsync(It.IsAny<EndUserWithMemberships>()))
.Returns(Task.FromResult<Result<AccessTokens, Error>>(
new AccessTokens("anaccesstoken2", "arefreshtoken2", expiresOn2)));
new AccessTokens("anaccesstoken2", expiresOn2, "arefreshtoken2", expiresOn2)));

var result = await _application.RefreshTokenAsync(_caller.Object, "arefreshtoken1", CancellationToken.None);

result.Value.AccessToken.Should().Be("anaccesstoken2");
result.Value.RefreshToken.Should().Be("arefreshtoken2");
result.Value.ExpiresOn.Should().Be(expiresOn2);
result.Value.AccessTokenExpiresOn.Should().Be(expiresOn2);
_jwtTokensService.Verify(jts => jts.IssueTokensAsync(user));
_repository.Verify(rep => rep.SaveAsync(It.Is<AuthTokensRoot>(at =>
at.Id == "anid"
&& at.AccessToken == "anaccesstoken2"
&& at.RefreshToken == "arefreshtoken2"
&& at.AccessTokenExpiresOn == expiresOn2
&& at.RefreshTokenExpiresOn == expiresOn2
), It.IsAny<CancellationToken>()));
}

Expand All @@ -170,7 +173,7 @@ public async Task WhenRevokeRefreshTokenAsync_ThenRevokes()
{
var expiresOn = DateTime.UtcNow.AddMinutes(1);
var authTokens = AuthTokensRoot.Create(_recorder.Object, _idFactory.Object, "auserid".ToId()).Value;
authTokens.SetTokens("anaccesstoken", "arefreshtoken", expiresOn);
authTokens.SetTokens("anaccesstoken", "arefreshtoken", expiresOn, expiresOn);
_repository.Setup(rep => rep.FindByRefreshTokenAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.Returns(Task.FromResult<Result<Optional<AuthTokensRoot>, Error>>(authTokens.ToOptional()));

Expand All @@ -181,6 +184,7 @@ public async Task WhenRevokeRefreshTokenAsync_ThenRevokes()
&& at.AccessToken == Optional<string>.None
&& at.RefreshToken == Optional<string>.None
&& at.AccessTokenExpiresOn == Optional<DateTime>.None
&& at.RefreshTokenExpiresOn == Optional<DateTime>.None
), It.IsAny<CancellationToken>()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -259,16 +259,17 @@ public async Task WhenAuthenticateAsyncWithCorrectPassword_ThenReturnsError()
_authTokensService.Setup(jts =>
jts.IssueTokensAsync(It.IsAny<ICallerContext>(), It.IsAny<EndUserWithMemberships>(),
It.IsAny<CancellationToken>()))
.Returns(Task.FromResult<Result<AccessTokens, Error>>(new AccessTokens("anaccesstoken", "arefreshtoken",
expiresOn)));
.Returns(Task.FromResult<Result<AccessTokens, Error>>(new AccessTokens("anaccesstoken", expiresOn,
"arefreshtoken", expiresOn)));

var result =
await _application.AuthenticateAsync(_caller.Object, "ausername", "apassword", CancellationToken.None);

result.Should().BeSuccess();
result.Value.AccessToken.Should().Be("anaccesstoken");
result.Value.RefreshToken.Should().Be("arefreshtoken");
result.Value.ExpiresOn.Should().Be(expiresOn);
result.Value.AccessTokenExpiresOn.Should().Be(expiresOn);
result.Value.RefreshTokenExpiresOn.Should().Be(expiresOn);
_repository.Verify(rep => rep.SaveAsync(It.IsAny<PasswordCredentialRoot>(), It.IsAny<CancellationToken>()));
_recorder.Verify(rec => rec.AuditAgainst(It.IsAny<ICallContext>(), "auserid",
Audits.PasswordCredentialsApplication_Authenticate_Succeeded, It.IsAny<string>(),
Expand Down
10 changes: 7 additions & 3 deletions src/IdentityApplication/ApplicationServices/IJWTTokensService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,20 @@ public interface IJWTTokensService

public struct AccessTokens
{
public AccessTokens(string accessToken, string refreshToken, DateTime expiresOn)
public AccessTokens(string accessToken, DateTime accessTokenExpiresOn, string refreshToken,
DateTime refreshTokenExpiresOn)
{
AccessToken = accessToken;
RefreshToken = refreshToken;
ExpiresOn = expiresOn;
AccessTokenExpiresOn = accessTokenExpiresOn;
RefreshTokenExpiresOn = refreshTokenExpiresOn;
}

public string AccessToken { get; }

public string RefreshToken { get; }

public DateTime ExpiresOn { get; }
public DateTime AccessTokenExpiresOn { get; }

public DateTime RefreshTokenExpiresOn { get; }
}
6 changes: 4 additions & 2 deletions src/IdentityApplication/AuthTokensApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ public async Task<Result<AccessTokens, Error>> IssueTokensAsync(ICallerContext c
authTokens = root.Value;
}

var set = authTokens.SetTokens(tokens.AccessToken, tokens.RefreshToken, tokens.ExpiresOn);
var set = authTokens.SetTokens(tokens.AccessToken, tokens.RefreshToken, tokens.AccessTokenExpiresOn,
tokens.RefreshTokenExpiresOn);
if (!set.IsSuccessful)
{
return set.Error;
Expand Down Expand Up @@ -107,7 +108,8 @@ public async Task<Result<AccessTokens, Error>> RefreshTokenAsync(ICallerContext
}

var tokens = issued.Value;
var renewed = authTokens.RenewTokens(refreshToken, tokens.AccessToken, tokens.RefreshToken, tokens.ExpiresOn);
var renewed = authTokens.RenewTokens(refreshToken, tokens.AccessToken, tokens.RefreshToken,
tokens.AccessTokenExpiresOn, tokens.RefreshTokenExpiresOn);
if (!renewed.IsSuccessful)
{
return Error.NotAuthenticated();
Expand Down
3 changes: 2 additions & 1 deletion src/IdentityApplication/PasswordCredentialsApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,8 @@ public async Task<Result<AuthenticateTokens, Error>> AuthenticateAsync(ICallerCo
{
AccessToken = tokens.AccessToken,
RefreshToken = tokens.RefreshToken,
ExpiresOn = tokens.ExpiresOn,
AccessTokenExpiresOn = tokens.AccessTokenExpiresOn,
RefreshTokenExpiresOn = tokens.RefreshTokenExpiresOn,
UserId = user.Id
});

Expand Down
4 changes: 3 additions & 1 deletion src/IdentityApplication/Persistence/ReadModels/AuthToken.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ public class AuthToken : ReadModelEntity
{
public Optional<string> AccessToken { get; set; }

public Optional<DateTime> ExpiresOn { get; set; }
public Optional<DateTime> AccessTokenExpiresOn { get; set; }

public Optional<string> RefreshToken { get; set; }

public Optional<string> UserId { get; set; }

public Optional<DateTime> RefreshTokenExpiresOn { get; set; }
}
59 changes: 35 additions & 24 deletions src/IdentityDomain.UnitTests/AuthTokensRootSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,74 +36,84 @@ public void WhenConstructed_ThenInitialized()
[Fact]
public void WhenSetTokens_ThenSetsTokens()
{
var expiresOn = DateTime.UtcNow.AddMinutes(1);
var accessTokenExpiresOn = DateTime.UtcNow.AddMinutes(1);
var refreshTokenExpiresOn = DateTime.UtcNow.AddMinutes(2);

_authTokens.SetTokens("anaccesstoken", "arefreshtoken", expiresOn);
_authTokens.SetTokens("anaccesstoken", "arefreshtoken", accessTokenExpiresOn, refreshTokenExpiresOn);

_authTokens.AccessToken.Should().BeSome("anaccesstoken");
_authTokens.RefreshToken.Should().BeSome("arefreshtoken");
_authTokens.AccessTokenExpiresOn.Should().BeSome(expiresOn);
_authTokens.AccessTokenExpiresOn.Should().BeSome(accessTokenExpiresOn);
_authTokens.RefreshTokenExpiresOn.Should().BeSome(refreshTokenExpiresOn);
_authTokens.Events.Last().Should().BeOfType<Events.AuthTokens.TokensChanged>();
}

[Fact]
public void WhenRenewTokensAndOldTokenNotMatch_ThenReturnsError()
{
var expiresOn1 = DateTime.UtcNow.AddMinutes(1);
var expiresOn2 = expiresOn1.AddMinutes(2);
_authTokens.SetTokens("anaccesstoken1", "arefreshtoken1", expiresOn1);
var expiresOn = DateTime.UtcNow.AddMinutes(1);
_authTokens.SetTokens("anaccesstoken1", "arefreshtoken1", expiresOn, expiresOn);

var result = _authTokens.RenewTokens("anotherrefreshtoken", "anaccesstoken2", "arefreshtoken2", expiresOn2);
var result = _authTokens.RenewTokens("anotherrefreshtoken", "anaccesstoken2", "arefreshtoken2", expiresOn,
expiresOn);

result.Should().BeError(ErrorCode.RuleViolation, Resources.AuthTokensRoot_RefreshTokenNotMatched);
}

[Fact]
public void WhenRenewTokensAndRevoked_ThenReturnsError()
{
var expiresOn1 = DateTime.UtcNow.AddMinutes(1);
var expiresOn2 = expiresOn1.AddMinutes(2);
_authTokens.SetTokens("anaccesstoken1", "arefreshtoken1", expiresOn1);
var expiresOn = DateTime.UtcNow.AddMinutes(1);
_authTokens.SetTokens("anaccesstoken1", "arefreshtoken1", expiresOn, expiresOn);
_authTokens.Revoke("arefreshtoken1");

var result = _authTokens.RenewTokens("arefreshtoken1", "anaccesstoken2", "arefreshtoken2", expiresOn2);
var result =
_authTokens.RenewTokens("arefreshtoken1", "anaccesstoken2", "arefreshtoken2", expiresOn, expiresOn);

result.Should().BeError(ErrorCode.RuleViolation, Resources.AuthTokensRoot_TokensRevoked);
}

[Fact]
public void WhenRenewTokensAndOldTokenIsExpired_ThenReturnsError()
{
var expiresOn1 = DateTime.UtcNow.SubtractSeconds(1);
var expiresOn2 = DateTime.UtcNow.AddMinutes(2);
var accessTokenExpiresOn1 = DateTime.UtcNow.AddMinutes(1);
var refreshTokenExpiresOn1 = DateTime.UtcNow.SubtractSeconds(1);
var accessTokenExpiresOn2 = DateTime.UtcNow.AddMinutes(2);
var refreshTokenExpiresOn2 = accessTokenExpiresOn1.AddMinutes(2);
#if TESTINGONLY
_authTokens.TestingOnly_SetTokens("anaccesstoken1", "arefreshtoken1", expiresOn1);
_authTokens.TestingOnly_SetTokens("anaccesstoken1", "arefreshtoken1", accessTokenExpiresOn1,
refreshTokenExpiresOn1);
#endif
var result = _authTokens.RenewTokens("arefreshtoken1", "anaccesstoken2", "arefreshtoken2", expiresOn2);
var result = _authTokens.RenewTokens("arefreshtoken1", "anaccesstoken2", "arefreshtoken2",
accessTokenExpiresOn2, refreshTokenExpiresOn2);

result.Should().BeError(ErrorCode.RuleViolation, Resources.AuthTokensRoot_RefreshTokenExpired);
}

[Fact]
public void WhenRenewTokens_ThenUpdatesTokens()
{
var expiresOn1 = DateTime.UtcNow.AddMinutes(1);
var expiresOn2 = expiresOn1.AddMinutes(2);
_authTokens.SetTokens("anaccesstoken1", "arefreshtoken1", expiresOn1);
var accessTokenExpiresOn1 = DateTime.UtcNow.AddMinutes(1);
var refreshTokenExpiresOn1 = DateTime.UtcNow.AddMinutes(1);
var accessTokenExpiresOn2 = accessTokenExpiresOn1.AddMinutes(2);
var refreshTokenExpiresOn2 = accessTokenExpiresOn1.AddMinutes(2);
_authTokens.SetTokens("anaccesstoken1", "arefreshtoken1", accessTokenExpiresOn1, refreshTokenExpiresOn1);

_authTokens.RenewTokens("arefreshtoken1", "anaccesstoken2", "arefreshtoken2", expiresOn2);
_authTokens.RenewTokens("arefreshtoken1", "anaccesstoken2", "arefreshtoken2", accessTokenExpiresOn2,
refreshTokenExpiresOn2);

_authTokens.AccessToken.Should().BeSome("anaccesstoken2");
_authTokens.RefreshToken.Should().BeSome("arefreshtoken2");
_authTokens.AccessTokenExpiresOn.Should().BeSome(expiresOn2);
_authTokens.AccessTokenExpiresOn.Should().BeSome(accessTokenExpiresOn2);
_authTokens.RefreshTokenExpiresOn.Should().BeSome(refreshTokenExpiresOn2);
_authTokens.Events.Last().Should().BeOfType<Events.AuthTokens.TokensRefreshed>();
}

[Fact]
public void WhenRevokeAndRevoked_ThenReturnsError()
{
var expiresOn = DateTime.UtcNow.AddMinutes(1);
_authTokens.SetTokens("anaccesstoken1", "arefreshtoken1", expiresOn);
_authTokens.SetTokens("anaccesstoken1", "arefreshtoken1", expiresOn, expiresOn);
_authTokens.Revoke("arefreshtoken1");

var result = _authTokens.Revoke("arefreshtoken1");
Expand All @@ -114,8 +124,8 @@ public void WhenRevokeAndRevoked_ThenReturnsError()
[Fact]
public void WhenRevokeAndOldTokenNotMatched_ThenReturnsError()
{
var expiresOn1 = DateTime.UtcNow.AddMinutes(1);
_authTokens.SetTokens("anaccesstoken1", "arefreshtoken1", expiresOn1);
var expiresOn = DateTime.UtcNow.AddMinutes(1);
_authTokens.SetTokens("anaccesstoken1", "arefreshtoken1", expiresOn, expiresOn);

var result = _authTokens.Revoke("anotherrefreshtoken");

Expand All @@ -126,13 +136,14 @@ public void WhenRevokeAndOldTokenNotMatched_ThenReturnsError()
public void WhenRevoke_ThenDeletesTokens()
{
var expiresOn = DateTime.UtcNow.AddMinutes(1);
_authTokens.SetTokens("anaccesstoken", "arefreshtoken", expiresOn);
_authTokens.SetTokens("anaccesstoken", "arefreshtoken", expiresOn, expiresOn);

_authTokens.Revoke("arefreshtoken");

_authTokens.AccessToken.Should().BeNone();
_authTokens.RefreshToken.Should().BeNone();
_authTokens.AccessTokenExpiresOn.Should().BeNone();
_authTokens.RefreshTokenExpiresOn.Should().BeNone();
_authTokens.Events.Last().Should().BeOfType<Events.AuthTokens.TokensRevoked>();
}
}
Loading

0 comments on commit 22b05c5

Please sign in to comment.