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

Clean up header and cookie handling + change logout endpoint #129

Merged
merged 1 commit into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
55 changes: 0 additions & 55 deletions API/Controller/Account/Authenticated/Logout.cs

This file was deleted.

10 changes: 1 addition & 9 deletions API/Controller/Account/Login.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,8 @@ public async Task<IActionResult> Login(
}, cancellationToken);

if (loginAction.IsT1) return Problem(LoginError.InvalidCredentials);


HttpContext.Response.Cookies.Append("openShockSession", loginAction.AsT0.Value, new CookieOptions
{
Expires = new DateTimeOffset(DateTime.UtcNow.Add(Duration.LoginSessionLifetime)),
Secure = true,
HttpOnly = true,
SameSite = SameSiteMode.Strict,
Domain = "." + cookieDomainToUse
});
HttpContext.SetSessionKeyCookie(loginAction.AsT0.Value, "." + cookieDomainToUse);

return RespondSuccessSimple("Successfully logged in");
}
Expand Down
12 changes: 2 additions & 10 deletions API/Controller/Account/LoginV2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,8 @@ public async Task<IActionResult> LoginV2(
}, cancellationToken);

if (loginAction.IsT1) return Problem(LoginError.InvalidCredentials);


HttpContext.Response.Cookies.Append("openShockSession", loginAction.AsT0.Value, new CookieOptions
{
Expires = new DateTimeOffset(DateTime.UtcNow.Add(Duration.LoginSessionLifetime)),
Secure = true,
HttpOnly = true,
SameSite = SameSiteMode.Strict,
Domain = "." + cookieDomainToUse
});

HttpContext.SetSessionKeyCookie(loginAction.AsT0.Value, "." + cookieDomainToUse);

return RespondSuccessSimple("Successfully logged in");
}
Expand Down
43 changes: 43 additions & 0 deletions API/Controller/Account/Logout.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc;
using OpenShock.API.Services.Session;
using OpenShock.Common.Authentication.Attributes;
using OpenShock.Common.Authentication.Services;
using OpenShock.Common.Problems;
using OpenShock.Common.Utils;

namespace OpenShock.API.Controller.Account;

public sealed partial class AccountController
{
[HttpPost("logout")]
[ProducesSlimSuccess]
[MapToApiVersion("1")]
public async Task<IActionResult> Logout(
[FromServices] ISessionService sessionService,
[FromServices] ApiConfig apiConfig)
{
// Remove session if valid
if (HttpContext.TryGetSessionKey(out var sessionKey))
{
await sessionService.DeleteSessionById(sessionKey);
}

// Make sure cookie is removed, no matter if authenticated or not
var cookieDomainToUse = apiConfig.Frontend.CookieDomain.Split(',').FirstOrDefault(domain => Request.Headers.Host.ToString().EndsWith(domain, StringComparison.OrdinalIgnoreCase));
if (cookieDomainToUse != null)
{
HttpContext.RemoveSessionKeyCookie("." + cookieDomainToUse);
}
else // Fallback to all domains
{
foreach (var domain in apiConfig.Frontend.CookieDomain.Split(','))
{
HttpContext.RemoveSessionKeyCookie("." + domain);
}
}

// its always a success, logout endpoints should be idempotent
return RespondSlimSuccess();
}
}
2 changes: 1 addition & 1 deletion API/Controller/Sessions/DeleteSessions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public sealed partial class SessionsController
[ProducesProblem(HttpStatusCode.NotFound, "SessionNotFound")]
public async Task<IActionResult> DeleteSession(Guid sessionId)
{
var loginSession = await _sessionService.GetSession(sessionId);
var loginSession = await _sessionService.GetSessionByPulbicId(sessionId);

// If the session was not found, or the user does not have the privledges to access it, return NotFound
if (loginSession == null || !CurrentUser.IsUserOrRank(loginSession.UserId, RankType.Admin))
Expand Down
2 changes: 1 addition & 1 deletion API/Controller/Sessions/ListSessions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public sealed partial class SessionsController
[ProducesSlimSuccess<IEnumerable<LoginSessionResponse>>]
public async Task<IEnumerable<LoginSessionResponse>> ListSessions()
{
var sessions = await _sessionService.ListSessions(CurrentUser.DbUser.Id);
var sessions = await _sessionService.ListSessionsByUserId(CurrentUser.DbUser.Id);

return sessions.Select(LoginSessionResponse.MapFrom);
}
Expand Down
14 changes: 7 additions & 7 deletions API/Services/Session/ISessionService.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
using OneOf;
using OneOf.Types;
using OpenShock.API.Models.Response;
using OpenShock.Common.Redis;
using OpenShock.Common.Redis;

namespace OpenShock.API.Services.Session;

public interface ISessionService
{
public Task<IEnumerable<LoginSession>> ListSessions(Guid userId);
public Task<IEnumerable<LoginSession>> ListSessionsByUserId(Guid userId);

public Task<LoginSession?> GetSession(Guid sessionId);
public Task<LoginSession?> GetSessionByPulbicId(Guid publicSessionId);

public Task<bool> DeleteSessionById(string sessionId);

public Task<bool> DeleteSessionByPublicId(Guid publicSessionId);

public Task<bool> DeleteSession(Guid sessionId);
public Task DeleteSession(LoginSession loginSession);
}
33 changes: 16 additions & 17 deletions API/Services/Session/SessionService.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
using OneOf;
using OneOf.Types;
using OpenShock.API.Models.Response;
using OpenShock.Common;
using Microsoft.EntityFrameworkCore;
using OpenShock.Common.Authentication.Handlers;
using OpenShock.Common.OpenShockDb;
using OpenShock.Common.Redis;
using Redis.OM;
using Redis.OM.Contracts;
Expand All @@ -27,33 +23,36 @@ public SessionService(IRedisConnectionProvider redisConnectionProvider)
_loginSessions = redisConnectionProvider.RedisCollection<LoginSession>();
}

