LogoPly
Features

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

FieldMeaning
AccessTokenBearer token for API requests
RefreshTokenToken used to obtain a new access token
TokenTypeUsually Bearer
ExpiresInLifetime in seconds from the token response
ExpiresAtLocal expiry time computed from ExpiresIn
ScopeGranted scopes
IDTokenOpenID Connect ID token, when issued
RawFull 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:

StyleBehavior
AuthStylePostSends client_id and client_secret in the form body
AuthStyleBasicSends 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.