Просмотр исходного кода

Merge pull request #30 from IamTaoChen/oidc

Add General OIDC Login
1 год назад
Родитель
Сommit
bc92ffc106

+ 26 - 0
.dockerignore

@@ -0,0 +1,26 @@
1
+# Ignore Docker Compose configuration files
2
+docker-compose.yaml
3
+
4
+# Ignore development Dockerfile
5
+Dockerfile.dev
6
+
7
+# Ignore the data directory
8
+data/
9
+
10
+# Ignore version control system directories
11
+.git/
12
+
13
+# Ignore log and temporary files
14
+*.log
15
+*.tmp
16
+*.swp
17
+
18
+# Ignore editor/IDE configuration files
19
+.vscode/
20
+.idea/
21
+
22
+# Ignore binaries and build cache
23
+release/
24
+bin/
25
+*.exe
26
+*.out

+ 72 - 0
Dockerfile.dev

@@ -0,0 +1,72 @@
1
+# Use build arguments for Go version and architecture
2
+ARG GO_VERSION=1.22
3
+ARG BUILDARCH=amd64
4
+
5
+# Stage 1: Builder Stage
6
+# FROM golang:${GO_VERSION}-alpine AS builder
7
+FROM crazymax/xgo:${GO_VERSION} AS builder
8
+
9
+# Set up working directory
10
+WORKDIR /app
11
+
12
+# Step 1: Copy the source code
13
+COPY . .
14
+
15
+# Step 2: Download dependencies
16
+RUN go mod tidy && go mod download
17
+
18
+
19
+# Step 3: Install swag and  Run the build script
20
+RUN go install github.com/swaggo/swag/cmd/swag@latest && \
21
+    swag init -g cmd/apimain.go --output docs/api --instanceName api --exclude http/controller/admin && \
22
+    swag init -g cmd/apimain.go --output docs/admin --instanceName admin --exclude http/controller/api
23
+
24
+# Build the Go application with CGO enabled and specified ldflags
25
+RUN CGO_ENABLED=1 GOOS=linux go build -a \
26
+    -ldflags "-s -w --extldflags '-static -fpic'" \
27
+    -installsuffix cgo -o release/apimain cmd/apimain.go
28
+
29
+# Stage 2: Frontend Build Stage (builder2)
30
+FROM node:18-alpine AS builder2
31
+
32
+# Set working directory
33
+WORKDIR /frontend
34
+
35
+RUN apk update && apk add git --no-cache
36
+
37
+# Clone the frontend repository
38
+RUN git clone https://github.com/lejianwen/rustdesk-api-web .
39
+
40
+# Install npm dependencies and build the frontend
41
+RUN npm install && npm run build
42
+
43
+# Stage 2: Final Image
44
+FROM alpine:latest
45
+
46
+# Set up working directory
47
+WORKDIR /app
48
+
49
+# Install necessary runtime dependencies
50
+RUN apk add --no-cache tzdata file
51
+
52
+# Copy the built application and resources from the builder stage
53
+COPY --from=builder /app/release /app/
54
+COPY --from=builder /app/conf /app/conf/
55
+COPY --from=builder /app/resources /app/resources/
56
+COPY --from=builder /app/docs /app/docs/
57
+# Copy frontend build from builder2 stage
58
+COPY --from=builder2 /frontend/dist/ /app/resources/admin/
59
+
60
+# Ensure the binary is correctly built and linked
61
+RUN file /app/apimain && \
62
+    mkdir -p /app/data && \
63
+    mkdir -p /app/runtime
64
+
65
+# Set up a volume for persistent data
66
+VOLUME /app/data
67
+
68
+# Expose the necessary port
69
+EXPOSE 21114
70
+
71
+# Define the command to run the application
72
+CMD ["./apimain"]

+ 34 - 4
build.sh

@@ -1,16 +1,46 @@
1
 #!/bin/sh
1
 #!/bin/sh
2
 
2
 
3
-rm release -rf
3
+set -e
4
+# Automatically get the current environment's GOARCH; if not defined, use the detected system architecture
5
+GOARCH=${GOARCH:-$(go env GOARCH)}
6
+DOCS="true"
7
+# Safely remove the old release directory
8
+rm -rf release
9
+
10
+# Set Go environment variables
4
 go env -w GO111MODULE=on
11
 go env -w GO111MODULE=on
