OAuth2 Server (Passport)

Passport turns your Nimbus app into a full OAuth2 authorization server, so third-party (and first-party) applications can obtain tokens to call your API on a user's behalf. It supports the authorization_code grant (with PKCE), client_credentials, and refresh_token, plus token introspection (RFC 7662) and revocation (RFC 7009). Tokens are opaque and stored hashed, so they are fully revocable.

Passport vs. Sanctum. Use access tokens (Sanctum-style) when your own app/SPA/mobile client calls your API. Use Passport when other people's applications need delegated access to your users' data via the standard OAuth2 flow.

Installation

import "github.com/CodeSyncr/nimbus/plugins/passport"

app.Use(passport.NewPlugin(db, passport.Config{}))

The plugin migrates four tables (oauth_clients, oauth_auth_codes, oauth_access_tokens, oauth_refresh_tokens), mounts the OAuth endpoints under /oauth, and binds the server as passport in the container. Defaults: access token 1h, refresh token 30 days, auth code 10m, PKCE required for public clients.

Endpoints

Method & PathPurpose
GET /oauth/authorizeConsent screen (requires a logged-in user)
POST /oauth/authorizeApprove/deny → redirect back with ?code=
POST /oauth/tokenExchange a grant for tokens
POST /oauth/introspectToken introspection (client-authenticated)
POST /oauth/revokeRevoke an access token

Registering clients

Resolve the server and create a client. Confidential clients get a secret (shown once); public clients (SPA/mobile/CLI) get no secret and must use PKCE.

srv := app.Container.MustMake("passport").(*passport.Server)

res, _ := srv.CreateClient(ctx, "Acme Integration", ownerUserID,
    []string{"https://acme.test/callback"},                 // redirect allowlist
    []string{passport.GrantAuthorizationCode, passport.GrantRefreshToken},
    []string{"read:profile", "write:posts"},                // allowed scopes
    true)                                                    // confidential
// res.Client.ClientID and res.PlainSecret — store the secret now; it is hashed at rest.

The authorization code flow (with PKCE)

  1. Client redirects the user to /oauth/authorize?response_type=code&client_id=…&redirect_uri=…&scope=read:profile&state=…&code_challenge=…&code_challenge_method=S256.
  2. The user logs in (if needed) and approves the consent screen.
  3. Passport redirects back to the client with ?code=…&state=….
  4. The client POSTs to /oauth/token with grant_type=authorization_code, the code, redirect_uri, and code_verifier and receives an access + refresh token.

Client credentials may be sent via HTTP Basic auth or in the form body. Auth codes are single-use and short-lived; refresh tokens rotate on use (the old pair is revoked).

Protecting your API (resource server)

srv := app.Container.MustMake("passport").(*passport.Server)

api := app.Router.Group("/api", passport.RequireAccessToken(srv))
api.Use(passport.RequireScope("read:profile"))
api.Get("/me", func(c *http.Context) error {
    at := passport.AccessTokenFrom(c.Ctx()) // token record: UserID, Scopes…
    return c.JSON(200, map[string]string{"user": at.UserID})
})

RequireAccessToken validates the Bearer token (401 + WWW-Authenticate on failure); RequireScope enforces a granted scope (403 otherwise). The wildcard scope * grants all.