Turnkey Cbox ID client for Go, built for command-line tools: log a CLI in with the device authorization grant (RFC 8628) — the flow the GitHub CLI uses — where the user approves a short code in a browser on any device while your program polls.
It also supports the standard authorization-code + PKCE flow for server apps, plus
machine tokens, UserInfo, RFC 7662 introspection, RFC 7009 revocation and webhook
verification. The id_token and OIDC plumbing are handled by the vetted
go-oidc and
x/oauth2 — no hand-rolled crypto.
Everything else here assumes a confidential client. A publishable key is the opposite — public on purpose, useful only from the origins you registered. Reading the environment's own sign-in configuration lets a Go template render a themed sign-in box without shipping a JavaScript SDK to do it:
frontend, err := cboxid.NewFrontendClient("https://id.acme.com", "pk_live_…", nil)
config, err := frontend.Config(ctx) // endpoints, social buttons, the theme
session, err := frontend.Session(ctx, token) // session.User is nil when nobody is signed inAn empty token yields an empty session rather than an error, and the key grants nothing on
its own: the access token is the entire authority. A client secret passed there returns
ErrNotPublishableKey at construction.
Cbox ID can ask your service whether an email and password it has never seen are good, and import that person on the yes. You write the lookup; the handler owns the signature, the freshness window and the constant-time compare:
handler, err := cboxid.LegacyLoginHandler(secret, func(email, password string) (*cboxid.LegacyUser, error) {
row, err := db.FindUser(email)
if err != nil {
return nil, err // could not decide → 503
}
if row == nil || !bcrypt.Match(row.Hash, password) {
return nil, nil // no
}
return &cboxid.LegacyUser{Email: row.Email, Name: row.Name, PasswordHash: row.Hash}, nil
})
http.Handle("/cbox-legacy", handler)Returning (nil, nil) is a wrong password. Returning an error is different: your store
could not decide, and it answers 503 so Cbox ID refuses the sign-in rather than reading an
outage as a bad credential. The secret is checked when the handler is built, so a missing
one fails at startup rather than as a 500 that reads as an outage.
Where do
issuer,clientIdandredirectUricome from? Register an application in your environment console — see Integrate your app.
go get github.com/cboxdk/id-goclient, _ := cboxid.New(ctx, cboxid.Config{
Issuer: "https://id.acme.com",
ClientID: "client_...",
RedirectURI: "http://localhost", // unused by the device flow, but required
Scopes: []string{"openid", "profile", "email", "offline_access"},
})
auth, _ := client.RequestDeviceAuthorization(ctx, cboxid.DeviceParams{})
fmt.Printf("Visit %s and enter code %s\n", auth.VerificationURI, auth.UserCode)
// Blocks until the user approves (or the code expires); honors the poll interval.
user, err := client.PollDeviceToken(ctx, auth)
fmt.Printf("Signed in as %s\n", user.Email)
// Persist user.Token (with its refresh token) to your CLI config for next time.A complete, runnable example is in examples/cli.
req := client.CreateAuthorizationRequest(cboxid.AuthParams{})
// persist req.State, req.CodeVerifier, req.Nonce; redirect the user to req.URL
// on the callback:
user, err := client.Authenticate(ctx,
cboxid.Callback{Code: code, State: state},
cboxid.Stored{State: req.State, CodeVerifier: req.CodeVerifier, Nonce: req.Nonce},
)token, _ := client.MachineToken(ctx, cboxid.MachineTokenParams{Scopes: []string{"reports.read"}})
claims, _ := client.UserInfo(ctx, user.AccessToken)
result, _ := client.Introspect(ctx, someToken) // RFC 7662
err := client.Revoke(ctx, user.RefreshToken, cboxid.HintRefreshToken) // RFC 7009Revoking a refresh token drops the whole token family — that's what "sign out
everywhere" needs. Both calls are confidential-client, so they require a ClientSecret.
ok := cboxid.VerifyWebhook(rawBody, r.Header.Get("X-Cbox-Signature"), webhookSecret, 300)Declare your app's authorization roles and permissions in code and push them to
Cbox ID on deploy. Cbox ID owns identity and who holds what; your app owns what a role
means. Assigned roles then arrive in the token's claims for you to enforce. Requires a
ClientSecret and a client that holds the apps.manifest scope.
client, _ := cboxid.New(ctx, cboxid.Config{
Issuer: "https://id.acme.com",
ClientID: "client_...",
ClientSecret: "secret_...",
RedirectURI: "http://localhost", // unused when only publishing, but required
Permissions: []cboxid.Permission{
{Key: "invoices:create", Description: "Create invoices"},
{Key: "invoices:read", Description: "View invoices"},
},
Roles: []cboxid.Role{
{Key: "billing-admin", Name: "Billing Admin", Description: "Full billing access",
Permissions: []string{"invoices:create", "invoices:read"}},
},
})
// Run on deploy. Idempotent — republishing an unchanged manifest is a no-op.
summary, err := client.PublishManifest(ctx)
// summary.Unchanged, summary.RolesDeclared, summary.PermissionsDeclaredPublishManifest mints a client-credentials token scoped to apps.manifest, then POSTs
the manifest to {issuer}/api/v1/apps/manifest. A rejected push wraps
cboxid.ErrManifestRejected. A complete example is in
examples/publish-manifest. This mirrors the Laravel
client's php artisan cbox-id:publish-manifest, so the manifest contract is identical
across SDKs.
Errors wrap the sentinels cboxid.ErrInvalidState, cboxid.ErrAuthentication and
cboxid.ErrConfiguration — match them with errors.Is. A state mismatch is
ErrInvalidState; treat it as a fresh start, not a user-facing error.
Login is hardened by default — PKCE, state, nonce (auth-code flow), and full
id_token verification (signature/issuer/audience) via go-oidc. Key accounts on
user.ID (the stable subject), not on email.
This is a client. It authenticates users and calls a Cbox ID instance's standard
endpoints; it does not configure SSO, run SCIM, or manage organizations — those are
platform capabilities of cboxdk/laravel-id.
Report vulnerabilities via this repo's GitHub Private Vulnerability Reporting.
MIT © Cbox.