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
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):
{"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
| Field | Default | Notes |
|---|---|---|
ClientId | — | Required, and unique within the list |
Enabled | true | |
ClientSecrets | empty | See "Secret hashing" below |
RequireClientSecret | true | Set to false for public clients (SPAs, mobile apps) |
ClientName, Description, ClientUri, LogoUri | empty | Display information for the consent page |
Secret hashing
Secret.Value holds a hash, not the plaintext:
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:
err := client.SetAllowedGrantTypes(wicket.GrantTypesCode) // replace
err = client.AddAllowedGrantTypes(wicket.GrantTypeClientCredentials) // append oneSetAllowedGrantTypes 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
| Field | Default | Notes |
|---|---|---|
RedirectUris | empty | Redirect URI allowlist; the authorization request's redirect_uri must exactly match one of them |
PostLogoutRedirectUris | empty | Addresses allowed as redirect targets after end session |
RequirePkce | true | |
AllowPlainTextPkce | false | Only S256 is accepted |
RequireConsent | false | When true, the user is sent to ConsentUrl |
AllowRememberConsent | true | |
ConsentLifetime | nil | Not read in v0.1.4 |
AllowAccessTokensViaBrowser | false | |
AllowOfflineAccess | false | When true and the request includes the offline_access scope, a refresh token is issued |
AllowedScopes | empty | Scope names the client may request (identity resource names or API scope names) |
Token lifetimes (in seconds)
| Field | Default |
|---|---|
AccessTokenLifetime | 3600 (1 hour) |
IdentityTokenLifetime | 300 |
AuthorizationCodeLifetime | 300 |
AbsoluteRefreshTokenLifetime | 2592000 (30 days), not read in v0.1.4 |
SlidingRefreshTokenLifetime | 1296000 (15 days), not read in v0.1.4 |
DeviceCodeLifetime | 300 |
Other token-related fields:
AccessTokenTypedefaults towicket.AccessTokenTypeJwt. Setting it toAccessTokenTypeReferencehas 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(defaultTokenUsageOneTimeOnly),RefreshTokenExpiration(defaultTokenExpirationAbsolute), the two refresh token lifetimes,ConsentLifetime, andClientClaimsPrefix(default"client_"). IncludeJwtIddefaults totrue, but the value is not passed through to the token; in testing, issued access tokens contain nojti.
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:
| Constructor | Scope | Notes |
|---|---|---|
models.IdentityResourcesOpenId() | openid | Required = true, claim sub |
models.IdentityResourcesProfile() | profile | Standard profile claims such as name and family_name |
models.IdentityResourcesEmail() | email | |
models.IdentityResourcesPhone() | phone | |
models.IdentityResourcesAddress() | address |
A custom identity resource:
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 embeddedResource:Name,DisplayName,Description,Enabled(defaulttrue),ShowInDiscoveryDocument(defaulttrue; controls whether it appears in the discovery document'sscopes_supported),UserClaims, plusRequiredandEmphasize. In v0.1.4, theUserClaimsof API scopes and API resources are only used for the discovery document'sclaims_supportedlist; they don't add user claims to the access token.ApiResource: an API.Scopeslists the names of the API scopes it contains;ApiSecretsare the secrets this API uses to call the introspection endpoint (also hashed withutils.Sha256); theAllowedAccessTokenSigningAlgorithmsfield 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:
| Static | Dynamic |
|---|---|
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.
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
})