---
title: "OAuth 2.0 e OpenID Connect em Go: Login Seguro"
url: "https://golang.com.br/blog/oauth2-oidc-go-login-google-keycloak/"
markdown_url: "https://golang.com.br/blog/oauth2-oidc-go-login-google-keycloak.MD"
description: "Implemente OAuth 2.0 e OpenID Connect em Go com Authorization Code, PKCE, state, nonce, Google ou Keycloak, sessão segura e validação de ID Token."
date: "2026-08-23"
author: "Golang Brasil"
---

# OAuth 2.0 e OpenID Connect em Go: Login Seguro

Implemente OAuth 2.0 e OpenID Connect em Go com Authorization Code, PKCE, state, nonce, Google ou Keycloak, sessão segura e validação de ID Token.


**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;
- `email` e `email_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](/aprenda/golang-jwt-autenticacao/). 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:

1. O usuário acessa `/login`.
2. O backend cria `state`, `nonce` e, se usado, o verificador PKCE.
3. Esses valores são vinculados a uma tentativa de login curta e de uso único.
4. O navegador é redirecionado ao authorization endpoint do provedor.
5. O provedor autentica o usuário e redireciona para `/callback?code=...&state=...`.
6. O backend compara `state` e troca o código pelo canal servidor-servidor.
7. O backend verifica assinatura, issuer, audience, expiração e nonce do ID Token.
8. A aplicação cria ou localiza o usuário e abre uma sessão própria.
9. 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:

```bash
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:

```bash
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:

```bash
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:

```go
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](/blog/context-timeout-cancelamento-go/).

## state, nonce e PKCE: três valores com funções diferentes

Não use um único valor constante para tudo.

- **`state`** protege a continuidade do fluxo e ajuda a impedir CSRF no callback.
- **`nonce`** vincula 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`:

```go
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:

```go
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:

```go
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](/blog/slog-go-logging-estruturado/) 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.

```go
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:

```go
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](/blog/cors-go-net-http-chi-gin/) 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 | Google | 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](/blog/autenticacao-autorizacao-go-apis/).

### 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:

1. geração e persistência da tentativa de login;
2. cliente OIDC/OAuth e validação de token;
3. 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`;
- `state` desconhecido, expirado e reutilizado;
- erro devolvido pelo provedor;
- troca do código com falha;
- ID Token ausente;
- assinatura, issuer ou audience inválidos;
- nonce diferente;
- `sub` ausente;
- 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](/blog/go-testcontainers-testes-integracao-containers/). Não coloque client secrets reais em fixtures ou no repositório. Rode também `go test -race ./...`, análise estática e [govulncheck](/blog/govulncheck-go-vulnerabilidades-dependencias/) 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.
- [ ] `state` e `nonce` usam `crypto/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`, `SameSite` e 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](/blog/middleware-go-net-http-chi-gin-producao/), [segurança de APIs em Go](/tutoriais/go-security/) e [rate limiting em produção](/blog/rate-limiting-go-api-producao/).