5
 go env -w GOPROXY=https://goproxy.cn,direct
12
 go env -w GOPROXY=https://goproxy.cn,direct
6
 go env -w CGO_ENABLED=1
13
 go env -w CGO_ENABLED=1
7
 go env -w GOOS=linux
14
 go env -w GOOS=linux
8
-go env -w GOARCH=amd64
9
-swag init -g cmd/apimain.go --output docs/api --instanceName api --exclude http/controller/admin
10
-swag init -g cmd/apimain.go --output docs/admin --instanceName admin --exclude http/controller/api
15
+go env -w GOARCH=${GOARCH}
16
+
17
+
18
+# Generate Swagger documentation if DOCS is not empty
19
+if [ -n "${DOCS}" ]; then
20
+    # Check if swag is installed
21
+    if ! command -v swag &> /dev/null; then
22
+        echo "swag command not found. Please install it using:"
23
+        echo "go install github.com/swaggo/swag/cmd/swag@latest"
24
+        echo "Skipping Swagger documentation generation due to missing swag tool."
25
+    else
26
+        echo "Generating Swagger documentation..."
27
+        swag init -g cmd/apimain.go --output docs/api --instanceName api --exclude http/controller/admin
28
+        swag init -g cmd/apimain.go --output docs/admin --instanceName admin --exclude http/controller/api
29
+    fi
30
+else
31
+    echo "Skipping Swagger documentation generation due to DOCS is empty."
32
+fi
33
+
34
+# Compile the Go code and output it to the release directory
11
 go build -o release/apimain cmd/apimain.go
35
 go build -o release/apimain cmd/apimain.go
36
+
37
+# Copy resource files to the release directory
12
 cp -ar resources release/
38
 cp -ar resources release/
13
 cp -ar docs release/
39
 cp -ar docs release/
14
 cp -ar conf release/
40
 cp -ar conf release/
41
+
42
+# Create necessary directory structures
15
 mkdir -p release/data
43
 mkdir -p release/data
16
 mkdir -p release/runtime
44
 mkdir -p release/runtime
45
+
46
+echo "Build and setup completed successfully."

+ 7 - 0
config/oauth.go

@@ -11,3 +11,10 @@ type GoogleOauth struct {
11
 	ClientSecret string `mapstructure:"client-secret"`
11
 	ClientSecret string `mapstructure:"client-secret"`
12
 	RedirectUrl  string `mapstructure:"redirect-url"`
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
+}

+ 8 - 3
docker-compose.yaml

@@ -1,15 +1,20 @@
1
 services:
1
 services:
2
   rustdesk-api:
2
   rustdesk-api:
3
-    image: lejianwen/rustdesk-api
3
+    build: 
4
+      context: .
5
+      dockerfile: Dockerfile.dev
6
+    # image: lejianwen/rustdesk-api
4
     container_name: rustdesk-api
7
     container_name: rustdesk-api
5
     environment:
8
     environment:
6
       - TZ=Asia/Shanghai
9
       - TZ=Asia/Shanghai
7
       - RUSTDESK_API_RUSTDESK_ID_SERVER=192.168.1.66:21116
10
       - RUSTDESK_API_RUSTDESK_ID_SERVER=192.168.1.66:21116
8
       - RUSTDESK_API_RUSTDESK_RELAY_SERVER=192.168.1.66:21117
11
       - RUSTDESK_API_RUSTDESK_RELAY_SERVER=192.168.1.66:21117
9
-      - RUSTDESK_API_RUSTDESK_API_SERVER=http://192.168.1.66:21114
12
+      - RUSTDESK_API_RUSTDESK_API_SERVER=http://127.0.0.1:21114
10
       - RUSTDESK_API_RUSTDESK_KEY=123456789
13
       - RUSTDESK_API_RUSTDESK_KEY=123456789
11
     ports:
14
     ports:
12
       - 21114:21114
15
       - 21114:21114
13
     volumes:
16
     volumes:
14
-      - /data/rustdesk/api:/app/data #将数据库挂载出来方便备份
17
+      - ./data/rustdesk/api:/app/data #将数据库挂载出来方便备份
18
+      - ./conf:/app/conf # config
19
+      # - ./resources:/app/resources # 静态资源
15
     restart: unless-stopped
20
     restart: unless-stopped

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

