149 lines
5.1 KiB
C#
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();
|
|
}
|
|
}
|