Compare commits

...

4 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
d6f1cf40d2 Add comprehensive documentation for custom token attributes
Co-authored-by: nomeguy <85475922+nomeguy@users.noreply.github.com>
2026-02-09 13:38:21 +00:00
copilot-swe-agent[bot]
23c6011403 Add comprehensive tests for token attributes with $user variables
Co-authored-by: nomeguy <85475922+nomeguy@users.noreply.github.com>
2026-02-09 13:37:15 +00:00
copilot-swe-agent[bot]
4a5ad83fbb Add helpful placeholders and tooltips for Token Attributes
Co-authored-by: nomeguy <85475922+nomeguy@users.noreply.github.com>
2026-02-09 13:33:45 +00:00
copilot-swe-agent[bot]
e646556715 Initial plan 2026-02-09 13:30:07 +00:00
5 changed files with 398 additions and 8 deletions

View File

@@ -0,0 +1,163 @@
# Custom Token Attributes (JWT-Custom) - Examples
This document provides examples of how to use custom token attributes in Casdoor applications with the JWT-Custom token format.
## Overview
When you select **JWT-Custom** as the Token Format for your application, you can define custom JWT claims using the **Token Attributes** table. This allows you to include dynamic user data in your access tokens.
## Supported Dynamic Values
You can use the following placeholders in the "Value" field to include dynamic user data:
| Placeholder | Description | Returns |
|------------|-------------|---------|
| `$user.roles` | User's role names | Array of role names |
| `$user.permissions` | User's permission names | Array of permission names |
| `$user.groups` | User's groups | Array of group names |
| `$user.owner` | User's organization | String |
| `$user.name` | User's username | String |
| `$user.email` | User's email address | String |
| `$user.id` | User's unique ID | String |
| `$user.phone` | User's phone number | String |
## Example 1: Custom Roles Field (warpgate_roles)
**Use Case:** You want to include user roles in a custom claim named "warpgate_roles" as an array.
**Configuration:**
- **Name:** `warpgate_roles`
- **Value:** `$user.roles`
- **Type:** `Array`
**Result in JWT:**
```json
{
"warpgate_roles": ["admin", "developer", "viewer"],
"sub": "admin",
"aud": ["your-app-client-id"],
...
}
```
## Example 2: Custom Permissions Field
**Use Case:** You want to include user permissions in a custom claim named "app_permissions".
**Configuration:**
- **Name:** `app_permissions`
- **Value:** `$user.permissions`
- **Type:** `Array`
**Result in JWT:**
```json
{
"app_permissions": ["read", "write", "delete"],
...
}
```
## Example 3: User Groups
**Use Case:** You want to include user groups in a custom claim named "user_groups".
**Configuration:**
- **Name:** `user_groups`
- **Value:** `$user.groups`
- **Type:** `Array`
**Result in JWT:**
```json
{
"user_groups": ["engineering", "product"],
...
}
```
## Example 4: Single Value Fields
**Use Case:** You want to include the user's organization as a string.
**Configuration:**
- **Name:** `org`
- **Value:** `$user.owner`
- **Type:** `String`
**Result in JWT:**
```json
{
"org": "my-company",
...
}
```
## Example 5: Multiple Attributes
You can define multiple custom attributes for a single application:
| Name | Value | Type |
|------|-------|------|
| `warpgate_roles` | `$user.roles` | Array |
| `user_email` | `$user.email` | String |
| `user_org` | `$user.owner` | String |
| `user_groups` | `$user.groups` | Array |
**Result in JWT:**
```json
{
"warpgate_roles": ["admin", "developer"],
"user_email": "user@example.com",
"user_org": "my-company",
"user_groups": ["engineering"],
...
}
```
## Example 6: Template Strings
You can also use template strings by combining placeholders with static text:
**Configuration:**
- **Name:** `full_user_id`
- **Value:** `$user.owner/$user.name`
- **Type:** `String`
**Result in JWT:**
```json
{
"full_user_id": "my-company/johndoe",
...
}
```
## How to Configure
1. Navigate to your application's edit page
2. Set **Token Format** to `JWT-Custom`
3. Scroll to the **Token Attributes** section
4. Click **Add** to create a new attribute
5. Fill in:
- **Name**: The name of the JWT claim (e.g., `warpgate_roles`)
- **Value**: The dynamic value using placeholders (e.g., `$user.roles`)
- **Type**: Choose `Array` for lists or `String` for single values
6. Save your application
## Testing Your Configuration
After configuring your token attributes:
1. Obtain an access token by logging in through your application
2. Decode the JWT token (you can use https://jwt.io)
3. Verify that your custom claims appear in the token payload
## Notes
- Array types will include all values from the placeholder (e.g., all user roles)
- String types will use only the first value from array placeholders
- Static values (without placeholders) are also supported
- All custom attributes are in addition to standard JWT claims (iss, sub, aud, exp, iat, etc.)
## Related Documentation
- [Token Format Options](https://casdoor.org/docs/token/overview/#token-format-options)
- [Application Configuration](https://casdoor.org/docs/application/overview)

View File

@@ -0,0 +1,210 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"reflect"
"testing"
)
func TestReplaceAttributeValue(t *testing.T) {
// Create a test user with roles
user := &User{
Owner: "test-org",
Name: "test-user",
Email: "test@example.com",
Id: "test-id-123",
Phone: "+1234567890",
Roles: []*Role{
{Name: "admin"},
{Name: "developer"},
{Name: "viewer"},
},
Permissions: []*Permission{
{Name: "read"},
{Name: "write"},
{Name: "delete"},
},
Groups: []string{"engineering", "product"},
}
tests := []struct {
name string
value string
expected []string
}{
{
name: "Replace $user.roles",
value: "$user.roles",
expected: []string{"admin", "developer", "viewer"},
},
{
name: "Replace $user.permissions",
value: "$user.permissions",
expected: []string{"read", "write", "delete"},
},
{
name: "Replace $user.groups",
value: "$user.groups",
expected: []string{"engineering", "product"},
},
{
name: "Replace $user.owner",
value: "$user.owner",
expected: []string{"test-org"},
},
{
name: "Replace $user.name",
value: "$user.name",
expected: []string{"test-user"},
},
{
name: "Replace $user.email",
value: "$user.email",
expected: []string{"test@example.com"},
},
{
name: "Replace $user.id",
value: "$user.id",
expected: []string{"test-id-123"},
},
{
name: "Replace $user.phone",
value: "$user.phone",
expected: []string{"+1234567890"},
},
{
name: "Multiple replacements in template",
value: "User $user.name has email $user.email",
expected: []string{"User test-user has email test@example.com"},
},
{
name: "Static value (no replacement)",
value: "static-value",
expected: []string{"static-value"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := replaceAttributeValue(user, tt.value)
if !reflect.DeepEqual(result, tt.expected) {
t.Errorf("replaceAttributeValue() = %v, want %v", result, tt.expected)
}
})
}
}
func TestGetClaimsCustomWithTokenAttributes(t *testing.T) {
// Create a test user with roles
user := &User{
Owner: "test-org",
Name: "test-user",
Email: "test@example.com",
Roles: []*Role{
{Name: "admin"},
{Name: "developer"},
},
Permissions: []*Permission{
{Name: "read"},
{Name: "write"},
},
Groups: []string{"engineering"},
}
claims := Claims{
User: user,
}
// Test the warpgate_roles use case
tokenAttributes := []*JwtItem{
{
Name: "warpgate_roles",
Value: "$user.roles",
Type: "Array",
},
{
Name: "custom_email",
Value: "$user.email",
Type: "String",
},
{
Name: "user_groups",
Value: "$user.groups",
Type: "Array",
},
}
result := getClaimsCustom(claims, []string{}, tokenAttributes)
// Check warpgate_roles is an array
if rolesValue, ok := result["warpgate_roles"]; ok {
roles, ok := rolesValue.([]string)
if !ok {
t.Errorf("warpgate_roles should be []string, got %T", rolesValue)
}
expectedRoles := []string{"admin", "developer"}
if !reflect.DeepEqual(roles, expectedRoles) {
t.Errorf("warpgate_roles = %v, want %v", roles, expectedRoles)
}
} else {
t.Error("warpgate_roles not found in claims")
}
// Check custom_email is a string (first element of array)
if emailValue, ok := result["custom_email"]; ok {
email, ok := emailValue.(string)
if !ok {
t.Errorf("custom_email should be string, got %T", emailValue)
}
if email != "test@example.com" {
t.Errorf("custom_email = %v, want %v", email, "test@example.com")
}
} else {
t.Error("custom_email not found in claims")
}
// Check user_groups is an array
if groupsValue, ok := result["user_groups"]; ok {
groups, ok := groupsValue.([]string)
if !ok {
t.Errorf("user_groups should be []string, got %T", groupsValue)
}
expectedGroups := []string{"engineering"}
if !reflect.DeepEqual(groups, expectedGroups) {
t.Errorf("user_groups = %v, want %v", groups, expectedGroups)
}
} else {
t.Error("user_groups not found in claims")
}
}
func TestGetClaimsCustomWithEmptyUser(t *testing.T) {
// Test with nil user
nilResult := replaceAttributeValue(nil, "$user.roles")
if nilResult != nil {
t.Errorf("replaceAttributeValue with nil user should return nil, got %v", nilResult)
}
// Test with user without roles - should return empty slice
userWithoutRoles := &User{
Name: "test-user",
}
result := replaceAttributeValue(userWithoutRoles, "$user.roles")
// When user has no roles, getUserRoleNames returns an empty slice
if len(result) != 0 {
t.Errorf("replaceAttributeValue with empty roles should return empty array, got %v with length %d", result, len(result))
}
}

View File

@@ -523,8 +523,11 @@
"Timestamp": "Timestamp",
"Title": "Title",
"Title - Tooltip": "Browser page title",
"Token attribute name placeholder": "e.g., warpgate_roles",
"Token attribute value placeholder": "e.g., $user.roles",
"Token attributes": "Token attributes",
"Token attributes - Tooltip": "Token attributes",
"Token attributes - Tooltip": "Define custom JWT claims with dynamic values. Use $user.roles for roles array, $user.permissions for permissions, $user.groups for groups, or any user field like $user.email, $user.name, etc.",
"Token attributes help text": "Supported values: $user.roles, $user.permissions, $user.groups, $user.owner, $user.name, $user.email, $user.id, $user.phone",
"Tokens": "Tokens",
"Tour": "Tour",
"Transactions": "Transactions",

View File

@@ -523,8 +523,11 @@
"Timestamp": "时间",
"Title": "标题",
"Title - Tooltip": "浏览器页面标题",
"Token attribute name placeholder": "例如warpgate_roles",
"Token attribute value placeholder": "例如:$user.roles",
"Token attributes": "Token属性",
"Token attributes - Tooltip": "Token中包含的属性",
"Token attributes - Tooltip": "定义具有动态值的自定义JWT声明。使用 $user.roles 获取角色数组,$user.permissions 获取权限,$user.groups 获取分组,或使用任何用户字段如 $user.email, $user.name 等。",
"Token attributes help text": "支持的值:$user.roles, $user.permissions, $user.groups, $user.owner, $user.name, $user.email, $user.id, $user.phone",
"Tokens": "令牌",
"Tour": "引导",
"Transactions": "交易",

View File

@@ -68,9 +68,13 @@ class TokenAttributeTable extends React.Component {
width: "200px",
render: (text, record, index) => {
return (
<Input value={text} onChange={e => {
this.updateField(table, index, "name", e.target.value);
}} />
<Input
value={text}
placeholder={i18next.t("application:Token attribute name placeholder")}
onChange={e => {
this.updateField(table, index, "name", e.target.value);
}}
/>
);
},
},
@@ -81,9 +85,13 @@ class TokenAttributeTable extends React.Component {
width: "200px",
render: (text, record, index) => {
return (
<Input value={text} onChange={e => {
this.updateField(table, index, "value", e.target.value);
}} />
<Input
value={text}
placeholder={i18next.t("application:Token attribute value placeholder")}
onChange={e => {
this.updateField(table, index, "value", e.target.value);
}}
/>
);
},
},
@@ -136,6 +144,9 @@ class TokenAttributeTable extends React.Component {
<Table title={() => (
<div>
<Button style={{marginRight: "5px"}} type="primary" size="small" onClick={() => this.addRow(table)}>{i18next.t("general:Add")}</Button>
<span style={{marginLeft: "10px", color: "#666", fontSize: "12px"}}>
{i18next.t("application:Token attributes help text")}
</span>
</div>
)}
columns={columns} dataSource={table} rowKey="key" size="middle" bordered