With the scaffold#
vukem install:auth --social github,googleThis 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;
- 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#
s := social.New(app.Key.Derive("social.state", 32),
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
)
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
})| Feature | Detail |
|---|---|
| Flow | Authorization Code + PKCE (S256) for every provider; htmx requests get HX-Redirect |
| State | state, 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 |
| OIDC | discovery, JWKS cache (refreshed once for unknown key ids), RS256/ES256 only, checks of iss, aud/azp, exp/iat (1 minute leeway) and nonce |
| Email trust | EmailVerified only when the provider vouches: GitHub primary verified address, GitLab confirmed_at, Discord verified, OIDC email_verified (Microsoft xms_edov) |
| Stateless | s.UserFromToken(ctx, "google", accessToken) for API/SPA flows |
| Options | social.WithEndpoints (GitHub Enterprise, self-hosted GitLab), social.WithAuthParams, Config.Scopes, social.OAuth2(...) for any OAuth2 server |
Testing#
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.