Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,14 @@ TINYAUTH_OAUTH_PROVIDERS_name_USERINFOURL=
TINYAUTH_OAUTH_PROVIDERS_name_INSECURE=false
# Provider name in UI.
TINYAUTH_OAUTH_PROVIDERS_name_NAME=
# Username claim.
TINYAUTH_OAUTH_PROVIDERS_name_CLAIMS_USERNAME=
# Email claim.
TINYAUTH_OAUTH_PROVIDERS_name_CLAIMS_EMAIL=
# Name claim.
TINYAUTH_OAUTH_PROVIDERS_name_CLAIMS_NAME=
# Groups claim.
TINYAUTH_OAUTH_PROVIDERS_name_CLAIMS_GROUPS=

# oidc config

Expand Down
32 changes: 20 additions & 12 deletions internal/model/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,18 +253,26 @@ type TailscaleConfig struct {
// OAuth/OIDC config

type OAuthServiceConfig struct {
ClientID string `description:"OAuth client ID." yaml:"clientId,omitempty"`
ClientSecret string `description:"OAuth client secret." yaml:"clientSecret,omitempty"`
ClientSecretFile string `description:"Path to the file containing the OAuth client secret." yaml:"clientSecretFile,omitempty"`
Whitelist []string `description:"Comma-separated list of allowed OAuth domains for this provider." yaml:"whitelist,omitempty"`
WhitelistFile string `description:"Path to the OAuth whitelist file for this provider." yaml:"whitelistFile,omitempty"`
Scopes []string `description:"OAuth scopes." yaml:"scopes,omitempty"`
RedirectURL string `description:"OAuth redirect URL." yaml:"redirectUrl,omitempty"`
AuthURL string `description:"OAuth authorization URL." yaml:"authUrl,omitempty"`
TokenURL string `description:"OAuth token URL." yaml:"tokenUrl,omitempty"`
UserinfoURL string `description:"OAuth userinfo URL." yaml:"userinfoUrl,omitempty"`
Insecure bool `description:"Allow insecure OAuth connections." yaml:"insecure,omitempty"`
Name string `description:"Provider name in UI." yaml:"name,omitempty"`
ClientID string `description:"OAuth client ID." yaml:"clientId,omitempty"`
ClientSecret string `description:"OAuth client secret." yaml:"clientSecret,omitempty"`
ClientSecretFile string `description:"Path to the file containing the OAuth client secret." yaml:"clientSecretFile,omitempty"`
Whitelist []string `description:"Comma-separated list of allowed OAuth domains for this provider." yaml:"whitelist,omitempty"`
WhitelistFile string `description:"Path to the OAuth whitelist file for this provider." yaml:"whitelistFile,omitempty"`
Scopes []string `description:"OAuth scopes." yaml:"scopes,omitempty"`
RedirectURL string `description:"OAuth redirect URL." yaml:"redirectUrl,omitempty"`
AuthURL string `description:"OAuth authorization URL." yaml:"authUrl,omitempty"`
TokenURL string `description:"OAuth token URL." yaml:"tokenUrl,omitempty"`
UserinfoURL string `description:"OAuth userinfo URL." yaml:"userinfoUrl,omitempty"`
Insecure bool `description:"Allow insecure OAuth connections." yaml:"insecure,omitempty"`
Name string `description:"Provider name in UI." yaml:"name,omitempty"`
Claims OAuthServiceClaimsMap `description:"Map of claims to extract from the userinfo response." yaml:"claims,omitempty"`
}

type OAuthServiceClaimsMap struct {
Username string `description:"Username claim." yaml:"username,omitempty"`
Email string `description:"Email claim." yaml:"email,omitempty"`
Name string `description:"Name claim." yaml:"name,omitempty"`
Groups string `description:"Groups claim." yaml:"groups,omitempty"`
}

type OIDCClientConfig struct {
Expand Down
10 changes: 7 additions & 3 deletions internal/service/oauth_extractors.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,15 @@ type GithubUserinfoResponse struct {
ID int `json:"id"`
}

func defaultExtractor(client *http.Client, ctx context.Context, url string) (*model.Claims, error) {
return simpleReq[model.Claims](client, ctx, url, nil)
func defaultExtractor(client *http.Client, ctx context.Context, url string, mapClaims MapClaims) (*model.Claims, error) {
claims, err := simpleReq[map[string]any](client, ctx, url, nil)
if err != nil {
return nil, err
}
return new(mapClaims(*claims)), nil
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

func githubExtractor(client *http.Client, ctx context.Context, _ string) (*model.Claims, error) {
func githubExtractor(client *http.Client, ctx context.Context, _ string, _ MapClaims) (*model.Claims, error) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
var user model.Claims

userInfo, err := simpleReq[GithubUserinfoResponse](client, ctx, "https://api.github.com/user", map[string]string{
Expand Down
28 changes: 26 additions & 2 deletions internal/service/oauth_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import (
"golang.org/x/oauth2"
)

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

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

func (s *OAuthService) GetUserinfo(token *oauth2.Token) (*model.Claims, error) {
client := oauth2.NewClient(s.ctx, oauth2.StaticTokenSource(token))
return s.userinfoExtractor(client, s.ctx, s.serviceCfg.UserinfoURL)
return s.userinfoExtractor(client, s.ctx, s.serviceCfg.UserinfoURL, s.mapClaims)
}

func (s *OAuthService) GetConfig() model.OAuthServiceConfig {
Expand All @@ -97,3 +98,26 @@ func (s *OAuthService) UpdateConfig(config model.OAuthServiceConfig) {
s.config.Endpoint.TokenURL = config.TokenURL
s.config.RedirectURL = config.RedirectURL
}

func (s *OAuthService) mapClaims(claims map[string]any) model.Claims {
return model.Claims{
Sub: mapClaim[string]("sub", "", claims),
Name: mapClaim[string]("name", s.serviceCfg.Claims.Name, claims),
PreferredUsername: mapClaim[string]("preferred_username", s.serviceCfg.Claims.Username, claims),
Email: mapClaim[string]("email", s.serviceCfg.Claims.Email, claims),
Groups: mapClaim[any]("groups", s.serviceCfg.Claims.Groups, claims),
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

func mapClaim[T any](fallback, override string, kv map[string]any) T {
key := fallback
if override != "" {
key = override
}
v, ok := kv[key].(T)
if !ok {
var zero T
return zero
}
return v
}