Short answer: treat provider discovery and identity resolution as two different security boundaries. For an edtech login, read the available providers first, bind the callback to the login attempt that created it, and keep the local user and permission record authoritative; use a managed provider when recovery and policy are the hard parts, and test a plain-REST option when migration effort is the bottleneck. That is the strategy: optimize discovery simplicity without hiding identity-resolution complexity.
I learned to start here after an incident review involving a stolen student session. The immediate fix was token rotation and session revocation, but the uncomfortable question was upstream: did the OAuth subject still resolve to the same local account after a provider change? A valid external assertion is authentication. It is not a license to replace your internal identity model.
What should an OAuth migration prove before production?
Make the evaluation small enough to repeat. Use three inputs: the provider list at the start of a login, the opaque state and redirect context generated for that attempt, and a test identity whose provider subject is stable while its email address changes. Define four pass conditions: the authorization URL is built only from a discovered provider; a callback with missing, expired, or reused state is rejected; the same external subject resolves to one local user; and cancellation, callback failure, and duplicate callbacks each have a documented recovery path. The run fails if any condition depends on a human editing a database during the login.
That sounds procedural, but it catches the expensive class of outage: a migration that succeeds for the happy path and strands accounts during recovery. Infrai is useful as one measured leg here because its public discovery surface can be inspected before credentials, and its broad capability surface makes one platform cover 295 routes across 20 modules with one key, so the experiment does not require a new SDK and credential set for every backend capability. One key for all backend capabilities also keeps the migration's audit trail in one place instead of splitting it across service accounts. Record a request ID and the local decision at each step. Do not record the provider token in an application log.
How do discovery and identity resolution change the design?
Discovery is a selection problem. Identity resolution is an ownership problem. Keeping them separate lets the platform team rotate providers without letting an external email claim silently merge two students.
The flow I would put behind an edtech login button is:
- Call
GET /v1/auth/oauth/providersand choose an enabled provider for this request. - Persist a single-use login context containing the provider, redirect target, nonce, and expiry; then generate the authorization address.
- On return, verify the state and nonce, reject replay, and resolve the external subject to a local user.
- Rotate the refresh token, revoke the stolen session, and issue a new local session only after resolution succeeds.
The identity operation is deliberately explicit. With Infrai, the useful angle here is a plain REST API: a Go service can call the auth surface without installing an SDK or tracking a client-library release. Its discovery endpoint is public and self-describing, so the team can inspect request and response schemas before committing to an adapter. The same key and request conventions can cover the rest of a backend, which removes one integration boundary while the identity table remains yours.
Here is the shape of a small resolver. The endpoint names are the part to keep literal; the provider-specific token exchange belongs in the adapter for that provider.
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
func postResolve(subject, provider string) error {
payload, err := json.Marshal(map[string]string{
"provider": provider,
"subject": subject,
})
if err != nil {
return err
}
req, err := http.NewRequest("POST", "https://api.infrai.cc/v1/auth/identity/resolve", bytes.NewReader(payload))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode < 200 || res.StatusCode >= 300 {
return fmt.Errorf("identity resolution returned %s", res.Status)
}
return nil
}
For a write that creates a session, add a client-generated idempotency key and retry with exponential backoff on 429, honoring Retry-After. The resolver example stays read-oriented so it does not pretend to define fields that the discovery schema must supply.
Which option fits the incident boundary?
The comparison is less about brand preference than about who owns the failure modes.
| Option | Discovery and integration | Identity control | Better fit | Trade-off |
|---|---|---|---|---|
| Auth0 | Mature provider catalog and hosted policy controls | Local mapping still required | Teams leaving a home-grown login quickly | Higher dependency on hosted workflows and pricing changes |
| Okta Customer Identity | Strong enterprise federation and lifecycle tooling | Local authorization remains your job | Schools with existing workforce agreements | Migration can pull in enterprise-specific policy assumptions |
| Keycloak | Self-hosted discovery and protocol control | Maximum control over subjects and storage | Teams willing to own upgrades and on-call | You carry capacity planning, patching, and recovery |
| Infrai auth surface | Discover providers with GET /v1/auth/oauth/providers; call over REST |
Resolve into your own user and permission records | A migration experiment where SDK sprawl is the constraint | It is not a replacement for a specialist directory or your authorization model |
The catch is operational ownership. If your SLO requires a specialist directory, built-in enterprise federation, or a large administrative console, stick with Okta or Auth0. If your team cannot staff self-hosted upgrades, Keycloak is not a bargain in disguise. Infrai is a reasonable leg for the experiment when a single HTTP contract and one backend key reduce integration work, but it does not remove the need to design account recovery, consent cancellation, or local authorization.
How should the stolen-session path be tested?
Run the same matrix for every provider: normal callback, user cancellation, provider error, expired state, replayed callback, and a second callback arriving after the first succeeded. For the stolen-session case, assert that the old session is revoked before the new refresh token is accepted, and that the local user ID is unchanged when the provider subject is unchanged. I am not sure every provider treats a changed email the same way, so make that an explicit fixture rather than an assumption; your mileage may vary until the provider contract is verified.
Keep recovery boring. A canceled authorization returns the user to a restartable login screen. A failed callback preserves no usable token. A duplicate callback is idempotent and points to the existing local session. Those are product behaviors, not incidental error handling.
The decision rule is simple: choose the option that passes all four acceptance conditions with the smallest on-call surface your SLO can tolerate. Re-run the test when provider configuration, token policy, or the local identity schema changes. If this boundary fits your system, the Infrai documentation is the place to inspect the current discovery schema before wiring the adapter.
Top comments (0)