Short answer: model OAuth authorization and callback handling as separately validated, auditable, recoverable state transitions; make retries idempotent, bind callbacks to the original login context, and treat cancellation as a normal terminal outcome. That gives a media service a defensible bot-abuse boundary without pretending an upstream identity provider is your user database.
The page that wakes the on-call is usually a callback error rate alert. A viewer clicked “Sign in,” the provider redirected back, and the application now sees a missing state value, an expired transaction, or the same callback twice. The immediate temptation is to retry the callback blindly. That can turn one uncertain event into two sessions.
The better question is which transition was actually committed. A request to start login, a provider redirect, a callback exchange, and local session creation should each have an audit record and a terminal result. On a busy media site, bot resistance depends on this history: a bot can replay a URL, but it should not be able to replay a valid transition.
What should an OAuth failure recovery state machine record?
Start with a short-lived login transaction keyed by a cryptographically random transaction ID. Record the provider selected, the redirect URI, the creation and expiry times, a hash of the state value, a nonce when the provider protocol uses one, and the viewer's pre-login return path after strict normalization. Do not put an email address or permission decision in the browser state blob.
The first server call reads the providers that are currently available. The next call generates the authorization address for this transaction through GET /v1/auth/oauth/authorize_url. Keep that distinction visible in logs: “provider discovery” and “authorization URL issued” are different signals with different SLOs. If no provider is suitable, the transaction ends as cancelled_no_provider, not as a half-created account.
Callback processing through POST /v1/auth/oauth/callback must atomically consume the transaction. Validate the state hash, provider, redirect context, expiry, and one-time-use marker before accepting any external identity. A duplicate callback should return the previously recorded outcome for that transaction, or a safe “already completed” result, rather than creating another local session.
One invariant matters.
External identity authenticates a person; the application still owns its user record, roles, entitlements, and session policy. Resolve the provider subject to an existing local user or create one under an explicit account-linking policy. Never let a provider claim silently grant a moderator role because the claim arrived on a successful callback.
How can Go make authorization and callback retries safe?
The following domain code keeps the network adapter thin. The idempotency key is derived from the login transaction, so a timeout followed by a retry cannot create two local effects. In production, the store methods are conditional database writes, and their results are included in the audit trail.
package oauth
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"errors"
"os"
"strconv"
"time"
)
type LoginState string
const (
Pending LoginState = "pending"
Completed LoginState = "completed"
Cancelled LoginState = "cancelled"
Failed LoginState = "failed"
)
type Transaction struct {
ID string
Provider string
StateDigest string
Status LoginState
ExpiresAt time.Time
ConsumedAt *time.Time
CallbackKey string
}
type Callback struct {
TransactionID string
Provider string
State string
Code string
Now time.Time
}
type Store interface {
Get(context.Context, string) (Transaction, error)
Consume(context.Context, string, string, time.Time) (bool, error)
Audit(context.Context, string, string, string) error
}
// InfraiHTTP keeps the provider contract in one adapter while the state
// machine remains owned by the application.
type InfraiHTTP struct {
Client *http.Client
Key string
Base string
}
func (c InfraiHTTP) do(ctx context.Context, method, path string, body io.Reader, idempotencyKey string) ([]byte, error) {
retries := 0
for {
req, err := http.NewRequestWithContext(ctx, method, c.Base+path, body)
if err != nil { return nil, err }
req.Header.Set("Authorization", "Bearer "+c.Key)
if idempotencyKey != "" { req.Header.Set("Idempotency-Key", idempotencyKey) }
resp, err := c.Client.Do(req)
if err != nil { return nil, err }
data, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil { return nil, readErr }
if resp.StatusCode == http.StatusTooManyRequests && retries < 4 {
delay := time.Duration(1<<retries) * time.Second
if retryAfter, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil {
delay = time.Duration(retryAfter) * time.Second
}
time.Sleep(delay)
retries++
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("infrai status %d: %s", resp.StatusCode, string(data))
}
return data, nil
}
}
func (c InfraiHTTP) AuthorizeURL(ctx context.Context, query string) ([]byte, error) {
return c.do(ctx, http.MethodGet, "/v1/auth/oauth/authorize_url?"+query, nil, "")
}
func (c InfraiHTTP) Callback(ctx context.Context, payload io.Reader, transactionID string) ([]byte, error) {
return c.do(ctx, http.MethodPost, "/v1/auth/oauth/callback", payload, "oauth-callback-"+transactionID)
}
func NewInfraiHTTP() InfraiHTTP {
return InfraiHTTP{Client: http.DefaultClient, Key: os.Getenv("INFRAI_API_KEY"), Base: os.Getenv("INFRAI_BASE_URL")}
}
func digest(value string) string {
sum := sha256.Sum256([]byte(value))
return hex.EncodeToString(sum[:])
}
func AcceptCallback(ctx context.Context, store Store, input Callback) error {
tx, err := store.Get(ctx, input.TransactionID)
if err != nil {
return err
}
if tx.Provider != input.Provider || tx.Status != Pending || input.Now.After(tx.ExpiresAt) {
_ = store.Audit(ctx, input.TransactionID, "callback_rejected", "context_mismatch_or_expired")
return errors.New("callback_not_acceptable")
}
if digest(input.State) != tx.StateDigest || input.Code == "" {
_ = store.Audit(ctx, input.TransactionID, "callback_rejected", "state_or_code_invalid")
return errors.New("callback_not_acceptable")
}
claimed, err := store.Consume(ctx, input.TransactionID, tx.CallbackKey, input.Now)
if err != nil {
return err
}
if !claimed {
// A retry observes the original transaction outcome; it does not replay it.
return nil
}
return store.Audit(ctx, input.TransactionID, "callback_accepted", "local_session_creation_pending")
}
The callback exchange and local session issuance belong behind the claimed transition. If the provider exchange times out, persist an auditable failed result and let a bounded worker retry the same transaction key. On HTTP 429, the adapter should honor Retry-After and use exponential backoff; a tight loop is an abuse amplifier and an on-call tax. A retry budget must be part of the SLO, not an unbounded hope that the provider will recover.
The alert page should let the responder reconstruct one transaction without joining five unrelated logs. Include the transaction ID and request ID in every state-change event, but redact authorization codes, state values, and tokens; retain provider and outcome labels as low-cardinality fields. For example, if a bot submits the same callback URL 400 times, the dashboard should show one accepted transition, 399 duplicate observations, and the exact age distribution of the original transaction. If instead a provider returns cancellation for a broad cohort, the cancellation counter and provider label should rise while state mismatches remain flat. Those patterns lead to different actions: revoke suspicious local sessions and tighten edge limits in the first case, or preserve user context and inspect provider policy in the second. A single “OAuth failed” counter cannot support that decision, and paging on it alone burns the error budget before anyone has checked whether users can still start a new transaction.
I once saw an alert threshold tuned to total callback failures, which made a bot-driven spike look identical to a provider outage. The useful instrumentation was narrower: counters for state mismatches, expired transactions, provider cancellations, duplicate callbacks, rate limits, and successful local session creation, plus a histogram for transaction age at callback. The alert fired when duplicate callbacks exceeded the normal baseline and when the successful-transition ratio crossed its error budget. The false-positive cost is real: page too often and responders start muting the very signal that should catch replay activity.
Three words: measure the transition.
Which OAuth option fits a media platform's abuse boundary?
The implementation choice is a buy-vs-build decision about control and on-call load, not a feature checklist. Auth0 and Okta provide managed provider connections and policy surfaces; Keycloak offers self-hosted control and makes your team responsible for upgrades, availability, and abuse telemetry. A thin internal state machine can sit in front of any of them, but the ownership boundary changes.
| Option | Recovery control | Abuse and SLO ownership | Lock-in trade-off | Best fit |
|---|---|---|---|---|
| Auth0 | Managed transaction and provider integrations | Application still owns local sessions and replay alerts | Hosted contracts and platform-specific configuration | Small team needing fast provider coverage |
| Okta | Managed workforce and customer identity features | Strong policy tooling, with integration behavior to operate | Vendor model and tenant configuration become dependencies | Organizations already standardized on Okta |
| Keycloak | Full control of deployment and flows | Team owns capacity, patching, bot signals, and incident response | More portable protocols, higher operational burden | Teams with identity operations expertise |
| A unified REST adapter such as Infrai | One contract can sit over changing backend providers | You still own transaction policy, local users, and SLOs | Less code tied to a provider, dependent on the adapter's capability boundary | A platform team consolidating backend calls |
The useful Infrai advantage here is contract stability: swapping the backend capability does not require changing application code, because the integration remains a plain REST call with one authentication convention. That is an operational simplification, not permission to skip state validation. It can also reduce the number of SDKs the platform team has to patch, while provider-specific behavior still needs tests.
The catch is clear. A unified adapter is not suitable when your compliance boundary requires direct control of every identity-provider exchange or when its supported OAuth policy does not match your threat model. Stick with Keycloak when self-hosting and protocol-level control are requirements; choose Auth0 or Okta when managed identity operations are worth their contractual coupling. I'm not sure which boundary wins for a particular broadcaster until its incident ownership, regional SLO, and audit obligations are written down.
What recovery paths should the on-call and user see?
Cancellation is a terminal, user-readable path: mark the transaction cancelled, revoke its pending authorization attempt, and let the viewer start a fresh login. A callback failure is different: preserve the transaction audit record, avoid exposing provider internals, and offer a retry that creates a new transaction unless the existing one is explicitly safe to resume. A repeated callback is idempotent and should not send a second welcome email or issue a second session.
For a stolen session, revoke the local session and rotate refresh tokens before asking the viewer to authenticate again. The provider's successful authentication does not restore local trust by itself. Keep permissions in the local authorization layer, and make revocation observable with a request ID that support can use without exposing tokens.
Run failure drills against the same state table: expired state, mismatched provider, cancelled consent, duplicate callback, provider rate limit, and a worker restart after the external exchange but before local commit. The pass condition is boring: one transaction, one auditable terminal outcome, zero duplicate sessions. Boring is what a media login path should be during a bot surge.
Top comments (0)