| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015 |
- package service
- import (
- "context"
- "encoding/json"
- "fmt"
- "log/slog"
- "strconv"
- "strings"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/config"
- infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
- "github.com/imroc/req/v3"
- )
- // CoerceDingTalkCorpPolicyForWrite 是 coerceDeprecatedDingTalkCorpPolicy 的导出版本,
- // 用于 admin handler 在写入路径上对客户端直传的入参做防御性 coerce(前端 UI 虽已无 whitelist 选项,
- // 但 API 可被直接调用)。
- func CoerceDingTalkCorpPolicyForWrite(policy string) string {
- return coerceDeprecatedDingTalkCorpPolicy(policy)
- }
- // coerceDeprecatedDingTalkCorpPolicy 把已废弃的 corp_restriction_policy 值替换成安全的等价值。
- // 升级前残留在 DB 中的 "whitelist" 会导致 callback 链路在 default case 静默 fail-closed
- // (所有钉钉登录被拒)。这里统一退化为 "none" 让服务保持可用,并 warn 日志提醒 admin 重新保存设置。
- func coerceDeprecatedDingTalkCorpPolicy(policy string) string {
- if policy == "whitelist" {
- slog.Warn("dingtalk: corp_restriction_policy=whitelist is deprecated and unsupported, coercing to none",
- "hint", "re-save DingTalk settings in admin UI to clear this warning")
- return "none"
- }
- return policy
- }
- func normalizeWeChatConnectModeSetting(raw string) string {
- switch strings.ToLower(strings.TrimSpace(raw)) {
- case "mp":
- return "mp"
- case "mobile":
- return "mobile"
- default:
- return "open"
- }
- }
- func defaultWeChatConnectScopeForMode(mode string) string {
- switch normalizeWeChatConnectModeSetting(mode) {
- case "mp":
- return "snsapi_userinfo"
- case "mobile":
- return ""
- }
- return defaultWeChatConnectScopes
- }
- func normalizeWeChatConnectScopeSetting(raw, mode string) string {
- switch normalizeWeChatConnectModeSetting(mode) {
- case "mp":
- switch strings.TrimSpace(raw) {
- case "snsapi_base":
- return "snsapi_base"
- case "snsapi_userinfo":
- return "snsapi_userinfo"
- default:
- return defaultWeChatConnectScopeForMode(mode)
- }
- case "mobile":
- return ""
- default:
- return defaultWeChatConnectScopes
- }
- }
- func parseWeChatConnectCapabilitySettings(settings map[string]string, enabled bool, mode string) (bool, bool, bool) {
- mode = normalizeWeChatConnectModeSetting(mode)
- rawOpen, hasOpen := settings[SettingKeyWeChatConnectOpenEnabled]
- rawMP, hasMP := settings[SettingKeyWeChatConnectMPEnabled]
- rawMobile, hasMobile := settings[SettingKeyWeChatConnectMobileEnabled]
- openConfigured := hasOpen && strings.TrimSpace(rawOpen) != ""
- mpConfigured := hasMP && strings.TrimSpace(rawMP) != ""
- mobileConfigured := hasMobile && strings.TrimSpace(rawMobile) != ""
- if openConfigured || mpConfigured || mobileConfigured {
- openEnabled := strings.TrimSpace(rawOpen) == "true"
- mpEnabled := strings.TrimSpace(rawMP) == "true"
- mobileEnabled := strings.TrimSpace(rawMobile) == "true"
- return openEnabled, mpEnabled, mobileEnabled
- }
- if !enabled {
- return false, false, false
- }
- if mode == "mp" {
- return false, true, false
- }
- if mode == "mobile" {
- return false, false, true
- }
- return true, false, false
- }
- func normalizeWeChatConnectStoredMode(openEnabled, mpEnabled, mobileEnabled bool, mode string) string {
- mode = normalizeWeChatConnectModeSetting(mode)
- switch mode {
- case "open":
- if openEnabled {
- return "open"
- }
- case "mp":
- if mpEnabled {
- return "mp"
- }
- case "mobile":
- if mobileEnabled {
- return "mobile"
- }
- }
- switch {
- case openEnabled:
- return "open"
- case mpEnabled:
- return "mp"
- case mobileEnabled:
- return "mobile"
- default:
- return mode
- }
- }
- func mergeWeChatConnectCapabilitySettings(settings map[string]string, base config.WeChatConnectConfig, enabled bool, mode string) (bool, bool, bool) {
- mode = normalizeWeChatConnectModeSetting(firstNonEmpty(mode, base.Mode))
- rawOpen, hasOpen := settings[SettingKeyWeChatConnectOpenEnabled]
- rawMP, hasMP := settings[SettingKeyWeChatConnectMPEnabled]
- rawMobile, hasMobile := settings[SettingKeyWeChatConnectMobileEnabled]
- openConfigured := hasOpen && strings.TrimSpace(rawOpen) != ""
- mpConfigured := hasMP && strings.TrimSpace(rawMP) != ""
- mobileConfigured := hasMobile && strings.TrimSpace(rawMobile) != ""
- if openConfigured || mpConfigured || mobileConfigured {
- openEnabled := strings.TrimSpace(rawOpen) == "true"
- mpEnabled := strings.TrimSpace(rawMP) == "true"
- mobileEnabled := strings.TrimSpace(rawMobile) == "true"
- _, enabledConfigured := settings[SettingKeyWeChatConnectEnabled]
- if !enabledConfigured &&
- enabled &&
- !openEnabled &&
- !mpEnabled &&
- !mobileEnabled &&
- (base.OpenEnabled || base.MPEnabled || base.MobileEnabled) {
- return base.OpenEnabled, base.MPEnabled, base.MobileEnabled
- }
- return openEnabled, mpEnabled, mobileEnabled
- }
- if !enabled {
- return false, false, false
- }
- if base.OpenEnabled || base.MPEnabled || base.MobileEnabled {
- return base.OpenEnabled, base.MPEnabled, base.MobileEnabled
- }
- return parseWeChatConnectCapabilitySettings(settings, enabled, mode)
- }
- func (s *SettingService) effectiveWeChatConnectOAuthConfig(settings map[string]string) WeChatConnectOAuthConfig {
- base := config.WeChatConnectConfig{}
- if s != nil && s.cfg != nil {
- base = s.cfg.WeChat
- }
- enabled := base.Enabled
- if raw, ok := settings[SettingKeyWeChatConnectEnabled]; ok {
- enabled = strings.TrimSpace(raw) == "true"
- }
- legacyAppID := strings.TrimSpace(firstNonEmpty(
- settings[SettingKeyWeChatConnectAppID],
- base.AppID,
- base.OpenAppID,
- base.MPAppID,
- base.MobileAppID,
- ))
- legacyAppSecret := strings.TrimSpace(firstNonEmpty(
- settings[SettingKeyWeChatConnectAppSecret],
- base.AppSecret,
- base.OpenAppSecret,
- base.MPAppSecret,
- base.MobileAppSecret,
- ))
- openAppID := strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectOpenAppID], base.OpenAppID, legacyAppID))
- openAppSecret := strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectOpenAppSecret], base.OpenAppSecret, legacyAppSecret))
- mpAppID := strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectMPAppID], base.MPAppID, legacyAppID))
- mpAppSecret := strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectMPAppSecret], base.MPAppSecret, legacyAppSecret))
- mobileAppID := strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectMobileAppID], base.MobileAppID, legacyAppID))
- mobileAppSecret := strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectMobileAppSecret], base.MobileAppSecret, legacyAppSecret))
- modeRaw := firstNonEmpty(settings[SettingKeyWeChatConnectMode], base.Mode)
- openEnabled, mpEnabled, mobileEnabled := mergeWeChatConnectCapabilitySettings(settings, base, enabled, modeRaw)
- mode := normalizeWeChatConnectStoredMode(openEnabled, mpEnabled, mobileEnabled, modeRaw)
- return WeChatConnectOAuthConfig{
- Enabled: enabled,
- LegacyAppID: legacyAppID,
- LegacyAppSecret: legacyAppSecret,
- OpenAppID: openAppID,
- OpenAppSecret: openAppSecret,
- MPAppID: mpAppID,
- MPAppSecret: mpAppSecret,
- MobileAppID: mobileAppID,
- MobileAppSecret: mobileAppSecret,
- OpenEnabled: openEnabled,
- MPEnabled: mpEnabled,
- MobileEnabled: mobileEnabled,
- Mode: mode,
- Scopes: normalizeWeChatConnectScopeSetting(firstNonEmpty(settings[SettingKeyWeChatConnectScopes], base.Scopes), mode),
- RedirectURL: strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectRedirectURL], base.RedirectURL)),
- FrontendRedirectURL: strings.TrimSpace(firstNonEmpty(settings[SettingKeyWeChatConnectFrontendRedirectURL], base.FrontendRedirectURL, defaultWeChatConnectFrontend)),
- }
- }
- func DefaultWeChatConnectScopesForMode(mode string) string {
- return defaultWeChatConnectScopeForMode(mode)
- }
- func (s *SettingService) parseWeChatConnectOAuthConfig(settings map[string]string) (WeChatConnectOAuthConfig, error) {
- cfg := s.effectiveWeChatConnectOAuthConfig(settings)
- if !cfg.Enabled || (!cfg.OpenEnabled && !cfg.MPEnabled) {
- return WeChatConnectOAuthConfig{}, infraerrors.NotFound("OAUTH_DISABLED", "wechat oauth is disabled")
- }
- if cfg.OpenEnabled {
- if cfg.AppIDForMode("open") == "" {
- return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth pc app id not configured")
- }
- if cfg.AppSecretForMode("open") == "" {
- return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth pc app secret not configured")
- }
- }
- if cfg.MPEnabled {
- if cfg.AppIDForMode("mp") == "" {
- return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth official account app id not configured")
- }
- if cfg.AppSecretForMode("mp") == "" {
- return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth official account app secret not configured")
- }
- }
- if cfg.MobileEnabled {
- if cfg.AppIDForMode("mobile") == "" {
- return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth mobile app id not configured")
- }
- if cfg.AppSecretForMode("mobile") == "" {
- return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth mobile app secret not configured")
- }
- }
- if v := strings.TrimSpace(cfg.RedirectURL); v != "" {
- if err := config.ValidateAbsoluteHTTPURL(v); err != nil {
- return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth redirect url invalid")
- }
- }
- if err := config.ValidateFrontendRedirectURL(cfg.FrontendRedirectURL); err != nil {
- return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth frontend redirect url invalid")
- }
- return cfg, nil
- }
- func (s *SettingService) weChatOAuthCapabilitiesFromSettings(settings map[string]string) (bool, bool, bool, bool) {
- cfg := s.effectiveWeChatConnectOAuthConfig(settings)
- if !cfg.Enabled {
- return false, false, false, false
- }
- openReady := cfg.OpenEnabled && cfg.AppIDForMode("open") != "" && cfg.AppSecretForMode("open") != ""
- mpReady := cfg.MPEnabled && cfg.AppIDForMode("mp") != "" && cfg.AppSecretForMode("mp") != ""
- mobileReady := cfg.MobileEnabled && cfg.AppIDForMode("mobile") != "" && cfg.AppSecretForMode("mobile") != ""
- return openReady || mpReady, openReady, mpReady, mobileReady
- }
- func (s *SettingService) emailOAuthBaseConfig(provider string) config.EmailOAuthProviderConfig {
- switch strings.ToLower(strings.TrimSpace(provider)) {
- case "github":
- cfg := config.EmailOAuthProviderConfig{
- AuthorizeURL: defaultGitHubOAuthAuthorize,
- TokenURL: defaultGitHubOAuthToken,
- UserInfoURL: defaultGitHubOAuthUserInfo,
- EmailsURL: defaultGitHubOAuthEmails,
- Scopes: defaultGitHubOAuthScopes,
- FrontendRedirectURL: defaultGitHubOAuthFrontend,
- }
- if s != nil && s.cfg != nil {
- cfg = mergeEmailOAuthBaseConfig(cfg, s.cfg.GitHubOAuth)
- }
- return cfg
- case "google":
- cfg := config.EmailOAuthProviderConfig{
- AuthorizeURL: defaultGoogleOAuthAuthorize,
- TokenURL: defaultGoogleOAuthToken,
- UserInfoURL: defaultGoogleOAuthUserInfo,
- Scopes: defaultGoogleOAuthScopes,
- FrontendRedirectURL: defaultGoogleOAuthFrontend,
- }
- if s != nil && s.cfg != nil {
- cfg = mergeEmailOAuthBaseConfig(cfg, s.cfg.GoogleOAuth)
- }
- return cfg
- default:
- return config.EmailOAuthProviderConfig{}
- }
- }
- func mergeEmailOAuthBaseConfig(base, override config.EmailOAuthProviderConfig) config.EmailOAuthProviderConfig {
- base.Enabled = override.Enabled
- if strings.TrimSpace(override.ClientID) != "" {
- base.ClientID = strings.TrimSpace(override.ClientID)
- }
- if strings.TrimSpace(override.ClientSecret) != "" {
- base.ClientSecret = strings.TrimSpace(override.ClientSecret)
- }
- if strings.TrimSpace(override.AuthorizeURL) != "" {
- base.AuthorizeURL = strings.TrimSpace(override.AuthorizeURL)
- }
- if strings.TrimSpace(override.TokenURL) != "" {
- base.TokenURL = strings.TrimSpace(override.TokenURL)
- }
- if strings.TrimSpace(override.UserInfoURL) != "" {
- base.UserInfoURL = strings.TrimSpace(override.UserInfoURL)
- }
- if strings.TrimSpace(override.EmailsURL) != "" {
- base.EmailsURL = strings.TrimSpace(override.EmailsURL)
- }
- if strings.TrimSpace(override.Scopes) != "" {
- base.Scopes = strings.TrimSpace(override.Scopes)
- }
- if strings.TrimSpace(override.RedirectURL) != "" {
- base.RedirectURL = strings.TrimSpace(override.RedirectURL)
- }
- if strings.TrimSpace(override.FrontendRedirectURL) != "" {
- base.FrontendRedirectURL = strings.TrimSpace(override.FrontendRedirectURL)
- }
- return base
- }
- func (s *SettingService) emailOAuthPublicEnabled(settings map[string]string, provider string) bool {
- cfg := s.effectiveEmailOAuthConfig(settings, provider)
- return cfg.Enabled && strings.TrimSpace(cfg.ClientID) != "" && strings.TrimSpace(cfg.ClientSecret) != ""
- }
- func (s *SettingService) effectiveEmailOAuthConfig(settings map[string]string, provider string) config.EmailOAuthProviderConfig {
- cfg := s.emailOAuthBaseConfig(provider)
- switch strings.ToLower(strings.TrimSpace(provider)) {
- case "github":
- if raw, ok := settings[SettingKeyGitHubOAuthEnabled]; ok {
- cfg.Enabled = raw == "true"
- }
- cfg.ClientID = firstNonEmpty(settings[SettingKeyGitHubOAuthClientID], cfg.ClientID)
- cfg.ClientSecret = firstNonEmpty(settings[SettingKeyGitHubOAuthClientSecret], cfg.ClientSecret)
- cfg.RedirectURL = firstNonEmpty(settings[SettingKeyGitHubOAuthRedirectURL], cfg.RedirectURL)
- cfg.FrontendRedirectURL = firstNonEmpty(settings[SettingKeyGitHubOAuthFrontendRedirectURL], cfg.FrontendRedirectURL, defaultGitHubOAuthFrontend)
- case "google":
- if raw, ok := settings[SettingKeyGoogleOAuthEnabled]; ok {
- cfg.Enabled = raw == "true"
- }
- cfg.ClientID = firstNonEmpty(settings[SettingKeyGoogleOAuthClientID], cfg.ClientID)
- cfg.ClientSecret = firstNonEmpty(settings[SettingKeyGoogleOAuthClientSecret], cfg.ClientSecret)
- cfg.RedirectURL = firstNonEmpty(settings[SettingKeyGoogleOAuthRedirectURL], cfg.RedirectURL)
- cfg.FrontendRedirectURL = firstNonEmpty(settings[SettingKeyGoogleOAuthFrontendRedirectURL], cfg.FrontendRedirectURL, defaultGoogleOAuthFrontend)
- }
- return cfg
- }
- func oidcUsePKCECompatibilityDefault(base config.OIDCConnectConfig) bool {
- if base.UsePKCEExplicit {
- return base.UsePKCE
- }
- return true
- }
- func oidcValidateIDTokenCompatibilityDefault(base config.OIDCConnectConfig) bool {
- if base.ValidateIDTokenExplicit {
- return base.ValidateIDToken
- }
- return true
- }
- func oidcCompatibilityWriteDefault(base config.OIDCConnectConfig, configured bool, raw string, explicit bool, explicitValue bool) bool {
- if configured {
- return strings.TrimSpace(raw) == "true"
- }
- if explicit {
- return explicitValue
- }
- return false
- }
- func (s *SettingService) OIDCSecurityWriteDefaults(ctx context.Context) (bool, bool, error) {
- rawSettings, err := s.settingRepo.GetMultiple(ctx, []string{
- SettingKeyOIDCConnectUsePKCE,
- SettingKeyOIDCConnectValidateIDToken,
- })
- if err != nil {
- return false, false, fmt.Errorf("get oidc security write defaults: %w", err)
- }
- base := config.OIDCConnectConfig{}
- if s != nil && s.cfg != nil {
- base = s.cfg.OIDC
- }
- rawUsePKCE, hasUsePKCE := rawSettings[SettingKeyOIDCConnectUsePKCE]
- rawValidateIDToken, hasValidateIDToken := rawSettings[SettingKeyOIDCConnectValidateIDToken]
- return oidcCompatibilityWriteDefault(base, hasUsePKCE, rawUsePKCE, base.UsePKCEExplicit, base.UsePKCE),
- oidcCompatibilityWriteDefault(base, hasValidateIDToken, rawValidateIDToken, base.ValidateIDTokenExplicit, base.ValidateIDToken),
- nil
- }
- func (s *SettingService) GetEmailOAuthProviderConfig(ctx context.Context, provider string) (config.EmailOAuthProviderConfig, error) {
- provider = strings.ToLower(strings.TrimSpace(provider))
- if provider != "github" && provider != "google" {
- return config.EmailOAuthProviderConfig{}, infraerrors.NotFound("OAUTH_PROVIDER_NOT_FOUND", "oauth provider not found")
- }
- keys := []string{
- SettingKeyGitHubOAuthEnabled,
- SettingKeyGitHubOAuthClientID,
- SettingKeyGitHubOAuthClientSecret,
- SettingKeyGitHubOAuthRedirectURL,
- SettingKeyGitHubOAuthFrontendRedirectURL,
- SettingKeyGoogleOAuthEnabled,
- SettingKeyGoogleOAuthClientID,
- SettingKeyGoogleOAuthClientSecret,
- SettingKeyGoogleOAuthRedirectURL,
- SettingKeyGoogleOAuthFrontendRedirectURL,
- }
- settings, err := s.settingRepo.GetMultiple(ctx, keys)
- if err != nil {
- return config.EmailOAuthProviderConfig{}, fmt.Errorf("get email oauth settings: %w", err)
- }
- cfg := s.effectiveEmailOAuthConfig(settings, provider)
- if !cfg.Enabled {
- return config.EmailOAuthProviderConfig{}, infraerrors.NotFound("OAUTH_DISABLED", "oauth login is disabled")
- }
- if strings.TrimSpace(cfg.ClientID) == "" {
- return config.EmailOAuthProviderConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth client id not configured")
- }
- if strings.TrimSpace(cfg.ClientSecret) == "" {
- return config.EmailOAuthProviderConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth client secret not configured")
- }
- for label, rawURL := range map[string]string{
- "authorize": cfg.AuthorizeURL,
- "token": cfg.TokenURL,
- "userinfo": cfg.UserInfoURL,
- "redirect": cfg.RedirectURL,
- } {
- if strings.TrimSpace(rawURL) == "" {
- return config.EmailOAuthProviderConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth "+label+" url not configured")
- }
- if err := config.ValidateAbsoluteHTTPURL(rawURL); err != nil {
- return config.EmailOAuthProviderConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth "+label+" url invalid")
- }
- }
- if strings.TrimSpace(cfg.EmailsURL) != "" {
- if err := config.ValidateAbsoluteHTTPURL(cfg.EmailsURL); err != nil {
- return config.EmailOAuthProviderConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth emails url invalid")
- }
- }
- if err := config.ValidateFrontendRedirectURL(cfg.FrontendRedirectURL); err != nil {
- return config.EmailOAuthProviderConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth frontend redirect url invalid")
- }
- return cfg, nil
- }
- // GetLinuxDoConnectOAuthConfig 返回用于登录的"最终生效" LinuxDo Connect 配置。
- //
- // 优先级:
- // - 若对应系统设置键存在,则覆盖 config.yaml/env 的值
- // - 否则回退到 config.yaml/env 的值
- func (s *SettingService) GetLinuxDoConnectOAuthConfig(ctx context.Context) (config.LinuxDoConnectConfig, error) {
- if s == nil || s.cfg == nil {
- return config.LinuxDoConnectConfig{}, infraerrors.ServiceUnavailable("CONFIG_NOT_READY", "config not loaded")
- }
- effective := s.cfg.LinuxDo
- keys := []string{
- SettingKeyLinuxDoConnectEnabled,
- SettingKeyLinuxDoConnectClientID,
- SettingKeyLinuxDoConnectClientSecret,
- SettingKeyLinuxDoConnectRedirectURL,
- }
- settings, err := s.settingRepo.GetMultiple(ctx, keys)
- if err != nil {
- return config.LinuxDoConnectConfig{}, fmt.Errorf("get linuxdo connect settings: %w", err)
- }
- if raw, ok := settings[SettingKeyLinuxDoConnectEnabled]; ok {
- effective.Enabled = raw == "true"
- }
- if v, ok := settings[SettingKeyLinuxDoConnectClientID]; ok && strings.TrimSpace(v) != "" {
- effective.ClientID = strings.TrimSpace(v)
- }
- if v, ok := settings[SettingKeyLinuxDoConnectClientSecret]; ok && strings.TrimSpace(v) != "" {
- effective.ClientSecret = strings.TrimSpace(v)
- }
- if v, ok := settings[SettingKeyLinuxDoConnectRedirectURL]; ok && strings.TrimSpace(v) != "" {
- effective.RedirectURL = strings.TrimSpace(v)
- }
- if !effective.Enabled {
- return config.LinuxDoConnectConfig{}, infraerrors.NotFound("OAUTH_DISABLED", "oauth login is disabled")
- }
- // 基础健壮性校验(避免把用户重定向到一个必然失败或不安全的 OAuth 流程里)。
- if strings.TrimSpace(effective.ClientID) == "" {
- return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth client id not configured")
- }
- if strings.TrimSpace(effective.AuthorizeURL) == "" {
- return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth authorize url not configured")
- }
- if strings.TrimSpace(effective.TokenURL) == "" {
- return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth token url not configured")
- }
- if strings.TrimSpace(effective.UserInfoURL) == "" {
- return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth userinfo url not configured")
- }
- if strings.TrimSpace(effective.RedirectURL) == "" {
- return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth redirect url not configured")
- }
- if strings.TrimSpace(effective.FrontendRedirectURL) == "" {
- return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth frontend redirect url not configured")
- }
- if err := config.ValidateAbsoluteHTTPURL(effective.AuthorizeURL); err != nil {
- return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth authorize url invalid")
- }
- if err := config.ValidateAbsoluteHTTPURL(effective.TokenURL); err != nil {
- return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth token url invalid")
- }
- if err := config.ValidateAbsoluteHTTPURL(effective.UserInfoURL); err != nil {
- return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth userinfo url invalid")
- }
- if err := config.ValidateAbsoluteHTTPURL(effective.RedirectURL); err != nil {
- return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth redirect url invalid")
- }
- if err := config.ValidateFrontendRedirectURL(effective.FrontendRedirectURL); err != nil {
- return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth frontend redirect url invalid")
- }
- method := strings.ToLower(strings.TrimSpace(effective.TokenAuthMethod))
- switch method {
- case "", "client_secret_post", "client_secret_basic":
- if strings.TrimSpace(effective.ClientSecret) == "" {
- return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth client secret not configured")
- }
- case "none":
- default:
- return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth token_auth_method invalid")
- }
- return effective, nil
- }
- // GetDingTalkConnectOAuthConfig 返回用于登录的"最终生效" DingTalk Connect 配置。
- //
- // 优先级:
- // - 若对应系统设置键存在,则覆盖 config.yaml/env 的值
- // - 否则回退到 config.yaml/env 的值
- func (s *SettingService) GetDingTalkConnectOAuthConfig(ctx context.Context) (config.DingTalkConnectConfig, error) {
- if s == nil || s.cfg == nil {
- return config.DingTalkConnectConfig{}, infraerrors.ServiceUnavailable("CONFIG_NOT_READY", "config not loaded")
- }
- effective := s.cfg.DingTalk
- keys := []string{
- SettingKeyDingTalkConnectEnabled,
- SettingKeyDingTalkConnectClientID,
- SettingKeyDingTalkConnectClientSecret,
- SettingKeyDingTalkConnectRedirectURL,
- SettingKeyDingTalkConnectCorpRestrictionPolicy,
- SettingKeyDingTalkConnectInternalCorpID,
- SettingKeyDingTalkConnectBypassRegistration,
- SettingKeyDingTalkConnectSyncCorpEmail,
- SettingKeyDingTalkConnectSyncDisplayName,
- SettingKeyDingTalkConnectSyncDept,
- SettingKeyDingTalkConnectSyncCorpEmailAttrKey,
- SettingKeyDingTalkConnectSyncDisplayNameAttrKey,
- SettingKeyDingTalkConnectSyncDeptAttrKey,
- }
- settings, err := s.settingRepo.GetMultiple(ctx, keys)
- if err != nil {
- return config.DingTalkConnectConfig{}, fmt.Errorf("get dingtalk connect settings: %w", err)
- }
- if raw, ok := settings[SettingKeyDingTalkConnectEnabled]; ok {
- effective.Enabled = raw == "true"
- }
- if v, ok := settings[SettingKeyDingTalkConnectClientID]; ok && strings.TrimSpace(v) != "" {
- effective.ClientID = strings.TrimSpace(v)
- }
- if v, ok := settings[SettingKeyDingTalkConnectClientSecret]; ok && strings.TrimSpace(v) != "" {
- effective.ClientSecret = strings.TrimSpace(v)
- }
- if v, ok := settings[SettingKeyDingTalkConnectRedirectURL]; ok && strings.TrimSpace(v) != "" {
- effective.RedirectURL = strings.TrimSpace(v)
- }
- if v, ok := settings[SettingKeyDingTalkConnectCorpRestrictionPolicy]; ok && strings.TrimSpace(v) != "" {
- effective.CorpRestrictionPolicy = strings.TrimSpace(v)
- }
- effective.CorpRestrictionPolicy = coerceDeprecatedDingTalkCorpPolicy(effective.CorpRestrictionPolicy)
- if v, ok := settings[SettingKeyDingTalkConnectInternalCorpID]; ok && strings.TrimSpace(v) != "" {
- effective.InternalCorpID = strings.TrimSpace(v)
- }
- if v, ok := settings[SettingKeyDingTalkConnectBypassRegistration]; ok && strings.TrimSpace(v) != "" {
- effective.BypassRegistration = strings.EqualFold(strings.TrimSpace(v), "true")
- }
- // bypass_registration 仅在 internal_only 模式下有意义;其它策略下强制 false,
- // 以保证 OAuth callback 看到的 effective config 永远是一致状态。
- if effective.CorpRestrictionPolicy != "internal_only" {
- effective.BypassRegistration = false
- }
- if v, ok := settings[SettingKeyDingTalkConnectSyncCorpEmail]; ok && strings.TrimSpace(v) != "" {
- effective.SyncCorpEmail = strings.EqualFold(strings.TrimSpace(v), "true")
- }
- if v, ok := settings[SettingKeyDingTalkConnectSyncDisplayName]; ok && strings.TrimSpace(v) != "" {
- effective.SyncDisplayName = strings.EqualFold(strings.TrimSpace(v), "true")
- }
- if v, ok := settings[SettingKeyDingTalkConnectSyncDept]; ok && strings.TrimSpace(v) != "" {
- effective.SyncDept = strings.EqualFold(strings.TrimSpace(v), "true")
- }
- // 身份同步三开关仅在 internal_only 模式下有意义;其它策略强制 false。
- if effective.CorpRestrictionPolicy != "internal_only" {
- effective.SyncCorpEmail = false
- effective.SyncDisplayName = false
- effective.SyncDept = false
- }
- // 身份同步目标 attr key(DB 空 → fallback 默认值)
- if v := strings.TrimSpace(settings[SettingKeyDingTalkConnectSyncCorpEmailAttrKey]); v != "" {
- effective.SyncCorpEmailAttrKey = v
- }
- if effective.SyncCorpEmailAttrKey == "" {
- effective.SyncCorpEmailAttrKey = "dingtalk_email"
- }
- if v := strings.TrimSpace(settings[SettingKeyDingTalkConnectSyncDisplayNameAttrKey]); v != "" {
- effective.SyncDisplayNameAttrKey = v
- }
- if effective.SyncDisplayNameAttrKey == "" {
- effective.SyncDisplayNameAttrKey = "dingtalk_name"
- }
- if v := strings.TrimSpace(settings[SettingKeyDingTalkConnectSyncDeptAttrKey]); v != "" {
- effective.SyncDeptAttrKey = v
- }
- if effective.SyncDeptAttrKey == "" {
- effective.SyncDeptAttrKey = "dingtalk_department"
- }
- if !effective.Enabled {
- return config.DingTalkConnectConfig{}, infraerrors.NotFound("OAUTH_DISABLED", "dingtalk oauth login is disabled")
- }
- // 基础健壮性校验(避免把用户重定向到一个必然失败或不安全的 OAuth 流程里)。
- if strings.TrimSpace(effective.ClientID) == "" {
- return config.DingTalkConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "dingtalk oauth client id not configured")
- }
- if strings.TrimSpace(effective.AuthorizeURL) == "" {
- return config.DingTalkConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "dingtalk oauth authorize url not configured")
- }
- if strings.TrimSpace(effective.TokenURL) == "" {
- return config.DingTalkConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "dingtalk oauth token url not configured")
- }
- if strings.TrimSpace(effective.UserInfoURL) == "" {
- return config.DingTalkConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "dingtalk oauth userinfo url not configured")
- }
- if strings.TrimSpace(effective.RedirectURL) == "" {
- return config.DingTalkConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "dingtalk oauth redirect url not configured")
- }
- if strings.TrimSpace(effective.FrontendRedirectURL) == "" {
- return config.DingTalkConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "dingtalk oauth frontend redirect url not configured")
- }
- if err := config.ValidateAbsoluteHTTPURL(effective.AuthorizeURL); err != nil {
- return config.DingTalkConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "dingtalk oauth authorize url invalid")
- }
- if err := config.ValidateAbsoluteHTTPURL(effective.TokenURL); err != nil {
- return config.DingTalkConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "dingtalk oauth token url invalid")
- }
- if err := config.ValidateAbsoluteHTTPURL(effective.UserInfoURL); err != nil {
- return config.DingTalkConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "dingtalk oauth userinfo url invalid")
- }
- if err := config.ValidateAbsoluteHTTPURL(effective.RedirectURL); err != nil {
- return config.DingTalkConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "dingtalk oauth redirect url invalid")
- }
- if err := config.ValidateFrontendRedirectURL(effective.FrontendRedirectURL); err != nil {
- return config.DingTalkConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "dingtalk oauth frontend redirect url invalid")
- }
- if strings.TrimSpace(effective.ClientSecret) == "" {
- return config.DingTalkConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "dingtalk oauth client secret not configured")
- }
- // 镜像 admin handler 行为:internal_only policy 隐式要求 AppType=internal
- if effective.CorpRestrictionPolicy == "internal_only" {
- effective.AppType = "internal"
- }
- if err := config.ValidateDingTalkConfig(effective); err != nil {
- return config.DingTalkConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", err.Error())
- }
- return effective, nil
- }
- // GetWeChatConnectOAuthConfig 返回用于登录的最终生效 WeChat Connect 配置。
- //
- // WeChat Connect 已回归 DB 系统设置模型,不再回退到 config/env。
- func (s *SettingService) GetWeChatConnectOAuthConfig(ctx context.Context) (WeChatConnectOAuthConfig, error) {
- keys := []string{
- SettingKeyWeChatConnectEnabled,
- SettingKeyWeChatConnectAppID,
- SettingKeyWeChatConnectAppSecret,
- SettingKeyWeChatConnectOpenAppID,
- SettingKeyWeChatConnectOpenAppSecret,
- SettingKeyWeChatConnectMPAppID,
- SettingKeyWeChatConnectMPAppSecret,
- SettingKeyWeChatConnectMobileAppID,
- SettingKeyWeChatConnectMobileAppSecret,
- SettingKeyWeChatConnectOpenEnabled,
- SettingKeyWeChatConnectMPEnabled,
- SettingKeyWeChatConnectMobileEnabled,
- SettingKeyWeChatConnectMode,
- SettingKeyWeChatConnectScopes,
- SettingKeyWeChatConnectRedirectURL,
- SettingKeyWeChatConnectFrontendRedirectURL,
- }
- settings, err := s.settingRepo.GetMultiple(ctx, keys)
- if err != nil {
- return WeChatConnectOAuthConfig{}, fmt.Errorf("get wechat connect settings: %w", err)
- }
- return s.parseWeChatConnectOAuthConfig(settings)
- }
- // GetOIDCConnectOAuthConfig 返回用于登录的“最终生效” OIDC 配置。
- //
- // 优先级:
- // - 若对应系统设置键存在,则覆盖 config.yaml/env 的值
- // - 否则回退到 config.yaml/env 的值
- func (s *SettingService) GetOIDCConnectOAuthConfig(ctx context.Context) (config.OIDCConnectConfig, error) {
- if s == nil || s.cfg == nil {
- return config.OIDCConnectConfig{}, infraerrors.ServiceUnavailable("CONFIG_NOT_READY", "config not loaded")
- }
- effective := s.cfg.OIDC
- keys := []string{
- SettingKeyOIDCConnectEnabled,
- SettingKeyOIDCConnectProviderName,
- SettingKeyOIDCConnectClientID,
- SettingKeyOIDCConnectClientSecret,
- SettingKeyOIDCConnectIssuerURL,
- SettingKeyOIDCConnectDiscoveryURL,
- SettingKeyOIDCConnectAuthorizeURL,
- SettingKeyOIDCConnectTokenURL,
- SettingKeyOIDCConnectUserInfoURL,
- SettingKeyOIDCConnectJWKSURL,
- SettingKeyOIDCConnectScopes,
- SettingKeyOIDCConnectRedirectURL,
- SettingKeyOIDCConnectFrontendRedirectURL,
- SettingKeyOIDCConnectTokenAuthMethod,
- SettingKeyOIDCConnectUsePKCE,
- SettingKeyOIDCConnectValidateIDToken,
- SettingKeyOIDCConnectAllowedSigningAlgs,
- SettingKeyOIDCConnectClockSkewSeconds,
- SettingKeyOIDCConnectRequireEmailVerified,
- SettingKeyOIDCConnectUserInfoEmailPath,
- SettingKeyOIDCConnectUserInfoIDPath,
- SettingKeyOIDCConnectUserInfoUsernamePath,
- }
- settings, err := s.settingRepo.GetMultiple(ctx, keys)
- if err != nil {
- return config.OIDCConnectConfig{}, fmt.Errorf("get oidc connect settings: %w", err)
- }
- if raw, ok := settings[SettingKeyOIDCConnectEnabled]; ok {
- effective.Enabled = raw == "true"
- }
- if v, ok := settings[SettingKeyOIDCConnectProviderName]; ok && strings.TrimSpace(v) != "" {
- effective.ProviderName = strings.TrimSpace(v)
- }
- if v, ok := settings[SettingKeyOIDCConnectClientID]; ok && strings.TrimSpace(v) != "" {
- effective.ClientID = strings.TrimSpace(v)
- }
- if v, ok := settings[SettingKeyOIDCConnectClientSecret]; ok && strings.TrimSpace(v) != "" {
- effective.ClientSecret = strings.TrimSpace(v)
- }
- if v, ok := settings[SettingKeyOIDCConnectIssuerURL]; ok && strings.TrimSpace(v) != "" {
- effective.IssuerURL = strings.TrimSpace(v)
- }
- if v, ok := settings[SettingKeyOIDCConnectDiscoveryURL]; ok && strings.TrimSpace(v) != "" {
- effective.DiscoveryURL = strings.TrimSpace(v)
- }
- if v, ok := settings[SettingKeyOIDCConnectAuthorizeURL]; ok && strings.TrimSpace(v) != "" {
- effective.AuthorizeURL = strings.TrimSpace(v)
- }
- if v, ok := settings[SettingKeyOIDCConnectTokenURL]; ok && strings.TrimSpace(v) != "" {
- effective.TokenURL = strings.TrimSpace(v)
- }
- if v, ok := settings[SettingKeyOIDCConnectUserInfoURL]; ok && strings.TrimSpace(v) != "" {
- effective.UserInfoURL = strings.TrimSpace(v)
- }
- if v, ok := settings[SettingKeyOIDCConnectJWKSURL]; ok && strings.TrimSpace(v) != "" {
- effective.JWKSURL = strings.TrimSpace(v)
- }
- if v, ok := settings[SettingKeyOIDCConnectScopes]; ok && strings.TrimSpace(v) != "" {
- effective.Scopes = strings.TrimSpace(v)
- }
- if v, ok := settings[SettingKeyOIDCConnectRedirectURL]; ok && strings.TrimSpace(v) != "" {
- effective.RedirectURL = strings.TrimSpace(v)
- }
- if v, ok := settings[SettingKeyOIDCConnectFrontendRedirectURL]; ok && strings.TrimSpace(v) != "" {
- effective.FrontendRedirectURL = strings.TrimSpace(v)
- }
- if v, ok := settings[SettingKeyOIDCConnectTokenAuthMethod]; ok && strings.TrimSpace(v) != "" {
- effective.TokenAuthMethod = strings.ToLower(strings.TrimSpace(v))
- }
- if raw, ok := settings[SettingKeyOIDCConnectUsePKCE]; ok {
- effective.UsePKCE = raw == "true"
- } else {
- effective.UsePKCE = oidcUsePKCECompatibilityDefault(effective)
- }
- if raw, ok := settings[SettingKeyOIDCConnectValidateIDToken]; ok {
- effective.ValidateIDToken = raw == "true"
- } else {
- effective.ValidateIDToken = oidcValidateIDTokenCompatibilityDefault(effective)
- }
- if v, ok := settings[SettingKeyOIDCConnectAllowedSigningAlgs]; ok && strings.TrimSpace(v) != "" {
- effective.AllowedSigningAlgs = strings.TrimSpace(v)
- }
- if raw, ok := settings[SettingKeyOIDCConnectClockSkewSeconds]; ok && strings.TrimSpace(raw) != "" {
- if parsed, parseErr := strconv.Atoi(strings.TrimSpace(raw)); parseErr == nil {
- effective.ClockSkewSeconds = parsed
- }
- }
- if raw, ok := settings[SettingKeyOIDCConnectRequireEmailVerified]; ok {
- effective.RequireEmailVerified = raw == "true"
- }
- if v, ok := settings[SettingKeyOIDCConnectUserInfoEmailPath]; ok {
- effective.UserInfoEmailPath = strings.TrimSpace(v)
- }
- if v, ok := settings[SettingKeyOIDCConnectUserInfoIDPath]; ok {
- effective.UserInfoIDPath = strings.TrimSpace(v)
- }
- if v, ok := settings[SettingKeyOIDCConnectUserInfoUsernamePath]; ok {
- effective.UserInfoUsernamePath = strings.TrimSpace(v)
- }
- if !effective.Enabled {
- return config.OIDCConnectConfig{}, infraerrors.NotFound("OAUTH_DISABLED", "oauth login is disabled")
- }
- if strings.TrimSpace(effective.ProviderName) == "" {
- effective.ProviderName = "OIDC"
- }
- if strings.TrimSpace(effective.ClientID) == "" {
- return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth client id not configured")
- }
- if strings.TrimSpace(effective.IssuerURL) == "" {
- return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth issuer url not configured")
- }
- if strings.TrimSpace(effective.RedirectURL) == "" {
- return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth redirect url not configured")
- }
- if strings.TrimSpace(effective.FrontendRedirectURL) == "" {
- return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth frontend redirect url not configured")
- }
- if !scopesContainOpenID(effective.Scopes) {
- return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth scopes must contain openid")
- }
- if effective.ClockSkewSeconds < 0 || effective.ClockSkewSeconds > 600 {
- return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth clock skew must be between 0 and 600")
- }
- if err := config.ValidateAbsoluteHTTPURL(effective.IssuerURL); err != nil {
- return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth issuer url invalid")
- }
- discoveryURL := strings.TrimSpace(effective.DiscoveryURL)
- if discoveryURL == "" {
- discoveryURL = oidcDefaultDiscoveryURL(effective.IssuerURL)
- effective.DiscoveryURL = discoveryURL
- }
- if discoveryURL != "" {
- if err := config.ValidateAbsoluteHTTPURL(discoveryURL); err != nil {
- return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth discovery url invalid")
- }
- }
- needsDiscovery := strings.TrimSpace(effective.AuthorizeURL) == "" ||
- strings.TrimSpace(effective.TokenURL) == "" ||
- (effective.ValidateIDToken && strings.TrimSpace(effective.JWKSURL) == "")
- if needsDiscovery && discoveryURL != "" {
- metadata, resolveErr := oidcResolveProviderMetadata(ctx, discoveryURL)
- if resolveErr != nil {
- return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth discovery resolve failed").WithCause(resolveErr)
- }
- if strings.TrimSpace(effective.AuthorizeURL) == "" {
- effective.AuthorizeURL = strings.TrimSpace(metadata.AuthorizationEndpoint)
- }
- if strings.TrimSpace(effective.TokenURL) == "" {
- effective.TokenURL = strings.TrimSpace(metadata.TokenEndpoint)
- }
- if strings.TrimSpace(effective.UserInfoURL) == "" {
- effective.UserInfoURL = strings.TrimSpace(metadata.UserInfoEndpoint)
- }
- if strings.TrimSpace(effective.JWKSURL) == "" {
- effective.JWKSURL = strings.TrimSpace(metadata.JWKSURI)
- }
- }
- if strings.TrimSpace(effective.AuthorizeURL) == "" {
- return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth authorize url not configured")
- }
- if strings.TrimSpace(effective.TokenURL) == "" {
- return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth token url not configured")
- }
- if err := config.ValidateAbsoluteHTTPURL(effective.AuthorizeURL); err != nil {
- return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth authorize url invalid")
- }
- if err := config.ValidateAbsoluteHTTPURL(effective.TokenURL); err != nil {
- return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth token url invalid")
- }
- if v := strings.TrimSpace(effective.UserInfoURL); v != "" {
- if err := config.ValidateAbsoluteHTTPURL(v); err != nil {
- return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth userinfo url invalid")
- }
- }
- if effective.ValidateIDToken {
- if strings.TrimSpace(effective.JWKSURL) == "" {
- return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth jwks url not configured")
- }
- if strings.TrimSpace(effective.AllowedSigningAlgs) == "" {
- return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth signing algs not configured")
- }
- }
- if v := strings.TrimSpace(effective.JWKSURL); v != "" {
- if err := config.ValidateAbsoluteHTTPURL(v); err != nil {
- return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth jwks url invalid")
- }
- }
- if err := config.ValidateAbsoluteHTTPURL(effective.RedirectURL); err != nil {
- return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth redirect url invalid")
- }
- if err := config.ValidateFrontendRedirectURL(effective.FrontendRedirectURL); err != nil {
- return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth frontend redirect url invalid")
- }
- method := strings.ToLower(strings.TrimSpace(effective.TokenAuthMethod))
- switch method {
- case "", "client_secret_post", "client_secret_basic":
- if strings.TrimSpace(effective.ClientSecret) == "" {
- return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth client secret not configured")
- }
- case "none":
- default:
- return config.OIDCConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth token_auth_method invalid")
- }
- return effective, nil
- }
- func scopesContainOpenID(scopes string) bool {
- for _, scope := range strings.Fields(strings.ToLower(strings.TrimSpace(scopes))) {
- if scope == "openid" {
- return true
- }
- }
- return false
- }
- type oidcProviderMetadata struct {
- AuthorizationEndpoint string `json:"authorization_endpoint"`
- TokenEndpoint string `json:"token_endpoint"`
- UserInfoEndpoint string `json:"userinfo_endpoint"`
- JWKSURI string `json:"jwks_uri"`
- }
- func oidcDefaultDiscoveryURL(issuerURL string) string {
- issuerURL = strings.TrimSpace(issuerURL)
- if issuerURL == "" {
- return ""
- }
- return strings.TrimRight(issuerURL, "/") + "/.well-known/openid-configuration"
- }
- func oidcResolveProviderMetadata(ctx context.Context, discoveryURL string) (*oidcProviderMetadata, error) {
- discoveryURL = strings.TrimSpace(discoveryURL)
- if discoveryURL == "" {
- return nil, fmt.Errorf("discovery url is empty")
- }
- resp, err := req.C().
- SetTimeout(15*time.Second).
- R().
- SetContext(ctx).
- SetHeader("Accept", "application/json").
- Get(discoveryURL)
- if err != nil {
- return nil, fmt.Errorf("request discovery document: %w", err)
- }
- if !resp.IsSuccessState() {
- return nil, fmt.Errorf("discovery request failed: status=%d", resp.StatusCode)
- }
- metadata := &oidcProviderMetadata{}
- if err := json.Unmarshal(resp.Bytes(), metadata); err != nil {
- return nil, fmt.Errorf("parse discovery document: %w", err)
- }
- return metadata, nil
- }
|