feat: Improve oauth redirect (#303)

* fix: redirects after oauth can potentially misalign with server's actually hostname

* feat: remove `RedirectURL` from oauth config, as it should checked by provider rather than client

* feat: align oauth endpoint with the hostname in requests
This commit is contained in:
k3-cat
2025-07-04 16:30:07 +10:00
committed by GitHub
parent af4813db30
commit 73a8461a2d
11 changed files with 40 additions and 57 deletions

View File

@@ -2,6 +2,10 @@ package main
import ( import (
"fmt" "fmt"
"os"
"strconv"
"time"
"github.com/go-redis/redis/v8" "github.com/go-redis/redis/v8"
"github.com/lejianwen/rustdesk-api/v2/config" "github.com/lejianwen/rustdesk-api/v2/config"
"github.com/lejianwen/rustdesk-api/v2/global" "github.com/lejianwen/rustdesk-api/v2/global"
@@ -17,9 +21,6 @@ import (
"github.com/lejianwen/rustdesk-api/v2/utils" "github.com/lejianwen/rustdesk-api/v2/utils"
"github.com/nicksnyder/go-i18n/v2/i18n" "github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"os"
"strconv"
"time"
) )
// @title 管理系统API // @title 管理系统API
@@ -210,7 +211,7 @@ func InitGlobal() {
} }
func DatabaseAutoUpdate() { func DatabaseAutoUpdate() {
version := 262 version := 263
db := global.DB db := global.DB

View File

@@ -3,24 +3,20 @@ package config
type GithubOauth struct { type GithubOauth struct {
ClientId string `mapstructure:"client-id"` ClientId string `mapstructure:"client-id"`
ClientSecret string `mapstructure:"client-secret"` ClientSecret string `mapstructure:"client-secret"`
RedirectUrl string `mapstructure:"redirect-url"`
} }
type GoogleOauth struct { type GoogleOauth struct {
ClientId string `mapstructure:"client-id"` ClientId string `mapstructure:"client-id"`
ClientSecret string `mapstructure:"client-secret"` ClientSecret string `mapstructure:"client-secret"`
RedirectUrl string `mapstructure:"redirect-url"`
} }
type OidcOauth struct { type OidcOauth struct {
Issuer string `mapstructure:"issuer"` Issuer string `mapstructure:"issuer"`
ClientId string `mapstructure:"client-id"` ClientId string `mapstructure:"client-id"`
ClientSecret string `mapstructure:"client-secret"` ClientSecret string `mapstructure:"client-secret"`
RedirectUrl string `mapstructure:"redirect-url"`
} }
type LinuxdoOauth struct { type LinuxdoOauth struct {
ClientId string `mapstructure:"client-id"` ClientId string `mapstructure:"client-id"`
ClientSecret string `mapstructure:"client-secret"` ClientSecret string `mapstructure:"client-secret"`
RedirectUrl string `mapstructure:"redirect-url"`
} }

View File

@@ -5569,8 +5569,7 @@ const docTemplateadmin = `{
"required": [ "required": [
"client_id", "client_id",
"client_secret", "client_secret",
"oauth_type", "oauth_type"
"redirect_url"
], ],
"properties": { "properties": {
"auto_register": { "auto_register": {
@@ -5600,9 +5599,6 @@ const docTemplateadmin = `{
"pkce_method": { "pkce_method": {
"type": "string" "type": "string"
}, },
"redirect_url": {
"type": "string"
},
"scopes": { "scopes": {
"type": "string" "type": "string"
} }
@@ -6296,9 +6292,6 @@ const docTemplateadmin = `{
"pkce_method": { "pkce_method": {
"type": "string" "type": "string"
}, },
"redirect_url": {
"type": "string"
},
"scopes": { "scopes": {
"type": "string" "type": "string"
}, },

View File

@@ -5562,8 +5562,7 @@
"required": [ "required": [
"client_id", "client_id",
"client_secret", "client_secret",
"oauth_type", "oauth_type"
"redirect_url"
], ],
"properties": { "properties": {
"auto_register": { "auto_register": {
@@ -5593,9 +5592,6 @@
"pkce_method": { "pkce_method": {
"type": "string" "type": "string"
}, },
"redirect_url": {
"type": "string"
},
"scopes": { "scopes": {
"type": "string" "type": "string"
} }
@@ -6289,9 +6285,6 @@
"pkce_method": { "pkce_method": {
"type": "string" "type": "string"
}, },
"redirect_url": {
"type": "string"
},
"scopes": { "scopes": {
"type": "string" "type": "string"
}, },

View File

@@ -143,15 +143,12 @@ definitions:
type: boolean type: boolean
pkce_method: pkce_method:
type: string type: string
redirect_url:
type: string
scopes: scopes:
type: string type: string
required: required:
- client_id - client_id
- client_secret - client_secret
- oauth_type - oauth_type
- redirect_url
type: object type: object
admin.PeerBatchDeleteForm: admin.PeerBatchDeleteForm:
properties: properties:
@@ -611,8 +608,6 @@ definitions:
type: boolean type: boolean
pkce_method: pkce_method:
type: string type: string
redirect_url:
type: string
scopes: scopes:
type: string type: string
updated_at: updated_at:

View File

@@ -2,6 +2,7 @@ package admin
import ( import (
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global" "github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/controller/api" "github.com/lejianwen/rustdesk-api/v2/http/controller/api"
@@ -188,7 +189,7 @@ func (ct *Login) OidcAuth(c *gin.Context) {
return return
} }
err, state, verifier, nonce, url := service.AllService.OauthService.BeginAuth(f.Op) err, state, verifier, nonce, url := service.AllService.OauthService.BeginAuth(c, f.Op)
if err != nil { if err != nil {
response.Error(c, response.TranslateMsg(c, err.Error())) response.Error(c, response.TranslateMsg(c, err.Error()))
return return

View File

@@ -1,13 +1,14 @@
package admin package admin
import ( import (
"strconv"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global" "github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/request/admin" "github.com/lejianwen/rustdesk-api/v2/http/request/admin"
adminReq "github.com/lejianwen/rustdesk-api/v2/http/request/admin" adminReq "github.com/lejianwen/rustdesk-api/v2/http/request/admin"
"github.com/lejianwen/rustdesk-api/v2/http/response" "github.com/lejianwen/rustdesk-api/v2/http/response"
"github.com/lejianwen/rustdesk-api/v2/service" "github.com/lejianwen/rustdesk-api/v2/service"
"strconv"
) )
type Oauth struct { type Oauth struct {
@@ -43,7 +44,7 @@ func (o *Oauth) ToBind(c *gin.Context) {
return return
} }
err, state, verifier, nonce, url := service.AllService.OauthService.BeginAuth(f.Op) err, state, verifier, nonce, url := service.AllService.OauthService.BeginAuth(c, f.Op)
if err != nil { if err != nil {
response.Error(c, response.TranslateMsg(c, err.Error())) response.Error(c, response.TranslateMsg(c, err.Error()))
return return

View File

@@ -1,6 +1,8 @@
package api package api
import ( import (
"net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/global" "github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/http/request/api" "github.com/lejianwen/rustdesk-api/v2/http/request/api"
@@ -10,7 +12,6 @@ import (
"github.com/lejianwen/rustdesk-api/v2/service" "github.com/lejianwen/rustdesk-api/v2/service"
"github.com/lejianwen/rustdesk-api/v2/utils" "github.com/lejianwen/rustdesk-api/v2/utils"
"github.com/nicksnyder/go-i18n/v2/i18n" "github.com/nicksnyder/go-i18n/v2/i18n"
"net/http"
) )
type Oauth struct { type Oauth struct {
@@ -35,7 +36,7 @@ func (o *Oauth) OidcAuth(c *gin.Context) {
oauthService := service.AllService.OauthService oauthService := service.AllService.OauthService
err, state, verifier, nonce, url := oauthService.BeginAuth(f.Op) err, state, verifier, nonce, url := oauthService.BeginAuth(c, f.Op)
if err != nil { if err != nil {
response.Error(c, response.TranslateMsg(c, err.Error())) response.Error(c, response.TranslateMsg(c, err.Error()))
return return
@@ -169,7 +170,7 @@ func (o *Oauth) OauthCallback(c *gin.Context) {
var user *model.User var user *model.User
// 获取用户信息 // 获取用户信息
code := c.Query("code") code := c.Query("code")
err, oauthUser := oauthService.Callback(code, verifier, op, nonce) err, oauthUser := oauthService.Callback(c, code, verifier, op, nonce)
if err != nil { if err != nil {
c.HTML(http.StatusOK, "oauth_fail.html", gin.H{ c.HTML(http.StatusOK, "oauth_fail.html", gin.H{
"message": "OauthFailed", "message": "OauthFailed",
@@ -225,8 +226,7 @@ func (o *Oauth) OauthCallback(c *gin.Context) {
if !*oauthConfig.AutoRegister { if !*oauthConfig.AutoRegister {
//c.String(http.StatusInternalServerError, "还未绑定用户,请先绑定") //c.String(http.StatusInternalServerError, "还未绑定用户,请先绑定")
oauthCache.UpdateFromOauthUser(oauthUser) oauthCache.UpdateFromOauthUser(oauthUser)
url := global.Config.Rustdesk.ApiServer + "/_admin/#/oauth/bind/" + cacheKey c.Redirect(http.StatusFound, "/_admin/#/oauth/bind/"+cacheKey)
c.Redirect(http.StatusFound, url)
return return
} }
@@ -251,8 +251,7 @@ func (o *Oauth) OauthCallback(c *gin.Context) {
Type: model.LoginLogTypeOauth, Type: model.LoginLogTypeOauth,
Platform: oauthService.DeviceOs, Platform: oauthService.DeviceOs,
})*/ })*/
url := global.Config.Rustdesk.ApiServer + "/_admin/#/" c.Redirect(http.StatusFound, "/_admin/#/")
c.Redirect(http.StatusFound, url)
return return
} }
c.HTML(http.StatusOK, "oauth_success.html", gin.H{ c.HTML(http.StatusOK, "oauth_success.html", gin.H{

View File

@@ -22,7 +22,6 @@ type OauthForm struct {
Scopes string `json:"scopes" validate:"omitempty"` Scopes string `json:"scopes" validate:"omitempty"`
ClientId string `json:"client_id" validate:"required"` ClientId string `json:"client_id" validate:"required"`
ClientSecret string `json:"client_secret" validate:"required"` ClientSecret string `json:"client_secret" validate:"required"`
RedirectUrl string `json:"redirect_url" validate:"required"`
AutoRegister *bool `json:"auto_register"` AutoRegister *bool `json:"auto_register"`
PkceEnable *bool `json:"pkce_enable"` PkceEnable *bool `json:"pkce_enable"`
PkceMethod string `json:"pkce_method"` PkceMethod string `json:"pkce_method"`
@@ -34,7 +33,6 @@ func (of *OauthForm) ToOauth() *model.Oauth {
OauthType: of.OauthType, OauthType: of.OauthType,
ClientId: of.ClientId, ClientId: of.ClientId,
ClientSecret: of.ClientSecret, ClientSecret: of.ClientSecret,
RedirectUrl: of.RedirectUrl,
AutoRegister: of.AutoRegister, AutoRegister: of.AutoRegister,
Issuer: of.Issuer, Issuer: of.Issuer,
Scopes: of.Scopes, Scopes: of.Scopes,

View File

@@ -41,7 +41,6 @@ type Oauth struct {
OauthType string `json:"oauth_type"` OauthType string `json:"oauth_type"`
ClientId string `json:"client_id"` ClientId string `json:"client_id"`
ClientSecret string `json:"client_secret"` ClientSecret string `json:"client_secret"`
RedirectUrl string `json:"redirect_url"`
AutoRegister *bool `json:"auto_register"` AutoRegister *bool `json:"auto_register"`
Scopes string `json:"scopes"` Scopes string `json:"scopes"`
Issuer string `json:"issuer"` Issuer string `json:"issuer"`

View File

@@ -4,11 +4,14 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc"
"github.com/gin-gonic/gin"
"github.com/lejianwen/rustdesk-api/v2/model" "github.com/lejianwen/rustdesk-api/v2/model"
"github.com/lejianwen/rustdesk-api/v2/utils" "github.com/lejianwen/rustdesk-api/v2/utils"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"golang.org/x/oauth2/github" "golang.org/x/oauth2/github"
// "golang.org/x/oauth2/google" // "golang.org/x/oauth2/google"
"gorm.io/gorm" "gorm.io/gorm"
// "io" // "io"
@@ -93,16 +96,20 @@ func (os *OauthService) DeleteOauthCache(key string) {
OauthCache.Delete(key) OauthCache.Delete(key)
} }
func (os *OauthService) BeginAuth(op string) (error error, state, verifier, nonce, url string) { func (os *OauthService) BeginAuth(c *gin.Context, op string) (error error, state, verifier, nonce, url string) {
state = utils.RandomString(10) + strconv.FormatInt(time.Now().Unix(), 10) state = utils.RandomString(10) + strconv.FormatInt(time.Now().Unix(), 10)
verifier = "" verifier = ""
nonce = "" nonce = ""
if op == model.OauthTypeWebauth { if op == model.OauthTypeWebauth {
url = Config.Rustdesk.ApiServer + "/_admin/#/oauth/" + state host := c.GetHeader("Origin")
if host == "" {
host = Config.Rustdesk.ApiServer
}
url = host + "/_admin/#/oauth/" + state
//url = "http://localhost:8888/_admin/#/oauth/" + code //url = "http://localhost:8888/_admin/#/oauth/" + code
return nil, state, verifier, nonce, url return nil, state, verifier, nonce, url
} }
err, oauthInfo, oauthConfig, _ := os.GetOauthConfig(op) err, oauthInfo, oauthConfig, _ := os.GetOauthConfig(c, op)
if err == nil { if err == nil {
extras := make([]oauth2.AuthCodeOption, 0, 3) extras := make([]oauth2.AuthCodeOption, 0, 3)
@@ -167,20 +174,20 @@ func (os *OauthService) LinuxdoProvider() *oidc.Provider {
} }
// GetOauthConfig retrieves the OAuth2 configuration based on the provider name // GetOauthConfig retrieves the OAuth2 configuration based on the provider name
func (os *OauthService) GetOauthConfig(op string) (err error, oauthInfo *model.Oauth, oauthConfig *oauth2.Config, provider *oidc.Provider) { func (os *OauthService) GetOauthConfig(c *gin.Context, op string) (err error, oauthInfo *model.Oauth, oauthConfig *oauth2.Config, provider *oidc.Provider) {
//err, oauthInfo, oauthConfig = os.getOauthConfigGeneral(op) //err, oauthInfo, oauthConfig = os.getOauthConfigGeneral(op)
oauthInfo = os.InfoByOp(op) oauthInfo = os.InfoByOp(op)
if oauthInfo.Id == 0 || oauthInfo.ClientId == "" || oauthInfo.ClientSecret == "" { if oauthInfo.Id == 0 || oauthInfo.ClientId == "" || oauthInfo.ClientSecret == "" {
return errors.New("ConfigNotFound"), nil, nil, nil return errors.New("ConfigNotFound"), nil, nil, nil
} }
// If the redirect URL is empty, use the default redirect URL host := c.GetHeader("Origin")
if oauthInfo.RedirectUrl == "" { if host == "" {
oauthInfo.RedirectUrl = Config.Rustdesk.ApiServer + "/api/oidc/callback" host = Config.Rustdesk.ApiServer
} }
oauthConfig = &oauth2.Config{ oauthConfig = &oauth2.Config{
ClientID: oauthInfo.ClientId, ClientID: oauthInfo.ClientId,
ClientSecret: oauthInfo.ClientSecret, ClientSecret: oauthInfo.ClientSecret,
RedirectURL: oauthInfo.RedirectUrl, RedirectURL: host + "/api/oidc/callback",
} }
// Maybe should validate the oauthConfig here // Maybe should validate the oauthConfig here
@@ -335,8 +342,8 @@ func (os *OauthService) oidcCallback(oauthConfig *oauth2.Config, provider *oidc.
} }
// Callback: Get user information by code and op(Oauth provider) // Callback: Get user information by code and op(Oauth provider)
func (os *OauthService) Callback(code, verifier, op, nonce string) (err error, oauthUser *model.OauthUser) { func (os *OauthService) Callback(c *gin.Context, code, verifier, op, nonce string) (err error, oauthUser *model.OauthUser) {
err, oauthInfo, oauthConfig, provider := os.GetOauthConfig(op) err, oauthInfo, oauthConfig, provider := os.GetOauthConfig(c, op)
// oauthType is already validated in GetOauthConfig // oauthType is already validated in GetOauthConfig
if err != nil { if err != nil {
return err, nil return err, nil