Skip to content

Commit

Permalink
Moved to cookie authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
umerfaruk committed Nov 16, 2024
1 parent 9946912 commit fd65c34
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 10 deletions.
39 changes: 33 additions & 6 deletions src/Inshapardaz.Api/Controllers/AccountsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
using Inshapardaz.Api.Views.Accounts;
using Paramore.Brighter;
using Inshapardaz.Api.Views;
using Inshapardaz.Domain.Adapters.Configuration;
using Inshapardaz.Domain.Ports.Command.Account;
using Inshapardaz.Domain.Ports.Query.Account;
using Microsoft.Extensions.Options;

namespace Inshapardaz.Api.Controllers;

Expand All @@ -17,15 +19,18 @@ public class AccountsController : Controller
private readonly IAmACommandProcessor _commandProcessor;
private readonly IQueryProcessor _queryProcessor;
private readonly IRenderAccount _accountRenderer;
private readonly Settings _settings;

public AccountsController(
IAmACommandProcessor commandProcessor,
IQueryProcessor queryProcessor,
IRenderAccount accountRenderer)
IRenderAccount accountRenderer,
IOptions<Settings> settings)
{
_commandProcessor = commandProcessor;
_queryProcessor = queryProcessor;
_accountRenderer = accountRenderer;
_settings = settings.Value;
}


Expand All @@ -37,7 +42,8 @@ public async Task<ActionResult<AuthenticateResponse>> Authenticate(AuthenticateR
{
var command = new AuthenticateCommand(model.Email, model.Password);
await _commandProcessor.SendAsync(command, cancellationToken: cancellationToken);
setTokenCookie(command.Response.RefreshToken);
SetRefreshTokenCookie(command.Response.RefreshToken);
SetAccessTokenCookie(command.Response.AccessToken);

return Ok(_accountRenderer.Render(command.Response));
}
Expand All @@ -51,7 +57,8 @@ public async Task<ActionResult<AuthenticateResponse>> RefreshToken([FromBody] Re
var refreshToken = model.RefreshToken ?? Request.Cookies["refreshToken"];
var command = new RefreshTokenCommand(refreshToken);
await _commandProcessor.SendAsync(command, cancellationToken: cancellationToken);
setTokenCookie(command.Response.RefreshToken);
SetRefreshTokenCookie(command.Response.RefreshToken);
SetAccessTokenCookie(command.Response.AccessToken);
return Ok(_accountRenderer.Render(command.Response));
}

Expand All @@ -65,6 +72,8 @@ public async Task<IActionResult> RevokeToken([FromBody] RevokeTokenRequest model
var token = model.Token ?? Request.Cookies["refreshToken"];
var command = new RevokeTokenCommand(token);
await _commandProcessor.SendAsync(command, cancellationToken: cancellationToken);
SetRefreshTokenCookie("expired-refresh-token", true);
SetAccessTokenCookie("expired-token", true);
return Ok();
}

Expand Down Expand Up @@ -179,7 +188,7 @@ public async Task<IActionResult> ChangePassword(ChangePasswordRequest model, Can
return Ok();
}

[HttpGet(Name = nameof(AccountsController.GetAll))]
[HttpGet(Name = nameof(GetAll))]
public async Task<IActionResult> GetAll(string query, int pageNumber = 1, int pageSize = 10, CancellationToken token = default(CancellationToken))
{
var accountsQuery = new GetAccountsQuery(pageNumber, pageSize) { Query = query };
Expand Down Expand Up @@ -275,12 +284,13 @@ public IActionResult Delete(int id)
return NotFound();
}

private void setTokenCookie(string token)
private void SetRefreshTokenCookie(string token, bool expire = false)
{
var cookieOptions = new CookieOptions
{
HttpOnly = true,
Expires = DateTime.UtcNow.AddDays(7),
Expires = expire ? DateTimeOffset.MinValue : DateTime.UtcNow.AddDays(20),
Domain = _settings.Domain,
#if DEBUG
SameSite = SameSiteMode.Lax
#else
Expand All @@ -290,4 +300,21 @@ private void setTokenCookie(string token)
};
Response.Cookies.Append("refreshToken", token, cookieOptions);
}

private void SetAccessTokenCookie(string token, bool expire = false)
{
var cookieOptions = new CookieOptions
{
HttpOnly = true,
Expires = expire ? DateTimeOffset.MinValue : DateTime.UtcNow.AddHours(1),
Domain = _settings.Domain,
#if DEBUG
SameSite = SameSiteMode.Lax
#else
SameSite = SameSiteMode.None,
Secure = true
#endif
};
Response.Cookies.Append("token", token, cookieOptions);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System.IdentityModel.Tokens.Jwt;
using System.Text;
using Microsoft.IdentityModel.Tokens;
using Inshapardaz.Domain.Adapters.Configuration;
using Microsoft.Extensions.Options;
using Inshapardaz.Domain.Adapters.Repositories;

namespace Inshapardaz.Api.Infrastructure.Middleware;

public class CookieAuthenticationMiddleware
{
private readonly RequestDelegate _next;
private readonly Settings _appSettings;

public CookieAuthenticationMiddleware(RequestDelegate next, IOptions<Settings> appSettings)
{
_next = next;
_appSettings = appSettings.Value;
}

public async Task Invoke(HttpContext context, IAccountRepository accountRepository)
{
if (context.Items["AccountId"] is null)
{
var token = context.Request.Cookies["token"];

if (token != null)
{
await AttachAccountToContext(context, token, accountRepository);
}
}

await _next(context);
}

private async Task AttachAccountToContext(HttpContext context, string token, IAccountRepository accountRepository)
{
try
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_appSettings.Security.Secret);
tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false,
// set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later)
ClockSkew = TimeSpan.FromMinutes(1),
}, out SecurityToken validatedToken);

var accessToken = (JwtSecurityToken)validatedToken;
var accountId = int.Parse(accessToken.Claims.First(x => x.Type == "id").Value);

context.Items["AccountId"] = accountId;
context.Items["Account"] = await accountRepository.GetAccountById(accountId, CancellationToken.None);
}
catch (SecurityTokenValidationException)
{
throw new UnauthorizedAccessException("JWT token invalid or expired");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ public async Task Invoke(HttpContext context)
case KeyNotFoundException e:
// not found error
response.StatusCode = (int)HttpStatusCode.NotFound;
break;
case UnauthorizedAccessException e:
response.StatusCode = (int)HttpStatusCode.Unauthorized;
break;
default:
// unhandled error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ private async Task AttachAccountToContext(HttpContext context, string token, IAc
ValidateIssuer = false,
ValidateAudience = false,
// set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later)
ClockSkew = TimeSpan.Zero,
ClockSkew = TimeSpan.FromMinutes(1),
}, out SecurityToken validatedToken);

