Skip to content

Commit be48d71

Browse files
authored
feat: support for custom claims in oauth (#1087)
1 parent b541b61 commit be48d71

4 files changed

Lines changed: 61 additions & 17 deletions

File tree

‎.env.example‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,14 @@ TINYAUTH_OAUTH_PROVIDERS_name_USERINFOURL=
171171
TINYAUTH_OAUTH_PROVIDERS_name_INSECURE=false
172172
# Provider name in UI.
173173
TINYAUTH_OAUTH_PROVIDERS_name_NAME=
174+
# Username claim.
175+
TINYAUTH_OAUTH_PROVIDERS_name_CLAIMS_USERNAME=
176+
# Email claim.
177+
TINYAUTH_OAUTH_PROVIDERS_name_CLAIMS_EMAIL=
178+
# Name claim.
179+
TINYAUTH_OAUTH_PROVIDERS_name_CLAIMS_NAME=
180+
# Groups claim.
181+
TINYAUTH_OAUTH_PROVIDERS_name_CLAIMS_GROUPS=
174182

175183
# oidc config
176184

‎internal/model/config.go‎

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -253,18 +253,26 @@ type TailscaleConfig struct {
253253
// OAuth/OIDC config
254254

255255
type OAuthServiceConfig struct {
256-
ClientID string `description:"OAuth client ID." yaml:"clientId,omitempty"`
257-
ClientSecret string `description:"OAuth client secret." yaml:"clientSecret,omitempty"`
258-
ClientSecretFile string `description:"Path to the file containing the OAuth client secret." yaml:"clientSecretFile,omitempty"`
259-
Whitelist []string `description:"Comma-separated list of allowed OAuth domains for this provider." yaml:"whitelist,omitempty"`
260-
WhitelistFile string `description:"Path to the OAuth whitelist file for this provider." yaml:"whitelistFile,omitempty"`
261-
Scopes []string `description:"OAuth scopes." yaml:"scopes,omitempty"`
262-
RedirectURL string `description:"OAuth redirect URL." yaml:"redirectUrl,omitempty"`
263-
AuthURL string `description:"OAuth authorization URL." yaml:"authUrl,omitempty"`
264-
TokenURL string `description:"OAuth token URL." yaml:"tokenUrl,omitempty"`
265-
UserinfoURL string `description:"OAuth userinfo URL." yaml:"userinfoUrl,omitempty"`
266-
Insecure bool `description:"Allow insecure OAuth connections." yaml:"insecure,omitempty"`
267-
Name string `description:"Provider name in UI." yaml:"name,omitempty"`
256+
ClientID string `description:"OAuth client ID." yaml:"clientId,omitempty"`
257+
ClientSecret string `description:"OAuth client secret." yaml:"clientSecret,omitempty"`
258+
ClientSecretFile string `description:"Path to the file containing the OAuth client secret." yaml:"clientSecretFile,omitempty"`
259+
Whitelist []string `description:"Comma-separated list of allowed OAuth domains for this provider." yaml:"whitelist,omitempty"`
260+
WhitelistFile string `description:"Path to the OAuth whitelist file for this provider." yaml:"whitelistFile,omitempty"`
261+
Scopes []string `description:"OAuth scopes." yaml:"scopes,omitempty"`
262+
RedirectURL string `description:"OAuth redirect URL." yaml:"redirectUrl,omitempty"`
263+
AuthURL string `description:"OAuth authorization URL." yaml:"authUrl,omitempty"`
264+
TokenURL string `description:"OAuth token URL." yaml:"tokenUrl,omitempty"`
265+
UserinfoURL string `description:"OAuth userinfo URL." yaml:"userinfoUrl,omitempty"`
266+
Insecure bool `description:"Allow insecure OAuth connections." yaml:"insecure,omitempty"`
267+
Name string `description:"Provider name in UI." yaml:"name,omitempty"`
268+
Claims OAuthServiceClaimsMap `description:"Map of claims to extract from the userinfo response." yaml:"claims,omitempty"`
269+
}
270+
271+
type OAuthServiceClaimsMap struct {
272+
Username string `description:"Username claim." yaml:"username,omitempty"`
273+
Email string `description:"Email claim." yaml:"email,omitempty"`
274+
Name string `description:"Name claim." yaml:"name,omitempty"`
275+
Groups string `description:"Groups claim." yaml:"groups,omitempty"`
268276
}
269277

270278
type OIDCClientConfig struct {

‎internal/service/oauth_extractors.go‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,15 @@ type GithubUserinfoResponse struct {
2121
ID int `json:"id"`
2222
}
2323

24-
func defaultExtractor(client *http.Client, ctx context.Context, url string) (*model.Claims, error) {
25-
return simpleReq[model.Claims](client, ctx, url, nil)
24+
func defaultExtractor(client *http.Client, ctx context.Context, url string, mapClaims MapClaims) (*model.Claims, error) {
25+
claims, err := simpleReq[map[string]any](client, ctx, url, nil)
26+
if err != nil {
27+
return nil, err
28+
}
29+
return new(mapClaims(*claims)), nil
2630
}
2731

28-
func githubExtractor(client *http.Client, ctx context.Context, _ string) (*model.Claims, error) {
32+
func githubExtractor(client *http.Client, ctx context.Context, _ string, _ MapClaims) (*model.Claims, error) {
2933
var user model.Claims
3034

3135
userInfo, err := simpleReq[GithubUserinfoResponse](client, ctx, "https://api.github.com/user", map[string]string{

‎internal/service/oauth_service.go‎

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import (
1010
"golang.org/x/oauth2"
1111
)
1212

13-
type OAuthUserinfoExtractor func(client *http.Client, ctx context.Context, url string) (*model.Claims, error)
13+
type MapClaims func(claims map[string]any) model.Claims
14+
type OAuthUserinfoExtractor func(client *http.Client, ctx context.Context, url string, mapClaims MapClaims) (*model.Claims, error)
1415

1516
type OAuthService struct {
1617
serviceCfg model.OAuthServiceConfig
@@ -81,7 +82,7 @@ func (s *OAuthService) GetToken(code string, verifier string) (*oauth2.Token, er
8182

8283
func (s *OAuthService) GetUserinfo(token *oauth2.Token) (*model.Claims, error) {
8384
client := oauth2.NewClient(s.ctx, oauth2.StaticTokenSource(token))
84-
return s.userinfoExtractor(client, s.ctx, s.serviceCfg.UserinfoURL)
85+
return s.userinfoExtractor(client, s.ctx, s.serviceCfg.UserinfoURL, s.mapClaims)
8586
}
8687

8788
func (s *OAuthService) GetConfig() model.OAuthServiceConfig {
@@ -97,3 +98,26 @@ func (s *OAuthService) UpdateConfig(config model.OAuthServiceConfig) {
9798
s.config.Endpoint.TokenURL = config.TokenURL
9899
s.config.RedirectURL = config.RedirectURL
99100
}
101+
102+
func (s *OAuthService) mapClaims(claims map[string]any) model.Claims {
103+
return model.Claims{
104+
Sub: mapClaim[string]("sub", "", claims),
105+
Name: mapClaim[string]("name", s.serviceCfg.Claims.Name, claims),
106+
PreferredUsername: mapClaim[string]("preferred_username", s.serviceCfg.Claims.Username, claims),
107+
Email: mapClaim[string]("email", s.serviceCfg.Claims.Email, claims),
108+
Groups: mapClaim[any]("groups", s.serviceCfg.Claims.Groups, claims),
109+
}
110+
}
111+
112+
func mapClaim[T any](fallback, override string, kv map[string]any) T {
113+
key := fallback
114+
if override != "" {
115+
key = override
116+
}
117+
v, ok := kv[key].(T)
118+
if !ok {
119+
var zero T
120+
return zero
121+
}
122+
return v
123+
}

0 commit comments

Comments
 (0)