Skip to content
This repository was archived by the owner on Oct 10, 2025. It is now read-only.

Commit 6c3ed7b

Browse files
authored
feat: apply defaults for customClaims (#661)
1 parent c575700 commit 6c3ed7b

9 files changed

Lines changed: 90 additions & 9 deletions

File tree

docs/environment-variables.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
| AUTH_ACCESS_TOKEN_EXPIRES_IN | Number of seconds before the access token (JWT) expires. | `900`(15 minutes) |
5353
| AUTH_REFRESH_TOKEN_EXPIRES_IN | Number of seconds before the refresh token expires. | `2592000` (30 days) |
5454
| AUTH_JWT_CUSTOM_CLAIMS | | |
55+
| AUTH_JWT_CUSTOM_CLAIMS_DEFAULTS | This optional setting enables you to overwrite null custom claims with default values | |
5556
| AUTH_WEBAUTHN_ENABLED | When enabled, passwordless Webauthn authentication can be done via device supported strong authenticators like fingerprint, Face ID, etc. | false |
5657
| AUTH_WEBAUTHN_RP_NAME | Relying party name. Friendly name visual to the user informing who requires the authentication. Probably your app's name. | |
5758
| AUTH_WEBAUTHN_RP_ID | Relying party id. If not set `AUTH_CLIENT_URL` will be used as a default. | |

go/cmd/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ func getConfig(cCtx *cli.Context) (controller.Config, error) { //nolint:funlen
8181
BlockedEmails: blockedEmails,
8282
ClientURL: clientURL,
8383
CustomClaims: cCtx.String(flagCustomClaims),
84+
CustomClaimsDefaults: cCtx.String(flagCustomClaimsDefaults),
8485
ConcealErrors: cCtx.Bool(flagConcealErrors),
8586
DisableSignup: cCtx.Bool(flagDisableSignup),
8687
DisableNewUsers: cCtx.Bool(flagDisableNewUsers),

go/cmd/jwt_getter.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,28 @@ import (
1212

1313
func getJWTGetter(cCtx *cli.Context, db controller.DBClient) (*controller.JWTGetter, error) {
1414
var rawClaims map[string]string
15+
var defaults map[string]any
1516

1617
if cCtx.String(flagCustomClaims) != "" {
1718
if err := json.Unmarshal([]byte(cCtx.String(flagCustomClaims)), &rawClaims); err != nil {
1819
return nil, fmt.Errorf("failed to unmarshal custom claims: %w", err)
1920
}
2021
}
2122

23+
if cCtx.String(flagCustomClaimsDefaults) != "" {
24+
if err := json.Unmarshal([]byte(cCtx.String(flagCustomClaimsDefaults)), &defaults); err != nil {
25+
return nil, fmt.Errorf("failed to unmarshal custom claims defaults: %w", err)
26+
}
27+
}
28+
2229
var customClaimer controller.CustomClaimer
2330
var err error
2431
if len(rawClaims) > 0 {
2532
customClaimer, err = controller.NewCustomClaims(
2633
rawClaims,
2734
&http.Client{}, //nolint:exhaustruct
2835
cCtx.String(flagGraphqlURL),
36+
defaults,
2937
controller.CustomClaimerAddAdminSecret(cCtx.String(flagHasuraAdminSecret)),
3038
)
3139
if err != nil {

go/cmd/serve.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ const (
5959
flagAllowRedirectURLs = "allow-redirect-urls"
6060
flagEnableChangeEnv = "enable-change-env"
6161
flagCustomClaims = "custom-claims"
62+
flagCustomClaimsDefaults = "custom-claims-defaults"
6263
flagGraphqlURL = "graphql-url"
6364
flagHasuraAdminSecret = "hasura-admin-secret" //nolint:gosec
6465
flagPasswordMinLength = "password-min-length"
@@ -419,6 +420,12 @@ func CommandServe() *cli.Command { //nolint:funlen,maintidx
419420
Category: "jwt",
420421
EnvVars: []string{"AUTH_JWT_CUSTOM_CLAIMS"},
421422
},
423+
&cli.StringFlag{ //nolint: exhaustruct
424+
Name: flagCustomClaimsDefaults,
425+
Usage: "Custom claims defaults",
426+
Category: "jwt",
427+
EnvVars: []string{"AUTH_JWT_CUSTOM_CLAIMS_DEFAULTS"},
428+
},
422429
&cli.StringFlag{ //nolint: exhaustruct
423430
Name: flagGraphqlURL,
424431
Usage: "Hasura GraphQL endpoint. Required for custom claims",

go/controller/change_env.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,23 @@ func (ctrl *Controller) PostChangeEnv(c *gin.Context) { //nolint:funlen,cyclop
3636
} else {
3737
var rawClaims map[string]string
3838
if err := json.Unmarshal([]byte(ctrl.config.CustomClaims), &rawClaims); err != nil {
39-
c.JSON(http.StatusBadRequest, gin.H{"message": "failed to unmarhsal custom claims", "error": err.Error()})
39+
c.JSON(http.StatusBadRequest, gin.H{"message": "failed to unmarshal custom claims", "error": err.Error()})
40+
}
41+
var defaults map[string]any
42+
if ctrl.config.CustomClaimsDefaults == "" {
43+
defaults = nil
44+
} else {
45+
if err := json.Unmarshal([]byte(ctrl.config.CustomClaimsDefaults), &defaults); err != nil {
46+
c.JSON(http.StatusBadRequest, gin.H{"message": "failed to unmarshal custom claims defaults", "error": err.Error()})
47+
}
4048
}
4149

4250
if len(rawClaims) > 0 {
4351
cc, err := NewCustomClaims(
4452
rawClaims,
4553
&http.Client{}, //nolint:exhaustruct
4654
ctrl.config.HasuraGraphqlURL,
55+
defaults,
4756
CustomClaimerAddAdminSecret(ctrl.config.HasuraAdminSecret),
4857
)
4958
if err != nil {

go/controller/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ type Config struct {
3737
BlockedEmails stringlice `json:"AUTH_ACCESS_CONTROL_BLOCKED_EMAILS"`
3838
ClientURL *url.URL `json:"AUTH_CLIENT_URL"`
3939
CustomClaims string `json:"AUTH_JWT_CUSTOM_CLAIMS"`
40+
CustomClaimsDefaults string `json:"AUTH_JWT_CUSTOM_CLAIMS_DEFAULTS"`
4041
ConcealErrors bool `json:"AUTH_CONCEAL_ERRORS"`
4142
DisableSignup bool `json:"AUTH_DISABLE_SIGNUP"`
4243
DisableNewUsers bool `json:"AUTH_DISABLE_NEW_USERS"`

go/controller/custom_claims.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ type CustomClaims struct {
192192
httpclient *http.Client
193193
graphqlURL string
194194
requestInterceptor []RequestInterceptor
195+
defaults map[string]any
195196
}
196197

197198
func CustomClaimerAddAdminSecret(adminSecret string) RequestInterceptor {
@@ -204,6 +205,7 @@ func NewCustomClaims(
204205
rawClaims map[string]string,
205206
httpclient *http.Client,
206207
graphqlURL string,
208+
defaults map[string]any,
207209
requestInterceptor ...RequestInterceptor,
208210
) (*CustomClaims, error) {
209211
claims := make(map[string]any)
@@ -261,6 +263,7 @@ func NewCustomClaims(
261263
httpclient: httpclient,
262264
graphqlURL: graphqlURL,
263265
requestInterceptor: requestInterceptor,
266+
defaults: defaults,
264267
}, nil
265268
}
266269

@@ -296,6 +299,15 @@ func (c *CustomClaims) getClaimsBackwardsCompatibility(data any, path []string)
296299
return nil
297300
}
298301

302+
func (c *CustomClaims) defaultOrNil(name string) any {
303+
if c.defaults != nil {
304+
if val, exists := c.defaults[name]; exists {
305+
return val
306+
}
307+
}
308+
return nil
309+
}
310+
299311
func (c *CustomClaims) ExtractClaims(data any) (map[string]any, error) {
300312
claims := make(map[string]any)
301313
for name, j := range c.jsonPaths {
@@ -305,7 +317,7 @@ func (c *CustomClaims) ExtractClaims(data any) (map[string]any, error) {
305317
} else {
306318
v, err := j.jpath.FindResults(data)
307319
if err != nil {
308-
claims[name] = nil
320+
claims[name] = c.defaultOrNil(name)
309321
continue
310322
}
311323

@@ -319,7 +331,12 @@ func (c *CustomClaims) ExtractClaims(data any) (map[string]any, error) {
319331
got = v[0][0].Interface()
320332
}
321333
}
322-
claims[name] = got
334+
335+
if got == nil {
336+
claims[name] = c.defaultOrNil(name)
337+
} else {
338+
claims[name] = got
339+
}
323340
}
324341
return claims, nil
325342
}

go/controller/custom_claims_test.go

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,14 @@ func TestCustomClaims(t *testing.T) {
5151
}
5252

5353
cases := []struct {
54-
name string
55-
claims map[string]string
56-
expectedGraphql string
57-
expectedData map[string]any
54+
name string
55+
claims map[string]string
56+
expectedGraphql string
57+
expectedData map[string]any
58+
customClaimsDefaults map[string]any
5859
}{
5960
{
60-
name: "",
61+
name: "without custom claim defaults",
6162
claims: map[string]string{
6263
"root": "m",
6364
"key": "m.k",
@@ -90,14 +91,49 @@ func TestCustomClaims(t *testing.T) {
9091
3,
9192
},
9293
},
94+
customClaimsDefaults: nil,
95+
},
96+
{
97+
name: "with custom claims defaults",
98+
claims: map[string]string{
99+
"root": "m",
100+
"key": "m.k",
101+
"element": "m.l[2]",
102+
"array[]": "m.l[]",
103+
"array[*]": "m.l[*]",
104+
"array[].ids": "m.lm[].id",
105+
"array[*].ids": "m.lm[*].id",
106+
"array.ids[]": "m.lm.id[]",
107+
"arrayOneElement[]": "m.l2[]",
108+
"metadata.m1": "metadata.m1",
109+
"nonexistent": "nonexistent.nonexistent",
110+
},
111+
expectedGraphql: "query GetClaims($id: uuid!) { user(id:$id) {m{k l l2 lm{id }}metadata nonexistent{nonexistent }} }", //nolint:lll
112+
expectedData: map[string]any{
113+
"root": data["m"],
114+
"key": "v",
115+
"element": "c",
116+
"arrayOneElement[]": []any{string("a")},
117+
"array[]": []any{"a", "b", "c"},
118+
"array[*]": []any{"a", "b", "c"},
119+
"array[].ids": []any{1, 2, 3},
120+
"array[*].ids": []any{1, 2, 3},
121+
"array.ids[]": []any{1, 2, 3},
122+
"metadata.m1": 1,
123+
"nonexistent": "defaultNonExistent",
124+
},
125+
customClaimsDefaults: map[string]any{
126+
"root": "defaultRoot",
127+
"nonexistent": "defaultNonExistent",
128+
},
93129
},
94130
}
95131

96132
for _, tc := range cases {
97133
t.Run(tc.name, func(t *testing.T) {
98134
t.Parallel()
99135

100-
c, err := controller.NewCustomClaims(tc.claims, nil, "")
136+
c, err := controller.NewCustomClaims(tc.claims, nil, "", tc.customClaimsDefaults)
101137
if err != nil {
102138
t.Fatalf("failed to get custom claims: %v", err)
103139
}

go/controller/validator_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ func getConfig() *controller.Config {
2727
BlockedEmails: []string{},
2828
ClientURL: clientURL,
2929
CustomClaims: "",
30+
CustomClaimsDefaults: "",
3031
ConcealErrors: false,
3132
DisableSignup: false,
3233
DisableNewUsers: false,

0 commit comments

Comments
 (0)