var accessToken = (JwtSecurityToken)validatedToken;
Expand All @@ -52,8 +52,9 @@ private async Task AttachAccountToContext(HttpContext context, string token, IAc
context.Items["AccountId"] = accountId;
context.Items["Account"] = await accountRepository.GetAccountById(accountId, CancellationToken.None);
}
catch
catch (SecurityTokenValidationException)
{
throw new UnauthorizedAccessException("JWT token invalid or expired");
}
}
}
1 change: 1 addition & 0 deletions src/Inshapardaz.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@
app.UseMiddleware<LibraryConfigurationMiddleware>();
app.UseStatusCodeMiddleWare();
app.UseMiddleware<JwtMiddleware>();
app.UseMiddleware<CookieAuthenticationMiddleware>();

app.MapControllers();

Expand Down
5 changes: 3 additions & 2 deletions src/Inshapardaz.Api/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
},
"Security": {
"Secret": "52F2133CD6854CC2B68DCAD0CF8F9802",
"AccessTokenTTLInMinutes": 20,
"AccessTokenTTLInMinutes": 10,
"ResetTokenTTLInDays": 1,
"RefreshTokenTTLInDays": 2,
"ResetPasswordPagePath": "/account/forgot-password",
Expand All @@ -36,6 +36,7 @@
},
"FrontEndUrl": "",
"Allowed_Origins": "",
"DefaultLibraryId" : 1
"DefaultLibraryId" : 1,
"Domain": "localhost"
}
}
2 changes: 2 additions & 0 deletions src/Inshapardaz.Domain/Adapters/Configuration/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ public record Settings
public int DefaultLibraryId { get; init; }

public bool SaveDownloadsToStorage { get; init; } = false;

public string? Domain { get; set; }
}

0 comments on commit fd65c34

Please sign in to comment.