Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
02a66a01
Commit
02a66a01
authored
Mar 13, 2026
by
ruiqurm
Committed by
Glorhop
Apr 09, 2026
Browse files
feat: support OIDC login.
parent
155d3474
Changes
28
Expand all
Hide whitespace changes
Inline
Side-by-side
backend/internal/config/config.go
View file @
02a66a01
...
@@ -65,6 +65,7 @@ type Config struct {
...
@@ -65,6 +65,7 @@ type Config struct {
JWT
JWTConfig
`mapstructure:"jwt"`
JWT
JWTConfig
`mapstructure:"jwt"`
Totp
TotpConfig
`mapstructure:"totp"`
Totp
TotpConfig
`mapstructure:"totp"`
LinuxDo
LinuxDoConnectConfig
`mapstructure:"linuxdo_connect"`
LinuxDo
LinuxDoConnectConfig
`mapstructure:"linuxdo_connect"`
OIDC
OIDCConnectConfig
`mapstructure:"oidc_connect"`
Default
DefaultConfig
`mapstructure:"default"`
Default
DefaultConfig
`mapstructure:"default"`
RateLimit
RateLimitConfig
`mapstructure:"rate_limit"`
RateLimit
RateLimitConfig
`mapstructure:"rate_limit"`
Pricing
PricingConfig
`mapstructure:"pricing"`
Pricing
PricingConfig
`mapstructure:"pricing"`
...
@@ -184,6 +185,34 @@ type LinuxDoConnectConfig struct {
...
@@ -184,6 +185,34 @@ type LinuxDoConnectConfig struct {
UserInfoUsernamePath
string
`mapstructure:"userinfo_username_path"`
UserInfoUsernamePath
string
`mapstructure:"userinfo_username_path"`
}
}
type
OIDCConnectConfig
struct
{
Enabled
bool
`mapstructure:"enabled"`
ProviderName
string
`mapstructure:"provider_name"`
// 显示名: "Keycloak" 等
ClientID
string
`mapstructure:"client_id"`
ClientSecret
string
`mapstructure:"client_secret"`
IssuerURL
string
`mapstructure:"issuer_url"`
DiscoveryURL
string
`mapstructure:"discovery_url"`
AuthorizeURL
string
`mapstructure:"authorize_url"`
TokenURL
string
`mapstructure:"token_url"`
UserInfoURL
string
`mapstructure:"userinfo_url"`
JWKSURL
string
`mapstructure:"jwks_url"`
Scopes
string
`mapstructure:"scopes"`
// 默认 "openid email profile"
RedirectURL
string
`mapstructure:"redirect_url"`
// 后端回调地址(需在提供方后台登记)
FrontendRedirectURL
string
`mapstructure:"frontend_redirect_url"`
// 前端接收 token 的路由(默认:/auth/oidc/callback)
TokenAuthMethod
string
`mapstructure:"token_auth_method"`
// client_secret_post / client_secret_basic / none
UsePKCE
bool
`mapstructure:"use_pkce"`
ValidateIDToken
bool
`mapstructure:"validate_id_token"`
AllowedSigningAlgs
string
`mapstructure:"allowed_signing_algs"`
// 默认 "RS256,ES256,PS256"
ClockSkewSeconds
int
`mapstructure:"clock_skew_seconds"`
// 默认 120
RequireEmailVerified
bool
`mapstructure:"require_email_verified"`
// 默认 false
// 可选:用于从 userinfo JSON 中提取字段的 gjson 路径。
// 为空时,服务端会尝试一组常见字段名。
UserInfoEmailPath
string
`mapstructure:"userinfo_email_path"`
UserInfoIDPath
string
`mapstructure:"userinfo_id_path"`
UserInfoUsernamePath
string
`mapstructure:"userinfo_username_path"`
}
// TokenRefreshConfig OAuth token自动刷新配置
// TokenRefreshConfig OAuth token自动刷新配置
type
TokenRefreshConfig
struct
{
type
TokenRefreshConfig
struct
{
// 是否启用自动刷新
// 是否启用自动刷新
...
@@ -968,6 +997,23 @@ func load(allowMissingJWTSecret bool) (*Config, error) {
...
@@ -968,6 +997,23 @@ func load(allowMissingJWTSecret bool) (*Config, error) {
cfg
.
LinuxDo
.
UserInfoEmailPath
=
strings
.
TrimSpace
(
cfg
.
LinuxDo
.
UserInfoEmailPath
)
cfg
.
LinuxDo
.
UserInfoEmailPath
=
strings
.
TrimSpace
(
cfg
.
LinuxDo
.
UserInfoEmailPath
)
cfg
.
LinuxDo
.
UserInfoIDPath
=
strings
.
TrimSpace
(
cfg
.
LinuxDo
.
UserInfoIDPath
)
cfg
.
LinuxDo
.
UserInfoIDPath
=
strings
.
TrimSpace
(
cfg
.
LinuxDo
.
UserInfoIDPath
)
cfg
.
LinuxDo
.
UserInfoUsernamePath
=
strings
.
TrimSpace
(
cfg
.
LinuxDo
.
UserInfoUsernamePath
)
cfg
.
LinuxDo
.
UserInfoUsernamePath
=
strings
.
TrimSpace
(
cfg
.
LinuxDo
.
UserInfoUsernamePath
)
cfg
.
OIDC
.
ProviderName
=
strings
.
TrimSpace
(
cfg
.
OIDC
.
ProviderName
)
cfg
.
OIDC
.
ClientID
=
strings
.
TrimSpace
(
cfg
.
OIDC
.
ClientID
)
cfg
.
OIDC
.
ClientSecret
=
strings
.
TrimSpace
(
cfg
.
OIDC
.
ClientSecret
)
cfg
.
OIDC
.
IssuerURL
=
strings
.
TrimSpace
(
cfg
.
OIDC
.
IssuerURL
)
cfg
.
OIDC
.
DiscoveryURL
=
strings
.
TrimSpace
(
cfg
.
OIDC
.
DiscoveryURL
)
cfg
.
OIDC
.
AuthorizeURL
=
strings
.
TrimSpace
(
cfg
.
OIDC
.
AuthorizeURL
)
cfg
.
OIDC
.
TokenURL
=
strings
.
TrimSpace
(
cfg
.
OIDC
.
TokenURL
)
cfg
.
OIDC
.
UserInfoURL
=
strings
.
TrimSpace
(
cfg
.
OIDC
.
UserInfoURL
)
cfg
.
OIDC
.
JWKSURL
=
strings
.
TrimSpace
(
cfg
.
OIDC
.
JWKSURL
)
cfg
.
OIDC
.
Scopes
=
strings
.
TrimSpace
(
cfg
.
OIDC
.
Scopes
)
cfg
.
OIDC
.
RedirectURL
=
strings
.
TrimSpace
(
cfg
.
OIDC
.
RedirectURL
)
cfg
.
OIDC
.
FrontendRedirectURL
=
strings
.
TrimSpace
(
cfg
.
OIDC
.
FrontendRedirectURL
)
cfg
.
OIDC
.
TokenAuthMethod
=
strings
.
ToLower
(
strings
.
TrimSpace
(
cfg
.
OIDC
.
TokenAuthMethod
))
cfg
.
OIDC
.
AllowedSigningAlgs
=
strings
.
TrimSpace
(
cfg
.
OIDC
.
AllowedSigningAlgs
)
cfg
.
OIDC
.
UserInfoEmailPath
=
strings
.
TrimSpace
(
cfg
.
OIDC
.
UserInfoEmailPath
)
cfg
.
OIDC
.
UserInfoIDPath
=
strings
.
TrimSpace
(
cfg
.
OIDC
.
UserInfoIDPath
)
cfg
.
OIDC
.
UserInfoUsernamePath
=
strings
.
TrimSpace
(
cfg
.
OIDC
.
UserInfoUsernamePath
)
cfg
.
Dashboard
.
KeyPrefix
=
strings
.
TrimSpace
(
cfg
.
Dashboard
.
KeyPrefix
)
cfg
.
Dashboard
.
KeyPrefix
=
strings
.
TrimSpace
(
cfg
.
Dashboard
.
KeyPrefix
)
cfg
.
CORS
.
AllowedOrigins
=
normalizeStringSlice
(
cfg
.
CORS
.
AllowedOrigins
)
cfg
.
CORS
.
AllowedOrigins
=
normalizeStringSlice
(
cfg
.
CORS
.
AllowedOrigins
)
cfg
.
Security
.
ResponseHeaders
.
AdditionalAllowed
=
normalizeStringSlice
(
cfg
.
Security
.
ResponseHeaders
.
AdditionalAllowed
)
cfg
.
Security
.
ResponseHeaders
.
AdditionalAllowed
=
normalizeStringSlice
(
cfg
.
Security
.
ResponseHeaders
.
AdditionalAllowed
)
...
@@ -1138,6 +1184,30 @@ func setDefaults() {
...
@@ -1138,6 +1184,30 @@ func setDefaults() {
viper
.
SetDefault
(
"linuxdo_connect.userinfo_id_path"
,
""
)
viper
.
SetDefault
(
"linuxdo_connect.userinfo_id_path"
,
""
)
viper
.
SetDefault
(
"linuxdo_connect.userinfo_username_path"
,
""
)
viper
.
SetDefault
(
"linuxdo_connect.userinfo_username_path"
,
""
)
// Generic OIDC OAuth 登录
viper
.
SetDefault
(
"oidc_connect.enabled"
,
false
)
viper
.
SetDefault
(
"oidc_connect.provider_name"
,
"OIDC"
)
viper
.
SetDefault
(
"oidc_connect.client_id"
,
""
)
viper
.
SetDefault
(
"oidc_connect.client_secret"
,
""
)
viper
.
SetDefault
(
"oidc_connect.issuer_url"
,
""
)
viper
.
SetDefault
(
"oidc_connect.discovery_url"
,
""
)
viper
.
SetDefault
(
"oidc_connect.authorize_url"
,
""
)
viper
.
SetDefault
(
"oidc_connect.token_url"
,
""
)
viper
.
SetDefault
(
"oidc_connect.userinfo_url"
,
""
)
viper
.
SetDefault
(
"oidc_connect.jwks_url"
,
""
)
viper
.
SetDefault
(
"oidc_connect.scopes"
,
"openid email profile"
)
viper
.
SetDefault
(
"oidc_connect.redirect_url"
,
""
)
viper
.
SetDefault
(
"oidc_connect.frontend_redirect_url"
,
"/auth/oidc/callback"
)
viper
.
SetDefault
(
"oidc_connect.token_auth_method"
,
"client_secret_post"
)
viper
.
SetDefault
(
"oidc_connect.use_pkce"
,
false
)
viper
.
SetDefault
(
"oidc_connect.validate_id_token"
,
true
)
viper
.
SetDefault
(
"oidc_connect.allowed_signing_algs"
,
"RS256,ES256,PS256"
)
viper
.
SetDefault
(
"oidc_connect.clock_skew_seconds"
,
120
)
viper
.
SetDefault
(
"oidc_connect.require_email_verified"
,
false
)
viper
.
SetDefault
(
"oidc_connect.userinfo_email_path"
,
""
)
viper
.
SetDefault
(
"oidc_connect.userinfo_id_path"
,
""
)
viper
.
SetDefault
(
"oidc_connect.userinfo_username_path"
,
""
)
// Database
// Database
viper
.
SetDefault
(
"database.host"
,
"localhost"
)
viper
.
SetDefault
(
"database.host"
,
"localhost"
)
viper
.
SetDefault
(
"database.port"
,
5432
)
viper
.
SetDefault
(
"database.port"
,
5432
)
...
@@ -1572,6 +1642,87 @@ func (c *Config) Validate() error {
...
@@ -1572,6 +1642,87 @@ func (c *Config) Validate() error {
warnIfInsecureURL
(
"linuxdo_connect.redirect_url"
,
c
.
LinuxDo
.
RedirectURL
)
warnIfInsecureURL
(
"linuxdo_connect.redirect_url"
,
c
.
LinuxDo
.
RedirectURL
)
warnIfInsecureURL
(
"linuxdo_connect.frontend_redirect_url"
,
c
.
LinuxDo
.
FrontendRedirectURL
)
warnIfInsecureURL
(
"linuxdo_connect.frontend_redirect_url"
,
c
.
LinuxDo
.
FrontendRedirectURL
)
}
}
if
c
.
OIDC
.
Enabled
{
if
strings
.
TrimSpace
(
c
.
OIDC
.
ClientID
)
==
""
{
return
fmt
.
Errorf
(
"oidc_connect.client_id is required when oidc_connect.enabled=true"
)
}
if
strings
.
TrimSpace
(
c
.
OIDC
.
IssuerURL
)
==
""
{
return
fmt
.
Errorf
(
"oidc_connect.issuer_url is required when oidc_connect.enabled=true"
)
}
if
strings
.
TrimSpace
(
c
.
OIDC
.
RedirectURL
)
==
""
{
return
fmt
.
Errorf
(
"oidc_connect.redirect_url is required when oidc_connect.enabled=true"
)
}
if
strings
.
TrimSpace
(
c
.
OIDC
.
FrontendRedirectURL
)
==
""
{
return
fmt
.
Errorf
(
"oidc_connect.frontend_redirect_url is required when oidc_connect.enabled=true"
)
}
if
!
scopeContainsOpenID
(
c
.
OIDC
.
Scopes
)
{
return
fmt
.
Errorf
(
"oidc_connect.scopes must contain openid"
)
}
method
:=
strings
.
ToLower
(
strings
.
TrimSpace
(
c
.
OIDC
.
TokenAuthMethod
))
switch
method
{
case
""
,
"client_secret_post"
,
"client_secret_basic"
,
"none"
:
default
:
return
fmt
.
Errorf
(
"oidc_connect.token_auth_method must be one of: client_secret_post/client_secret_basic/none"
)
}
if
method
==
"none"
&&
!
c
.
OIDC
.
UsePKCE
{
return
fmt
.
Errorf
(
"oidc_connect.use_pkce must be true when oidc_connect.token_auth_method=none"
)
}
if
(
method
==
""
||
method
==
"client_secret_post"
||
method
==
"client_secret_basic"
)
&&
strings
.
TrimSpace
(
c
.
OIDC
.
ClientSecret
)
==
""
{
return
fmt
.
Errorf
(
"oidc_connect.client_secret is required when oidc_connect.enabled=true and token_auth_method is client_secret_post/client_secret_basic"
)
}
if
c
.
OIDC
.
ClockSkewSeconds
<
0
||
c
.
OIDC
.
ClockSkewSeconds
>
600
{
return
fmt
.
Errorf
(
"oidc_connect.clock_skew_seconds must be between 0 and 600"
)
}
if
c
.
OIDC
.
ValidateIDToken
&&
strings
.
TrimSpace
(
c
.
OIDC
.
AllowedSigningAlgs
)
==
""
{
return
fmt
.
Errorf
(
"oidc_connect.allowed_signing_algs is required when oidc_connect.validate_id_token=true"
)
}
if
err
:=
ValidateAbsoluteHTTPURL
(
c
.
OIDC
.
IssuerURL
);
err
!=
nil
{
return
fmt
.
Errorf
(
"oidc_connect.issuer_url invalid: %w"
,
err
)
}
if
v
:=
strings
.
TrimSpace
(
c
.
OIDC
.
DiscoveryURL
);
v
!=
""
{
if
err
:=
ValidateAbsoluteHTTPURL
(
v
);
err
!=
nil
{
return
fmt
.
Errorf
(
"oidc_connect.discovery_url invalid: %w"
,
err
)
}
}
if
v
:=
strings
.
TrimSpace
(
c
.
OIDC
.
AuthorizeURL
);
v
!=
""
{
if
err
:=
ValidateAbsoluteHTTPURL
(
v
);
err
!=
nil
{
return
fmt
.
Errorf
(
"oidc_connect.authorize_url invalid: %w"
,
err
)
}
}
if
v
:=
strings
.
TrimSpace
(
c
.
OIDC
.
TokenURL
);
v
!=
""
{
if
err
:=
ValidateAbsoluteHTTPURL
(
v
);
err
!=
nil
{
return
fmt
.
Errorf
(
"oidc_connect.token_url invalid: %w"
,
err
)
}
}
if
v
:=
strings
.
TrimSpace
(
c
.
OIDC
.
UserInfoURL
);
v
!=
""
{
if
err
:=
ValidateAbsoluteHTTPURL
(
v
);
err
!=
nil
{
return
fmt
.
Errorf
(
"oidc_connect.userinfo_url invalid: %w"
,
err
)
}
}
if
v
:=
strings
.
TrimSpace
(
c
.
OIDC
.
JWKSURL
);
v
!=
""
{
if
err
:=
ValidateAbsoluteHTTPURL
(
v
);
err
!=
nil
{
return
fmt
.
Errorf
(
"oidc_connect.jwks_url invalid: %w"
,
err
)
}
}
if
err
:=
ValidateAbsoluteHTTPURL
(
c
.
OIDC
.
RedirectURL
);
err
!=
nil
{
return
fmt
.
Errorf
(
"oidc_connect.redirect_url invalid: %w"
,
err
)
}
if
err
:=
ValidateFrontendRedirectURL
(
c
.
OIDC
.
FrontendRedirectURL
);
err
!=
nil
{
return
fmt
.
Errorf
(
"oidc_connect.frontend_redirect_url invalid: %w"
,
err
)
}
warnIfInsecureURL
(
"oidc_connect.issuer_url"
,
c
.
OIDC
.
IssuerURL
)
warnIfInsecureURL
(
"oidc_connect.discovery_url"
,
c
.
OIDC
.
DiscoveryURL
)
warnIfInsecureURL
(
"oidc_connect.authorize_url"
,
c
.
OIDC
.
AuthorizeURL
)
warnIfInsecureURL
(
"oidc_connect.token_url"
,
c
.
OIDC
.
TokenURL
)
warnIfInsecureURL
(
"oidc_connect.userinfo_url"
,
c
.
OIDC
.
UserInfoURL
)
warnIfInsecureURL
(
"oidc_connect.jwks_url"
,
c
.
OIDC
.
JWKSURL
)
warnIfInsecureURL
(
"oidc_connect.redirect_url"
,
c
.
OIDC
.
RedirectURL
)
warnIfInsecureURL
(
"oidc_connect.frontend_redirect_url"
,
c
.
OIDC
.
FrontendRedirectURL
)
}
if
c
.
Billing
.
CircuitBreaker
.
Enabled
{
if
c
.
Billing
.
CircuitBreaker
.
Enabled
{
if
c
.
Billing
.
CircuitBreaker
.
FailureThreshold
<=
0
{
if
c
.
Billing
.
CircuitBreaker
.
FailureThreshold
<=
0
{
return
fmt
.
Errorf
(
"billing.circuit_breaker.failure_threshold must be positive"
)
return
fmt
.
Errorf
(
"billing.circuit_breaker.failure_threshold must be positive"
)
...
@@ -2184,6 +2335,15 @@ func ValidateFrontendRedirectURL(raw string) error {
...
@@ -2184,6 +2335,15 @@ func ValidateFrontendRedirectURL(raw string) error {
return
nil
return
nil
}
}
func
scopeContainsOpenID
(
scopes
string
)
bool
{
for
_
,
scope
:=
range
strings
.
Fields
(
strings
.
ToLower
(
strings
.
TrimSpace
(
scopes
)))
{
if
scope
==
"openid"
{
return
true
}
}
return
false
}
// isHTTPScheme 检查是否为 HTTP 或 HTTPS 协议
// isHTTPScheme 检查是否为 HTTP 或 HTTPS 协议
func
isHTTPScheme
(
scheme
string
)
bool
{
func
isHTTPScheme
(
scheme
string
)
bool
{
return
strings
.
EqualFold
(
scheme
,
"http"
)
||
strings
.
EqualFold
(
scheme
,
"https"
)
return
strings
.
EqualFold
(
scheme
,
"http"
)
||
strings
.
EqualFold
(
scheme
,
"https"
)
...
...
backend/internal/config/config_test.go
View file @
02a66a01
...
@@ -351,6 +351,60 @@ func TestValidateLinuxDoPKCERequiredForPublicClient(t *testing.T) {
...
@@ -351,6 +351,60 @@ func TestValidateLinuxDoPKCERequiredForPublicClient(t *testing.T) {
}
}
}
}
func
TestValidateOIDCScopesMustContainOpenID
(
t
*
testing
.
T
)
{
resetViperWithJWTSecret
(
t
)
cfg
,
err
:=
Load
()
if
err
!=
nil
{
t
.
Fatalf
(
"Load() error: %v"
,
err
)
}
cfg
.
OIDC
.
Enabled
=
true
cfg
.
OIDC
.
ClientID
=
"oidc-client"
cfg
.
OIDC
.
ClientSecret
=
"oidc-secret"
cfg
.
OIDC
.
IssuerURL
=
"https://issuer.example.com"
cfg
.
OIDC
.
AuthorizeURL
=
"https://issuer.example.com/auth"
cfg
.
OIDC
.
TokenURL
=
"https://issuer.example.com/token"
cfg
.
OIDC
.
JWKSURL
=
"https://issuer.example.com/jwks"
cfg
.
OIDC
.
RedirectURL
=
"https://example.com/api/v1/auth/oauth/oidc/callback"
cfg
.
OIDC
.
FrontendRedirectURL
=
"/auth/oidc/callback"
cfg
.
OIDC
.
Scopes
=
"profile email"
err
=
cfg
.
Validate
()
if
err
==
nil
{
t
.
Fatalf
(
"Validate() expected error when scopes do not include openid, got nil"
)
}
if
!
strings
.
Contains
(
err
.
Error
(),
"oidc_connect.scopes"
)
{
t
.
Fatalf
(
"Validate() expected oidc_connect.scopes error, got: %v"
,
err
)
}
}
func
TestValidateOIDCAllowsIssuerOnlyEndpointsWithDiscoveryFallback
(
t
*
testing
.
T
)
{
resetViperWithJWTSecret
(
t
)
cfg
,
err
:=
Load
()
if
err
!=
nil
{
t
.
Fatalf
(
"Load() error: %v"
,
err
)
}
cfg
.
OIDC
.
Enabled
=
true
cfg
.
OIDC
.
ClientID
=
"oidc-client"
cfg
.
OIDC
.
ClientSecret
=
"oidc-secret"
cfg
.
OIDC
.
IssuerURL
=
"https://issuer.example.com"
cfg
.
OIDC
.
AuthorizeURL
=
""
cfg
.
OIDC
.
TokenURL
=
""
cfg
.
OIDC
.
JWKSURL
=
""
cfg
.
OIDC
.
RedirectURL
=
"https://example.com/api/v1/auth/oauth/oidc/callback"
cfg
.
OIDC
.
FrontendRedirectURL
=
"/auth/oidc/callback"
cfg
.
OIDC
.
Scopes
=
"openid email profile"
cfg
.
OIDC
.
ValidateIDToken
=
true
err
=
cfg
.
Validate
()
if
err
!=
nil
{
t
.
Fatalf
(
"Validate() expected issuer-only OIDC config to pass with discovery fallback, got: %v"
,
err
)
}
}
func
TestLoadDefaultDashboardCacheConfig
(
t
*
testing
.
T
)
{
func
TestLoadDefaultDashboardCacheConfig
(
t
*
testing
.
T
)
{
resetViperWithJWTSecret
(
t
)
resetViperWithJWTSecret
(
t
)
...
...
backend/internal/handler/admin/setting_handler.go
View file @
02a66a01
...
@@ -35,6 +35,15 @@ func generateMenuItemID() (string, error) {
...
@@ -35,6 +35,15 @@ func generateMenuItemID() (string, error) {
return
hex
.
EncodeToString
(
b
),
nil
return
hex
.
EncodeToString
(
b
),
nil
}
}
func
scopesContainOpenID
(
scopes
string
)
bool
{
for
_
,
scope
:=
range
strings
.
Fields
(
strings
.
ToLower
(
strings
.
TrimSpace
(
scopes
)))
{
if
scope
==
"openid"
{
return
true
}
}
return
false
}
// SettingHandler 系统设置处理器
// SettingHandler 系统设置处理器
type
SettingHandler
struct
{
type
SettingHandler
struct
{
settingService
*
service
.
SettingService
settingService
*
service
.
SettingService
...
@@ -96,6 +105,28 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
...
@@ -96,6 +105,28 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
LinuxDoConnectClientID
:
settings
.
LinuxDoConnectClientID
,
LinuxDoConnectClientID
:
settings
.
LinuxDoConnectClientID
,
LinuxDoConnectClientSecretConfigured
:
settings
.
LinuxDoConnectClientSecretConfigured
,
LinuxDoConnectClientSecretConfigured
:
settings
.
LinuxDoConnectClientSecretConfigured
,
LinuxDoConnectRedirectURL
:
settings
.
LinuxDoConnectRedirectURL
,
LinuxDoConnectRedirectURL
:
settings
.
LinuxDoConnectRedirectURL
,
OIDCConnectEnabled
:
settings
.
OIDCConnectEnabled
,
OIDCConnectProviderName
:
settings
.
OIDCConnectProviderName
,
OIDCConnectClientID
:
settings
.
OIDCConnectClientID
,
OIDCConnectClientSecretConfigured
:
settings
.
OIDCConnectClientSecretConfigured
,
OIDCConnectIssuerURL
:
settings
.
OIDCConnectIssuerURL
,
OIDCConnectDiscoveryURL
:
settings
.
OIDCConnectDiscoveryURL
,
OIDCConnectAuthorizeURL
:
settings
.
OIDCConnectAuthorizeURL
,
OIDCConnectTokenURL
:
settings
.
OIDCConnectTokenURL
,
OIDCConnectUserInfoURL
:
settings
.
OIDCConnectUserInfoURL
,
OIDCConnectJWKSURL
:
settings
.
OIDCConnectJWKSURL
,
OIDCConnectScopes
:
settings
.
OIDCConnectScopes
,
OIDCConnectRedirectURL
:
settings
.
OIDCConnectRedirectURL
,
OIDCConnectFrontendRedirectURL
:
settings
.
OIDCConnectFrontendRedirectURL
,
OIDCConnectTokenAuthMethod
:
settings
.
OIDCConnectTokenAuthMethod
,
OIDCConnectUsePKCE
:
settings
.
OIDCConnectUsePKCE
,
OIDCConnectValidateIDToken
:
settings
.
OIDCConnectValidateIDToken
,
OIDCConnectAllowedSigningAlgs
:
settings
.
OIDCConnectAllowedSigningAlgs
,
OIDCConnectClockSkewSeconds
:
settings
.
OIDCConnectClockSkewSeconds
,
OIDCConnectRequireEmailVerified
:
settings
.
OIDCConnectRequireEmailVerified
,
OIDCConnectUserInfoEmailPath
:
settings
.
OIDCConnectUserInfoEmailPath
,
OIDCConnectUserInfoIDPath
:
settings
.
OIDCConnectUserInfoIDPath
,
OIDCConnectUserInfoUsernamePath
:
settings
.
OIDCConnectUserInfoUsernamePath
,
SiteName
:
settings
.
SiteName
,
SiteName
:
settings
.
SiteName
,
SiteLogo
:
settings
.
SiteLogo
,
SiteLogo
:
settings
.
SiteLogo
,
SiteSubtitle
:
settings
.
SiteSubtitle
,
SiteSubtitle
:
settings
.
SiteSubtitle
,
...
@@ -164,6 +195,30 @@ type UpdateSettingsRequest struct {
...
@@ -164,6 +195,30 @@ type UpdateSettingsRequest struct {
LinuxDoConnectClientSecret
string
`json:"linuxdo_connect_client_secret"`
LinuxDoConnectClientSecret
string
`json:"linuxdo_connect_client_secret"`
LinuxDoConnectRedirectURL
string
`json:"linuxdo_connect_redirect_url"`
LinuxDoConnectRedirectURL
string
`json:"linuxdo_connect_redirect_url"`
// Generic OIDC OAuth 登录
OIDCConnectEnabled
bool
`json:"oidc_connect_enabled"`
OIDCConnectProviderName
string
`json:"oidc_connect_provider_name"`
OIDCConnectClientID
string
`json:"oidc_connect_client_id"`
OIDCConnectClientSecret
string
`json:"oidc_connect_client_secret"`
OIDCConnectIssuerURL
string
`json:"oidc_connect_issuer_url"`
OIDCConnectDiscoveryURL
string
`json:"oidc_connect_discovery_url"`
OIDCConnectAuthorizeURL
string
`json:"oidc_connect_authorize_url"`
OIDCConnectTokenURL
string
`json:"oidc_connect_token_url"`
OIDCConnectUserInfoURL
string
`json:"oidc_connect_userinfo_url"`
OIDCConnectJWKSURL
string
`json:"oidc_connect_jwks_url"`
OIDCConnectScopes
string
`json:"oidc_connect_scopes"`
OIDCConnectRedirectURL
string
`json:"oidc_connect_redirect_url"`
OIDCConnectFrontendRedirectURL
string
`json:"oidc_connect_frontend_redirect_url"`
OIDCConnectTokenAuthMethod
string
`json:"oidc_connect_token_auth_method"`
OIDCConnectUsePKCE
bool
`json:"oidc_connect_use_pkce"`
OIDCConnectValidateIDToken
bool
`json:"oidc_connect_validate_id_token"`
OIDCConnectAllowedSigningAlgs
string
`json:"oidc_connect_allowed_signing_algs"`
OIDCConnectClockSkewSeconds
int
`json:"oidc_connect_clock_skew_seconds"`
OIDCConnectRequireEmailVerified
bool
`json:"oidc_connect_require_email_verified"`
OIDCConnectUserInfoEmailPath
string
`json:"oidc_connect_userinfo_email_path"`
OIDCConnectUserInfoIDPath
string
`json:"oidc_connect_userinfo_id_path"`
OIDCConnectUserInfoUsernamePath
string
`json:"oidc_connect_userinfo_username_path"`
// OEM设置
// OEM设置
SiteName
string
`json:"site_name"`
SiteName
string
`json:"site_name"`
SiteLogo
string
`json:"site_logo"`
SiteLogo
string
`json:"site_logo"`
...
@@ -324,6 +379,122 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
...
@@ -324,6 +379,122 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
}
}
}
}
// Generic OIDC 参数验证
if
req
.
OIDCConnectEnabled
{
req
.
OIDCConnectProviderName
=
strings
.
TrimSpace
(
req
.
OIDCConnectProviderName
)
req
.
OIDCConnectClientID
=
strings
.
TrimSpace
(
req
.
OIDCConnectClientID
)
req
.
OIDCConnectClientSecret
=
strings
.
TrimSpace
(
req
.
OIDCConnectClientSecret
)
req
.
OIDCConnectIssuerURL
=
strings
.
TrimSpace
(
req
.
OIDCConnectIssuerURL
)
req
.
OIDCConnectDiscoveryURL
=
strings
.
TrimSpace
(
req
.
OIDCConnectDiscoveryURL
)
req
.
OIDCConnectAuthorizeURL
=
strings
.
TrimSpace
(
req
.
OIDCConnectAuthorizeURL
)
req
.
OIDCConnectTokenURL
=
strings
.
TrimSpace
(
req
.
OIDCConnectTokenURL
)
req
.
OIDCConnectUserInfoURL
=
strings
.
TrimSpace
(
req
.
OIDCConnectUserInfoURL
)
req
.
OIDCConnectJWKSURL
=
strings
.
TrimSpace
(
req
.
OIDCConnectJWKSURL
)
req
.
OIDCConnectScopes
=
strings
.
TrimSpace
(
req
.
OIDCConnectScopes
)
req
.
OIDCConnectRedirectURL
=
strings
.
TrimSpace
(
req
.
OIDCConnectRedirectURL
)
req
.
OIDCConnectFrontendRedirectURL
=
strings
.
TrimSpace
(
req
.
OIDCConnectFrontendRedirectURL
)
req
.
OIDCConnectTokenAuthMethod
=
strings
.
ToLower
(
strings
.
TrimSpace
(
req
.
OIDCConnectTokenAuthMethod
))
req
.
OIDCConnectAllowedSigningAlgs
=
strings
.
TrimSpace
(
req
.
OIDCConnectAllowedSigningAlgs
)
req
.
OIDCConnectUserInfoEmailPath
=
strings
.
TrimSpace
(
req
.
OIDCConnectUserInfoEmailPath
)
req
.
OIDCConnectUserInfoIDPath
=
strings
.
TrimSpace
(
req
.
OIDCConnectUserInfoIDPath
)
req
.
OIDCConnectUserInfoUsernamePath
=
strings
.
TrimSpace
(
req
.
OIDCConnectUserInfoUsernamePath
)
if
req
.
OIDCConnectProviderName
==
""
{
req
.
OIDCConnectProviderName
=
"OIDC"
}
if
req
.
OIDCConnectClientID
==
""
{
response
.
BadRequest
(
c
,
"OIDC Client ID is required when enabled"
)
return
}
if
req
.
OIDCConnectIssuerURL
==
""
{
response
.
BadRequest
(
c
,
"OIDC Issuer URL is required when enabled"
)
return
}
if
err
:=
config
.
ValidateAbsoluteHTTPURL
(
req
.
OIDCConnectIssuerURL
);
err
!=
nil
{
response
.
BadRequest
(
c
,
"OIDC Issuer URL must be an absolute http(s) URL"
)
return
}
if
req
.
OIDCConnectDiscoveryURL
!=
""
{
if
err
:=
config
.
ValidateAbsoluteHTTPURL
(
req
.
OIDCConnectDiscoveryURL
);
err
!=
nil
{
response
.
BadRequest
(
c
,
"OIDC Discovery URL must be an absolute http(s) URL"
)
return
}
}
if
req
.
OIDCConnectAuthorizeURL
!=
""
{
if
err
:=
config
.
ValidateAbsoluteHTTPURL
(
req
.
OIDCConnectAuthorizeURL
);
err
!=
nil
{
response
.
BadRequest
(
c
,
"OIDC Authorize URL must be an absolute http(s) URL"
)
return
}
}
if
req
.
OIDCConnectTokenURL
!=
""
{
if
err
:=
config
.
ValidateAbsoluteHTTPURL
(
req
.
OIDCConnectTokenURL
);
err
!=
nil
{
response
.
BadRequest
(
c
,
"OIDC Token URL must be an absolute http(s) URL"
)
return
}
}
if
req
.
OIDCConnectUserInfoURL
!=
""
{
if
err
:=
config
.
ValidateAbsoluteHTTPURL
(
req
.
OIDCConnectUserInfoURL
);
err
!=
nil
{
response
.
BadRequest
(
c
,
"OIDC UserInfo URL must be an absolute http(s) URL"
)
return
}
}
if
req
.
OIDCConnectRedirectURL
==
""
{
response
.
BadRequest
(
c
,
"OIDC Redirect URL is required when enabled"
)
return
}
if
err
:=
config
.
ValidateAbsoluteHTTPURL
(
req
.
OIDCConnectRedirectURL
);
err
!=
nil
{
response
.
BadRequest
(
c
,
"OIDC Redirect URL must be an absolute http(s) URL"
)
return
}
if
req
.
OIDCConnectFrontendRedirectURL
==
""
{
response
.
BadRequest
(
c
,
"OIDC Frontend Redirect URL is required when enabled"
)
return
}
if
err
:=
config
.
ValidateFrontendRedirectURL
(
req
.
OIDCConnectFrontendRedirectURL
);
err
!=
nil
{
response
.
BadRequest
(
c
,
"OIDC Frontend Redirect URL is invalid"
)
return
}
if
!
scopesContainOpenID
(
req
.
OIDCConnectScopes
)
{
response
.
BadRequest
(
c
,
"OIDC scopes must contain openid"
)
return
}
switch
req
.
OIDCConnectTokenAuthMethod
{
case
""
,
"client_secret_post"
,
"client_secret_basic"
,
"none"
:
default
:
response
.
BadRequest
(
c
,
"OIDC Token Auth Method must be one of client_secret_post/client_secret_basic/none"
)
return
}
if
req
.
OIDCConnectTokenAuthMethod
==
"none"
&&
!
req
.
OIDCConnectUsePKCE
{
response
.
BadRequest
(
c
,
"OIDC PKCE must be enabled when token_auth_method=none"
)
return
}
if
req
.
OIDCConnectClockSkewSeconds
<
0
||
req
.
OIDCConnectClockSkewSeconds
>
600
{
response
.
BadRequest
(
c
,
"OIDC clock skew seconds must be between 0 and 600"
)
return
}
if
req
.
OIDCConnectValidateIDToken
{
if
req
.
OIDCConnectAllowedSigningAlgs
==
""
{
response
.
BadRequest
(
c
,
"OIDC Allowed Signing Algs is required when validate_id_token=true"
)
return
}
}
if
req
.
OIDCConnectJWKSURL
!=
""
{
if
err
:=
config
.
ValidateAbsoluteHTTPURL
(
req
.
OIDCConnectJWKSURL
);
err
!=
nil
{
response
.
BadRequest
(
c
,
"OIDC JWKS URL must be an absolute http(s) URL"
)
return
}
}
if
req
.
OIDCConnectTokenAuthMethod
==
""
||
req
.
OIDCConnectTokenAuthMethod
==
"client_secret_post"
||
req
.
OIDCConnectTokenAuthMethod
==
"client_secret_basic"
{
if
req
.
OIDCConnectClientSecret
==
""
{
if
previousSettings
.
OIDCConnectClientSecret
==
""
{
response
.
BadRequest
(
c
,
"OIDC Client Secret is required when enabled"
)
return
}
req
.
OIDCConnectClientSecret
=
previousSettings
.
OIDCConnectClientSecret
}
}
}
// “购买订阅”页面配置验证
// “购买订阅”页面配置验证
purchaseEnabled
:=
previousSettings
.
PurchaseSubscriptionEnabled
purchaseEnabled
:=
previousSettings
.
PurchaseSubscriptionEnabled
if
req
.
PurchaseSubscriptionEnabled
!=
nil
{
if
req
.
PurchaseSubscriptionEnabled
!=
nil
{
...
@@ -554,6 +725,28 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
...
@@ -554,6 +725,28 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
LinuxDoConnectClientID
:
req
.
LinuxDoConnectClientID
,
LinuxDoConnectClientID
:
req
.
LinuxDoConnectClientID
,
LinuxDoConnectClientSecret
:
req
.
LinuxDoConnectClientSecret
,
LinuxDoConnectClientSecret
:
req
.
LinuxDoConnectClientSecret
,
LinuxDoConnectRedirectURL
:
req
.
LinuxDoConnectRedirectURL
,
LinuxDoConnectRedirectURL
:
req
.
LinuxDoConnectRedirectURL
,
OIDCConnectEnabled
:
req
.
OIDCConnectEnabled
,
OIDCConnectProviderName
:
req
.
OIDCConnectProviderName
,
OIDCConnectClientID
:
req
.
OIDCConnectClientID
,
OIDCConnectClientSecret
:
req
.
OIDCConnectClientSecret
,
OIDCConnectIssuerURL
:
req
.
OIDCConnectIssuerURL
,
OIDCConnectDiscoveryURL
:
req
.
OIDCConnectDiscoveryURL
,
OIDCConnectAuthorizeURL
:
req
.
OIDCConnectAuthorizeURL
,
OIDCConnectTokenURL
:
req
.
OIDCConnectTokenURL
,
OIDCConnectUserInfoURL
:
req
.
OIDCConnectUserInfoURL
,
OIDCConnectJWKSURL
:
req
.
OIDCConnectJWKSURL
,
OIDCConnectScopes
:
req
.
OIDCConnectScopes
,
OIDCConnectRedirectURL
:
req
.
OIDCConnectRedirectURL
,
OIDCConnectFrontendRedirectURL
:
req
.
OIDCConnectFrontendRedirectURL
,
OIDCConnectTokenAuthMethod
:
req
.
OIDCConnectTokenAuthMethod
,
OIDCConnectUsePKCE
:
req
.
OIDCConnectUsePKCE
,
OIDCConnectValidateIDToken
:
req
.
OIDCConnectValidateIDToken
,
OIDCConnectAllowedSigningAlgs
:
req
.
OIDCConnectAllowedSigningAlgs
,
OIDCConnectClockSkewSeconds
:
req
.
OIDCConnectClockSkewSeconds
,
OIDCConnectRequireEmailVerified
:
req
.
OIDCConnectRequireEmailVerified
,
OIDCConnectUserInfoEmailPath
:
req
.
OIDCConnectUserInfoEmailPath
,
OIDCConnectUserInfoIDPath
:
req
.
OIDCConnectUserInfoIDPath
,
OIDCConnectUserInfoUsernamePath
:
req
.
OIDCConnectUserInfoUsernamePath
,
SiteName
:
req
.
SiteName
,
SiteName
:
req
.
SiteName
,
SiteLogo
:
req
.
SiteLogo
,
SiteLogo
:
req
.
SiteLogo
,
SiteSubtitle
:
req
.
SiteSubtitle
,
SiteSubtitle
:
req
.
SiteSubtitle
,
...
@@ -669,6 +862,28 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
...
@@ -669,6 +862,28 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
LinuxDoConnectClientID
:
updatedSettings
.
LinuxDoConnectClientID
,
LinuxDoConnectClientID
:
updatedSettings
.
LinuxDoConnectClientID
,
LinuxDoConnectClientSecretConfigured
:
updatedSettings
.
LinuxDoConnectClientSecretConfigured
,
LinuxDoConnectClientSecretConfigured
:
updatedSettings
.
LinuxDoConnectClientSecretConfigured
,
LinuxDoConnectRedirectURL
:
updatedSettings
.
LinuxDoConnectRedirectURL
,
LinuxDoConnectRedirectURL
:
updatedSettings
.
LinuxDoConnectRedirectURL
,
OIDCConnectEnabled
:
updatedSettings
.
OIDCConnectEnabled
,
OIDCConnectProviderName
:
updatedSettings
.
OIDCConnectProviderName
,
OIDCConnectClientID
:
updatedSettings
.
OIDCConnectClientID
,
OIDCConnectClientSecretConfigured
:
updatedSettings
.
OIDCConnectClientSecretConfigured
,
OIDCConnectIssuerURL
:
updatedSettings
.
OIDCConnectIssuerURL
,
OIDCConnectDiscoveryURL
:
updatedSettings
.
OIDCConnectDiscoveryURL
,
OIDCConnectAuthorizeURL
:
updatedSettings
.
OIDCConnectAuthorizeURL
,
OIDCConnectTokenURL
:
updatedSettings
.
OIDCConnectTokenURL
,
OIDCConnectUserInfoURL
:
updatedSettings
.
OIDCConnectUserInfoURL
,
OIDCConnectJWKSURL
:
updatedSettings
.
OIDCConnectJWKSURL
,
OIDCConnectScopes
:
updatedSettings
.
OIDCConnectScopes
,
OIDCConnectRedirectURL
:
updatedSettings
.
OIDCConnectRedirectURL
,
OIDCConnectFrontendRedirectURL
:
updatedSettings
.
OIDCConnectFrontendRedirectURL
,
OIDCConnectTokenAuthMethod
:
updatedSettings
.
OIDCConnectTokenAuthMethod
,
OIDCConnectUsePKCE
:
updatedSettings
.
OIDCConnectUsePKCE
,
OIDCConnectValidateIDToken
:
updatedSettings
.
OIDCConnectValidateIDToken
,
OIDCConnectAllowedSigningAlgs
:
updatedSettings
.
OIDCConnectAllowedSigningAlgs
,
OIDCConnectClockSkewSeconds
:
updatedSettings
.
OIDCConnectClockSkewSeconds
,
OIDCConnectRequireEmailVerified
:
updatedSettings
.
OIDCConnectRequireEmailVerified
,
OIDCConnectUserInfoEmailPath
:
updatedSettings
.
OIDCConnectUserInfoEmailPath
,
OIDCConnectUserInfoIDPath
:
updatedSettings
.
OIDCConnectUserInfoIDPath
,
OIDCConnectUserInfoUsernamePath
:
updatedSettings
.
OIDCConnectUserInfoUsernamePath
,
SiteName
:
updatedSettings
.
SiteName
,
SiteName
:
updatedSettings
.
SiteName
,
SiteLogo
:
updatedSettings
.
SiteLogo
,
SiteLogo
:
updatedSettings
.
SiteLogo
,
SiteSubtitle
:
updatedSettings
.
SiteSubtitle
,
SiteSubtitle
:
updatedSettings
.
SiteSubtitle
,
...
@@ -787,6 +1002,72 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
...
@@ -787,6 +1002,72 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
if
before
.
LinuxDoConnectRedirectURL
!=
after
.
LinuxDoConnectRedirectURL
{
if
before
.
LinuxDoConnectRedirectURL
!=
after
.
LinuxDoConnectRedirectURL
{
changed
=
append
(
changed
,
"linuxdo_connect_redirect_url"
)
changed
=
append
(
changed
,
"linuxdo_connect_redirect_url"
)
}
}
if
before
.
OIDCConnectEnabled
!=
after
.
OIDCConnectEnabled
{
changed
=
append
(
changed
,
"oidc_connect_enabled"
)
}
if
before
.
OIDCConnectProviderName
!=
after
.
OIDCConnectProviderName
{
changed
=
append
(
changed
,
"oidc_connect_provider_name"
)
}
if
before
.
OIDCConnectClientID
!=
after
.
OIDCConnectClientID
{
changed
=
append
(
changed
,
"oidc_connect_client_id"
)
}
if
req
.
OIDCConnectClientSecret
!=
""
{
changed
=
append
(
changed
,
"oidc_connect_client_secret"
)
}
if
before
.
OIDCConnectIssuerURL
!=
after
.
OIDCConnectIssuerURL
{
changed
=
append
(
changed
,
"oidc_connect_issuer_url"
)
}
if
before
.
OIDCConnectDiscoveryURL
!=
after
.
OIDCConnectDiscoveryURL
{
changed
=
append
(
changed
,
"oidc_connect_discovery_url"
)
}
if
before
.
OIDCConnectAuthorizeURL
!=
after
.
OIDCConnectAuthorizeURL
{
changed
=
append
(
changed
,
"oidc_connect_authorize_url"
)
}
if
before
.
OIDCConnectTokenURL
!=
after
.
OIDCConnectTokenURL
{
changed
=
append
(
changed
,
"oidc_connect_token_url"
)
}
if
before
.
OIDCConnectUserInfoURL
!=
after
.
OIDCConnectUserInfoURL
{
changed
=
append
(
changed
,
"oidc_connect_userinfo_url"
)
}
if
before
.
OIDCConnectJWKSURL
!=
after
.
OIDCConnectJWKSURL
{
changed
=
append
(
changed
,
"oidc_connect_jwks_url"
)
}
if
before
.
OIDCConnectScopes
!=
after
.
OIDCConnectScopes
{
changed
=
append
(
changed
,
"oidc_connect_scopes"
)
}
if
before
.
OIDCConnectRedirectURL
!=
after
.
OIDCConnectRedirectURL
{
changed
=
append
(
changed
,
"oidc_connect_redirect_url"
)
}
if
before
.
OIDCConnectFrontendRedirectURL
!=
after
.
OIDCConnectFrontendRedirectURL
{
changed
=
append
(
changed
,
"oidc_connect_frontend_redirect_url"
)
}
if
before
.
OIDCConnectTokenAuthMethod
!=
after
.
OIDCConnectTokenAuthMethod
{
changed
=
append
(
changed
,
"oidc_connect_token_auth_method"
)
}
if
before
.
OIDCConnectUsePKCE
!=
after
.
OIDCConnectUsePKCE
{
changed
=
append
(
changed
,
"oidc_connect_use_pkce"
)
}
if
before
.
OIDCConnectValidateIDToken
!=
after
.
OIDCConnectValidateIDToken
{
changed
=
append
(
changed
,
"oidc_connect_validate_id_token"
)
}
if
before
.
OIDCConnectAllowedSigningAlgs
!=
after
.
OIDCConnectAllowedSigningAlgs
{
changed
=
append
(
changed
,
"oidc_connect_allowed_signing_algs"
)
}
if
before
.
OIDCConnectClockSkewSeconds
!=
after
.
OIDCConnectClockSkewSeconds
{
changed
=
append
(
changed
,
"oidc_connect_clock_skew_seconds"
)
}
if
before
.
OIDCConnectRequireEmailVerified
!=
after
.
OIDCConnectRequireEmailVerified
{
changed
=
append
(
changed
,
"oidc_connect_require_email_verified"
)
}
if
before
.
OIDCConnectUserInfoEmailPath
!=
after
.
OIDCConnectUserInfoEmailPath
{
changed
=
append
(
changed
,
"oidc_connect_userinfo_email_path"
)
}
if
before
.
OIDCConnectUserInfoIDPath
!=
after
.
OIDCConnectUserInfoIDPath
{
changed
=
append
(
changed
,
"oidc_connect_userinfo_id_path"
)
}
if
before
.
OIDCConnectUserInfoUsernamePath
!=
after
.
OIDCConnectUserInfoUsernamePath
{
changed
=
append
(
changed
,
"oidc_connect_userinfo_username_path"
)
}
if
before
.
SiteName
!=
after
.
SiteName
{
if
before
.
SiteName
!=
after
.
SiteName
{
changed
=
append
(
changed
,
"site_name"
)
changed
=
append
(
changed
,
"site_name"
)
}
}
...
...
backend/internal/handler/auth_oidc_oauth.go
0 → 100644
View file @
02a66a01
This diff is collapsed.
Click to expand it.
backend/internal/handler/auth_oidc_oauth_test.go
0 → 100644
View file @
02a66a01
package
handler
import
(
"context"
"crypto/rand"
"crypto/rsa"
"encoding/base64"
"encoding/json"
"math/big"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/require"
)
func
TestOIDCSyntheticEmailStableAndDistinct
(
t
*
testing
.
T
)
{
k1
:=
oidcIdentityKey
(
"https://issuer.example.com"
,
"subject-a"
)
k2
:=
oidcIdentityKey
(
"https://issuer.example.com"
,
"subject-b"
)
e1
:=
oidcSyntheticEmailFromIdentityKey
(
k1
)
e1Again
:=
oidcSyntheticEmailFromIdentityKey
(
k1
)
e2
:=
oidcSyntheticEmailFromIdentityKey
(
k2
)
require
.
Equal
(
t
,
e1
,
e1Again
)
require
.
NotEqual
(
t
,
e1
,
e2
)
require
.
Contains
(
t
,
e1
,
"@oidc-connect.invalid"
)
}
func
TestBuildOIDCAuthorizeURLIncludesNonceAndPKCE
(
t
*
testing
.
T
)
{
cfg
:=
config
.
OIDCConnectConfig
{
AuthorizeURL
:
"https://issuer.example.com/auth"
,
ClientID
:
"cid"
,
Scopes
:
"openid email profile"
,
UsePKCE
:
true
,
}
u
,
err
:=
buildOIDCAuthorizeURL
(
cfg
,
"state123"
,
"nonce123"
,
"challenge123"
,
"https://app.example.com/callback"
)
require
.
NoError
(
t
,
err
)
require
.
Contains
(
t
,
u
,
"nonce=nonce123"
)
require
.
Contains
(
t
,
u
,
"code_challenge=challenge123"
)
require
.
Contains
(
t
,
u
,
"code_challenge_method=S256"
)
require
.
Contains
(
t
,
u
,
"scope=openid+email+profile"
)
}
func
TestOIDCParseAndValidateIDToken
(
t
*
testing
.
T
)
{
priv
,
err
:=
rsa
.
GenerateKey
(
rand
.
Reader
,
2048
)
require
.
NoError
(
t
,
err
)
kid
:=
"kid-1"
jwks
:=
oidcJWKSet
{
Keys
:
[]
oidcJWK
{
buildRSAJWK
(
kid
,
&
priv
.
PublicKey
)}}
srv
:=
httptest
.
NewServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
require
.
NoError
(
t
,
json
.
NewEncoder
(
w
)
.
Encode
(
jwks
))
}))
defer
srv
.
Close
()
now
:=
time
.
Now
()
claims
:=
oidcIDTokenClaims
{
Nonce
:
"nonce-ok"
,
Azp
:
"client-1"
,
RegisteredClaims
:
jwt
.
RegisteredClaims
{
Issuer
:
"https://issuer.example.com"
,
Subject
:
"subject-1"
,
Audience
:
jwt
.
ClaimStrings
{
"client-1"
,
"another-aud"
},
IssuedAt
:
jwt
.
NewNumericDate
(
now
),
NotBefore
:
jwt
.
NewNumericDate
(
now
.
Add
(
-
30
*
time
.
Second
)),
ExpiresAt
:
jwt
.
NewNumericDate
(
now
.
Add
(
5
*
time
.
Minute
)),
},
}
tok
:=
jwt
.
NewWithClaims
(
jwt
.
SigningMethodRS256
,
claims
)
tok
.
Header
[
"kid"
]
=
kid
signed
,
err
:=
tok
.
SignedString
(
priv
)
require
.
NoError
(
t
,
err
)
cfg
:=
config
.
OIDCConnectConfig
{
ClientID
:
"client-1"
,
IssuerURL
:
"https://issuer.example.com"
,
JWKSURL
:
srv
.
URL
,
AllowedSigningAlgs
:
"RS256"
,
ClockSkewSeconds
:
120
,
}
parsed
,
err
:=
oidcParseAndValidateIDToken
(
context
.
Background
(),
cfg
,
signed
,
"nonce-ok"
)
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
"subject-1"
,
parsed
.
Subject
)
require
.
Equal
(
t
,
"https://issuer.example.com"
,
parsed
.
Issuer
)
_
,
err
=
oidcParseAndValidateIDToken
(
context
.
Background
(),
cfg
,
signed
,
"bad-nonce"
)
require
.
Error
(
t
,
err
)
}
func
buildRSAJWK
(
kid
string
,
pub
*
rsa
.
PublicKey
)
oidcJWK
{
n
:=
base64
.
RawURLEncoding
.
EncodeToString
(
pub
.
N
.
Bytes
())
e
:=
base64
.
RawURLEncoding
.
EncodeToString
(
big
.
NewInt
(
int64
(
pub
.
E
))
.
Bytes
())
return
oidcJWK
{
Kty
:
"RSA"
,
Kid
:
kid
,
Use
:
"sig"
,
Alg
:
"RS256"
,
N
:
n
,
E
:
e
,
}
}
backend/internal/handler/dto/settings.go
View file @
02a66a01
...
@@ -51,6 +51,29 @@ type SystemSettings struct {
...
@@ -51,6 +51,29 @@ type SystemSettings struct {
LinuxDoConnectClientSecretConfigured
bool
`json:"linuxdo_connect_client_secret_configured"`
LinuxDoConnectClientSecretConfigured
bool
`json:"linuxdo_connect_client_secret_configured"`
LinuxDoConnectRedirectURL
string
`json:"linuxdo_connect_redirect_url"`
LinuxDoConnectRedirectURL
string
`json:"linuxdo_connect_redirect_url"`
OIDCConnectEnabled
bool
`json:"oidc_connect_enabled"`
OIDCConnectProviderName
string
`json:"oidc_connect_provider_name"`
OIDCConnectClientID
string
`json:"oidc_connect_client_id"`
OIDCConnectClientSecretConfigured
bool
`json:"oidc_connect_client_secret_configured"`
OIDCConnectIssuerURL
string
`json:"oidc_connect_issuer_url"`
OIDCConnectDiscoveryURL
string
`json:"oidc_connect_discovery_url"`
OIDCConnectAuthorizeURL
string
`json:"oidc_connect_authorize_url"`
OIDCConnectTokenURL
string
`json:"oidc_connect_token_url"`
OIDCConnectUserInfoURL
string
`json:"oidc_connect_userinfo_url"`
OIDCConnectJWKSURL
string
`json:"oidc_connect_jwks_url"`
OIDCConnectScopes
string
`json:"oidc_connect_scopes"`
OIDCConnectRedirectURL
string
`json:"oidc_connect_redirect_url"`
OIDCConnectFrontendRedirectURL
string
`json:"oidc_connect_frontend_redirect_url"`
OIDCConnectTokenAuthMethod
string
`json:"oidc_connect_token_auth_method"`
OIDCConnectUsePKCE
bool
`json:"oidc_connect_use_pkce"`
OIDCConnectValidateIDToken
bool
`json:"oidc_connect_validate_id_token"`
OIDCConnectAllowedSigningAlgs
string
`json:"oidc_connect_allowed_signing_algs"`
OIDCConnectClockSkewSeconds
int
`json:"oidc_connect_clock_skew_seconds"`
OIDCConnectRequireEmailVerified
bool
`json:"oidc_connect_require_email_verified"`
OIDCConnectUserInfoEmailPath
string
`json:"oidc_connect_userinfo_email_path"`
OIDCConnectUserInfoIDPath
string
`json:"oidc_connect_userinfo_id_path"`
OIDCConnectUserInfoUsernamePath
string
`json:"oidc_connect_userinfo_username_path"`
SiteName
string
`json:"site_name"`
SiteName
string
`json:"site_name"`
SiteLogo
string
`json:"site_logo"`
SiteLogo
string
`json:"site_logo"`
SiteSubtitle
string
`json:"site_subtitle"`
SiteSubtitle
string
`json:"site_subtitle"`
...
@@ -128,6 +151,9 @@ type PublicSettings struct {
...
@@ -128,6 +151,9 @@ type PublicSettings struct {
CustomMenuItems
[]
CustomMenuItem
`json:"custom_menu_items"`
CustomMenuItems
[]
CustomMenuItem
`json:"custom_menu_items"`
CustomEndpoints
[]
CustomEndpoint
`json:"custom_endpoints"`
CustomEndpoints
[]
CustomEndpoint
`json:"custom_endpoints"`
LinuxDoOAuthEnabled
bool
`json:"linuxdo_oauth_enabled"`
LinuxDoOAuthEnabled
bool
`json:"linuxdo_oauth_enabled"`
OIDCOAuthEnabled
bool
`json:"oidc_oauth_enabled"`
OIDCOAuthProviderName
string
`json:"oidc_oauth_provider_name"`
SoraClientEnabled
bool
`json:"sora_client_enabled"`
BackendModeEnabled
bool
`json:"backend_mode_enabled"`
BackendModeEnabled
bool
`json:"backend_mode_enabled"`
Version
string
`json:"version"`
Version
string
`json:"version"`
}
}
...
...
backend/internal/handler/setting_handler.go
View file @
02a66a01
...
@@ -54,6 +54,9 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) {
...
@@ -54,6 +54,9 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) {
CustomMenuItems
:
dto
.
ParseUserVisibleMenuItems
(
settings
.
CustomMenuItems
),
CustomMenuItems
:
dto
.
ParseUserVisibleMenuItems
(
settings
.
CustomMenuItems
),
CustomEndpoints
:
dto
.
ParseCustomEndpoints
(
settings
.
CustomEndpoints
),
CustomEndpoints
:
dto
.
ParseCustomEndpoints
(
settings
.
CustomEndpoints
),
LinuxDoOAuthEnabled
:
settings
.
LinuxDoOAuthEnabled
,
LinuxDoOAuthEnabled
:
settings
.
LinuxDoOAuthEnabled
,
OIDCOAuthEnabled
:
settings
.
OIDCOAuthEnabled
,
OIDCOAuthProviderName
:
settings
.
OIDCOAuthProviderName
,
SoraClientEnabled
:
settings
.
SoraClientEnabled
,
BackendModeEnabled
:
settings
.
BackendModeEnabled
,
BackendModeEnabled
:
settings
.
BackendModeEnabled
,
Version
:
h
.
version
,
Version
:
h
.
version
,
})
})
...
...
backend/internal/server/api_contract_test.go
View file @
02a66a01
...
@@ -462,6 +462,28 @@ func TestAPIContracts(t *testing.T) {
...
@@ -462,6 +462,28 @@ func TestAPIContracts(t *testing.T) {
service
.
SettingKeyTurnstileSiteKey
:
"site-key"
,
service
.
SettingKeyTurnstileSiteKey
:
"site-key"
,
service
.
SettingKeyTurnstileSecretKey
:
"secret-key"
,
service
.
SettingKeyTurnstileSecretKey
:
"secret-key"
,
service
.
SettingKeyOIDCConnectEnabled
:
"false"
,
service
.
SettingKeyOIDCConnectProviderName
:
"OIDC"
,
service
.
SettingKeyOIDCConnectClientID
:
""
,
service
.
SettingKeyOIDCConnectIssuerURL
:
""
,
service
.
SettingKeyOIDCConnectDiscoveryURL
:
""
,
service
.
SettingKeyOIDCConnectAuthorizeURL
:
""
,
service
.
SettingKeyOIDCConnectTokenURL
:
""
,
service
.
SettingKeyOIDCConnectUserInfoURL
:
""
,
service
.
SettingKeyOIDCConnectJWKSURL
:
""
,
service
.
SettingKeyOIDCConnectScopes
:
"openid email profile"
,
service
.
SettingKeyOIDCConnectRedirectURL
:
""
,
service
.
SettingKeyOIDCConnectFrontendRedirectURL
:
"/auth/oidc/callback"
,
service
.
SettingKeyOIDCConnectTokenAuthMethod
:
"client_secret_post"
,
service
.
SettingKeyOIDCConnectUsePKCE
:
"false"
,
service
.
SettingKeyOIDCConnectValidateIDToken
:
"true"
,
service
.
SettingKeyOIDCConnectAllowedSigningAlgs
:
"RS256,ES256,PS256"
,
service
.
SettingKeyOIDCConnectClockSkewSeconds
:
"120"
,
service
.
SettingKeyOIDCConnectRequireEmailVerified
:
"false"
,
service
.
SettingKeyOIDCConnectUserInfoEmailPath
:
""
,
service
.
SettingKeyOIDCConnectUserInfoIDPath
:
""
,
service
.
SettingKeyOIDCConnectUserInfoUsernamePath
:
""
,
service
.
SettingKeySiteName
:
"Sub2API"
,
service
.
SettingKeySiteName
:
"Sub2API"
,
service
.
SettingKeySiteLogo
:
""
,
service
.
SettingKeySiteLogo
:
""
,
service
.
SettingKeySiteSubtitle
:
"Subtitle"
,
service
.
SettingKeySiteSubtitle
:
"Subtitle"
,
...
@@ -503,10 +525,32 @@ func TestAPIContracts(t *testing.T) {
...
@@ -503,10 +525,32 @@ func TestAPIContracts(t *testing.T) {
"turnstile_enabled": true,
"turnstile_enabled": true,
"turnstile_site_key": "site-key",
"turnstile_site_key": "site-key",
"turnstile_secret_key_configured": true,
"turnstile_secret_key_configured": true,
"linuxdo_connect_enabled": false,
"linuxdo_connect_enabled": false,
"linuxdo_connect_client_id": "",
"linuxdo_connect_client_id": "",
"linuxdo_connect_client_secret_configured": false,
"linuxdo_connect_client_secret_configured": false,
"linuxdo_connect_redirect_url": "",
"linuxdo_connect_redirect_url": "",
"oidc_connect_enabled": false,
"oidc_connect_provider_name": "OIDC",
"oidc_connect_client_id": "",
"oidc_connect_client_secret_configured": false,
"oidc_connect_issuer_url": "",
"oidc_connect_discovery_url": "",
"oidc_connect_authorize_url": "",
"oidc_connect_token_url": "",
"oidc_connect_userinfo_url": "",
"oidc_connect_jwks_url": "",
"oidc_connect_scopes": "openid email profile",
"oidc_connect_redirect_url": "",
"oidc_connect_frontend_redirect_url": "/auth/oidc/callback",
"oidc_connect_token_auth_method": "client_secret_post",
"oidc_connect_use_pkce": false,
"oidc_connect_validate_id_token": true,
"oidc_connect_allowed_signing_algs": "RS256,ES256,PS256",
"oidc_connect_clock_skew_seconds": 120,
"oidc_connect_require_email_verified": false,
"oidc_connect_userinfo_email_path": "",
"oidc_connect_userinfo_id_path": "",
"oidc_connect_userinfo_username_path": "",
"ops_monitoring_enabled": false,
"ops_monitoring_enabled": false,
"ops_realtime_monitoring_enabled": true,
"ops_realtime_monitoring_enabled": true,
"ops_query_mode_default": "auto",
"ops_query_mode_default": "auto",
...
...
backend/internal/server/routes/auth.go
View file @
02a66a01
...
@@ -70,6 +70,14 @@ func RegisterAuthRoutes(
...
@@ -70,6 +70,14 @@ func RegisterAuthRoutes(
}),
}),
h
.
Auth
.
CompleteLinuxDoOAuthRegistration
,
h
.
Auth
.
CompleteLinuxDoOAuthRegistration
,
)
)
auth
.
GET
(
"/oauth/oidc/start"
,
h
.
Auth
.
OIDCOAuthStart
)
auth
.
GET
(
"/oauth/oidc/callback"
,
h
.
Auth
.
OIDCOAuthCallback
)
auth
.
POST
(
"/oauth/oidc/complete-registration"
,
rateLimiter
.
LimitWithOptions
(
"oauth-oidc-complete"
,
10
,
time
.
Minute
,
middleware
.
RateLimitOptions
{
FailureMode
:
middleware
.
RateLimitFailClose
,
}),
h
.
Auth
.
CompleteOIDCOAuthRegistration
,
)
}
}
// 公开设置(无需认证)
// 公开设置(无需认证)
...
...
backend/internal/service/auth_service.go
View file @
02a66a01
...
@@ -833,7 +833,8 @@ func randomHexString(byteLength int) (string, error) {
...
@@ -833,7 +833,8 @@ func randomHexString(byteLength int) (string, error) {
func
isReservedEmail
(
email
string
)
bool
{
func
isReservedEmail
(
email
string
)
bool
{
normalized
:=
strings
.
ToLower
(
strings
.
TrimSpace
(
email
))
normalized
:=
strings
.
ToLower
(
strings
.
TrimSpace
(
email
))
return
strings
.
HasSuffix
(
normalized
,
LinuxDoConnectSyntheticEmailDomain
)
return
strings
.
HasSuffix
(
normalized
,
LinuxDoConnectSyntheticEmailDomain
)
||
strings
.
HasSuffix
(
normalized
,
OIDCConnectSyntheticEmailDomain
)
}
}
// GenerateToken 生成JWT access token
// GenerateToken 生成JWT access token
...
...
backend/internal/service/domain_constants.go
View file @
02a66a01
...
@@ -71,6 +71,9 @@ const (
...
@@ -71,6 +71,9 @@ const (
// LinuxDoConnectSyntheticEmailDomain 是 LinuxDo Connect 用户的合成邮箱后缀(RFC 保留域名)。
// LinuxDoConnectSyntheticEmailDomain 是 LinuxDo Connect 用户的合成邮箱后缀(RFC 保留域名)。
const
LinuxDoConnectSyntheticEmailDomain
=
"@linuxdo-connect.invalid"
const
LinuxDoConnectSyntheticEmailDomain
=
"@linuxdo-connect.invalid"
// OIDCConnectSyntheticEmailDomain 是 OIDC 用户的合成邮箱后缀(RFC 保留域名)。
const
OIDCConnectSyntheticEmailDomain
=
"@oidc-connect.invalid"
// Setting keys
// Setting keys
const
(
const
(
// 注册设置
// 注册设置
...
@@ -105,6 +108,30 @@ const (
...
@@ -105,6 +108,30 @@ const (
SettingKeyLinuxDoConnectClientSecret
=
"linuxdo_connect_client_secret"
SettingKeyLinuxDoConnectClientSecret
=
"linuxdo_connect_client_secret"
SettingKeyLinuxDoConnectRedirectURL
=
"linuxdo_connect_redirect_url"
SettingKeyLinuxDoConnectRedirectURL
=
"linuxdo_connect_redirect_url"
// Generic OIDC OAuth 登录设置
SettingKeyOIDCConnectEnabled
=
"oidc_connect_enabled"
SettingKeyOIDCConnectProviderName
=
"oidc_connect_provider_name"
SettingKeyOIDCConnectClientID
=
"oidc_connect_client_id"
SettingKeyOIDCConnectClientSecret
=
"oidc_connect_client_secret"
SettingKeyOIDCConnectIssuerURL
=
"oidc_connect_issuer_url"
SettingKeyOIDCConnectDiscoveryURL
=
"oidc_connect_discovery_url"
SettingKeyOIDCConnectAuthorizeURL
=
"oidc_connect_authorize_url"
SettingKeyOIDCConnectTokenURL
=
"oidc_connect_token_url"
SettingKeyOIDCConnectUserInfoURL
=
"oidc_connect_userinfo_url"
SettingKeyOIDCConnectJWKSURL
=
"oidc_connect_jwks_url"
SettingKeyOIDCConnectScopes
=
"oidc_connect_scopes"
SettingKeyOIDCConnectRedirectURL
=
"oidc_connect_redirect_url"
SettingKeyOIDCConnectFrontendRedirectURL
=
"oidc_connect_frontend_redirect_url"
SettingKeyOIDCConnectTokenAuthMethod
=
"oidc_connect_token_auth_method"
SettingKeyOIDCConnectUsePKCE
=
"oidc_connect_use_pkce"
SettingKeyOIDCConnectValidateIDToken
=
"oidc_connect_validate_id_token"
SettingKeyOIDCConnectAllowedSigningAlgs
=
"oidc_connect_allowed_signing_algs"
SettingKeyOIDCConnectClockSkewSeconds
=
"oidc_connect_clock_skew_seconds"
SettingKeyOIDCConnectRequireEmailVerified
=
"oidc_connect_require_email_verified"
SettingKeyOIDCConnectUserInfoEmailPath
=
"oidc_connect_userinfo_email_path"
SettingKeyOIDCConnectUserInfoIDPath
=
"oidc_connect_userinfo_id_path"
SettingKeyOIDCConnectUserInfoUsernamePath
=
"oidc_connect_userinfo_username_path"
// OEM设置
// OEM设置
SettingKeySiteName
=
"site_name"
// 网站名称
SettingKeySiteName
=
"site_name"
// 网站名称
SettingKeySiteLogo
=
"site_logo"
// 网站Logo (base64)
SettingKeySiteLogo
=
"site_logo"
// 网站Logo (base64)
...
...
backend/internal/service/setting_service.go
View file @
02a66a01
This diff is collapsed.
Click to expand it.
backend/internal/service/setting_service_oidc_config_test.go
0 → 100644
View file @
02a66a01
//go:build unit
package
service
import
(
"context"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/stretchr/testify/require"
)
type
settingOIDCRepoStub
struct
{
values
map
[
string
]
string
}
func
(
s
*
settingOIDCRepoStub
)
Get
(
ctx
context
.
Context
,
key
string
)
(
*
Setting
,
error
)
{
panic
(
"unexpected Get call"
)
}
func
(
s
*
settingOIDCRepoStub
)
GetValue
(
ctx
context
.
Context
,
key
string
)
(
string
,
error
)
{
panic
(
"unexpected GetValue call"
)
}
func
(
s
*
settingOIDCRepoStub
)
Set
(
ctx
context
.
Context
,
key
,
value
string
)
error
{
panic
(
"unexpected Set call"
)
}
func
(
s
*
settingOIDCRepoStub
)
GetMultiple
(
ctx
context
.
Context
,
keys
[]
string
)
(
map
[
string
]
string
,
error
)
{
out
:=
make
(
map
[
string
]
string
,
len
(
keys
))
for
_
,
key
:=
range
keys
{
if
value
,
ok
:=
s
.
values
[
key
];
ok
{
out
[
key
]
=
value
}
}
return
out
,
nil
}
func
(
s
*
settingOIDCRepoStub
)
SetMultiple
(
ctx
context
.
Context
,
settings
map
[
string
]
string
)
error
{
panic
(
"unexpected SetMultiple call"
)
}
func
(
s
*
settingOIDCRepoStub
)
GetAll
(
ctx
context
.
Context
)
(
map
[
string
]
string
,
error
)
{
panic
(
"unexpected GetAll call"
)
}
func
(
s
*
settingOIDCRepoStub
)
Delete
(
ctx
context
.
Context
,
key
string
)
error
{
panic
(
"unexpected Delete call"
)
}
func
TestGetOIDCConnectOAuthConfig_ResolvesEndpointsFromIssuerDiscovery
(
t
*
testing
.
T
)
{
var
discoveryHits
int
var
baseURL
string
srv
:=
httptest
.
NewServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
if
r
.
URL
.
Path
!=
"/issuer/.well-known/openid-configuration"
{
http
.
NotFound
(
w
,
r
)
return
}
discoveryHits
++
w
.
Header
()
.
Set
(
"Content-Type"
,
"application/json"
)
_
,
_
=
w
.
Write
([]
byte
(
fmt
.
Sprintf
(
`{
"authorization_endpoint":"%s/issuer/protocol/openid-connect/auth",
"token_endpoint":"%s/issuer/protocol/openid-connect/token",
"userinfo_endpoint":"%s/issuer/protocol/openid-connect/userinfo",
"jwks_uri":"%s/issuer/protocol/openid-connect/certs"
}`
,
baseURL
,
baseURL
,
baseURL
,
baseURL
)))
}))
defer
srv
.
Close
()
baseURL
=
srv
.
URL
cfg
:=
&
config
.
Config
{
OIDC
:
config
.
OIDCConnectConfig
{
Enabled
:
true
,
ProviderName
:
"OIDC"
,
ClientID
:
"oidc-client"
,
ClientSecret
:
"oidc-secret"
,
IssuerURL
:
srv
.
URL
+
"/issuer"
,
RedirectURL
:
"https://example.com/api/v1/auth/oauth/oidc/callback"
,
FrontendRedirectURL
:
"/auth/oidc/callback"
,
Scopes
:
"openid email profile"
,
TokenAuthMethod
:
"client_secret_post"
,
ValidateIDToken
:
true
,
AllowedSigningAlgs
:
"RS256"
,
ClockSkewSeconds
:
120
,
},
}
repo
:=
&
settingOIDCRepoStub
{
values
:
map
[
string
]
string
{}}
svc
:=
NewSettingService
(
repo
,
cfg
)
got
,
err
:=
svc
.
GetOIDCConnectOAuthConfig
(
context
.
Background
())
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
1
,
discoveryHits
)
require
.
Equal
(
t
,
srv
.
URL
+
"/issuer/.well-known/openid-configuration"
,
got
.
DiscoveryURL
)
require
.
Equal
(
t
,
srv
.
URL
+
"/issuer/protocol/openid-connect/auth"
,
got
.
AuthorizeURL
)
require
.
Equal
(
t
,
srv
.
URL
+
"/issuer/protocol/openid-connect/token"
,
got
.
TokenURL
)
require
.
Equal
(
t
,
srv
.
URL
+
"/issuer/protocol/openid-connect/userinfo"
,
got
.
UserInfoURL
)
require
.
Equal
(
t
,
srv
.
URL
+
"/issuer/protocol/openid-connect/certs"
,
got
.
JWKSURL
)
}
backend/internal/service/settings_view.go
View file @
02a66a01
...
@@ -31,6 +31,31 @@ type SystemSettings struct {
...
@@ -31,6 +31,31 @@ type SystemSettings struct {
LinuxDoConnectClientSecretConfigured
bool
LinuxDoConnectClientSecretConfigured
bool
LinuxDoConnectRedirectURL
string
LinuxDoConnectRedirectURL
string
// Generic OIDC OAuth 登录
OIDCConnectEnabled
bool
OIDCConnectProviderName
string
OIDCConnectClientID
string
OIDCConnectClientSecret
string
OIDCConnectClientSecretConfigured
bool
OIDCConnectIssuerURL
string
OIDCConnectDiscoveryURL
string
OIDCConnectAuthorizeURL
string
OIDCConnectTokenURL
string
OIDCConnectUserInfoURL
string
OIDCConnectJWKSURL
string
OIDCConnectScopes
string
OIDCConnectRedirectURL
string
OIDCConnectFrontendRedirectURL
string
OIDCConnectTokenAuthMethod
string
OIDCConnectUsePKCE
bool
OIDCConnectValidateIDToken
bool
OIDCConnectAllowedSigningAlgs
string
OIDCConnectClockSkewSeconds
int
OIDCConnectRequireEmailVerified
bool
OIDCConnectUserInfoEmailPath
string
OIDCConnectUserInfoIDPath
string
OIDCConnectUserInfoUsernamePath
string
SiteName
string
SiteName
string
SiteLogo
string
SiteLogo
string
SiteSubtitle
string
SiteSubtitle
string
...
@@ -110,9 +135,11 @@ type PublicSettings struct {
...
@@ -110,9 +135,11 @@ type PublicSettings struct {
CustomMenuItems
string
// JSON array of custom menu items
CustomMenuItems
string
// JSON array of custom menu items
CustomEndpoints
string
// JSON array of custom endpoints
CustomEndpoints
string
// JSON array of custom endpoints
LinuxDoOAuthEnabled
bool
LinuxDoOAuthEnabled
bool
BackendModeEnabled
bool
BackendModeEnabled
bool
Version
string
OIDCOAuthEnabled
bool
OIDCOAuthProviderName
string
Version
string
}
}
// StreamTimeoutSettings 流超时处理配置(仅控制超时后的处理方式,超时判定由网关配置控制)
// StreamTimeoutSettings 流超时处理配置(仅控制超时后的处理方式,超时判定由网关配置控制)
...
...
deploy/config.example.yaml
View file @
02a66a01
...
@@ -820,6 +820,46 @@ linuxdo_connect:
...
@@ -820,6 +820,46 @@ linuxdo_connect:
userinfo_id_path
:
"
"
userinfo_id_path
:
"
"
userinfo_username_path
:
"
"
userinfo_username_path
:
"
"
# =============================================================================
# Generic OIDC OAuth Login (SSO)
# 通用 OIDC OAuth 登录(用于 Sub2API 用户登录)
# =============================================================================
oidc_connect
:
enabled
:
false
provider_name
:
"
OIDC"
client_id
:
"
"
client_secret
:
"
"
# 例如: "https://keycloak.example.com/realms/myrealm"
issuer_url
:
"
"
# 可选: OIDC Discovery URL。为空时可手动填写 authorize/token/userinfo/jwks
discovery_url
:
"
"
authorize_url
:
"
"
token_url
:
"
"
# 可选(仅补充 email/username,不用于 sub 可信绑定)
userinfo_url
:
"
"
# validate_id_token=true 时必填
jwks_url
:
"
"
scopes
:
"
openid
email
profile"
# 示例: "https://your-domain.com/api/v1/auth/oauth/oidc/callback"
redirect_url
:
"
"
# 安全提示:
# - 建议使用同源相对路径(以 / 开头),避免把 token 重定向到意外的第三方域名
# - 该地址不应包含 #fragment(本实现使用 URL fragment 传递 access_token)
frontend_redirect_url
:
"
/auth/oidc/callback"
token_auth_method
:
"
client_secret_post"
# client_secret_post | client_secret_basic | none
# 注意:当 token_auth_method=none(public client)时,必须启用 PKCE
use_pkce
:
false
# 开启后强制校验 id_token 的签名和 claims(推荐)
validate_id_token
:
true
allowed_signing_algs
:
"
RS256,ES256,PS256"
# 允许的时钟偏移(秒)
clock_skew_seconds
:
120
# 若 Provider 返回 email_verified=false,是否拒绝登录
require_email_verified
:
false
userinfo_email_path
:
"
"
userinfo_id_path
:
"
"
userinfo_username_path
:
"
"
# =============================================================================
# =============================================================================
# Default Settings
# Default Settings
# 默认设置
# 默认设置
...
...
frontend/src/api/admin/settings.ts
View file @
02a66a01
...
@@ -62,6 +62,30 @@ export interface SystemSettings {
...
@@ -62,6 +62,30 @@ export interface SystemSettings {
linuxdo_connect_client_secret_configured
:
boolean
linuxdo_connect_client_secret_configured
:
boolean
linuxdo_connect_redirect_url
:
string
linuxdo_connect_redirect_url
:
string
// Generic OIDC OAuth settings
oidc_connect_enabled
:
boolean
oidc_connect_provider_name
:
string
oidc_connect_client_id
:
string
oidc_connect_client_secret_configured
:
boolean
oidc_connect_issuer_url
:
string
oidc_connect_discovery_url
:
string
oidc_connect_authorize_url
:
string
oidc_connect_token_url
:
string
oidc_connect_userinfo_url
:
string
oidc_connect_jwks_url
:
string
oidc_connect_scopes
:
string
oidc_connect_redirect_url
:
string
oidc_connect_frontend_redirect_url
:
string
oidc_connect_token_auth_method
:
string
oidc_connect_use_pkce
:
boolean
oidc_connect_validate_id_token
:
boolean
oidc_connect_allowed_signing_algs
:
string
oidc_connect_clock_skew_seconds
:
number
oidc_connect_require_email_verified
:
boolean
oidc_connect_userinfo_email_path
:
string
oidc_connect_userinfo_id_path
:
string
oidc_connect_userinfo_username_path
:
string
// Model fallback configuration
// Model fallback configuration
enable_model_fallback
:
boolean
enable_model_fallback
:
boolean
fallback_model_anthropic
:
string
fallback_model_anthropic
:
string
...
@@ -131,6 +155,28 @@ export interface UpdateSettingsRequest {
...
@@ -131,6 +155,28 @@ export interface UpdateSettingsRequest {
linuxdo_connect_client_id
?:
string
linuxdo_connect_client_id
?:
string
linuxdo_connect_client_secret
?:
string
linuxdo_connect_client_secret
?:
string
linuxdo_connect_redirect_url
?:
string
linuxdo_connect_redirect_url
?:
string
oidc_connect_enabled
?:
boolean
oidc_connect_provider_name
?:
string
oidc_connect_client_id
?:
string
oidc_connect_client_secret
?:
string
oidc_connect_issuer_url
?:
string
oidc_connect_discovery_url
?:
string
oidc_connect_authorize_url
?:
string
oidc_connect_token_url
?:
string
oidc_connect_userinfo_url
?:
string
oidc_connect_jwks_url
?:
string
oidc_connect_scopes
?:
string
oidc_connect_redirect_url
?:
string
oidc_connect_frontend_redirect_url
?:
string
oidc_connect_token_auth_method
?:
string
oidc_connect_use_pkce
?:
boolean
oidc_connect_validate_id_token
?:
boolean
oidc_connect_allowed_signing_algs
?:
string
oidc_connect_clock_skew_seconds
?:
number
oidc_connect_require_email_verified
?:
boolean
oidc_connect_userinfo_email_path
?:
string
oidc_connect_userinfo_id_path
?:
string
oidc_connect_userinfo_username_path
?:
string
enable_model_fallback
?:
boolean
enable_model_fallback
?:
boolean
fallback_model_anthropic
?:
string
fallback_model_anthropic
?:
string
fallback_model_openai
?:
string
fallback_model_openai
?:
string
...
...
frontend/src/api/auth.ts
View file @
02a66a01
...
@@ -357,6 +357,28 @@ export async function completeLinuxDoOAuthRegistration(
...
@@ -357,6 +357,28 @@ export async function completeLinuxDoOAuthRegistration(
return
data
return
data
}
}
/**
* Complete OIDC OAuth registration by supplying an invitation code
* @param pendingOAuthToken - Short-lived JWT from the OAuth callback
* @param invitationCode - Invitation code entered by the user
* @returns Token pair on success
*/
export
async
function
completeOIDCOAuthRegistration
(
pendingOAuthToken
:
string
,
invitationCode
:
string
):
Promise
<
{
access_token
:
string
;
refresh_token
:
string
;
expires_in
:
number
;
token_type
:
string
}
>
{
const
{
data
}
=
await
apiClient
.
post
<
{
access_token
:
string
refresh_token
:
string
expires_in
:
number
token_type
:
string
}
>
(
'
/auth/oauth/oidc/complete-registration
'
,
{
pending_oauth_token
:
pendingOAuthToken
,
invitation_code
:
invitationCode
})
return
data
}
export
const
authAPI
=
{
export
const
authAPI
=
{
login
,
login
,
login2FA
,
login2FA
,
...
@@ -380,7 +402,8 @@ export const authAPI = {
...
@@ -380,7 +402,8 @@ export const authAPI = {
resetPassword
,
resetPassword
,
refreshToken
,
refreshToken
,
revokeAllSessions
,
revokeAllSessions
,
completeLinuxDoOAuthRegistration
completeLinuxDoOAuthRegistration
,
completeOIDCOAuthRegistration
}
}
export
default
authAPI
export
default
authAPI
frontend/src/components/auth/LinuxDoOAuthSection.vue
View file @
02a66a01
...
@@ -29,10 +29,10 @@
...
@@ -29,10 +29,10 @@
{{
t
(
'
auth.linuxdo.signIn
'
)
}}
{{
t
(
'
auth.linuxdo.signIn
'
)
}}
</button>
</button>
<div
class=
"flex items-center gap-3"
>
<div
v-if=
"showDivider"
class=
"flex items-center gap-3"
>
<div
class=
"h-px flex-1 bg-gray-200 dark:bg-dark-700"
></div>
<div
class=
"h-px flex-1 bg-gray-200 dark:bg-dark-700"
></div>
<span
class=
"text-xs text-gray-500 dark:text-dark-400"
>
<span
class=
"text-xs text-gray-500 dark:text-dark-400"
>
{{
t
(
'
auth.
linuxdo.o
rContinue
'
)
}}
{{
t
(
'
auth.
oauthO
rContinue
'
)
}}
</span>
</span>
<div
class=
"h-px flex-1 bg-gray-200 dark:bg-dark-700"
></div>
<div
class=
"h-px flex-1 bg-gray-200 dark:bg-dark-700"
></div>
</div>
</div>
...
@@ -43,9 +43,12 @@
...
@@ -43,9 +43,12 @@
import
{
useRoute
}
from
'
vue-router
'
import
{
useRoute
}
from
'
vue-router
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useI18n
}
from
'
vue-i18n
'
defineProps
<
{
withDefaults
(
defineProps
<
{
disabled
?:
boolean
disabled
?:
boolean
}
>
()
showDivider
?:
boolean
}
>
(),
{
showDivider
:
true
})
const
route
=
useRoute
()
const
route
=
useRoute
()
const
{
t
}
=
useI18n
()
const
{
t
}
=
useI18n
()
...
@@ -58,4 +61,3 @@ function startLogin(): void {
...
@@ -58,4 +61,3 @@ function startLogin(): void {
window
.
location
.
href
=
startURL
window
.
location
.
href
=
startURL
}
}
</
script
>
</
script
>
frontend/src/components/auth/OidcOAuthSection.vue
0 → 100644
View file @
02a66a01
<
template
>
<div
class=
"space-y-4"
>
<button
type=
"button"
:disabled=
"disabled"
class=
"btn btn-secondary w-full"
@
click=
"startLogin"
>
<span
class=
"mr-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-primary-100 text-xs font-semibold text-primary-700 dark:bg-primary-900/30 dark:text-primary-300"
>
{{
providerInitial
}}
</span>
{{
t
(
'
auth.oidc.signIn
'
,
{
providerName
:
normalizedProviderName
}
)
}}
<
/button
>
<
div
v
-
if
=
"
showDivider
"
class
=
"
flex items-center gap-3
"
>
<
div
class
=
"
h-px flex-1 bg-gray-200 dark:bg-dark-700
"
><
/div
>
<
span
class
=
"
text-xs text-gray-500 dark:text-dark-400
"
>
{{
t
(
'
auth.oauthOrContinue
'
)
}}
<
/span
>
<
div
class
=
"
h-px flex-1 bg-gray-200 dark:bg-dark-700
"
><
/div
>
<
/div
>
<
/div
>
<
/template
>
<
script
setup
lang
=
"
ts
"
>
import
{
computed
}
from
'
vue
'
import
{
useRoute
}
from
'
vue-router
'
import
{
useI18n
}
from
'
vue-i18n
'
const
props
=
withDefaults
(
defineProps
<
{
disabled
?:
boolean
providerName
?:
string
showDivider
?:
boolean
}
>
(),
{
providerName
:
'
OIDC
'
,
showDivider
:
true
}
)
const
route
=
useRoute
()
const
{
t
}
=
useI18n
()
const
normalizedProviderName
=
computed
(()
=>
{
const
name
=
props
.
providerName
?.
trim
()
return
name
||
'
OIDC
'
}
)
const
providerInitial
=
computed
(()
=>
normalizedProviderName
.
value
.
charAt
(
0
).
toUpperCase
()
||
'
O
'
)
function
startLogin
():
void
{
const
redirectTo
=
(
route
.
query
.
redirect
as
string
)
||
'
/dashboard
'
const
apiBase
=
(
import
.
meta
.
env
.
VITE_API_BASE_URL
as
string
|
undefined
)
||
'
/api/v1
'
const
normalized
=
apiBase
.
replace
(
/
\/
$/
,
''
)
const
startURL
=
`${normalized
}
/auth/oauth/oidc/start?redirect=${encodeURIComponent(redirectTo)
}
`
window
.
location
.
href
=
startURL
}
<
/script
>
frontend/src/i18n/locales/en.ts
View file @
02a66a01
...
@@ -428,6 +428,7 @@ export default {
...
@@ -428,6 +428,7 @@ export default {
invitationCodeInvalid
:
'
Invalid or used invitation code
'
,
invitationCodeInvalid
:
'
Invalid or used invitation code
'
,
invitationCodeValidating
:
'
Validating invitation code...
'
,
invitationCodeValidating
:
'
Validating invitation code...
'
,
invitationCodeInvalidCannotRegister
:
'
Invalid invitation code. Please check and try again
'
,
invitationCodeInvalidCannotRegister
:
'
Invalid invitation code. Please check and try again
'
,
oauthOrContinue
:
'
or continue with email
'
,
linuxdo
:
{
linuxdo
:
{
signIn
:
'
Continue with Linux.do
'
,
signIn
:
'
Continue with Linux.do
'
,
orContinue
:
'
or continue with email
'
,
orContinue
:
'
or continue with email
'
,
...
@@ -442,6 +443,20 @@ export default {
...
@@ -442,6 +443,20 @@ export default {
completing
:
'
Completing registration…
'
,
completing
:
'
Completing registration…
'
,
completeRegistrationFailed
:
'
Registration failed. Please check your invitation code and try again.
'
completeRegistrationFailed
:
'
Registration failed. Please check your invitation code and try again.
'
},
},
oidc
:
{
signIn
:
'
Continue with {providerName}
'
,
callbackTitle
:
'
Signing you in with {providerName}
'
,
callbackProcessing
:
'
Completing login with {providerName}, please wait...
'
,
callbackHint
:
'
If you are not redirected automatically, go back to the login page and try again.
'
,
callbackMissingToken
:
'
Missing login token, please try again.
'
,
backToLogin
:
'
Back to Login
'
,
invitationRequired
:
'
This {providerName} account is not yet registered. The site requires an invitation code — please enter one to complete registration.
'
,
invalidPendingToken
:
'
The registration token has expired. Please sign in again.
'
,
completeRegistration
:
'
Complete Registration
'
,
completing
:
'
Completing registration…
'
,
completeRegistrationFailed
:
'
Registration failed. Please check your invitation code and try again.
'
},
oauth
:
{
oauth
:
{
code
:
'
Code
'
,
code
:
'
Code
'
,
state
:
'
State
'
,
state
:
'
State
'
,
...
@@ -4227,6 +4242,57 @@ export default {
...
@@ -4227,6 +4242,57 @@ export default {
quickSetCopy
:
'
Generate & Copy (current site)
'
,
quickSetCopy
:
'
Generate & Copy (current site)
'
,
redirectUrlSetAndCopied
:
'
Redirect URL generated and copied to clipboard
'
redirectUrlSetAndCopied
:
'
Redirect URL generated and copied to clipboard
'
},
},
oidc
:
{
title
:
'
OIDC Login
'
,
description
:
'
Configure a standard OIDC provider (for example Keycloak)
'
,
enable
:
'
Enable OIDC Login
'
,
enableHint
:
'
Show OIDC login on the login/register pages
'
,
providerName
:
'
Provider Name
'
,
providerNamePlaceholder
:
'
for example Keycloak
'
,
clientId
:
'
Client ID
'
,
clientIdPlaceholder
:
'
OIDC client id
'
,
clientSecret
:
'
Client Secret
'
,
clientSecretPlaceholder
:
'
********
'
,
clientSecretHint
:
'
Used by backend to exchange tokens (keep it secret)
'
,
clientSecretConfiguredPlaceholder
:
'
********
'
,
clientSecretConfiguredHint
:
'
Secret configured. Leave empty to keep the current value.
'
,
issuerUrl
:
'
Issuer URL
'
,
issuerUrlPlaceholder
:
'
https://id.example.com/realms/main
'
,
discoveryUrl
:
'
Discovery URL
'
,
discoveryUrlPlaceholder
:
'
Optional, leave empty to auto-derive from issuer
'
,
authorizeUrl
:
'
Authorize URL
'
,
authorizeUrlPlaceholder
:
'
Optional, can be discovered automatically
'
,
tokenUrl
:
'
Token URL
'
,
tokenUrlPlaceholder
:
'
Optional, can be discovered automatically
'
,
userinfoUrl
:
'
UserInfo URL
'
,
userinfoUrlPlaceholder
:
'
Optional, can be discovered automatically
'
,
jwksUrl
:
'
JWKS URL
'
,
jwksUrlPlaceholder
:
'
Optional, required when strict ID token validation is enabled
'
,
scopes
:
'
Scopes
'
,
scopesPlaceholder
:
'
openid email profile
'
,
scopesHint
:
'
Must include openid
'
,
redirectUrl
:
'
Backend Redirect URL
'
,
redirectUrlPlaceholder
:
'
https://your-domain.com/api/v1/auth/oauth/oidc/callback
'
,
redirectUrlHint
:
'
Must match the callback URL configured in the OIDC provider
'
,
quickSetCopy
:
'
Generate & Copy (current site)
'
,
redirectUrlSetAndCopied
:
'
Redirect URL generated and copied to clipboard
'
,
frontendRedirectUrl
:
'
Frontend Callback Path
'
,
frontendRedirectUrlPlaceholder
:
'
/auth/oidc/callback
'
,
frontendRedirectUrlHint
:
'
Frontend route used after backend callback
'
,
tokenAuthMethod
:
'
Token Auth Method
'
,
clockSkewSeconds
:
'
Clock Skew (seconds)
'
,
allowedSigningAlgs
:
'
Allowed Signing Algs
'
,
allowedSigningAlgsPlaceholder
:
'
RS256,ES256,PS256
'
,
usePkce
:
'
Use PKCE
'
,
validateIdToken
:
'
Validate ID Token
'
,
requireEmailVerified
:
'
Require Email Verified
'
,
userinfoEmailPath
:
'
UserInfo Email Path
'
,
userinfoEmailPathPlaceholder
:
'
for example data.email
'
,
userinfoIdPath
:
'
UserInfo ID Path
'
,
userinfoIdPathPlaceholder
:
'
for example data.id
'
,
userinfoUsernamePath
:
'
UserInfo Username Path
'
,
userinfoUsernamePathPlaceholder
:
'
for example data.username
'
},
defaults
:
{
defaults
:
{
title
:
'
Default User Settings
'
,
title
:
'
Default User Settings
'
,
description
:
'
Default values for new users
'
,
description
:
'
Default values for new users
'
,
...
...
Prev
1
2
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment