Epilogue Auth Docs

C# / ASP.NET

Implement Epilogue Auth with C# and ASP.NET Core

Installation

dotnet new webapi -n MyApp
cd MyApp
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer

Configuration

Add to appsettings.json:

{
  "EpilogueAuth": {
    "AuthUrl": "https://auth.epilogue.team/authorize/your-app-id",
    "AppId": "your-app-id",
    "AppSecret": "your-app-secret"
  }
}

Models

// Models/User.cs
public class User
{
    public string Id { get; set; } = Guid.NewGuid().ToString();
    public string AuthUserId { get; set; } = string.Empty;
    public string Name { get; set; } = string.Empty;
    public string? ProfileImage { get; set; }
    public ICollection<Session> Sessions { get; set; } = new List<Session>();
}

// Models/Session.cs
public class Session
{
    public string Id { get; set; } = Guid.NewGuid().ToString();
    public string Token { get; set; } = string.Empty;
    public string UserId { get; set; } = string.Empty;
    public User User { get; set; } = null!;
    public DateTime ExpiresAt { get; set; }
}

DTOs

// DTOs/EpilogueAuthDtos.cs
public record TokenRequest(string AuthorizationCode, string ApplicationSecret);
public record TokenResponse(string Token);
public record EpilogueUser(string Id, string Username, string? IconUrl);

DbContext

// Data/AppDbContext.cs
public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
    
    public DbSet<User> Users => Set<User>();
    public DbSet<Session> Sessions => Set<Session>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<User>()
            .HasIndex(u => u.AuthUserId)
            .IsUnique();
            
        modelBuilder.Entity<Session>()
            .HasIndex(s => s.Token)
            .IsUnique();
    }
}

Auth Controller

// Controllers/AuthController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Security.Cryptography;

[ApiController]
[Route("api/auth")]
public class AuthController : ControllerBase
{
    private readonly AppDbContext _db;
    private readonly IConfiguration _config;
    private readonly HttpClient _http;

    public AuthController(AppDbContext db, IConfiguration config, HttpClient http)
    {
        _db = db;
        _config = config;
        _http = http;
    }

    [HttpGet("start")]
    public IActionResult Start()
    {
        var authUrl = _config["EpilogueAuth:AuthUrl"];
        return Redirect(authUrl!);
    }

    [HttpGet("callback")]
    public async Task<IActionResult> Callback([FromQuery] string? code)
    {
        if (string.IsNullOrEmpty(code))
            return BadRequest(new { error = "Missing code" });

        try
        {
            var appId = _config["EpilogueAuth:AppId"];
            var appSecret = _config["EpilogueAuth:AppSecret"];

            // Exchange code for token
            var tokenRequest = new TokenRequest(code, appSecret!);
            var tokenResponse = await _http.PostAsJsonAsync(
                $"https://auth.epilogue.team/api/v1/authorize/{appId}",
                tokenRequest
            );
            var tokenData = await tokenResponse.Content.ReadFromJsonAsync<TokenResponse>();

            // Fetch user info
            _http.DefaultRequestHeaders.Authorization = 
                new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", tokenData!.Token);
            var userResponse = await _http.GetFromJsonAsync<EpilogueUser>(
                "https://auth.epilogue.team/api/v1/app/me"
            );

            // Find or create user
            var user = await _db.Users.FirstOrDefaultAsync(u => u.AuthUserId == userResponse!.Id);
            
            if (user == null)
            {
                user = new User
                {
                    AuthUserId = userResponse!.Id,
                    Name = userResponse.Username ?? $"user_{userResponse.Id}",
                    ProfileImage = userResponse.IconUrl
                };
                _db.Users.Add(user);
                await _db.SaveChangesAsync();
            }

            // Create session
            var sessionToken = GenerateRandomString(200);
            var session = new Session
            {
                Token = sessionToken,
                UserId = user.Id,
                ExpiresAt = DateTime.UtcNow.AddDays(1)
            };
            _db.Sessions.Add(session);
            await _db.SaveChangesAsync();

            // Return token to client
            return Content($@"
                <html><body><script>
                    localStorage.setItem('token', '{sessionToken}');
                    window.location.href = '/';
                </script></body></html>
            ", "text/html");
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
            return StatusCode(500, new { error = "Internal server error" });
        }
    }

    [HttpGet("me")]
    public async Task<IActionResult> Me()
    {
        var token = Request.Headers.Authorization.ToString().Replace("Bearer ", "")
                    ?? Request.Query["token"].ToString();

        if (string.IsNullOrEmpty(token))
            return Unauthorized(new { error = "Authentication required" });

        var session = await _db.Sessions
            .Include(s => s.User)
            .FirstOrDefaultAsync(s => s.Token == token && s.ExpiresAt > DateTime.UtcNow);

        if (session == null)
            return Unauthorized(new { error = "Invalid or expired token" });

        return Ok(new
        {
            id = session.User.Id,
            name = session.User.Name,
            profileImage = session.User.ProfileImage
        });
    }

    private static string GenerateRandomString(int length)
    {
        const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
        return new string(Enumerable.Range(0, length)
            .Select(_ => chars[RandomNumberGenerator.GetInt32(chars.Length)])
            .ToArray());
    }
}

Program.cs

// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddHttpClient();
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));

var app = builder.Build();

app.MapControllers();
app.Run();