Pular para o conteúdo
Esta página ainda não foi traduzida. O conteúdo abaixo está em inglês. Ler em inglês

Security

Social login

Sign in with GitHub, Google, GitLab, Discord, Microsoft or any OAuth2/OIDC provider, with PKCE and verified ID tokens.

Última atualização:

With the scaffold#

Terminal
vukem install:auth --social github,google

This adds /auth/{provider}/redirect and /auth/{provider}/callback, the social_accounts model and migration (unique provider + provider id), provider setup from SOCIAL_<NAME>_CLIENT_ID and SOCIAL_<NAME>_CLIENT_SECRET (also appended to .env.example), and account linking:

  • an existing link signs in;
  • an email the provider verified links to the existing account; when that local account was never verified, its password is replaced with a random one and its sessions, remember-me and API tokens end (like a password reset), since they may belong to whoever signed up first with the address;
  • an unverified email matching an account is refused with 409, so a provider account cannot take over a password account;
  • otherwise a new user is created.

It also works on projects that already ran install:auth.

The social package#

Go
key, err := app.DeriveKey("social.state", 32) // config.ErrKeyMissing without APP_KEY
if err != nil {
	return err
}

s := social.New(key, // panics on a key shorter than 32 bytes
	social.GitHub(social.Config{ClientID: id, ClientSecret: secret, RedirectURL: "https://app.test/auth/github/callback"}),
	social.Google(googleCfg),                  // OpenID Connect
	social.Microsoft("common", microsoftCfg),  // OpenID Connect (Entra ID)
	social.GitLab(gitlabCfg), social.Discord(discordCfg),
	social.OIDC("okta", "https://example.okta.com", oktaCfg), // any OIDC issuer
).WithPreviousKeys(app.DerivePreviousKeys("social.state", 32)...) // APP_KEY rotation

r.Get("/auth/{provider}/redirect", func(c *web.Ctx) error {
	return s.Redirect(c, c.Param("provider")) // 404 for unknown providers
})

r.Get("/auth/{provider}/callback", func(c *web.Ctx) error {
	u, err := s.User(c, c.Param("provider"))
	if err != nil {
		return err // 400 bad or expired state, 403 cancelled, 502 provider failure
	}
	// u.Provider, u.ID, u.Email, u.EmailVerified, u.Name, u.Nickname, u.AvatarURL, u.Token
	return nil
})
FeatureDetail
FlowAuthorization Code + PKCE (S256) for every provider; htmx requests get HX-Redirect
Statestate, PKCE verifier and nonce in a 10-minute, HMAC-signed, HttpOnly, SameSite=Lax cookie; compared in constant time; used states are remembered, so a replayed callback fails. Set s.States to the cache store (c, _ := app.Cache(); s.States = c.Store(), done by install:auth --social) so this holds across replicas and restarts. In production use a persistent, shared store (CACHE_STORE=database or Redis): the memory store evicts entries past CACHE_MAX_ENTRIES, and an evicted state could be replayed until its cookie expires; a store error refuses the callback (503)
OIDCdiscovery, JWKS cache (refreshed once for unknown key ids), RS256/ES256 only, checks of iss, aud/azp, exp/iat (1 minute leeway) and nonce
Email trustEmailVerified only when the provider vouches: GitHub primary verified address, GitLab confirmed_at, Discord verified, OIDC email_verified (Microsoft xms_edov)
Statelesss.UserFromToken(ctx, "google", accessToken) for API/SPA flows
Optionssocial.WithEndpoints (GitHub Enterprise, self-hosted GitLab), social.WithAuthParams, Config.Scopes, social.OAuth2(...) for any OAuth2 server

Testing#

Go
user := social.User{ID: "gh-1", Email: "ana@example.com", EmailVerified: true, Name: "Ana"}
app := vukemtest.New(t, bootstrap.New, vukemtest.Options{Env: vukemtest.FakeSocial(user)})

social.Fake(name, callbackURL, user) is a provider without network calls; vukemtest.FakeSocial enables it in scaffolded test apps only.

Apple, LinkedIn and X#

social.LinkedIn(cfg) (OIDC), social.X(cfg) (OAuth 2.0 + PKCE; X returns no email) and social.Apple(social.AppleConfig{Config, TeamID, KeyID, PrivateKey}). vukem install:auth --social apple,linkedin,x wires all three.

Sign in with Apple signs its client secret (an ES256 JWT, cached for an hour) with the .p8 key and answers with response_mode=form_post: the callback is a cross-site POST from appleid.apple.com. Register POST on the callback route, serve the app over HTTPS, and add https://appleid.apple.com to CSRF_TRUSTED_ORIGINS. Apple sends the user's name only on the first sign-in. It reads SOCIAL_APPLE_CLIENT_ID, SOCIAL_APPLE_TEAM_ID, SOCIAL_APPLE_KEY_ID and SOCIAL_APPLE_PRIVATE_KEY_PATH.