Compare commits

..
7 changed files with 95 additions and 45 deletions
+5
View File
@@ -223,6 +223,11 @@ TINYAUTH_LDAP_AUTHKEY=
# Cache duration for LDAP group membership in seconds. # Cache duration for LDAP group membership in seconds.
TINYAUTH_LDAP_GROUPCACHETTL=900 TINYAUTH_LDAP_GROUPCACHETTL=900
# experimental config
# Enable the OAuth bridge, uses a new way to format OAuth user information.
TINYAUTH_EXPERIMENTAL_OAUTHBRIDGEENABLED=false
# tailscale config # tailscale config
# Enable Tailscale integration. # Enable Tailscale integration.
+5 -4
View File
@@ -3,6 +3,7 @@ package main
import ( import (
"fmt" "fmt"
"os" "os"
"reflect"
"strings" "strings"
"charm.land/huh/v2" "charm.land/huh/v2"
@@ -32,10 +33,10 @@ func main() {
Resources: loaders, Resources: loaders,
Run: func(_ []string) error { Run: func(_ []string) error {
// enable this on experimental features // enable this on experimental features
//if !reflect.DeepEqual(model.NewDefaultConfiguration(env).Experimental, tConfig.Experimental) { if !reflect.DeepEqual(model.NewDefaultConfiguration(env).Experimental, tConfig.Experimental) {
// colors := getColors() colors := getColors()
// fmt.Println(colors.yellow.Render("⚠") + " Experimental features are enabled, use with caution. Experimental features may change with each release.") fmt.Println(colors.yellow.Render("⚠") + " Experimental features are enabled, use with caution. Experimental features may change with each release.")
//} }
return runCmd(*tConfig) return runCmd(*tConfig)
}, },
} }
+1 -1
View File
@@ -31,7 +31,7 @@ export const ContinuePage = () => {
const searchParams = new URLSearchParams(search); const searchParams = new URLSearchParams(search);
const screenParams = useScreenParams(searchParams); const screenParams = useScreenParams(searchParams);
const redirectUri = screenParams.redirect_uri; const redirectUri = screenParams.redirect_uri;
const isAppLogin = screenParams.login_for === "app"; const isAppLogin = screenParams.login_for === "app" || !screenParams.login_for;
const compiledParams = (() => { const compiledParams = (() => {
const params = searchParamsFromObject(screenParams).toString(); const params = searchParamsFromObject(screenParams).toString();
if (params.length > 0) { if (params.length > 0) {
+3 -4
View File
@@ -77,7 +77,7 @@ export const LoginPage = () => {
const [isOauthAutoRedirect, setIsOauthAutoRedirect] = useState( const [isOauthAutoRedirect, setIsOauthAutoRedirect] = useState(
providers.find((provider) => provider.id === oauth.autoRedirect) !== providers.find((provider) => provider.id === oauth.autoRedirect) !==
undefined && screenParams.redirect_uri !== undefined, undefined && (screenParams.redirect_uri || screenParams.oidc_ticket),
); );
const oauthProviders = providers.filter( const oauthProviders = providers.filter(
@@ -174,8 +174,7 @@ export const LoginPage = () => {
!auth.authenticated && !auth.authenticated &&
isOauthAutoRedirect && isOauthAutoRedirect &&
!hasAutoRedirectedRef.current && !hasAutoRedirectedRef.current &&
screenParams.redirect_uri && (screenParams.redirect_uri || screenParams.oidc_ticket)
screenParams.login_for
) { ) {
hasAutoRedirectedRef.current = true; hasAutoRedirectedRef.current = true;
oauthMutate(oauth.autoRedirect); oauthMutate(oauth.autoRedirect);
@@ -186,8 +185,8 @@ export const LoginPage = () => {
hasAutoRedirectedRef, hasAutoRedirectedRef,
oauth.autoRedirect, oauth.autoRedirect,
isOauthAutoRedirect, isOauthAutoRedirect,
screenParams.login_for,
screenParams.redirect_uri, screenParams.redirect_uri,
screenParams.oidc_ticket
]); ]);
useEffect(() => { useEffect(() => {
+64 -27
View File
@@ -220,35 +220,16 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
return return
} }
var name string oauthUserInfo := controller.createOAuthUserInfo(oauthUserInfo{
Username: user.PreferredUsername,
if strings.TrimSpace(user.Name) != "" { Email: user.Email,
controller.log.App.Debug().Msg("Using name from OAuth provider") Name: user.Name,
name = user.Name })
} else {
controller.log.App.Debug().Msg("No name from OAuth provider, generating from email")
parts := strings.SplitN(user.Email, "@", 2)
if len(parts) == 2 {
name = fmt.Sprintf("%s (%s)", utils.Capitalize(parts[0]), parts[1])
} else {
name = utils.Capitalize(user.Email)
}
}
var username string
if strings.TrimSpace(user.PreferredUsername) != "" {
controller.log.App.Debug().Msg("Using preferred username from OAuth provider")
username = user.PreferredUsername
} else {
controller.log.App.Debug().Msg("No preferred username from OAuth provider, generating from email")
username = strings.Replace(user.Email, "@", "_", 1)
}
sessionCookie := repository.Session{ sessionCookie := repository.Session{
Username: username, Username: oauthUserInfo.Username,
Name: name, Name: oauthUserInfo.Name,
Email: user.Email, Email: oauthUserInfo.Email,
Provider: svc.ID(), Provider: svc.ID(),
OAuthGroups: utils.CoalesceToString(user.Groups), OAuthGroups: utils.CoalesceToString(user.Groups),
OAuthName: svc.Name(), OAuthName: svc.Name(),
@@ -355,3 +336,59 @@ func (controller *OAuthController) isRedirectSafe(redirectURI string) bool {
return false return false
} }
type oauthUserInfo struct {
Email string
Username string
Name string
}
func (controller *OAuthController) createOAuthUserInfo(input oauthUserInfo) oauthUserInfo {
info := oauthUserInfo{
Email: input.Email,
}
if controller.config.Experimental.OAuthBridgeEnabled {
if input.Username != "" {
info.Username = input.Username
} else {
parts := strings.SplitN(input.Email, "@", 2)
if len(parts) != 2 {
controller.log.App.Error().Str("email", input.Email).Msg("Invalid email address")
} else {
info.Username = parts[0]
}
}
if input.Name != "" {
info.Name = input.Name
} else {
info.Name = utils.Capitalize(info.Username)
}
return info
}
if input.Name != "" {
controller.log.App.Debug().Msg("Using name from OAuth provider")
info.Name = input.Name
} else {
controller.log.App.Debug().Msg("No name from OAuth provider, generating from email")
parts := strings.SplitN(input.Email, "@", 2)
if len(parts) != 2 {
controller.log.App.Error().Str("email", input.Email).Msg("Invalid email address")
} else {
info.Name = fmt.Sprintf("%s (%s)", utils.Capitalize(parts[0]), parts[1])
}
}
if input.Username != "" {
controller.log.App.Debug().Msg("Using preferred username from OAuth provider")
info.Username = input.Username
} else {
controller.log.App.Debug().Msg("No preferred username from OAuth provider, generating from email")
info.Username = strings.Replace(info.Email, "@", "_", 1)
}
return info
}
+11 -5
View File
@@ -40,6 +40,7 @@ var (
type ContextMiddleware struct { type ContextMiddleware struct {
log *logger.Logger log *logger.Logger
runtime *model.RuntimeConfig runtime *model.RuntimeConfig
config *model.Config
auth *service.AuthService auth *service.AuthService
broker *service.OAuthBrokerService broker *service.OAuthBrokerService
tailscale *service.TailscaleService tailscale *service.TailscaleService
@@ -50,6 +51,7 @@ type ContextMiddlewareInput struct {
Log *logger.Logger Log *logger.Logger
RuntimeConfig *model.RuntimeConfig RuntimeConfig *model.RuntimeConfig
StaticConfig *model.Config
AuthService *service.AuthService AuthService *service.AuthService
BrokerService *service.OAuthBrokerService BrokerService *service.OAuthBrokerService
TailscaleService *service.TailscaleService TailscaleService *service.TailscaleService
@@ -59,6 +61,7 @@ func NewContextMiddleware(i ContextMiddlewareInput) *ContextMiddleware {
return &ContextMiddleware{ return &ContextMiddleware{
log: i.Log, log: i.Log,
runtime: i.RuntimeConfig, runtime: i.RuntimeConfig,
config: i.StaticConfig,
auth: i.AuthService, auth: i.AuthService,
broker: i.BrokerService, broker: i.BrokerService,
tailscale: i.TailscaleService, tailscale: i.TailscaleService,
@@ -332,16 +335,19 @@ func (m *ContextMiddleware) tailscaleWhois(ip string) (*model.TailscaleContext,
return nil, nil return nil, nil
} }
username := strings.Replace(whois.LoginName, "@", "_", 1)
uctx := model.TailscaleContext{ uctx := model.TailscaleContext{
BaseContext: model.BaseContext{ BaseContext: model.BaseContext{
Username: username, Email: whois.LoginName,
Email: whois.LoginName, Name: whois.DisplayName,
Name: whois.DisplayName,
}, },
NodeName: whois.NodeName, NodeName: whois.NodeName,
} }
if m.config.Experimental.OAuthBridgeEnabled {
uctx.BaseContext.Username = strings.SplitN(whois.LoginName, "@", 2)[0]
} else {
uctx.BaseContext.Username = strings.Replace(whois.LoginName, "@", "_", 1)
}
return &uctx, nil return &uctx, nil
} }
+6 -4
View File
@@ -115,9 +115,9 @@ type Config struct {
UI UIConfig `description:"UI customization." yaml:"ui,omitempty"` UI UIConfig `description:"UI customization." yaml:"ui,omitempty"`
LDAP LDAPConfig `description:"LDAP configuration." yaml:"ldap,omitempty"` LDAP LDAPConfig `description:"LDAP configuration." yaml:"ldap,omitempty"`
// enable the cli warning on experimental features // enable the cli warning on experimental features
//Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental,omitempty"` Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental,omitempty"`
Tailscale TailscaleConfig `description:"Tailscale configuration." yaml:"tailscale,omitempty"` Tailscale TailscaleConfig `description:"Tailscale configuration." yaml:"tailscale,omitempty"`
Log LogConfig `description:"Logging configuration." yaml:"log,omitempty"` Log LogConfig `description:"Logging configuration." yaml:"log,omitempty"`
} }
type DatabaseConfig struct { type DatabaseConfig struct {
@@ -238,7 +238,9 @@ type LogStreamConfig struct {
Level string `description:"Log level for this stream. Use global if empty." yaml:"level,omitempty"` Level string `description:"Log level for this stream. Use global if empty." yaml:"level,omitempty"`
} }
//type ExperimentalConfig struct{} type ExperimentalConfig struct {
OAuthBridgeEnabled bool `description:"Enable the OAuth bridge, uses a new way to format OAuth user information." yaml:"oauthBridgeEnabled,omitempty"`
}
type TailscaleConfig struct { type TailscaleConfig struct {
Enabled bool `description:"Enable Tailscale integration." yaml:"enabled,omitempty"` Enabled bool `description:"Enable Tailscale integration." yaml:"enabled,omitempty"`