Skip to content

Clients and resources ​

This page explains how to define "who can request tokens" (Client) and "what tokens can access" (identity resources, API resources, API scopes), and how to hand them to Wicket. All models live in the github.com/gonewx/wicket/storage/models package.

A complete example ​

go
import (
	wicket "github.com/gonewx/wicket"
	"github.com/gonewx/wicket/storage/models"
	"github.com/gonewx/wicket/utils"
)

// API scope: a permission a client can request
read := models.NewApiScope()
read.Name = "orders.read"

// API resource: a protected API that groups scopes and holds the introspection secret
apiSecret := models.NewSecret()
apiSecret.Value = utils.Sha256("orders-api-introspection-secret")
orders := models.NewApiResource()
orders.Name = "orders"
orders.Scopes = []string{"orders.read"}
orders.ApiSecrets = []*models.Secret{apiSecret}

// Machine client
secret := models.NewSecret()
secret.Value = utils.Sha256("change-me")
worker := models.NewClient()
worker.ClientId = "billing-worker"
worker.ClientSecrets = []*models.Secret{secret}
worker.AllowedScopes = []string{"orders.read"}
if err := worker.SetAllowedGrantTypes(wicket.GrantTypesClientCredentials); err != nil {
	log.Fatal(err)
}

cfg := root.ServerConfig()
cfg.SetApiScopes([]*models.ApiScope{read})
cfg.SetApiResources([]*models.ApiResource{orders})
cfg.SetClients([]*models.Client{worker})

An access token requested with this client has a payload like this (tested on v0.1.4):

json
{"aud":["orders"],"client_id":"billing-worker","iss":"http://localhost:5000","scope":["orders.read"], "exp":..., "iat":..., "nbf":...}

Client ​

Always construct clients with models.NewClient(), which fills in the defaults shown in the tables below. Writing &models.Client{} directly gives you all zero values (for example, Enabled = false and token lifetimes of 0).

Identity and secrets ​

FieldDefaultNotes
ClientId—Required, and unique within the list
Enabledtrue
ClientSecretsemptySee "Secret hashing" below
RequireClientSecrettrueSet to false for public clients (SPAs, mobile apps)
ClientName, Description, ClientUri, LogoUriemptyDisplay information for the consent page

Secret hashing ​

Secret.Value holds a hash, not the plaintext:

go
s := models.NewSecret()           // Type defaults to shared secret
s.Value = utils.Sha256("change-me") // SHA-256, then Base64 (standard encoding)
// A zero s.Expiration means it never expires; keep it zero in v0.1.4 (see the warning on the "Client credentials" page)

During validation, Wicket computes both SHA-256 and SHA-512 of the plaintext the client submits, and picks which to compare by the decoded length of the stored value (32 or 64 bytes), so utils.Sha512 also works. If the stored value isn't valid Base64 or has the wrong length, validation simply fails — don't put plaintext in Value.

Allowed grant types ​

The grant types field is unexported and must be set through methods:

go
err := client.SetAllowedGrantTypes(wicket.GrantTypesCode)        // replace
err  = client.AddAllowedGrantTypes(wicket.GrantTypeClientCredentials) // append one

SetAllowedGrantTypes rejects nil, values containing spaces, duplicate values, and the combinations implicit + authorization_code, implicit + hybrid, and authorization_code + hybrid. Common predefined combinations: GrantTypesClientCredentials, GrantTypesCode, GrantTypesCodeAndClientCredentials, GrantTypesDeviceFlow, GrantTypesBackchannelAuthentication, GrantTypesPassword.

Authorization code flow ​

FieldDefaultNotes
RedirectUrisemptyRedirect URI allowlist; the authorization request's redirect_uri must exactly match one of them
PostLogoutRedirectUrisemptyAddresses allowed as redirect targets after end session
RequirePkcetrue
AllowPlainTextPkcefalseOnly S256 is accepted
RequireConsentfalseWhen true, the user is sent to ConsentUrl
AllowRememberConsenttrue
ConsentLifetimenilNot read in v0.1.4
AllowAccessTokensViaBrowserfalse
AllowOfflineAccessfalseWhen true and the request includes the offline_access scope, a refresh token is issued
AllowedScopesemptyScope names the client may request (identity resource names or API scope names)

Token lifetimes (in seconds) ​

FieldDefault
AccessTokenLifetime3600 (1 hour)
IdentityTokenLifetime300
AuthorizationCodeLifetime300
AbsoluteRefreshTokenLifetime2592000 (30 days), not read in v0.1.4
SlidingRefreshTokenLifetime1296000 (15 days), not read in v0.1.4
DeviceCodeLifetime300

