OAuth2
Authorization code flow with PKCE, refresh, and userinfo requests.
OAuth2
The oauth2 package implements the OAuth 2.0 authorization code flow with
PKCE.
import "github.com/Dziqha/ply/oauth2"
cfg := oauth2.Config{
ClientID: os.Getenv("OAUTH_CLIENT_ID"),
ClientSecret: os.Getenv("OAUTH_CLIENT_SECRET"),
RedirectURL: "https://app.example.com/auth/callback",
Scopes: []string{"openid", "profile", "email"},
Endpoint: oauth2.Endpoint{
AuthURL: "https://accounts.example.com/authorize",
TokenURL: "https://accounts.example.com/token",
},
}ClientSecret may be empty for public clients. Public clients should always
use PKCE.
Login
Generate a state value and a PKCE verifier for each authorization request:
state, err := oauth2.NewState()
if err != nil {
c.Text(500, "failed to create state")
return
}
verifier, err := oauth2.NewVerifier()
if err != nil {
c.Text(500, "failed to create verifier")
return
}
manager.Set(c, "oauth_state", state)
manager.Set(c, "oauth_verifier", verifier)
c.Redirect(302, cfg.AuthCodeURL(state, verifier))Store the state and verifier in a server-side session or another trusted store. Do not put the verifier in a cookie.
NewState, NewVerifier, and NewNonce generate 32 random bytes with
crypto/rand and encode them with base64url.
Callback
Validate the state before exchanging the code:
state := c.Query("state")
expected, _ := manager.Get(c, "oauth_state").(string)
if !oauth2.ValidateState(expected, state) {
c.Text(400, "invalid state")
return
}
verifier, _ := manager.Get(c, "oauth_verifier").(string)
token, err := cfg.Exchange(c.Ctx, c.Query("code"), verifier)
if err != nil {
c.Text(401, "authorization failed")
return
}Exchange sends the code, redirect URI, client credentials, and verifier to
the token endpoint. It returns an *oauth2.Token.
Token
| Field | Meaning |
|---|---|
AccessToken | Bearer token for API requests |
RefreshToken | Token used to obtain a new access token |
TokenType | Usually Bearer |
ExpiresIn | Lifetime in seconds from the token response |
ExpiresAt | Local expiry time computed from ExpiresIn |
Scope | Granted scopes |
IDToken | OpenID Connect ID token, when issued |
Raw | Full decoded token response |
Token.Valid() returns false when the access token is empty or ExpiresAt has
passed. A token without an expiry is treated as valid.
Refresh
token, err := cfg.Refresh(c.Ctx, token.RefreshToken)Refresh only requires ClientID and Endpoint.TokenURL. It does not require
the authorization endpoint or redirect URL.
UserInfo
var profile struct {
Subject string `json:"sub"`
Email string `json:"email"`
Name string `json:"name"`
}
if err := cfg.UserInfo(c.Ctx, token, userInfoURL, &profile); err != nil {
c.Text(401, "userinfo failed")
return
}The request uses Authorization: Bearer <access_token>. The destination must
be a JSON-decodable value.
Client authentication
AuthStyle controls how client credentials are sent to the token endpoint:
| Style | Behavior |
|---|---|
AuthStylePost | Sends client_id and client_secret in the form body |
AuthStyleBasic | Sends HTTP Basic authentication |
AuthStylePost is the default. Use the style required by the provider.
Errors
Token endpoint errors are returned as *oauth2.Error:
var oauthErr *oauth2.Error
if errors.As(err, &oauthErr) {
logger.Warn("oauth error", "code", oauthErr.Code, "status", oauthErr.StatusCode)
}Error includes StatusCode, Code, Description, URI, and the raw
response body.
Security
- Use HTTPS for authorization and token endpoints.
- Validate the state value on every callback.
- Use PKCE for public clients and for clients that cannot keep a secret.
- Keep the verifier server-side and discard it after the exchange.
- Request only the scopes the application needs.
- Do not log authorization codes, access tokens, refresh tokens, or client secrets.

Ply