public async Task<IEnumerable<LoginSession>> ListSessions(Guid userId)
public async Task<IEnumerable<LoginSession>> ListSessionsByUserId(Guid userId)
{
var sessions = await _loginSessions.Where(x => x.UserId == userId).ToListAsync();

var needsSave = false;
foreach (var session in sessions)
{
if(LoginSessionAuthentication.UpdateOlderLoginSessions(session)) needsSave = true;
if (LoginSessionAuthentication.UpdateOlderLoginSessions(session)) needsSave = true;
}
if(needsSave) await _loginSessions.SaveAsync();
if (needsSave) await _loginSessions.SaveAsync();

return sessions;
}

public async Task<LoginSession?> GetSession(Guid sessionId)
public async Task<LoginSession?> GetSessionByPulbicId(Guid publicSessionId)
{
return await _loginSessions.Where(x => x.PublicId == sessionId)
return await _loginSessions.Where(x => x.PublicId == publicSessionId)
.FirstOrDefaultAsync();
}

public async Task<bool> DeleteSession(Guid sessionId)
public async Task<bool> DeleteSessionById(string sessionId)
{
int affected = await _loginSessions.Where(x => x.Id == sessionId).ExecuteDeleteAsync();
return affected > 0;
}

public async Task<bool> DeleteSessionByPublicId(Guid publicSessionId)
{
var session = await GetSession(sessionId);
if (session == null) return false;

await _loginSessions.DeleteAsync(session);
return true;
int affected = await _loginSessions.Where(x => x.PublicId == publicSessionId).ExecuteDeleteAsync();
return affected > 0;
}

