Tao Chen 1 год назад
Родитель
Сommit
4105f14a3f

+ 7 - 0
config/oauth.go

@@ -11,3 +11,10 @@ type GoogleOauth struct {
11 11
 	ClientSecret string `mapstructure:"client-secret"`
12 12
 	RedirectUrl  string `mapstructure:"redirect-url"`
13 13
 }
14
+
15
+type OidcOauth struct {
16
+	Issuer       string `mapstructure:"issuer"`
17
+	ClientId     string `mapstructure:"client-id"`
18
+	ClientSecret string `mapstructure:"client-secret"`
19
+	RedirectUrl  string `mapstructure:"redirect-url"`
20
+}

+ 7 - 0
http/controller/admin/oauth.go

@@ -140,6 +140,13 @@ func (o *Oauth) Unbind(c *gin.Context) {
140 140
 			return
141 141
 		}
142 142
 	}
143
+	if f.Op == model.OauthTypeOidc {
144
+		err = service.AllService.OauthService.UnBindOidcUser(u.Id)
145
+		if err != nil {
146
+			response.Fail(c, 101, response.TranslateMsg(c, "OperationFailed")+err.Error())
147
+			return
148
+		}
149
+	}
143 150
 
144 151
 	response.Success(c, nil)
145 152
 }

+ 4 - 0
http/controller/api/login.go

