OIDC clients
An OIDC client is your application's registration inside Huudis — the client_id, the redirect URIs Huudis will trust, the scopes it can request. Every product that wants to delegate sign-in to Huudis must register a client. The Go SDK exposes them on client.OidcClients. For the protocol see OIDC overview; for the HTTP surface see API → OIDC clients.
Field on the Client
client.OidcClients — type *huudis.OidcClientsResource. There's no Get — List returns the full record for every client (typically fewer than a dozen rows).
Methods
List
Signature. func (r *OidcClientsResource) List(ctx context.Context, auth RequestAuth) ([]OidcClient, error)
Returns every OIDC client registered in the active workspace. ClientSecret is never present on the list response — only at create-time and on RotateSecret.
all, err := client.OidcClients.List(ctx, huudis.RequestAuth{})
if err != nil {
return err
}
for _, c := range all {
kind := "(public)"
if c.Confidential {
kind = "(confidential)"
}
fmt.Println(c.ID, c.Name, kind)
}
Create
Signature. func (r *OidcClientsResource) Create(ctx context.Context, in CreateOidcClientInput, auth RequestAuth) (*OidcClient, error)
Registers a new client. RedirectUris must be exact-match HTTPS URLs (with localhost HTTP allowed in development). Confidential is a *bool — pass huudis.Ptr(true) to mint a client_secret, huudis.Ptr(false) for SPAs/native apps using PKCE alone.
confidential := true
oc, err := client.OidcClients.Create(ctx, huudis.CreateOidcClientInput{
Name: "Acme Web",
RedirectUris: []string{
"https://acme.example.com/callback",
"https://staging.acme.example.com/callback",
},
Scopes: []string{"openid", "profile", "email"},
Confidential: &confidential,
}, huudis.RequestAuth{})
if err != nil {
return err
}
log.Println("client_id:", oc.ID)
log.Println("client_secret:", oc.ClientSecret) // SAVE NOW — never shown again
ClientSecretis shown exactly once. It's set only on this create response and onRotateSecret. Lose it and the only recovery is rotation. Public clients (Confidential = false) never get a secret — they authenticate via PKCE alone.
Update
Signature. func (r *OidcClientsResource) Update(ctx context.Context, id string, patch map[string]any, auth RequestAuth) (*OidcClient, error)
PATCH — only keys in patch change. Renaming is fine. Flipping confidential from false to true after creation is rejected (state_conflict).
all, _ := client.OidcClients.List(ctx, huudis.RequestAuth{})
var existing huudis.OidcClient
for _, c := range all {
if c.ID == clientID {
existing = c
break
}
}
_, err := client.OidcClients.Update(ctx, clientID, map[string]any{
"redirectUris": append(existing.RedirectUris, "https://acme.example.com/auth/callback"),
}, huudis.RequestAuth{})
RotateSecret
Signature. func (r *OidcClientsResource) RotateSecret(ctx context.Context, id string, auth RequestAuth) (*RotateSecretResult, error)
Mints a new secret and invalidates the old one immediately. Confidential-only — public clients return state_conflict.
res, err := client.OidcClients.RotateSecret(ctx, "client_01HX...", huudis.RequestAuth{})
if err != nil {
return err
}
storeSecret(res.ClientSecret)
There's no overlap window. Coordinate with a deploy: write the new secret first, restart your service so it picks it up, then issue the rotate call.
Delete
Signature. func (r *OidcClientsResource) Delete(ctx context.Context, id string, auth RequestAuth) error
Permanently removes the client. Existing access tokens issued by this client continue to work until they expire (typically 1h); refresh tokens are revoked immediately.
if err := client.OidcClients.Delete(ctx, "client_01HX...", huudis.RequestAuth{}); err != nil {
return err
}
Types
type OidcClient struct {
ID string `json:"id"` // the client_id ("client_...")
AccountID string `json:"accountId"`
Name string `json:"name"`
RedirectUris []string `json:"redirectUris"`
Scopes []string `json:"scopes"`
ClientSecret string `json:"clientSecret,omitempty"` // only on create + RotateSecret
Confidential bool `json:"confidential"`
CreatedAt string `json:"createdAt"` // ISO-8601
UpdatedAt string `json:"updatedAt"`
}
type RotateSecretResult struct {
Secret string `json:"secret,omitempty"`
ClientSecret string `json:"clientSecret,omitempty"`
}
See API → OIDC clients for the field-level reference.
Common patterns
Per-environment clients
The cleanest model: one client per environment, with environment-specific redirect URIs.
func provisionEnvironments(ctx context.Context, c *huudis.Client) error {
confidential := true
for _, env := range []string{"production", "staging", "development"} {
oc, err := c.OidcClients.Create(ctx, huudis.CreateOidcClientInput{
Name: fmt.Sprintf("Acme Web (%s)", env),
RedirectUris: []string{fmt.Sprintf("https://%s.acme.example.com/callback", env)},
Confidential: &confidential,
}, huudis.RequestAuth{})
if err != nil {
return err
}
writeEnvFile(env, map[string]string{
"HUUDIS_CLIENT_ID": oc.ID,
"HUUDIS_CLIENT_SECRET": oc.ClientSecret,
})
}
return nil
}
Add a new redirect URI without downtime
Update replaces the array, so always include the existing entries when adding:
var existing huudis.OidcClient
all, _ := client.OidcClients.List(ctx, huudis.RequestAuth{})
for _, c := range all {
if c.ID == clientID {
existing = c
break
}
}
_, _ = client.OidcClients.Update(ctx, clientID, map[string]any{
"redirectUris": append(existing.RedirectUris, "https://new.example.com/callback"),
}, huudis.RequestAuth{})
Detect un-rotated secrets
A monthly audit:
func staleClients(ctx context.Context, c *huudis.Client, maxDays int) ([]huudis.OidcClient, error) {
all, err := c.OidcClients.List(ctx, huudis.RequestAuth{})
if err != nil {
return nil, err
}
cutoff := time.Now().Add(-time.Duration(maxDays) * 24 * time.Hour)
var stale []huudis.OidcClient
for _, oc := range all {
if !oc.Confidential {
continue
}
t, _ := time.Parse(time.RFC3339, oc.UpdatedAt)
if t.Before(cutoff) {
stale = append(stale, oc)
}
}
return stale, nil
}
Pair with a CI job that pages on the result — confidential client secrets should rotate at least every six months.
Errors
Code |
Status |
Cause |
|---|---|---|
validation_error |
400 | Bad redirect URI (non-HTTPS, fragment), unknown scope, name too long. |
not_found |
404 | Client doesn't exist or lives in another workspace. |
conflict |
409 | Name already taken in this workspace. |
state_conflict |
409 | Rotating a public client; flipping confidential after creation. |
forbidden |
403 | Caller isn't an admin/owner. |
Next
- OIDC overview — the protocol your client implements.
- End users — the people who sign into this client.
- Consents — what users have granted this client.
- API → OIDC clients — HTTP-level reference.