@@ -140,6 +140,13 @@ func (o *Oauth) Unbind(c *gin.Context) {
140
 			return
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
 	response.Success(c, nil)
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
 	if err == nil {
92
 	if err == nil {
93
 		oauthOks = append(oauthOks, model.OauthTypeGoogle)
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
 	oauthOks = append(oauthOks, model.OauthTypeWebauth)
99
 	oauthOks = append(oauthOks, model.OauthTypeWebauth)
96
 	var oidcItems []map[string]string
100
 	var oidcItems []map[string]string
97
 	for _, v := range oauthOks {
101
 	for _, v := range oauthOks {

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

@@ -32,7 +32,7 @@ func (o *Oauth) OidcAuth(c *gin.Context) {
32
 		response.Error(c, response.TranslateMsg(c, "ParamsError")+err.Error())
32
 		response.Error(c, response.TranslateMsg(c, "ParamsError")+err.Error())
33
 		return
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
 		response.Error(c, response.TranslateMsg(c, "ParamsError"))
36
 		response.Error(c, response.TranslateMsg(c, "ParamsError"))
37
 		return
37
 		return
38
 	}
38
 	}
@@ -254,6 +254,70 @@ func (o *Oauth) OauthCallback(c *gin.Context) {
254
 			return
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.PreferredUsername, 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.PreferredUsername
299
+					v.ThirdOpenId = userData.Sub
300
+					v.ThirdEmail = userData.Email
301
+					url := global.Config.Rustdesk.ApiServer + "/_admin/#/oauth/bind/" + cacheKey
302
+					c.Redirect(http.StatusFound, url)
303
+					return
304
+				}
305
+
306
+				//自动注册
307
+				u = service.AllService.UserService.RegisterByOidc(userData.PreferredUsername, userData.Sub)
308
+				if u.Id == 0 {
309
+					c.String(http.StatusInternalServerError, response.TranslateMsg(c, "OauthRegisterFailed"))
310
+					return
311
+				}
312
+			}
313
+
314
+			v.UserId = u.Id
315
+			service.AllService.OauthService.SetOauthCache(cacheKey, v, 0)
316
+			c.String(http.StatusOK, response.TranslateMsg(c, "OauthSuccess"))
317
+			return
318
+		}
319
+	}
320
+
257
 	c.String(http.StatusInternalServerError, response.TranslateMsg(c, "SystemError"))
321
 	c.String(http.StatusInternalServerError, response.TranslateMsg(c, "SystemError"))
258
 
322
 
259
 }
323
 }

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

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

+ 3 - 0
model/oauth.go

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

+ 165 - 26
service/oauth.go

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

+ 10 - 0
service/user.go

@@ -196,6 +196,11 @@ func (us *UserService) InfoByGoogleEmail(email string) *model.User {
196
 	return us.InfoByOauthId(model.OauthTypeGithub, email)
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
 // InfoByOauthId 根据oauth取用户信息
204
 // InfoByOauthId 根据oauth取用户信息
200
 func (us *UserService) InfoByOauthId(thirdType, uid string) *model.User {
205
 func (us *UserService) InfoByOauthId(thirdType, uid string) *model.User {
201
 	ut := AllService.OauthService.UserThirdInfo(thirdType, uid)
206
 	ut := AllService.OauthService.UserThirdInfo(thirdType, uid)
@@ -219,6 +224,11 @@ func (us *UserService) RegisterByGoogle(name string, email string) *model.User {
219
 	return us.RegisterByOauth(model.OauthTypeGoogle, name, email)
224
 	return us.RegisterByOauth(model.OauthTypeGoogle, name, email)
220
 }
225
 }
221
 
226
 
227
+// RegisterByOidc 注册, use PreferredUsername as username, sub as openid
228
+func (us *UserService) RegisterByOidc(PreferredUsername string, sub string) *model.User {
229
+	return us.RegisterByOauth(model.OauthTypeOidc, PreferredUsername, sub)
230
+}
231
+
222
 // RegisterByOauth 注册
232
 // RegisterByOauth 注册
223
 func (us *UserService) RegisterByOauth(thirdType, thirdName, uid string) *model.User {
233
 func (us *UserService) RegisterByOauth(thirdType, thirdName, uid string) *model.User {
224
 	global.Lock.Lock("registerByOauth")
234
 	global.Lock.Lock("registerByOauth")