@@ -92,6 +92,10 @@ func (l *Login) LoginOptions(c *gin.Context) {
92 92
 	if err == nil {
93 93
 		oauthOks = append(oauthOks, model.OauthTypeGoogle)
94 94
 	}
95
+	err, _ = service.AllService.OauthService.GetOauthConfig(model.OauthTypeOidc)
96
+	if err == nil {
97
+		oauthOks = append(oauthOks, model.OauthTypeOidc)
98
+	}
95 99
 	oauthOks = append(oauthOks, model.OauthTypeWebauth)
96 100
 	var oidcItems []map[string]string
97 101
 	for _, v := range oauthOks {

+ 64 - 1
http/controller/api/ouath.go

@@ -32,7 +32,7 @@ func (o *Oauth) OidcAuth(c *gin.Context) {
32 32
 		response.Error(c, response.TranslateMsg(c, "ParamsError")+err.Error())
33 33
 		return
34 34
 	}
35
-	if f.Op != model.OauthTypeWebauth && f.Op != model.OauthTypeGoogle && f.Op != model.OauthTypeGithub {
35
+	if f.Op != model.OauthTypeWebauth && f.Op != model.OauthTypeGoogle && f.Op != model.OauthTypeGithub && f.Op != model.OauthTypeOidc {
36 36
 		response.Error(c, response.TranslateMsg(c, "ParamsError"))
37 37
 		return
38 38
 	}
@@ -254,6 +254,69 @@ func (o *Oauth) OauthCallback(c *gin.Context) {
254 254
 			return
255 255
 		}
256 256
 	}
257
+	if ty == model.OauthTypeOidc {
258
+		code := c.Query("code")
259
+		err, userData := service.AllService.OauthService.OidcCallback(code)
260
+		if err != nil {
261
+			c.String(http.StatusInternalServerError, response.TranslateMsg(c, "OauthFailed")+response.TranslateMsg(c, err.Error()))
262
+			return
263
+		}
264
+		//将空格替换成_
265
+		// OidcName := strings.Replace(userData.Name, " ", "_", -1)
266
+		if ac == service.OauthActionTypeBind {
267
+			//fmt.Println("bind", ty, userData)
268
+			utr := service.AllService.OauthService.UserThirdInfo(ty, userData.Sub)
269
+			if utr.UserId > 0 {
270
+				c.String(http.StatusInternalServerError, response.TranslateMsg(c, "OauthHasBindOtherUser"))
271
+				return
272
+			}
273
+			//绑定
274
+			u := service.AllService.UserService.InfoById(v.UserId)
275
+			if u == nil {
276
+				c.String(http.StatusInternalServerError, response.TranslateMsg(c, "ItemNotFound"))
277
+				return
278
+			}
279
+			//绑定, user preffered_username as username
280
+			err = service.AllService.OauthService.BindOidcUser(userData.Sub, userData.PrefferedUsername, v.UserId)
281
+			if err != nil {
282
+				c.String(http.StatusInternalServerError, response.TranslateMsg(c, "BindFail"))
283
+				return
284
+			}
285
+			c.String(http.StatusOK, response.TranslateMsg(c, "BindSuccess"))
286
+			return
287
+		} else if ac == service.OauthActionTypeLogin {
288
+			if v.UserId != 0 {
289
+				c.String(http.StatusInternalServerError, response.TranslateMsg(c, "OauthHasBeenSuccess"))
290
+				return
291
+			}
292
+			u := service.AllService.UserService.InfoByOidcSub(userData.Sub)
293
+			if u == nil {
294
+				oa := service.AllService.OauthService.InfoByOp(ty)
295
+				if !*oa.AutoRegister {
296
+					//c.String(http.StatusInternalServerError, "还未绑定用户,请先绑定")
297
+
298
+					v.ThirdName = userData.PrefferedUsername
299
+					v.ThirdOpenId = userData.Sub
300
+					url := global.Config.Rustdesk.ApiServer + "/_admin/#/oauth/bind/" + cacheKey
301
+					c.Redirect(http.StatusFound, url)
302
+					return
303
+				}
304
+
305
+				//自动注册
306
+				u = service.AllService.UserService.RegisterByOidc(userData.PrefferedUsername, userData.Sub)
307
+				if u.Id == 0 {
308
+					c.String(http.StatusInternalServerError, response.TranslateMsg(c, "OauthRegisterFailed"))
309
+					return
310
+				}
311
+			}
312
+
313
+			v.UserId = u.Id
314
+			service.AllService.OauthService.SetOauthCache(cacheKey, v, 0)
315
+			c.String(http.StatusOK, response.TranslateMsg(c, "OauthSuccess"))
316
+			return
317
+		}
318
+	}
319
+
257 320
 	c.String(http.StatusInternalServerError, response.TranslateMsg(c, "SystemError"))
258 321
 
259 322
 }

+ 4 - 0
http/request/admin/oauth.go

@@ -15,6 +15,8 @@ type UnBindOauthForm struct {
15 15
 type OauthForm struct {
16 16
 	Id           uint   `json:"id"`
17 17
 	Op           string `json:"op" validate:"required"`
18
+	Issuer	     string `json:"issuer" validate:"omitempty,url"`
19
+	Scopes	   	 string `json:"scopes" validate:"omitempty"`
18 20
 	ClientId     string `json:"client_id" validate:"required"`
19 21
 	ClientSecret string `json:"client_secret" validate:"required"`
20 22
 	RedirectUrl  string `json:"redirect_url" validate:"required"`
@@ -28,6 +30,8 @@ func (of *OauthForm) ToOauth() *model.Oauth {
28 30
 		ClientSecret: of.ClientSecret,
29 31
 		RedirectUrl:  of.RedirectUrl,
30 32
 		AutoRegister: of.AutoRegister,
33
+		Issuer:       of.Issuer,
34
+		Scopes:       of.Scopes,
31 35
 	}
32 36
 	oa.Id = of.Id
33 37
 	return oa

+ 3 - 0
model/oauth.go

@@ -7,12 +7,15 @@ type Oauth struct {
7 7
 	ClientSecret string `json:"client_secret"`
8 8
 	RedirectUrl  string `json:"redirect_url"`
9 9
 	AutoRegister *bool  `json:"auto_register"`
10
+	Scopes       string `json:"scopes"`
11
+	Issuer	     string `json:"issuer"`
10 12
 	TimeModel
11 13
 }
12 14
 
13 15
 const (
14 16
 	OauthTypeGithub  = "github"
15 17
 	OauthTypeGoogle  = "google"
18
+	OauthTypeOidc    = "oidc"
16 19
 	OauthTypeWebauth = "webauth"
17 20
 )
18 21
 

+ 164 - 26
service/oauth.go

@@ -17,9 +17,19 @@ import (
17 17
 	"strconv"
18 18
 	"sync"
19 19
 	"time"
20
+	"strings"
20 21
 )
21 22
 
23
+// Define a struct to parse the .well-known/openid-configuration response
24
+type OidcEndpoint struct {
25
+	Issuer   string `json:"issuer"`
26
+	AuthURL  string `json:"authorization_endpoint"`
27
+	TokenURL string `json:"token_endpoint"`
28
+	UserInfo string `json:"userinfo_endpoint"`
29
+}
30
+
22 31
 type OauthService struct {
32
+	OidcEndpoint *OidcEndpoint
23 33
 }
24 34
 
25 35
 type GithubUserdata struct {
@@ -78,6 +88,15 @@ type GoogleUserdata struct {
78 88
 	Picture       string `json:"picture"`
79 89
 	VerifiedEmail bool   `json:"verified_email"`
80 90
 }
91
+type OidcUserdata struct {
92
+	Sub			  string `json:"sub"`
93
+	Email         string `json:"email"`
94
+	VerifiedEmail bool   `json:"email_verified"`
95
+	Name          string `json:"name"`
96
+	Picture       string `json:"picture"`
97
+	PrefferedUsername string `json:"preffered_username"`
98
+}
99
+
81 100
 type OauthCacheItem struct {
82 101
 	UserId      uint   `json:"user_id"`
83 102
 	Id          string `json:"id"` //rustdesk的设备ID
@@ -137,35 +156,105 @@ func (os *OauthService) BeginAuth(op string) (error error, code, url string) {
137 156
 	return err, code, ""
138 157
 }
139 158
 
140
-// GetOauthConfig 获取配置
159
+// Method to fetch OIDC configuration dynamically
160
+func (os *OauthService) FetchOIDCConfig(issuer string) error {
161
+	configURL := strings.TrimSuffix(issuer, "/") + "/.well-known/openid-configuration"
162
+
163
+	// Get the HTTP client (with or without proxy based on configuration)
164
+	client := getHTTPClientWithProxy()
165
+
166
+	resp, err := client.Get(configURL)
167
+	if err != nil {
168
+		return errors.New("failed to fetch OIDC configuration")
169
+	}
170
+	defer resp.Body.Close()
171
+
172
+	if resp.StatusCode != http.StatusOK {
173
+		return errors.New("OIDC configuration not found")
174
+	}
175
+
176
+	var endpoint OidcEndpoint
177
+	if err := json.NewDecoder(resp.Body).Decode(&endpoint); err != nil {
178
+		return errors.New("failed to parse OIDC configuration")
179
+	}
180
+
181
+	os.OidcEndpoint = &endpoint
182
+	return nil
183
+}
184
+
185
+// GetOauthConfig retrieves the OAuth2 configuration based on the provider type
141 186
 func (os *OauthService) GetOauthConfig(op string) (error, *oauth2.Config) {
142
-	if op == model.OauthTypeGithub {
143
-		g := os.InfoByOp(model.OauthTypeGithub)
144
-		if g.Id == 0 || g.ClientId == "" || g.ClientSecret == "" || g.RedirectUrl == "" {
145
-			return errors.New("ConfigNotFound"), nil
146
-		}
147
-		return nil, &oauth2.Config{
148
-			ClientID:     g.ClientId,
149
-			ClientSecret: g.ClientSecret,
150
-			RedirectURL:  g.RedirectUrl,
151
-			Endpoint:     github.Endpoint,
152
-			Scopes:       []string{"read:user", "user:email"},
153
-		}
187
+	switch op {
188
+	case model.OauthTypeGithub:
189
+		return os.getGithubConfig()
190
+	case model.OauthTypeGoogle:
191
+		return os.getGoogleConfig()
192
+	case model.OauthTypeOidc:
193
+		return os.getOidcConfig()
194
+	default:
195
+		return errors.New("unsupported OAuth type"), nil
154 196
 	}
155
-	if op == model.OauthTypeGoogle {
156
-		g := os.InfoByOp(model.OauthTypeGoogle)
157
-		if g.Id == 0 || g.ClientId == "" || g.ClientSecret == "" || g.RedirectUrl == "" {
158
-			return errors.New("ConfigNotFound"), nil
159
-		}
160
-		return nil, &oauth2.Config{
161
-			ClientID:     g.ClientId,
162
-			ClientSecret: g.ClientSecret,
163
-			RedirectURL:  g.RedirectUrl,
164
-			Endpoint:     google.Endpoint,
165
-			Scopes:       []string{"https://www.googleapis.com/auth/userinfo.profile", "https://www.googleapis.com/auth/userinfo.email"},
166
-		}
197
+}
198
+
199
+// Helper function to get GitHub OAuth2 configuration
200
+func (os *OauthService) getGithubConfig() (error, *oauth2.Config) {
201
+	g := os.InfoByOp(model.OauthTypeGithub)
202
+	if g.Id == 0 || g.ClientId == "" || g.ClientSecret == "" || g.RedirectUrl == "" {
203
+		return errors.New("ConfigNotFound"), nil
204
+	}
205
+	return nil, &oauth2.Config{
206
+		ClientID:     g.ClientId,
207
+		ClientSecret: g.ClientSecret,
208
+		RedirectURL:  g.RedirectUrl,
209
+		Endpoint:     github.Endpoint,
210
+		Scopes:       []string{"read:user", "user:email"},
211
+	}
212
+}
213
+
214
+// Helper function to get Google OAuth2 configuration
215
+func (os *OauthService) getGoogleConfig() (error, *oauth2.Config) {
216
+	g := os.InfoByOp(model.OauthTypeGoogle)
217
+	if g.Id == 0 || g.ClientId == "" || g.ClientSecret == "" || g.RedirectUrl == "" {
218
+		return errors.New("ConfigNotFound"), nil
219
+	}
220
+	return nil, &oauth2.Config{
221
+		ClientID:     g.ClientId,
222
+		ClientSecret: g.ClientSecret,
223
+		RedirectURL:  g.RedirectUrl,
224
+		Endpoint:     google.Endpoint,
225
+		Scopes:       []string{"https://www.googleapis.com/auth/userinfo.profile", "https://www.googleapis.com/auth/userinfo.email"},
226
+	}
227
+}
228
+
229
+// Helper function to get OIDC OAuth2 configuration
230
+func (os *OauthService) getOidcConfig() (error, *oauth2.Config) {
231
+	g := os.InfoByOp(model.OauthTypeOidc)
232
+	if g.Id == 0 || g.ClientId == "" || g.ClientSecret == "" || g.RedirectUrl == "" || g.Issuer == "" {
233
+		return errors.New("ConfigNotFound"), nil
234
+	}
235
+
236
+	// Set scopes
237
+	scopes := g.Scopes
238
+	if scopes == "" {
239
+		scopes = "openid,profile,email"
240
+	}
241
+	scopeList := strings.Split(scopes, ",")
242
+
243
+	// Fetch OIDC configuration
244
+	if err := os.FetchOIDCConfig(g.Issuer); err != nil {
245
+		return err, nil
246
+	}
247
+
248
+	return nil, &oauth2.Config{
249
+		ClientID:     g.ClientId,
250
+		ClientSecret: g.ClientSecret,
251
+		RedirectURL:  g.RedirectUrl,
252
+		Endpoint: oauth2.Endpoint{
253
+			AuthURL:  os.OidcEndpoint.AuthURL,
254
+			TokenURL: os.OidcEndpoint.TokenURL,
255
+		},
256
+		Scopes: scopeList,
167 257
 	}
168
-	return errors.New("ConfigNotFound"), nil
169 258
 }
170 259
 
171 260
 func getHTTPClientWithProxy() *http.Client {
@@ -269,6 +358,47 @@ func (os *OauthService) GoogleCallback(code string) (error error, userData *Goog
269 358
 	return
270 359
 }
271 360
 
361
+func (os *OauthService) OidcCallback(code string) (error error, userData *OidcUserdata) {
362
+	err, oauthConfig := os.GetOauthConfig(model.OauthTypeOidc)
363
+	if err != nil {
364
+		return err, nil
365
+	}
366
+
367
+	// 使用代理配置创建 HTTP 客户端
368
+	httpClient := getHTTPClientWithProxy()
369
+	ctx := context.WithValue(context.Background(), oauth2.HTTPClient, httpClient)
370
+
371
+	token, err := oauthConfig.Exchange(ctx, code)
372
+	if err != nil {
373
+		global.Logger.Warn("oauthConfig.Exchange() failed: ", err)
374
+		error = errors.New("GetOauthTokenError")
375
+		return
376
+	}
377
+
378
+	// 使用带有代理的 HTTP 客户端获取用户信息
379
+	client := oauthConfig.Client(ctx, token)
380
+	resp, err := client.Get(os.OidcEndpoint.UserInfo)
381
+	if err != nil {
382
+		global.Logger.Warn("failed getting user info: ", err)
383
+		error = errors.New("GetOauthUserInfoError")
384
+		return
385
+	}
386
+	defer func(Body io.ReadCloser) {
387
+		err := Body.Close()
388
+		if err != nil {
389
+			global.Logger.Warn("failed closing response body: ", err)
390
+		}
391
+	}(resp.Body)
392
+
393
+	// 解析用户信息
394
+	if err = json.NewDecoder(resp.Body).Decode(&userData); err != nil {
395
+		global.Logger.Warn("failed decoding user info: ", err)
396
+		error = errors.New("DecodeOauthUserInfoError")
397
+		return
398
+	}
399
+	return
400
+}
401
+
272 402
 func (os *OauthService) UserThirdInfo(op, openid string) *model.UserThird {
273 403
 	ut := &model.UserThird{}
274 404
 	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
282 412
 func (os *OauthService) BindGoogleUser(email, username string, userId uint) error {
283 413
 	return os.BindOauthUser(model.OauthTypeGoogle, email, username, userId)
284 414
 }
415
+
416
+func (os *OauthService) BindOidcUser(openid, username string, userId uint) error {
417
+	return os.BindOauthUser(model.OauthTypeOidc, openid, username, userId)
418
+}
419
+
285 420
 func (os *OauthService) BindOauthUser(thirdType, openid, username string, userId uint) error {
286 421
 	utr := &model.UserThird{
287 422
 		OpenId:    openid,
@@ -298,6 +433,9 @@ func (os *OauthService) UnBindGithubUser(userid uint) error {
298 433
 func (os *OauthService) UnBindGoogleUser(userid uint) error {
299 434
 	return os.UnBindThird(model.OauthTypeGoogle, userid)
300 435
 }
436
+func (os *OauthService) UnBindOidcUser(userid uint) error {
437
+	return os.UnBindThird(model.OauthTypeOidc, userid)
438
+}
301 439
 func (os *OauthService) UnBindThird(thirdType string, userid uint) error {
302 440
 	return global.DB.Where("user_id = ? and third_type = ?", userid, thirdType).Delete(&model.UserThird{}).Error
303 441
 }

+ 10 - 0
service/user.go

@@ -196,6 +196,11 @@ func (us *UserService) InfoByGoogleEmail(email string) *model.User {
196 196
 	return us.InfoByOauthId(model.OauthTypeGithub, email)
197 197
 }
198 198
 
199
+// InfoByOidcSub 根据oidc取用户信息
200
+func (us *UserService) InfoByOidcSub(sub string) *model.User {
201
+	return us.InfoByOauthId(model.OauthTypeOidc, sub)
202
+}
203
+
199 204
 // InfoByOauthId 根据oauth取用户信息
200 205
 func (us *UserService) InfoByOauthId(thirdType, uid string) *model.User {
201 206
 	ut := AllService.OauthService.UserThirdInfo(thirdType, uid)
@@ -219,6 +224,11 @@ func (us *UserService) RegisterByGoogle(name string, email string) *model.User {
219 224
 	return us.RegisterByOauth(model.OauthTypeGoogle, name, email)
220 225
 }
221 226
 
227
+// RegisterByOidc 注册, use prefferedUsername as username, sub as openid
228
+func (us *UserService) RegisterByOidc(prefferedUsername string, sub string) *model.User {
229
+	return us.RegisterByOauth(model.OauthTypeOidc, prefferedUsername, sub)
230
+}
231
+
222 232
 // RegisterByOauth 注册
223 233
 func (us *UserService) RegisterByOauth(thirdType, thirdName, uid string) *model.User {
224 234
 	tx := global.DB.Begin()