public async Task DeleteSession(LoginSession loginSession)
Expand Down
7 changes: 4 additions & 3 deletions API/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
using OpenShock.Common.Authentication;
using OpenShock.Common.Authentication.Handlers;
using OpenShock.Common.Authentication.Services;
using OpenShock.Common.Constants;
using OpenShock.Common.DataAnnotations;
using OpenShock.Common.ExceptionHandle;
using OpenShock.Common.Hubs;
Expand Down Expand Up @@ -245,9 +246,9 @@ public void ConfigureServices(IServiceCollection services)
options.ParameterFilter<AttributeFilter>();
options.OperationFilter<AttributeFilter>();
options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, "OpenShock.API.xml"), true);
options.AddSecurityDefinition("OpenShockToken", new OpenApiSecurityScheme
options.AddSecurityDefinition(AuthConstants.AuthTokenHeaderName, new OpenApiSecurityScheme
{
Name = "OpenShockToken",
Name = AuthConstants.AuthTokenHeaderName,
Type = SecuritySchemeType.ApiKey,
Scheme = "ApiKeyAuth",
In = ParameterLocation.Header,
Expand All @@ -261,7 +262,7 @@ public void ConfigureServices(IServiceCollection services)
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "OpenShockToken"
Id = AuthConstants.AuthTokenHeaderName
}
},
Array.Empty<string>()
Expand Down
14 changes: 3 additions & 11 deletions Common/Authentication/Handlers/DeviceAuthentication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using OpenShock.Common.Errors;
using OpenShock.Common.OpenShockDb;
using OpenShock.Common.Problems;
using OpenShock.Common.Utils;

namespace OpenShock.Common.Authentication.Handlers;

Expand Down Expand Up @@ -40,19 +41,10 @@ public DeviceAuthentication(

protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
string sessionKey;

if (Context.Request.Headers.TryGetValue("DeviceToken", out var sessionKeyHeader) &&
!string.IsNullOrEmpty(sessionKeyHeader))
{
sessionKey = sessionKeyHeader!;
}
else if (Context.Request.Headers.TryGetValue("Device-Token", out var sessionKeyHeader2) &&
!string.IsNullOrEmpty(sessionKeyHeader2))
if (!Context.TryGetDeviceTokenFromHeader(out string? sessionKey))
{
sessionKey = sessionKeyHeader2!;
return Fail(AuthResultError.CookieOrHeaderMissingOrInvalid);
}
else return Fail(AuthResultError.HeaderMissingOrInvalid);

var device = await _db.Devices.Where(x => x.Token == sessionKey).FirstOrDefaultAsync();
if (device == null) return Fail(AuthResultError.TokenInvalid);
Expand Down
23 changes: 10 additions & 13 deletions Common/Authentication/Handlers/LoginSessionAuthentication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,20 +50,17 @@ public LoginSessionAuthentication(

protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
if ((Context.Request.Headers.TryGetValue("OpenShockToken", out var tokenHeaderO) || Context.Request.Headers.TryGetValue("Open-Shock-Token", out tokenHeaderO)) &&
!string.IsNullOrEmpty(tokenHeaderO)) return TokenAuth(tokenHeaderO!);

if (Context.Request.Headers.TryGetValue("OpenShockSession", out var sessionKeyHeader) &&
!string.IsNullOrEmpty(sessionKeyHeader)) return SessionAuth(sessionKeyHeader!);

if (Context.Request.Cookies.TryGetValue("openShockSession", out var accessKeyCookie) &&
!string.IsNullOrEmpty(accessKeyCookie)) return SessionAuth(accessKeyCookie);

// Legacy to not break current applications
if (Context.Request.Headers.TryGetValue("ShockLinkToken", out var tokenHeader) &&
!string.IsNullOrEmpty(tokenHeader)) return TokenAuth(tokenHeader!);
if (Context.TryGetSessionKey(out var sessionKey))
{
return SessionAuth(sessionKey);
}

if (Context.TryGetAuthTokenFromHeader(out var token))
{
return TokenAuth(token);
}

return Task.FromResult(Fail(AuthResultError.HeaderMissingOrInvalid));
return Task.FromResult(Fail(AuthResultError.CookieOrHeaderMissingOrInvalid));
}

private async Task<AuthenticateResult> TokenAuth(string token)
Expand Down
7 changes: 6 additions & 1 deletion Common/Authentication/OpenShockAuthSchemas.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
namespace OpenShock.Common.Authentication;
using OpenShock.Common.Constants;

namespace OpenShock.Common.Authentication;

public static class OpenShockAuthSchemas
{
// TODO: What is this for?
public const string SessionTokenCombo = "session-token-combo";

/// TODO: Replace this with <see cref="AuthConstants.DeviceAuthTokenHeaderName"/>?
public const string DeviceToken = "device-token";
}
9 changes: 9 additions & 0 deletions Common/Constants/AuthConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace OpenShock.Common.Constants;

public static class AuthConstants
{
public const string SessionCookieName = "openShockSession";
public const string SessionHeaderName = "OpenShockSession";
public const string AuthTokenHeaderName = "OpenShockToken";
public const string DeviceAuthTokenHeaderName = "DeviceToken";
}
2 changes: 1 addition & 1 deletion Common/Errors/AuthResultError.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace OpenShock.Common.Errors;
public static class AuthResultError
{
public static OpenShockProblem UnknownError => new("Authentication.UnknownError", "An unknown error occurred.", HttpStatusCode.InternalServerError);
public static OpenShockProblem HeaderMissingOrInvalid => new("Authentication.HeaderMissingOrInvalid", "Missing a required header or it is invalid.", HttpStatusCode.Unauthorized);
public static OpenShockProblem CookieOrHeaderMissingOrInvalid => new("Authentication.HeaderMissingOrInvalid", "Missing a required authentication cookie or header or it is invalid.", HttpStatusCode.Unauthorized);

public static OpenShockProblem SessionInvalid => new("Authentication.SessionInvalid", "The session is invalid", HttpStatusCode.Unauthorized);
public static OpenShockProblem TokenInvalid => new("Authentication.TokenInvalid", "The token is invalid", HttpStatusCode.Unauthorized);
Expand Down
13 changes: 3 additions & 10 deletions Common/Hubs/ShareLinkHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,17 +54,10 @@ public override async Task OnConnectedAsync()
}

GenericIni? user = null;

if (httpContext.Request.Cookies.TryGetValue("openShockSession", out var accessKeyCookie) &&
!string.IsNullOrEmpty(accessKeyCookie))
{
user = await SessionAuth(accessKeyCookie);
}

if (httpContext.Request.Headers.TryGetValue("OpenShockSession", out var sessionKeyHeader) &&
!string.IsNullOrEmpty(sessionKeyHeader))

if (httpContext.TryGetSessionKey(out var sessionKey))
{
user = await SessionAuth(sessionKeyHeader!);
user = await SessionAuth(sessionKey);
}

// TODO: Add token auth
Expand Down
Loading
Loading