Executando verificação de segurança...
1

Keycloak em .NET: resolvendo auth de vez… ou só trocando o problema?

Projeto .NET crescendo, começando a surgir papo de SSO, múltiplos clientes, talvez multi-tenant no futuro… e lá vou eu pensar:
“Vou seguir com ASP.NET Identity + OpenIddict?”
“Faço algo mais simples agora e pago o preço depois?”
“Ou já meto um Keycloak e resolvo isso de uma vez?”

Já usei Keycloak antes. Não vou mentir: a primeira experiência não foi das mais amigáveis. Realms, mappers, roles, claims… no começo parece que você entrou numa sala escura cheia de botões. Mas depois que a ficha cai, a sensação é meio: “ok, isso aqui é poderoso”.

O que mais me pega é o trade-off.

Por um lado:

Não reinventar auth (porque todo mundo que já tentou sabe o inferno que é).

Ter algo que roda local, não te prende em cloud específica.

Integra bem com AD, LDAP, OIDC, etc.

Resolve muita coisa com configuração, não código.

Por outro:

É Java, pesado, consome recurso.

Customizar tela/tema é chato.

Upgrade grande sempre dá aquele frio na espinha.

Pra um projeto menor ou solo dev, às vezes parece overkill.

E aí fico pensando:
Será que usar Keycloak é maturidade arquitetural…
ou só trocar um tipo de dor de cabeça por outro?

Hoje, sinceramente, minha sensação é que auth nunca é “divertido”, só existe o “menos pior”. E talvez o erro seja achar que dá pra fugir disso escrevendo a própria solução.

Queria ouvir de vocês, sem papo de marketing:

Quem usa Keycloak em .NET hoje, se arrependeu?

Em projeto pequeno/médio, vocês iriam nele ou ficariam no Identity?

Vale a pena entrar no “ecossistema” Keycloak desde cedo ou só quando o bicho cresce?

Alguém já migrou pra fora dele e por quê?

Curioso pra saber experiências reais, boas e ruins. Auth sempre parece simples… até não ser 😅

Carregando publicação patrocinada...
1

O .NET tem o identity que é um sistema EXTREMAMENTE poderoso.

Com poucas linhas de código vc já tem o backend de autorização inteiro.

Porque usar uma ferramenta externa que só vai trazer dor de cabeça?

Imagina cada request Http ter que bater no keycloak para validar? isso é o que? 40ms a mais A CADA REQUEST, sendo que uma consulta no DB vai aumentar no máximo 1ms (se for necessário consultar no DB)


No DB Context:

AppDbContext : IdentityDbContext<User, Role, Guid>
AppDbContext : IdentityDbContext<IdentityUser<Guid>, IdentityRole<Guid>, Guid> // Se não precisar modificar

No program.cs:

services.AddIdentity<User, Role>(options =>
{
    options.Password.RequireDigit = true;
    options.Password.RequireLowercase = true;
    options.Password.RequireUppercase = true;
    options.Password.RequireNonAlphanumeric = true;
    options.Password.RequiredLength = 8;

    options.User.RequireUniqueEmail = true;

    options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);
    options.Lockout.MaxFailedAccessAttempts = 5;
    options.Lockout.AllowedForNewUsers = true;

    options.SignIn.RequireConfirmedEmail = false;
    options.SignIn.RequireConfirmedPhoneNumber = false;
})
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();

services.ConfigureApplicationCookie(options =>
{
    options.Cookie.HttpOnly = true;
    options.Cookie.SecurePolicy = environment.IsDevelopment() 
        ? CookieSecurePolicy.SameAsRequest 
        : CookieSecurePolicy.Always;
    options.Cookie.SameSite = SameSiteMode.Lax;
    options.Cookie.Name = "Auth";
    options.Cookie.Path = "/";
    options.ExpireTimeSpan = TimeSpan.FromDays(7);
    options.SlidingExpiration = true;

    options.Events.OnRedirectToLogin = context =>
    {
        context.Response.StatusCode = StatusCodes.Status401Unauthorized;
        return Task.CompletedTask;
    };

    options.Events.OnRedirectToAccessDenied = context =>
    {
        context.Response.StatusCode = StatusCodes.Status403Forbidden;
        return Task.CompletedTask;
    };
});

Services.AddAuthorization();

