Resposta rápida: para implementar login seguro em Go, use OpenID Connect (OIDC) sobre OAuth 2.0, com o fluxo Authorization Code, state aleatório, nonce, PKCE quando aplicável e validação completa do ID Token. O navegador deve receber apenas um cookie de sessão HttpOnly, Secure e SameSite; não confie em dados enviados pelo frontend e não use um access token genérico como identidade. Google, Keycloak, Auth0, Okta e Microsoft Entra ID seguem essa arquitetura quando oferecem discovery OIDC.
OAuth 2.0 e OpenID Connect aparecem juntos em documentação, bibliotecas e telas de configuração, mas resolvem problemas diferentes. OAuth 2.0 permite que uma aplicação obtenha autorização limitada para acessar um recurso. OIDC acrescenta uma camada de identidade: informa quem autenticou, para qual cliente o token foi emitido e em qual provedor a autenticação aconteceu.
Este guia mostra o fluxo completo em uma aplicação web Go: redirect para o provedor, callback, troca do código, validação do ID Token, criação de sessão e logout. O exemplo usa golang.org/x/oauth2 e github.com/coreos/go-oidc/v3/oidc, funcionando como base tanto para Google quanto para um realm do Keycloak.
OAuth 2.0 não é, sozinho, um protocolo de login
Uma confusão comum é receber um access token e concluir que o usuário está autenticado. Esse token foi criado para autorizar chamadas a uma API. Seu formato pode ser JWT ou opaco; as claims variam; o destinatário normalmente é o recurso protegido, não sua aplicação web.
No OIDC, o provedor também entrega um ID Token. Ele contém claims padronizadas e assinadas, como:
iss: emissor do token;sub: identificador estável do usuário naquele emissor;aud: cliente para o qual o token foi emitido;exp: expiração;iat: momento de emissão;nonce: valor vinculado à tentativa de login, quando enviado;emaileemail_verified: quando solicitados e disponibilizados.
A chave de usuário local mais segura costuma ser a combinação issuer + subject, não o e-mail. E-mails podem mudar, ser reutilizados ou existir em mais de um provedor. Guarde o e-mail como atributo atualizável e aplique a política do produto antes de autorizar acesso.
Se sua aplicação já usa tokens próprios para APIs, veja também o guia de autenticação JWT com Go. JWT e OIDC se relacionam, mas não são sinônimos: OIDC define fluxo, validações e significado das claims; JWT é apenas um formato possível de token.
Fluxo recomendado: Authorization Code
Para uma aplicação web com backend Go, o fluxo funciona assim:
- O usuário acessa
/login. - O backend cria
state,noncee, se usado, o verificador PKCE. - Esses valores são vinculados a uma tentativa de login curta e de uso único.
- O navegador é redirecionado ao authorization endpoint do provedor.
- O provedor autentica o usuário e redireciona para
/callback?code=...&state=.... - O backend compara
statee troca o código pelo canal servidor-servidor. - O backend verifica assinatura, issuer, audience, expiração e nonce do ID Token.
- A aplicação cria ou localiza o usuário e abre uma sessão própria.
- O navegador recebe apenas o cookie opaco da sessão.
O Implicit Flow não é a escolha atual para esse cenário. Também evite o fluxo em que a aplicação coleta usuário e senha do provedor. Sua aplicação não deve receber a senha da conta Google, Keycloak ou Microsoft.
Configuração por discovery OIDC
Provedores OIDC publicam um documento de discovery em /.well-known/openid-configuration. A biblioteca usa esse documento para descobrir endpoints e chaves públicas, evitando URLs espalhadas pelo código.
Variáveis de ambiente típicas:
OIDC_ISSUER=https://accounts.google.com
OIDC_CLIENT_ID=seu-client-id
OIDC_CLIENT_SECRET=seu-client-secret
OIDC_REDIRECT_URL=https://app.exemplo.com/auth/callback
Para Keycloak, o issuer costuma incluir o realm:
OIDC_ISSUER=https://sso.exemplo.com/realms/minha-empresa
O redirect URI cadastrado no provedor deve corresponder exatamente ao usado pela aplicação. Não aceite um redirect_uri arbitrário vindo da query string. Quando precisar voltar o usuário a uma página interna, armazene um caminho relativo validado dentro do estado da tentativa de login.
Instale as dependências:
go get golang.org/x/oauth2
go get github.com/coreos/go-oidc/v3/oidc
Inicializando o cliente OIDC em Go
A configuração pode ser montada no startup da aplicação:
package auth
import (
"context"
"fmt"
"github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/oauth2"
)
type Service struct {
oauth2Config oauth2.Config
verifier *oidc.IDTokenVerifier
}
func New(ctx context.Context, issuer, clientID, clientSecret, redirectURL string) (*Service, error) {
provider, err := oidc.NewProvider(ctx, issuer)
if err != nil {
return nil, fmt.Errorf("descobrir provedor OIDC: %w", err)
}
config := oauth2.Config{
ClientID: clientID,
ClientSecret: clientSecret,
Endpoint: provider.Endpoint(),
RedirectURL: redirectURL,
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
}
return &Service{
oauth2Config: config,
verifier: provider.Verifier(&oidc.Config{
ClientID: clientID,
}),
}, nil
}
Falhar ao carregar discovery no startup normalmente é melhor do que subir um serviço que só quebra quando o primeiro usuário tenta entrar. Configure timeout no contexto de inicialização e monitore erros de DNS, TLS e issuer incorreto. Para padrões de deadline, consulte context, timeout e cancelamento em Go.
state, nonce e PKCE: três valores com funções diferentes
Não use um único valor constante para tudo.
stateprotege a continuidade do fluxo e ajuda a impedir CSRF no callback.noncevincula o ID Token à solicitação iniciada pela aplicação.- PKCE vincula a troca do código a um segredo efêmero criado pelo cliente.
Gere valores com crypto/rand, nunca com math/rand:
package auth
import (
"crypto/rand"
"encoding/base64"
"fmt"
)
func randomURLSafe(n int) (string, error) {
buf := make([]byte, n)
if _, err := rand.Read(buf); err != nil {
return "", fmt.Errorf("gerar aleatório seguro: %w", err)
}
return base64.RawURLEncoding.EncodeToString(buf), nil
}
Armazene a tentativa no servidor, com expiração curta:
type LoginAttempt struct {
State string
Nonce string
PKCEVerifier string
ReturnPath string
ExpiresAt time.Time
}
Esse registro deve ser consumido uma única vez. Uma implementação pequena pode usar um store em memória protegido por mutex, mas isso falha quando há múltiplas réplicas. Em produção, use uma sessão assinada e cuidadosamente limitada ou um armazenamento compartilhado, como Redis, com TTL e operação atômica de leitura/remoção.
Handler de login com PKCE
As versões atuais de golang.org/x/oauth2 oferecem helpers para PKCE. O handler cria a tentativa e redireciona:
func (s *Service) Login(w http.ResponseWriter, r *http.Request) {
state, err := randomURLSafe(32)
if err != nil {
http.Error(w, "não foi possível iniciar o login", http.StatusInternalServerError)
return
}
nonce, err := randomURLSafe(32)
if err != nil {
http.Error(w, "não foi possível iniciar o login", http.StatusInternalServerError)
return
}
verifier := oauth2.GenerateVerifier()
attempt := LoginAttempt{
State: state,
Nonce: nonce,
PKCEVerifier: verifier,
ReturnPath: "/app",
ExpiresAt: time.Now().Add(10 * time.Minute),
}
if err := s.attempts.Save(r.Context(), attempt); err != nil {
http.Error(w, "não foi possível iniciar o login", http.StatusInternalServerError)
return
}
url := s.oauth2Config.AuthCodeURL(
state,
oidc.Nonce(nonce),
oauth2.S256ChallengeOption(verifier),
)
http.Redirect(w, r, url, http.StatusFound)
}
Não registre state, nonce, código, tokens ou client secret em logs. Para observabilidade, registre um identificador interno da tentativa e o motivo sanitizado da falha. O guia de logging estruturado com slog mostra como manter campos úteis sem vazar credenciais.
Callback: trocando o código e validando o ID Token
O callback precisa rejeitar ausência de código, state desconhecido, tentativa expirada e tentativa já consumida antes de criar sessão.
func (s *Service) Callback(w http.ResponseWriter, r *http.Request) {
if providerError := r.URL.Query().Get("error"); providerError != "" {
http.Error(w, "login cancelado ou recusado", http.StatusUnauthorized)
return
}
state := r.URL.Query().Get("state")
code := r.URL.Query().Get("code")
if state == "" || code == "" {
http.Error(w, "callback inválido", http.StatusBadRequest)
return
}
attempt, err := s.attempts.Take(r.Context(), state)
if err != nil || time.Now().After(attempt.ExpiresAt) {
http.Error(w, "tentativa de login inválida ou expirada", http.StatusUnauthorized)
return
}
token, err := s.oauth2Config.Exchange(
r.Context(),
code,
oauth2.VerifierOption(attempt.PKCEVerifier),
)
if err != nil {
http.Error(w, "não foi possível concluir o login", http.StatusUnauthorized)
return
}
rawIDToken, ok := token.Extra("id_token").(string)
if !ok {
http.Error(w, "provedor não retornou ID Token", http.StatusUnauthorized)
return
}
idToken, err := s.verifier.Verify(r.Context(), rawIDToken)
if err != nil {
http.Error(w, "ID Token inválido", http.StatusUnauthorized)
return
}
if idToken.Nonce != attempt.Nonce {
http.Error(w, "nonce inválido", http.StatusUnauthorized)
return
}
var claims struct {
Subject string `json:"sub"`
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
Name string `json:"name"`
}
if err := idToken.Claims(&claims); err != nil || claims.Subject == "" {
http.Error(w, "claims inválidas", http.StatusUnauthorized)
return
}
user, err := s.users.UpsertOIDCUser(r.Context(), idToken.Issuer, claims.Subject, claims.Email, claims.Name)
if err != nil {
http.Error(w, "não foi possível criar a sessão", http.StatusInternalServerError)
return
}
if err := s.sessions.Start(w, r, user.ID); err != nil {
http.Error(w, "não foi possível criar a sessão", http.StatusInternalServerError)
return
}
http.Redirect(w, r, attempt.ReturnPath, http.StatusSeeOther)
}
Verify valida assinatura, issuer, audience e expiração conforme a configuração. A verificação de nonce permanece responsabilidade da aplicação. Dependendo do risco, valide também políticas como domínio permitido, grupo, tenant, email_verified ou nível de autenticação. Essas regras devem ser explícitas e testadas — uma claim existir não significa que ela concede acesso automaticamente.
Sessão própria: cookie opaco em vez de tokens no navegador
Depois do callback, a aplicação normalmente não precisa enviar o ID Token ao browser. Crie um ID de sessão aleatório, armazene no servidor apenas o necessário e envie um cookie como:
http.SetCookie(w, &http.Cookie{
Name: "__Host-session",
Value: sessionID,
Path: "/",
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
MaxAge: 8 * 60 * 60,
})
O prefixo __Host- exige Secure, Path=/ e ausência de Domain, reduzindo ambiguidades de escopo em navegadores compatíveis. Rotacione a sessão após o login para evitar session fixation. No logout, invalide a sessão no servidor e expire o cookie.
SameSite=Lax costuma funcionar para o callback OIDC via navegação de nível superior. Se sua arquitetura exige SameSite=None, o cookie também precisa ser Secure, e você deve revisar com mais cuidado CSRF, iframes e contexto cross-site. CORS não resolve CSRF; veja CORS em Go para entender a separação.
Se a aplicação precisa chamar APIs do provedor, guarde access e refresh tokens criptografados no servidor, com acesso mínimo e política de rotação. Não solicite scopes que o produto não usa. Para apenas autenticar, openid profile email já deve ser questionado: talvez openid email baste.
Google e Keycloak: o que muda
A implementação central permanece a mesma, mas a configuração e as claims variam.
| Item | Keycloak | |
|---|---|---|
| Issuer | https://accounts.google.com | URL do realm |
| Cadastro do cliente | Google Cloud Console | Admin Console do realm |
| Usuários | Contas Google permitidas | Diretório configurado no realm |
| Claims customizadas | Mais limitadas | Mappers, roles e groups configuráveis |
| Logout federado | Regras do Google | Endpoint e sessão do realm |
| Multiempresa | Política da aplicação/domínio | Realms, clients, groups ou organizations |
No Keycloak, não autorize apenas porque a claim roles contém uma string que parece correta. Defina se a regra usa realm roles, client roles ou groups e normalize isso em uma camada de autorização. No Google, valide email_verified se o e-mail participar da política e confirme o domínio permitido de acordo com as claims e garantias documentadas pelo provedor — não apenas com strings.HasSuffix(email, "@empresa.com").
Erros comuns em OAuth 2.0 e OIDC com Go
Usar e-mail como chave primária imutável
Prefira issuer + sub. Atualize nome e e-mail como perfil, sem criar uma nova conta toda vez que um atributo mudar.
Aceitar qualquer issuer ou audience
Um JWT bem assinado por outro cliente ou outro tenant não deve autenticar na sua aplicação. Fixe issuer e client ID na configuração do servidor.
Não consumir state depois do callback
state deve expirar e ser de uso único. Apenas comparar com um cookie permanente não oferece o mesmo controle de replay e correlação.
Confiar em claims sem política
Autenticação responde quem é o usuário. Autorização decide o que ele pode fazer. Mantenha papéis e permissões em regras explícitas, como descrito no guia de autenticação e autorização em APIs Go.
Expor tokens em URL ou logs
Query strings aparecem em histórico, proxies, analytics e logs. O authorization code é curto e de uso único; tokens devem circular em respostas e armazenamento protegidos, nunca em redirects montados pela aplicação.
Ignorar rotação de chaves
Use discovery e JWKS do provedor por uma biblioteca mantida. Não copie uma chave pública para o código supondo que ela será eterna.
Como testar sem depender do provedor real
Separe três camadas:
- geração e persistência da tentativa de login;
- cliente OIDC/OAuth e validação de token;
- criação de usuário e sessão.
Nos testes de handler, use httptest e uma interface pequena para o serviço de identidade. Cubra pelo menos:
- callback sem
state; statedesconhecido, expirado e reutilizado;- erro devolvido pelo provedor;
- troca do código com falha;
- ID Token ausente;
- assinatura, issuer ou audience inválidos;
- nonce diferente;
subausente;- usuário válido com rotação de sessão;
- redirect interno permitido;
- tentativa de open redirect bloqueada.
Para um teste de integração, suba um emissor OIDC de teste ou um Keycloak descartável com Testcontainers em Go. Não coloque client secrets reais em fixtures ou no repositório. Rode também go test -race ./..., análise estática e govulncheck no CI.
Checklist para produção
- Authorization Code é usado; Implicit Flow não é usado.
- issuer, client ID e redirect URI vêm de configuração confiável.
-
stateenonceusamcrypto/rand, TTL curto e uso único. - PKCE é aplicado quando suportado pela arquitetura e pelo provedor.
- ID Token tem assinatura, issuer, audience, expiração e nonce validados.
- usuário local é identificado por
issuer + sub. - claims de e-mail, domínio, grupos e roles têm política explícita.
- tokens e secrets não aparecem em logs, URLs ou mensagens de erro.
- sessão é rotacionada no login e invalidada no logout.
- cookie usa
Secure,HttpOnly,SameSitee escopo mínimo. - redirects pós-login aceitam apenas destinos internos autorizados.
- access/refresh tokens ficam no servidor quando realmente necessários.
- falhas de discovery, JWKS e troca de token têm métricas e alertas sanitizados.
- callback e regras de autorização têm testes negativos.
Perguntas frequentes
Qual é a diferença entre OAuth 2.0 e OpenID Connect?
OAuth 2.0 delega acesso a recursos. OpenID Connect acrescenta autenticação e um ID Token com identidade verificável. Para “entrar com Google” ou autenticar contra Keycloak, use OIDC, não uma interpretação improvisada do access token.
Qual fluxo usar em uma aplicação web Go?
Use Authorization Code, com troca do código feita pelo backend. Acrescente PKCE quando aplicável e sempre use state. Para OIDC, envie e valide também nonce.
Posso guardar o ID Token no cookie?
É possível construir arquiteturas stateless, mas uma sessão opaca costuma simplificar revogação, rotação e redução de exposição. Se usar token em cookie, avalie tamanho, criptografia, revogação, renovação e risco de replay; assinatura sozinha não esconde o conteúdo.
Google e Keycloak exigem códigos diferentes?
A base pode ser a mesma graças ao discovery OIDC. Você troca issuer, credenciais, scopes e políticas de claims. Integrações específicas — roles do Keycloak ou regras de domínio do Google — devem ficar em adaptadores e autorização, não espalhadas pelos handlers.
OAuth 2.0 substitui autorização dentro da aplicação?
Não. O provedor autentica e pode oferecer claims, mas sua aplicação ainda decide quem pode acessar cada recurso. Verifique permissões no backend em toda operação sensível, mesmo que a interface esconda o botão.
Próximos passos
Comece com um único provedor e um fluxo pequeno: /login, /callback, sessão e /logout. Faça o caminho feliz funcionar, depois escreva primeiro os testes negativos: state reutilizado, nonce incorreto, audience errada, redirect externo e sessão expirada. Em autenticação, os casos que devem falhar são tão importantes quanto o login bem-sucedido.
Quando essa base estiver estável, conecte a sessão aos middlewares HTTP, modele autorização por recurso e adicione observabilidade sem dados sensíveis. Para completar a arquitetura, leia middleware em Go com net/http, Chi e Gin, segurança de APIs em Go e rate limiting em produção.