Files
oed/api/OED.Api/Endpoints/AuthEndpoints.cs

149 lines
5.1 KiB
C#

using OED.Api.Core.Interfaces.Services;
using OED.Api.Core.Models.Eve;
using OED.Api.Infrastructure.Auth;
using OED.Api.Infrastructure.Esi;
namespace OED.Api.Endpoints;
public static class AuthEndpoints
{
public static void MapAuthEndpoints(this WebApplication app)
{
app.MapGet("/auth/login", Login);
app.MapGet("/auth/callback", Callback);
app.MapGet("/auth/me", Me);
app.MapPost("/auth/logout", Logout);
}
private static async Task<IResult> Login(IConfiguration config, IStateStore stateStore)
{
var clientId = config["Eve:ClientId"]!;
var redirectUri = Uri.EscapeDataString(config["Eve:CallbackUrl"]!);
var scopes = Uri.EscapeDataString("esi-markets.structure_markets.v1");
var state = await stateStore.GenerateAsync();
var url = "https://login.eveonline.com/v2/oauth/authorize" +
$"?response_type=code" +
$"&redirect_uri={redirectUri}" +
$"&client_id={clientId}" +
$"&scope={scopes}" +
$"&state={state}";
return Results.Redirect(url);
}
private static async Task<IResult> Callback(
HttpContext context,
string code,
string state,
IConfiguration config,
IHttpClientFactory httpClientFactory,
EveJwtValidator jwtValidator,
ISessionService sessionService,
ITokenStore tokenStore,
IStateStore stateStore,
ILoggerFactory loggerFactory)
{
var logger = loggerFactory.CreateLogger("AuthEndpoints");
logger.LogInformation("Callback hit. Code: {Code}, State: {State}", code, state);
var frontendUrl = config["Frontend:Url"]!;
// Validate and immediately consume the state — prevents replay
if (!await stateStore.ValidateAndConsumeAsync(state))
{
logger.LogWarning("State validation failed for state: {State}", state);
return Results.Redirect($"{frontendUrl}/?error=invalid_state");
}
var clientId = config["Eve:ClientId"]!;
var secretKey = config["Eve:SecretKey"]!;
var credentials = Convert.ToBase64String(
System.Text.Encoding.UTF8.GetBytes($"{clientId}:{secretKey}")
);
var client = httpClientFactory.CreateClient();
var tokenRequest = new HttpRequestMessage(HttpMethod.Post, "https://login.eveonline.com/v2/oauth/token");
tokenRequest.Headers.Add("Authorization", $"Basic {credentials}");
tokenRequest.Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "authorization_code",
["code"] = code
});
var tokenResponse = await client.SendAsync(tokenRequest);
if (!tokenResponse.IsSuccessStatusCode)
return Results.Redirect($"{frontendUrl}/?error=token_exchange_failed");
var tokens = await tokenResponse.Content.ReadFromJsonAsync<EsiTokenResponse>()
?? throw new InvalidOperationException("Empty response from EVE SSO");
// Validate JWT — throws if tampered, expired, or wrong issuer
var jwt = await jwtValidator.ValidateAsync(tokens.AccessToken, clientId);
var sub = jwt.Subject; // "CHARACTER:EVE:123456789"
var characterId = long.Parse(sub.Split(':')[2]);
var characterName = jwt.Claims.First(c => c.Type == "name").Value;
var scopes = jwt.Claims
.Where(c => c.Type == "scp")
.Select(c => c.Value)
.ToArray();
await tokenStore.SetAccessTokenAsync(
characterId,
tokens.AccessToken,
TimeSpan.FromSeconds(tokens.ExpiresIn - 30)
);
await tokenStore.SetRefreshTokenAsync(characterId, tokens.RefreshToken);
var session = new EveSession
{
CharacterId = characterId,
CharacterName = characterName,
Scopes = scopes
};
var sessionId = await sessionService.CreateAsync(session);
context.Response.Cookies.Append("session", sessionId, new CookieOptions
{
HttpOnly = true,
Secure = false,
SameSite = SameSiteMode.Lax,
Domain = "localhost",
Expires = DateTimeOffset.UtcNow.AddDays(7)
});
logger.LogInformation("Session created: {SessionId}, redirecting to {Url}", sessionId, frontendUrl);
return Results.Redirect($"{frontendUrl}/auth/callback?success=true");
}
private static IResult Me(HttpContext context)
{
var session = context.Items["Session"] as EveSession;
if (session is null) return Results.Unauthorized();
return Results.Ok(new
{
session.CharacterId,
session.CharacterName,
session.Scopes
});
}
private static async Task<IResult> Logout(
HttpContext context,
ISessionService sessionService)
{
if (context.Request.Cookies.TryGetValue("session", out var sessionId))
{
await sessionService.DeleteAsync(sessionId);
context.Response.Cookies.Delete("session");
}
return Results.Ok();
}
}