Other token-related fields:

  • AccessTokenType defaults to wicket.AccessTokenTypeJwt. Setting it to AccessTokenTypeReference has no effect in v0.1.4 and JWTs are still issued; see Core concepts.
  • The following fields have defaults, but the v0.1.4 source doesn't read them, so setting them doesn't change behavior: RefreshTokenUsage (default TokenUsageOneTimeOnly), RefreshTokenExpiration (default TokenExpirationAbsolute), the two refresh token lifetimes, ConsentLifetime, and ClientClaimsPrefix (default "client_").
  • IncludeJwtId defaults to true, but the value is not passed through to the token; in testing, issued access tokens contain no jti.

For the actual behavior of refresh tokens, see Refresh token.

AllowedCorsOrigins

Wicket v0.1.4 emits no CORS response headers based on Client.AllowedCorsOrigins. It can serve as decision data for a host CORS middleware (through the CorsPolicyService in the container); see CORS for how.

IdentityResource: identity resources ​

Built-in constructors cover the standard OIDC scopes:

ConstructorScopeNotes
models.IdentityResourcesOpenId()openidRequired = true, claim sub
models.IdentityResourcesProfile()profileStandard profile claims such as name and family_name
models.IdentityResourcesEmail()email
models.IdentityResourcesPhone()phone
models.IdentityResourcesAddress()address

A custom identity resource:

go
membership := models.NewIdentityResource()
membership.Name = "membership"
membership.DisplayName = "Membership"
membership.UserClaims = []string{"member_tier", "member_since"}

cfg.SetIdentityScopes([]*models.IdentityResource{
	models.IdentityResourcesOpenId(),
	models.IdentityResourcesProfile(),
	membership,
})

Note that the injection method is named SetIdentityScopes. UserClaims only declares "which claim types this scope covers"; the actual claim values come from the profile service the host injects through cfg.SetProfileProvider, and the UserInfo endpoint filters the returned claims by it.

ApiScope and ApiResource ​

  • ApiScope: a single permission. Its fields come from the embedded Resource: Name, DisplayName, Description, Enabled (default true), ShowInDiscoveryDocument (default true; controls whether it appears in the discovery document's scopes_supported), UserClaims, plus Required and Emphasize. In v0.1.4, the UserClaims of API scopes and API resources are only used for the discovery document's claims_supported list; they don't add user claims to the access token.
  • ApiResource: an API. Scopes lists the names of the API scopes it contains; ApiSecrets are the secrets this API uses to call the introspection endpoint (also hashed with utils.Sha256); the AllowedAccessTokenSigningAlgorithms field exists, but the v0.1.4 issuance path doesn't read it.

The access token's aud comes from the names of the API resources that the requested scopes belong to. You can issue tokens with only an ApiScope and no ApiResource containing it, but then the token has no resource audience, and the resource can't call the introspection endpoint under its own identity.

Always construct with NewApiScope(), NewApiResource(), and NewIdentityResource() — they initialize the embedded *Resource pointer; otherwise accessing Name panics with a nil pointer.

Static lists and dynamic providers ​

Each kind of configuration data on ServerConfig has two injection methods:

StaticDynamic
SetClients([]*Client)SetClientsProvider(func() []*Client)
SetIdentityScopes(...)SetIdentityScopesProvider(func() ...)
SetApiResources(...)SetApiResourcesProvider(func() ...)
SetApiScopes(...)SetApiScopesProvider(func() ...)

The static methods simply wrap the slice in a provider that always returns it. The difference is that the provider function is called every time the client store or resource store is resolved — which happens often during request handling, possibly several times per request. Dynamic providers therefore suit cases where "configuration can change at runtime" (for example, clients stored in a database and editable from an admin console), but they must be:

  • Fast and non-blocking: don't query the database directly inside them. Refresh periodically in the background and have the provider read only an atomic snapshot.
  • Concurrency-safe: they are called by multiple request goroutines at once.
  • Valid: if the returned client list contains an empty or duplicate ClientId, constructing the client store panics.
go
var clients atomic.Pointer[[]*models.Client]

// The host refreshes in the background as needed
func reloadClients(list []*models.Client) { clients.Store(&list) }

cfg.SetClientsProvider(func() []*models.Client {
	if p := clients.Load(); p != nil {
		return *p
	}
	return nil
})

Released under the Apache License 2.0 · Documentation © 2026 Wicket authors