diff --git a/config/oauth.go b/config/oauth.go index 81108aa..ff9272f 100644 --- a/config/oauth.go +++ b/config/oauth.go @@ -11,3 +11,10 @@ type GoogleOauth struct { ClientSecret string `mapstructure:"client-secret"` RedirectUrl string `mapstructure:"redirect-url"` } + +type OidcOauth struct { + Issuer string `mapstructure:"issuer"` + ClientId string `mapstructure:"client-id"` + ClientSecret string `mapstructure:"client-secret"` + RedirectUrl string `mapstructure:"redirect-url"` +} \ No newline at end of file diff --git a/http/controller/admin/oauth.go b/http/controller/admin/oauth.go index a3571f2..fec011d 100644 --- a/http/controller/admin/oauth.go +++ b/http/controller/admin/oauth.go @@ -140,6 +140,13 @@ func (o *Oauth) Unbind(c *gin.Context) { return } } + if f.Op == model.OauthTypeOidc { + err = service.AllService.OauthService.UnBindOidcUser(u.Id) + if err != nil { + response.Fail(c, 101, response.TranslateMsg(c, "OperationFailed")+err.Error()) + return + } + } response.Success(c, nil) } diff --git a/http/controller/api/login.go b/http/controller/api/login.go index 57eeb48..bae13e1 100644 --- a/http/controller/api/login.go +++ b/http/controller/api/login.go @@ -92,6 +92,10 @@ func (l *Login) LoginOptions(c *gin.Context) { if err == nil { oauthOks = append(oauthOks, model.OauthTypeGoogle) } + err, _ = service.AllService.OauthService.GetOauthConfig(model.OauthTypeOidc) + if err == nil { + oauthOks = append(oauthOks, model.OauthTypeOidc) + } oauthOks = append(oauthOks, model.OauthTypeWebauth) var oidcItems []map[string]string for _, v := range oauthOks { diff --git a/http/controller/api/ouath.go b/http/controller/api/ouath.go index fc37322..7c95e36 100644 --- a/http/controller/api/ouath.go +++ b/http/controller/api/ouath.go @@ -32,7 +32,7 @@ func (o *Oauth) OidcAuth(c *gin.Context) { response.Error(c, response.TranslateMsg(c, "ParamsError")+err.Error()) return } - if f.Op != model.OauthTypeWebauth && f.Op != model.OauthTypeGoogle && f.Op != model.OauthTypeGithub { + if f.Op != model.OauthTypeWebauth && f.Op != model.OauthTypeGoogle && f.Op != model.OauthTypeGithub && f.Op != model.OauthTypeOidc { response.Error(c, response.TranslateMsg(c, "ParamsError")) return } @@ -254,6 +254,69 @@ func (o *Oauth) OauthCallback(c *gin.Context) { return } } + if ty == model.OauthTypeOidc { + code := c.Query("code") + err, userData := service.AllService.OauthService.OidcCallback(code) + if err != nil { + c.String(http.StatusInternalServerError, response.TranslateMsg(c, "OauthFailed")+response.TranslateMsg(c, err.Error())) + return + } + //将空格替换成_ + // OidcName := strings.Replace(userData.Name, " ", "_", -1) + if ac == service.OauthActionTypeBind { + //fmt.Println("bind", ty, userData) + utr := service.AllService.OauthService.UserThirdInfo(ty, userData.Sub) + if utr.UserId > 0 { + c.String(http.StatusInternalServerError, response.TranslateMsg(c, "OauthHasBindOtherUser")) + return + } + //绑定 + u := service.AllService.UserService.InfoById(v.UserId) + if u == nil { + c.String(http.StatusInternalServerError, response.TranslateMsg(c, "ItemNotFound")) + return + } + //绑定, user preffered_username as username + err = service.AllService.OauthService.BindOidcUser(userData.Sub, userData.PrefferedUsername, v.UserId) + if err != nil { + c.String(http.StatusInternalServerError, response.TranslateMsg(c, "BindFail")) + return + } + c.String(http.StatusOK, response.TranslateMsg(c, "BindSuccess")) + return + } else if ac == service.OauthActionTypeLogin { + if v.UserId != 0 { + c.String(http.StatusInternalServerError, response.TranslateMsg(c, "OauthHasBeenSuccess")) + return + } + u := service.AllService.UserService.InfoByOidcSub(userData.Sub) + if u == nil { + oa := service.AllService.OauthService.InfoByOp(ty) + if !*oa.AutoRegister { + //c.String(http.StatusInternalServerError, "还未绑定用户,请先绑定") + + v.ThirdName = userData.PrefferedUsername + v.ThirdOpenId = userData.Sub + url := global.Config.Rustdesk.ApiServer + "/_admin/#/oauth/bind/" + cacheKey + c.Redirect(http.StatusFound, url) + return + } + + //自动注册 + u = service.AllService.UserService.RegisterByOidc(userData.PrefferedUsername, userData.Sub) + if u.Id == 0 { + c.String(http.StatusInternalServerError, response.TranslateMsg(c, "OauthRegisterFailed")) + return + } + } + + v.UserId = u.Id + service.AllService.OauthService.SetOauthCache(cacheKey, v, 0) + c.String(http.StatusOK, response.TranslateMsg(c, "OauthSuccess")) + return + } + } + c.String(http.StatusInternalServerError, response.TranslateMsg(c, "SystemError")) } diff --git a/http/request/admin/oauth.go b/http/request/admin/oauth.go index 11698ee..db519f8 100644 --- a/http/request/admin/oauth.go +++ b/http/request/admin/oauth.go @@ -15,6 +15,8 @@ type UnBindOauthForm struct { type OauthForm struct { Id uint `json:"id"` Op string `json:"op" validate:"required"` + Issuer string `json:"issuer" validate:"omitempty,url"` + Scopes string `json:"scopes" validate:"omitempty"` ClientId string `json:"client_id" validate:"required"` ClientSecret string `json:"client_secret" validate:"required"` RedirectUrl string `json:"redirect_url" validate:"required"` @@ -28,6 +30,8 @@ func (of *OauthForm) ToOauth() *model.Oauth { ClientSecret: of.ClientSecret, RedirectUrl: of.RedirectUrl, AutoRegister: of.AutoRegister, + Issuer: of.Issuer, + Scopes: of.Scopes, } oa.Id = of.Id return oa diff --git a/model/oauth.go b/model/oauth.go index 64de80c..35e7b96 100644 --- a/model/oauth.go +++ b/model/oauth.go @@ -7,12 +7,15 @@ type Oauth struct { ClientSecret string `json:"client_secret"` RedirectUrl string `json:"redirect_url"` AutoRegister *bool `json:"auto_register"` + Scopes string `json:"scopes"` + Issuer string `json:"issuer"` TimeModel } const ( OauthTypeGithub = "github" OauthTypeGoogle = "google" + OauthTypeOidc = "oidc" OauthTypeWebauth = "webauth" ) diff --git a/service/oauth.go b/service/oauth.go index 0ee6add..eb9609c 100644 --- a/service/oauth.go +++ b/service/oauth.go @@ -17,9 +17,19 @@ import ( "strconv" "sync" "time" + "strings" ) +// Define a struct to parse the .well-known/openid-configuration response +type OidcEndpoint struct { + Issuer string `json:"issuer"` + AuthURL string `json:"authorization_endpoint"` + TokenURL string `json:"token_endpoint"` + UserInfo string `json:"userinfo_endpoint"` +} + type OauthService struct { + OidcEndpoint *OidcEndpoint } type GithubUserdata struct { @@ -78,6 +88,15 @@ type GoogleUserdata struct { Picture string `json:"picture"` VerifiedEmail bool `json:"verified_email"` } +type OidcUserdata struct { + Sub string `json:"sub"` + Email string `json:"email"` + VerifiedEmail bool `json:"email_verified"` + Name string `json:"name"` + Picture string `json:"picture"` + PrefferedUsername string `json:"preffered_username"` +} + type OauthCacheItem struct { UserId uint `json:"user_id"` Id string `json:"id"` //rustdesk的设备ID @@ -137,35 +156,105 @@ func (os *OauthService) BeginAuth(op string) (error error, code, url string) { return err, code, "" } -// GetOauthConfig 获取配置 +// Method to fetch OIDC configuration dynamically +func (os *OauthService) FetchOIDCConfig(issuer string) error { + configURL := strings.TrimSuffix(issuer, "/") + "/.well-known/openid-configuration" + + // Get the HTTP client (with or without proxy based on configuration) + client := getHTTPClientWithProxy() + + resp, err := client.Get(configURL) + if err != nil { + return errors.New("failed to fetch OIDC configuration") + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return errors.New("OIDC configuration not found") + } + + var endpoint OidcEndpoint + if err := json.NewDecoder(resp.Body).Decode(&endpoint); err != nil { + return errors.New("failed to parse OIDC configuration") + } + + os.OidcEndpoint = &endpoint + return nil +} + +// GetOauthConfig retrieves the OAuth2 configuration based on the provider type func (os *OauthService) GetOauthConfig(op string) (error, *oauth2.Config) { - if op == model.OauthTypeGithub { - g := os.InfoByOp(model.OauthTypeGithub) - if g.Id == 0 || g.ClientId == "" || g.ClientSecret == "" || g.RedirectUrl == "" { - return errors.New("ConfigNotFound"), nil - } - return nil, &oauth2.Config{ - ClientID: g.ClientId, - ClientSecret: g.ClientSecret, - RedirectURL: g.RedirectUrl, - Endpoint: github.Endpoint, - Scopes: []string{"read:user", "user:email"}, - } + switch op { + case model.OauthTypeGithub: + return os.getGithubConfig() + case model.OauthTypeGoogle: + return os.getGoogleConfig() + case model.OauthTypeOidc: + return os.getOidcConfig() + default: + return errors.New("unsupported OAuth type"), nil } - if op == model.OauthTypeGoogle { - g := os.InfoByOp(model.OauthTypeGoogle) - if g.Id == 0 || g.ClientId == "" || g.ClientSecret == "" || g.RedirectUrl == "" { - return errors.New("ConfigNotFound"), nil - } - return nil, &oauth2.Config{ - ClientID: g.ClientId, - ClientSecret: g.ClientSecret, - RedirectURL: g.RedirectUrl, - Endpoint: google.Endpoint, - Scopes: []string{"https://www.googleapis.com/auth/userinfo.profile", "https://www.googleapis.com/auth/userinfo.email"}, - } +} + +// Helper function to get GitHub OAuth2 configuration +func (os *OauthService) getGithubConfig() (error, *oauth2.Config) { + g := os.InfoByOp(model.OauthTypeGithub) + if g.Id == 0 || g.ClientId == "" || g.ClientSecret == "" || g.RedirectUrl == "" { + return errors.New("ConfigNotFound"), nil + } + return nil, &oauth2.Config{ + ClientID: g.ClientId, + ClientSecret: g.ClientSecret, + RedirectURL: g.RedirectUrl, + Endpoint: github.Endpoint, + Scopes: []string{"read:user", "user:email"}, + } +} + +// Helper function to get Google OAuth2 configuration +func (os *OauthService) getGoogleConfig() (error, *oauth2.Config) { + g := os.InfoByOp(model.OauthTypeGoogle) + if g.Id == 0 || g.ClientId == "" || g.ClientSecret == "" || g.RedirectUrl == "" { + return errors.New("ConfigNotFound"), nil + } + return nil, &oauth2.Config{ + ClientID: g.ClientId, + ClientSecret: g.ClientSecret, + RedirectURL: g.RedirectUrl, + Endpoint: google.Endpoint, + Scopes: []string{"https://www.googleapis.com/auth/userinfo.profile", "https://www.googleapis.com/auth/userinfo.email"}, + } +} + +// Helper function to get OIDC OAuth2 configuration +func (os *OauthService) getOidcConfig() (error, *oauth2.Config) { + g := os.InfoByOp(model.OauthTypeOidc) + if g.Id == 0 || g.ClientId == "" || g.ClientSecret == "" || g.RedirectUrl == "" || g.Issuer == "" { + return errors.New("ConfigNotFound"), nil + } + + // Set scopes + scopes := g.Scopes + if scopes == "" { + scopes = "openid,profile,email" + } + scopeList := strings.Split(scopes, ",") + + // Fetch OIDC configuration + if err := os.FetchOIDCConfig(g.Issuer); err != nil { + return err, nil + } + + return nil, &oauth2.Config{ + ClientID: g.ClientId, + ClientSecret: g.ClientSecret, + RedirectURL: g.RedirectUrl, + Endpoint: oauth2.Endpoint{ + AuthURL: os.OidcEndpoint.AuthURL, + TokenURL: os.OidcEndpoint.TokenURL, + }, + Scopes: scopeList, } - return errors.New("ConfigNotFound"), nil } func getHTTPClientWithProxy() *http.Client { @@ -269,6 +358,47 @@ func (os *OauthService) GoogleCallback(code string) (error error, userData *Goog return } +func (os *OauthService) OidcCallback(code string) (error error, userData *OidcUserdata) { + err, oauthConfig := os.GetOauthConfig(model.OauthTypeOidc) + if err != nil { + return err, nil + } + + // 使用代理配置创建 HTTP 客户端 + httpClient := getHTTPClientWithProxy() + ctx := context.WithValue(context.Background(), oauth2.HTTPClient, httpClient) + + token, err := oauthConfig.Exchange(ctx, code) + if err != nil { + global.Logger.Warn("oauthConfig.Exchange() failed: ", err) + error = errors.New("GetOauthTokenError") + return + } + + // 使用带有代理的 HTTP 客户端获取用户信息 + client := oauthConfig.Client(ctx, token) + resp, err := client.Get(os.OidcEndpoint.UserInfo) + if err != nil { + global.Logger.Warn("failed getting user info: ", err) + error = errors.New("GetOauthUserInfoError") + return + } + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + global.Logger.Warn("failed closing response body: ", err) + } + }(resp.Body) + + // 解析用户信息 + if err = json.NewDecoder(resp.Body).Decode(&userData); err != nil { + global.Logger.Warn("failed decoding user info: ", err) + error = errors.New("DecodeOauthUserInfoError") + return + } + return +} + func (os *OauthService) UserThirdInfo(op, openid string) *model.UserThird { ut := &model.UserThird{} global.DB.Where("open_id = ? and third_type = ?", openid, op).First(ut) @@ -282,6 +412,11 @@ func (os *OauthService) BindGithubUser(openid, username string, userId uint) err func (os *OauthService) BindGoogleUser(email, username string, userId uint) error { return os.BindOauthUser(model.OauthTypeGoogle, email, username, userId) } + +func (os *OauthService) BindOidcUser(openid, username string, userId uint) error { + return os.BindOauthUser(model.OauthTypeOidc, openid, username, userId) +} + func (os *OauthService) BindOauthUser(thirdType, openid, username string, userId uint) error { utr := &model.UserThird{ OpenId: openid, @@ -298,6 +433,9 @@ func (os *OauthService) UnBindGithubUser(userid uint) error { func (os *OauthService) UnBindGoogleUser(userid uint) error { return os.UnBindThird(model.OauthTypeGoogle, userid) } +func (os *OauthService) UnBindOidcUser(userid uint) error { + return os.UnBindThird(model.OauthTypeOidc, userid) +} func (os *OauthService) UnBindThird(thirdType string, userid uint) error { return global.DB.Where("user_id = ? and third_type = ?", userid, thirdType).Delete(&model.UserThird{}).Error } diff --git a/service/user.go b/service/user.go index d62a4eb..2c938af 100644 --- a/service/user.go +++ b/service/user.go @@ -196,6 +196,11 @@ func (us *UserService) InfoByGoogleEmail(email string) *model.User { return us.InfoByOauthId(model.OauthTypeGithub, email) } +// InfoByOidcSub 根据oidc取用户信息 +func (us *UserService) InfoByOidcSub(sub string) *model.User { + return us.InfoByOauthId(model.OauthTypeOidc, sub) +} + // InfoByOauthId 根据oauth取用户信息 func (us *UserService) InfoByOauthId(thirdType, uid string) *model.User { ut := AllService.OauthService.UserThirdInfo(thirdType, uid) @@ -219,6 +224,11 @@ func (us *UserService) RegisterByGoogle(name string, email string) *model.User { return us.RegisterByOauth(model.OauthTypeGoogle, name, email) } +// RegisterByOidc 注册, use prefferedUsername as username, sub as openid +func (us *UserService) RegisterByOidc(prefferedUsername string, sub string) *model.User { + return us.RegisterByOauth(model.OauthTypeOidc, prefferedUsername, sub) +} + // RegisterByOauth 注册 func (us *UserService) RegisterByOauth(thirdType, thirdName, uid string) *model.User { tx := global.DB.Begin()