Skip to content

Commit

Permalink
feat: added better exception handling for expired refresh token
Browse files Browse the repository at this point in the history
  • Loading branch information
ontehfritz committed Sep 28, 2024
1 parent 75bbe51 commit 2347e23
Show file tree
Hide file tree
Showing 8 changed files with 96 additions and 67 deletions.
30 changes: 19 additions & 11 deletions src/Bulwark.Auth.Core/Authentication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
using Bulwark.Auth.Repositories.Exception;
using Bulwark.Auth.Repositories.Model;
using Bulwark.Auth.Repositories.Util;
using JWT.Exceptions;

namespace Bulwark.Auth.Core;

public class Authentication
public class Authentication
{
private readonly JwtTokenizer _tokenizer;
private readonly IAccountRepository _accountRepository;
Expand All @@ -32,7 +33,7 @@ public Authentication(

/// <summary>
/// Classic authentication, user/password if successfully authenticated tokens are returned to
/// client. The authenticated tokens must be acknowledged by the client before proceeding
/// client. The authenticated tokens must be acknowledged by the client before proceeding
/// </summary>
/// <param name="email"></param>
/// <param name="password"></param>
Expand Down Expand Up @@ -87,7 +88,7 @@ await _tokenRepository.Acknowledge(accountModel.Id,
}
catch(BulwarkDbException exception)
{
throw new BulwarkAuthenticationException("Cannot acknowledge token",
throw new BulwarkAuthenticationException("Cannot acknowledge token",
exception);
}
}
Expand Down Expand Up @@ -127,12 +128,16 @@ public async Task<AccessToken> ValidateAccessToken(string email,

return token ?? null;
}
catch (TokenExpiredException exception)
{
throw new BulwarkTokenExpiredException("Access Token Expired", exception);
}
catch (BulwarkDbException exception)
{
throw new BulwarkTokenException("Tokens cannot be validated", exception);
}
}

/// <summary>
/// Renews an acknowledged token when the access token is expired.
/// Use case: when an account access token expires use the refresh token to get a new set of tokens
Expand All @@ -145,7 +150,7 @@ public async Task<AccessToken> ValidateAccessToken(string email,
/// <exception cref="BulwarkTokenException"></exception>
/// <exception cref="BulwarkAuthenticationException"></exception>
public async Task<Authenticated> Renew(string email, string refreshToken,
string deviceId, string tokenizerName = "jwt")
string deviceId)
{
try
{
Expand Down Expand Up @@ -175,24 +180,28 @@ public async Task<Authenticated> Renew(string email, string refreshToken,

return authenticated;
}
catch (TokenExpiredException exception)
{
throw new BulwarkTokenExpiredException("Refresh Token Expired", exception);
}
catch (BulwarkDbException exception)
{
throw new BulwarkAuthenticationException("Cannot renew tokens", exception);
}
}


/// <summary>
/// Deletes an acknowledged token.
/// Use case: logging users out of a device.
/// Use case: logging users out of a device.
/// </summary>
/// <param name="email"></param>
/// <param name="accessToken"></param>
/// <param name="deviceId"></param>
/// <exception cref="BulwarkAuthenticationException"></exception>
public async Task Revoke(string email, string accessToken,
string deviceId)
{
{
if(await ValidateAccessToken(email, accessToken, deviceId) != null)
{
try{
Expand All @@ -213,7 +222,7 @@ public async Task Revoke(string email, string accessToken,
/// <exception cref="BulwarkAccountException"></exception>
private static void CheckAccountHealth(AccountModel account)
{
//Keep this order :)
//Keep this order :)
if (account.IsDeleted)
{
throw new BulwarkAccountException("Account deleted");
Expand All @@ -229,5 +238,4 @@ private static void CheckAccountHealth(AccountModel account)
throw new BulwarkAccountException("Account disabled");
}
}
}

}
39 changes: 19 additions & 20 deletions src/Bulwark.Auth.Core/JwtTokenizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ public class JwtTokenizer

private readonly SortedList<DateTime,Key> _keys = new();
private readonly Dictionary<string, ISigningAlgorithm> _signingAlgorithms = new();
private readonly int _accessTokenExpirationInMins;
private readonly int _refreshTokenExpirationInHours;
private readonly int _accessTokenExpirationInSecs;
private readonly int _refreshTokenExpirationInSecs;

public JwtTokenizer(string issuer, string audience,
int accessTokenExpInMin,
int refreshTokenExpInHours,
public JwtTokenizer(string issuer, string audience,
int accessTokenExpInSec,
int refreshTokenExpInSecs,
List<ISigningAlgorithm> signingAlgorithms,
SigningKey signingKey)
{
Expand All @@ -41,12 +41,12 @@ public JwtTokenizer(string issuer, string audience,
{
_signingAlgorithms.Add(alg.Name, alg);
}

Name = "jwt";
Issuer = issuer;
Audience = audience;
_accessTokenExpirationInMins = accessTokenExpInMin;
_refreshTokenExpirationInHours = refreshTokenExpInHours;
_accessTokenExpirationInSecs = accessTokenExpInSec;
_refreshTokenExpirationInSecs = refreshTokenExpInSecs;
}

/// <summary>
Expand Down Expand Up @@ -76,15 +76,15 @@ public string CreateAccessToken(string userId, List<string> roles, List<string>
.AddClaim("roles", roles)
.AddClaim("permissions", permissions)
.AddClaim("exp",
DateTimeOffset.UtcNow.AddMinutes(_accessTokenExpirationInMins)
DateTimeOffset.UtcNow.AddSeconds(_accessTokenExpirationInSecs)
.ToUnixTimeSeconds())
.AddClaim("sub", userId)

.Encode();

return token;
}

/// <summary>
/// This will create a refresh token for a user. Refresh tokens are longer lived tokens that can be used to
/// create new access tokens.
Expand All @@ -99,7 +99,7 @@ public string CreateRefreshToken(string userId)
{
throw new BulwarkTokenException("No keys found");
}

var token = JwtBuilder.Create()
.WithAlgorithm(_signingAlgorithms[latestKey.Algorithm.ToUpper()].GetAlgorithm(latestKey.PrivateKey,
latestKey.PublicKey))
Expand All @@ -108,23 +108,23 @@ public string CreateRefreshToken(string userId)
.Id(Guid.NewGuid().ToString())
.Issuer(Issuer)
.Audience(Audience)
.AddClaim("exp", DateTimeOffset.UtcNow.AddHours(_refreshTokenExpirationInHours)
.AddClaim("exp", DateTimeOffset.UtcNow.AddSeconds(_refreshTokenExpirationInSecs)
.ToUnixTimeSeconds())
.AddClaim("sub", userId)
.Encode();

return token;
}

public AccessToken ValidateAccessToken(string userId, string token)
{
var json = ValidateToken(userId,token);
var accessToken = JsonSerializer
.Deserialize<AccessToken>(json);
return accessToken;
}


public RefreshToken ValidateRefreshToken(string userId, string token)
{
var json = ValidateToken(userId, token);
Expand All @@ -133,7 +133,7 @@ public RefreshToken ValidateRefreshToken(string userId, string token)
return refreshToken;
}
/// <summary>
/// This will validate a refresh or access token
/// This will validate a refresh or access token
/// </summary>
/// <param name="userId"></param>
/// <param name="token"></param>
Expand All @@ -147,7 +147,7 @@ public string ValidateToken(string userId, string token)
{
throw new BulwarkTokenException("Token user miss match");
}

var keyId = decodedValue.Header["kid"].ToString();
var key = GetKey(keyId);

Expand Down Expand Up @@ -179,5 +179,4 @@ private Key GetKey(string keyId)
{
return _keys.Values.FirstOrDefault(x => x.KeyId == keyId);
}
}

}
10 changes: 3 additions & 7 deletions src/Bulwark.Auth/.env
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@ VERIFICATION_URL=https://localhost:3000/verify
FORGOT_PASSWORD_URL=https://localhost:3000/reset-password
MAGIC_LINK_URL=https://localhost:3000/magic-link
MAGIC_CODE_EXPIRE_IN_MINUTES=10
ACCESS_TOKEN_EXPIRE_IN_MINUTES=5
REFRESH_TOKEN_EXPIRE_IN_HOURS=1
SERVICE_MODE=test




ACCESS_TOKEN_EXPIRE_IN_SECONDS=300
REFRESH_TOKEN_EXPIRE_IN_SECONDS=3600
SERVICE_MODE=test
28 changes: 14 additions & 14 deletions src/Bulwark.Auth/AppConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ public class AppConfig
public string ForgotPasswordUrl { get; }
public string EmailFromAddress { get; }
public int MagicCodeExpireInMinutes { get; }
public int AccessTokenExpireInMinutes { get; }
public int RefreshTokenExpireInHours { get; }
public int AccessTokenExpireInSeconds { get; }
public int RefreshTokenExpireInSeconds { get; }
public AppConfig()
{
DbConnection = Environment.GetEnvironmentVariable("DB_CONNECTION") ?? "mongodb://localhost:27017";
Expand All @@ -34,21 +34,21 @@ public AppConfig()
MicrosoftClientId = Environment.GetEnvironmentVariable("MICROSOFT_CLIENT_ID") ?? string.Empty;
MicrosoftTenantId = Environment.GetEnvironmentVariable("MICROSOFT_TENANT_ID") ?? string.Empty;
GithubAppName = Environment.GetEnvironmentVariable("GITHUB_APP_NAME") ?? string.Empty;
Domain = Environment.GetEnvironmentVariable("DOMAIN") ??
Domain = Environment.GetEnvironmentVariable("DOMAIN") ??
throw new BulwarkAuthConfigException("DOMAIN environment variable is required.");
WebsiteName = Environment.GetEnvironmentVariable("WEBSITE_NAME") ??
WebsiteName = Environment.GetEnvironmentVariable("WEBSITE_NAME") ??
throw new BulwarkAuthConfigException("WEBSITE_NAME environment variable is required.");
EmailTemplateDir = Environment.GetEnvironmentVariable("EMAIL_TEMPLATE_DIR") ??
EmailTemplateDir = Environment.GetEnvironmentVariable("EMAIL_TEMPLATE_DIR") ??
"Templates/Email/";
EmailFromAddress = Environment.GetEnvironmentVariable("EMAIL_FROM_ADDRESS") ??
EmailFromAddress = Environment.GetEnvironmentVariable("EMAIL_FROM_ADDRESS") ??
throw new BulwarkAuthConfigException(
"EMAIL_FROM_ADDRESS environment variable is required.");
EmailFromAddress = EmailFromAddress.Trim();
EnableSmtp = Environment.GetEnvironmentVariable("ENABLE_SMTP")?.ToLower() == "true";

if (EnableSmtp)
{
EmailSmtpHost = Environment.GetEnvironmentVariable("EMAIL_SMTP_HOST") ??
EmailSmtpHost = Environment.GetEnvironmentVariable("EMAIL_SMTP_HOST") ??
throw new BulwarkAuthConfigException(
"EMAIL_SMTP_HOST environment variable is required when SMTP is enabled.");
EmailSmtpPort = int.TryParse(Environment.GetEnvironmentVariable("EMAIL_SMTP_PORT"), out var port)
Expand All @@ -58,19 +58,19 @@ public AppConfig()
EmailSmtpPass = Environment.GetEnvironmentVariable("EMAIL_SMTP_PASS") ?? String.Empty;
EmailSmtpSecure = Environment.GetEnvironmentVariable("EMAIL_SMTP_SECURE")?.ToLower() == "true";
}
VerificationUrl = Environment.GetEnvironmentVariable("VERIFICATION_URL") ??

VerificationUrl = Environment.GetEnvironmentVariable("VERIFICATION_URL") ??
throw new BulwarkAuthConfigException("VERIFICATION_URL environment variable is required.");
ForgotPasswordUrl = Environment.GetEnvironmentVariable("FORGOT_PASSWORD_URL") ??
ForgotPasswordUrl = Environment.GetEnvironmentVariable("FORGOT_PASSWORD_URL") ??
throw new BulwarkAuthConfigException("FORGOT_PASSWORD_URL environment variable is required.");
EmailFromAddress = Environment.GetEnvironmentVariable("EMAIL_FROM_ADDRESS") ??
EmailFromAddress = Environment.GetEnvironmentVariable("EMAIL_FROM_ADDRESS") ??
throw new BulwarkAuthConfigException(
"EMAIL_FROM_ADDRESS environment variable is required.");
MagicCodeExpireInMinutes = int.TryParse(Environment.GetEnvironmentVariable("MAGIC_CODE_EXPIRE_IN_MINUTES"),
out var expireInMinutes) ? expireInMinutes : 10;
AccessTokenExpireInMinutes = int.TryParse(Environment.GetEnvironmentVariable("ACCESS_TOKEN_EXPIRE_IN_MINUTES"),
AccessTokenExpireInSeconds = int.TryParse(Environment.GetEnvironmentVariable("ACCESS_TOKEN_EXPIRE_IN_MINUTES"),
out var accessExpireInMinutes) ? accessExpireInMinutes : 30;
RefreshTokenExpireInHours = int.TryParse(Environment.GetEnvironmentVariable("REFRESH_TOKEN_EXPIRE_IN_HOURS"),
RefreshTokenExpireInSeconds = int.TryParse(Environment.GetEnvironmentVariable("REFRESH_TOKEN_EXPIRE_IN_HOURS"),
out var refreshExpireInHours) ? refreshExpireInHours : 24;
}
}
17 changes: 12 additions & 5 deletions src/Bulwark.Auth/Controllers/AuthenticationController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,21 @@ public async Task<ActionResult<Authenticated>> RenewCredentials(RefreshToken pay

return authenticated;
}
catch (BulwarkTokenExpiredException exception)
{
return Problem(
title: "Refresh Expired Token",
detail: exception.Message,
statusCode: StatusCodes.Status422UnprocessableEntity
);
}
catch (BulwarkTokenException exception)
{
return Problem(
title: "Bad Tokens",
detail: exception.Message,
statusCode: StatusCodes.Status422UnprocessableEntity
);
);
}
}

Expand All @@ -121,7 +129,6 @@ await _auth.Revoke(validate.Email,
detail: exception.Message,
statusCode: StatusCodes.Status422UnprocessableEntity
);
}
}
}

}
}
}
2 changes: 1 addition & 1 deletion src/Bulwark.Auth/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@

applicationBuilder.Services.AddSingleton(passwordPolicy);
applicationBuilder.Services.AddSingleton<JwtTokenizer>(t => new JwtTokenizer("bulwark", "bulwark",
appConfig.AccessTokenExpireInMinutes, appConfig.RefreshTokenExpireInHours,
appConfig.AccessTokenExpireInSeconds, appConfig.RefreshTokenExpireInSeconds,
signingAlgorithms, t.GetService<SigningKey>()));
applicationBuilder.Services.AddSingleton(mongoClient.GetDatabase(dbName));
applicationBuilder.Services.AddTransient<ITokenRepository, MongoDbAuthToken>();
Expand Down
Loading

0 comments on commit 2347e23

Please sign in to comment.