var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();

É só isso e está funcionando!

Não precisa de serviço externo,
Não precisa inventar a roda

Melhor ainda?

Toma o serviço de autenticação pronto pra ti:

internal class AuthenticationService : IAuthenticationService
{
    private readonly UserManager<User> userManager;
    private readonly SignInManager<User> signInManager;
    private readonly AppDbContext context;

    public AuthenticationService(
        UserManager<User> userManager,
        SignInManager<User> signInManager,
        AppDbContext context)
    {
        this.userManager = userManager;
        this.signInManager = signInManager;
        this.context = context;
    }

    public async Task<(bool Success, string? Message, User? User)> Login(
        string identifier,
        string password,
        bool rememberMe,
        CancellationToken cancellationToken = default)
    {
        var user = await userManager.Users
            .FirstOrDefaultAsync(u => 
                u.Email == identifier || u.NormalizedEmail == identifier.ToUpper(), 
                cancellationToken);

        if (user == null)
        {
            return (false, "The email or password you entered is incorrect. Please try again.", null);
        }

        var result = await signInManager.PasswordSignInAsync(
            user,
            password,
            rememberMe,
            lockoutOnFailure: true);

        if (!result.Succeeded)
        {
            if (result.IsLockedOut)
            {
                return (false, "Your account has been locked due to multiple failed login attempts. Please try again later.", null);
            }
            return (false, "The email or password you entered is incorrect. Please try again.", null);
        }

        await context.Entry(user)
            .LoadAsync(cancellationToken);

        return (true, "Login successful", user);
    }

    public async Task Logout(CancellationToken cancellationToken = default)
    {
        await signInManager.SignOutAsync();
    }

    public Task<User?> GetCurrentUser(Guid userId, CancellationToken cancellationToken = default)
    {
        return userManager.Users
            .AsNoTracking()
            .Where(u => u.Id == userId)
            .FirstOrDefaultAsync(cancellationToken);
    }

    public async Task<User> RegisterUser(
        string email,
        string password,
        string firstName,
        string lastName,
        string? phoneNumber,
        Guid? organizationId,
        CancellationToken cancellationToken = default)
    {
        var existingEmailUser = await userManager.FindByEmailAsync(email);
        if (existingEmailUser != null)
        {
            throw new Exception("Email is already registered");
        }

        var user = new User
        {
            Id = Guid.NewGuid(),
            UserName = email,
            Email = email,
            FirstName = firstName,
            LastName = lastName,
            EmailConfirmed = false
        };

        var result = await userManager.CreateAsync(user, password);

        if (!result.Succeeded)
        {
            throw new Exception(string.Join(", ", result.Errors.Select(e => e.Description)));
        }

        await signInManager.SignInAsync(user, isPersistent: false);

        return user;
    }

    public async Task<bool> AddUserToRole(Guid userId, string roleName, CancellationToken cancellationToken = default)
    {
        var user = await userManager.FindByIdAsync(userId.ToString());

        if (user == null)
            return false;

        var result = await userManager.AddToRoleAsync(user, roleName);
        return result.Succeeded;
    }

    public async Task<IEnumerable<string>> GetUserRoles(Guid userId, CancellationToken cancellationToken = default)
    {
        var user = await userManager.FindByIdAsync(userId.ToString());

        if (user == null)
            return Enumerable.Empty<string>();

        return await userManager.GetRolesAsync(user);
    }

    private async Task<bool> EnsureUserClaims(User user)
    {
        //// Exemplo de como colocar informações personalizadas do usuário no cookie
        var existingClaims = await userManager.GetClaimsAsync(user);
        var claimsToAdd = new List<Claim>();

        if (user.OrganizationId.HasValue && user.OrganizationId.Value != Guid.Empty)
        {
            var orgClaim = existingClaims.FirstOrDefault(c => c.Type == "OrganizationId");
            if (orgClaim == null)
            {
                claimsToAdd.Add(new Claim("OrganizationId", user.OrganizationId.ToString()!));
            }
        }

        if (claimsToAdd.Count > 0)
        {
            await userManager.AddClaimsAsync(user, claimsToAdd);
            await signInManager.RefreshSignInAsync(user);
            return true;
        }

        return false;
    }
}

Aceito um pix de presente por te dar toda a autenticação pronta hahahah