11package handles
22
33import (
4+ "crypto/sha256"
45 "encoding/base64"
56 "errors"
67 "fmt"
@@ -22,12 +23,25 @@ import (
2223 "github.com/coreos/go-oidc"
2324 "github.com/gin-gonic/gin"
2425 "github.com/go-resty/resty/v2"
26+ "github.com/golang-jwt/jwt/v4"
2527 "golang.org/x/oauth2"
2628 "gorm.io/gorm"
2729)
2830
2931const stateLength = 16
3032const stateExpire = time .Minute * 5
33+ const ssoBindingExpire = time .Minute * 5
34+ const ssoBindingCookie = "openlist_sso_binding"
35+ const ssoBindingStatePurpose = "sso_binding_state"
36+ const ssoBindingProofPurpose = "sso_binding_proof"
37+
38+ type ssoBindingClaims struct {
39+ Purpose string `json:"purpose"`
40+ Method string `json:"method"`
41+ SessionDigest string `json:"session_digest"`
42+ SsoID string `json:"sso_id,omitempty"`
43+ jwt.RegisteredClaims
44+ }
3145
3246var stateCache = cache.NewMemCache [string ](cache.WithShards [string ](stateLength ))
3347
@@ -46,6 +60,68 @@ func verifyState(clientID, ip, state string) bool {
4660 return ok && value == ip
4761}
4862
63+ func ssoBindingSessionDigest (session string ) string {
64+ digest := sha256 .Sum256 ([]byte (session ))
65+ return base64 .RawURLEncoding .EncodeToString (digest [:])
66+ }
67+
68+ func parseSSOBindingToken (c * gin.Context , rawToken , purpose string ) (* ssoBindingClaims , error ) {
69+ claims := & ssoBindingClaims {}
70+ token , err := jwt .ParseWithClaims (rawToken , claims , func (token * jwt.Token ) (interface {}, error ) {
71+ if token .Method != jwt .SigningMethodHS256 {
72+ return nil , errors .New ("invalid SSO binding token algorithm" )
73+ }
74+ return common .SecretKey , nil
75+ })
76+ if err != nil || ! token .Valid || claims .ExpiresAt == nil || claims .Purpose != purpose ||
77+ claims .Method != "get_sso_id" || len (claims .SessionDigest ) != 43 {
78+ return nil , errors .New ("invalid or expired SSO binding token" )
79+ }
80+ if (purpose == ssoBindingStatePurpose && claims .SsoID != "" ) ||
81+ (purpose == ssoBindingProofPurpose && claims .SsoID == "" ) {
82+ return nil , errors .New ("invalid SSO binding token payload" )
83+ }
84+ session , err := c .Cookie (ssoBindingCookie )
85+ if err != nil || ssoBindingSessionDigest (session ) != claims .SessionDigest {
86+ return nil , errors .New ("invalid SSO binding session" )
87+ }
88+ return claims , nil
89+ }
90+
91+ func generateSSOBindingToken (c * gin.Context , purpose , ssoID string ) (string , error ) {
92+ session , err := c .Cookie (ssoBindingCookie )
93+ expire := ssoBindingExpire
94+ if purpose == ssoBindingStatePurpose {
95+ session = random .String (32 )
96+ expire = stateExpire
97+ c .SetSameSite (http .SameSiteLaxMode )
98+ c .SetCookie (
99+ ssoBindingCookie ,
100+ session ,
101+ int ((stateExpire + ssoBindingExpire ).Seconds ()),
102+ path .Join (conf .URL .Path , "/api" ),
103+ "" ,
104+ strings .HasPrefix (common .GetApiUrl (c ), "https://" ),
105+ true ,
106+ )
107+ } else if err != nil {
108+ return "" , errors .New ("missing SSO binding session" )
109+ }
110+ now := time .Now ()
111+ claims := ssoBindingClaims {
112+ Purpose : purpose ,
113+ Method : "get_sso_id" ,
114+ SessionDigest : ssoBindingSessionDigest (session ),
115+ SsoID : ssoID ,
116+ RegisteredClaims : jwt.RegisteredClaims {
117+ ExpiresAt : jwt .NewNumericDate (now .Add (expire )),
118+ IssuedAt : jwt .NewNumericDate (now ),
119+ NotBefore : jwt .NewNumericDate (now ),
120+ },
121+ }
122+ return jwt .NewWithClaims (jwt .SigningMethodHS256 , claims ).SignedString (common .SecretKey )
123+ }
124+
49125func ssoRedirectUri (c * gin.Context , useCompatibility bool , method string ) string {
50126 if useCompatibility {
51127 return common .GetApiUrl (c ) + "/api/auth/" + method
@@ -74,6 +150,16 @@ func SSOLoginRedirect(c *gin.Context) {
74150 urlValues .Add ("response_type" , "code" )
75151 urlValues .Add ("redirect_uri" , redirectUri )
76152 urlValues .Add ("client_id" , clientId )
153+ bindingState := ""
154+ var err error
155+ if method == "get_sso_id" {
156+ bindingState , err = generateSSOBindingToken (c , ssoBindingStatePurpose , "" )
157+ if err != nil {
158+ common .ErrorResp (c , err , 500 )
159+ return
160+ }
161+ urlValues .Add ("state" , bindingState )
162+ }
77163 switch platform {
78164 case "Github" :
79165 rUrl = "https://github.com/login/oauth/authorize?"
@@ -94,15 +180,19 @@ func SSOLoginRedirect(c *gin.Context) {
94180 endpoint := strings .TrimSuffix (setting .GetStr (conf .SSOEndpointName ), "/" )
95181 rUrl = endpoint + "/login/oauth/authorize?"
96182 urlValues .Add ("scope" , "profile" )
97- urlValues .Add ("state" , endpoint )
183+ if bindingState == "" {
184+ urlValues .Add ("state" , endpoint )
185+ }
98186 case "OIDC" :
99187 oauth2Config , err := GetOIDCClient (c , useCompatibility , redirectUri , method )
100188 if err != nil {
101189 common .ErrorStrResp (c , err .Error (), 400 )
102190 return
103191 }
104- state := generateState (clientId , c .ClientIP ())
105- c .Redirect (http .StatusFound , oauth2Config .AuthCodeURL (state ))
192+ if bindingState == "" {
193+ bindingState = generateState (clientId , c .ClientIP ())
194+ }
195+ c .Redirect (http .StatusFound , oauth2Config .AuthCodeURL (bindingState ))
106196 return
107197 default :
108198 common .ErrorStrResp (c , "invalid platform" , 400 )
@@ -201,11 +291,15 @@ func OIDCLoginCallback(c *gin.Context) {
201291 common .ErrorResp (c , err , 400 )
202292 return
203293 }
204- if ! verifyState (clientId , c .ClientIP (), c .Query ("state" )) {
294+ if method == "get_sso_id" {
295+ if _ , err := parseSSOBindingToken (c , c .Query ("state" ), ssoBindingStatePurpose ); err != nil {
296+ common .ErrorStrResp (c , "incorrect or expired state parameter" , 400 )
297+ return
298+ }
299+ } else if ! verifyState (clientId , c .ClientIP (), c .Query ("state" )) {
205300 common .ErrorStrResp (c , "incorrect or expired state parameter" , 400 )
206301 return
207302 }
208-
209303 oauth2Token , err := oauth2Config .Exchange (c , c .Query ("code" ))
210304 if err != nil {
211305 common .ErrorResp (c , err , 400 )
@@ -235,8 +329,13 @@ func OIDCLoginCallback(c *gin.Context) {
235329 return
236330 }
237331 if method == "get_sso_id" {
332+ bindingProof , err := generateSSOBindingToken (c , ssoBindingProofPurpose , userID )
333+ if err != nil {
334+ common .ErrorResp (c , err , 500 )
335+ return
336+ }
238337 if useCompatibility {
239- c .Redirect (302 , common .GetApiUrl (c )+ "/@manage?sso_id=" + userID )
338+ c .Redirect (302 , common .GetApiUrl (c )+ "/@manage?sso_id=" + bindingProof )
240339 return
241340 }
242341 html := fmt .Sprintf (`<!DOCTYPE html>
@@ -246,7 +345,7 @@ func OIDCLoginCallback(c *gin.Context) {
246345 window.opener.postMessage({"sso_id": "%s"}, "*")
247346 window.close()
248347 </script>
249- </body>` , userID )
348+ </body>` , bindingProof )
250349 c .Data (200 , "text/html; charset=utf-8" , []byte (html ))
251350 return
252351 }
@@ -352,6 +451,12 @@ func SSOLoginCallback(c *gin.Context) {
352451 common .ErrorStrResp (c , "No code provided" , 400 )
353452 return
354453 }
454+ if argument == "get_sso_id" {
455+ if _ , err := parseSSOBindingToken (c , c .Query ("state" ), ssoBindingStatePurpose ); err != nil {
456+ common .ErrorStrResp (c , "incorrect or expired state parameter" , 400 )
457+ return
458+ }
459+ }
355460 var resp * resty.Response
356461 var err error
357462 if platform == "Dingtalk" {
@@ -402,8 +507,13 @@ func SSOLoginCallback(c *gin.Context) {
402507 return
403508 }
404509 if argument == "get_sso_id" {
510+ bindingProof , err := generateSSOBindingToken (c , ssoBindingProofPurpose , userID )
511+ if err != nil {
512+ common .ErrorResp (c , err , 500 )
513+ return
514+ }
405515 if usecompatibility {
406- c .Redirect (302 , common .GetApiUrl (c )+ "/@manage?sso_id=" + userID )
516+ c .Redirect (302 , common .GetApiUrl (c )+ "/@manage?sso_id=" + bindingProof )
407517 return
408518 }
409519 html := fmt .Sprintf (`<!DOCTYPE html>
@@ -413,7 +523,7 @@ func SSOLoginCallback(c *gin.Context) {
413523 window.opener.postMessage({"sso_id": "%s"}, "*")
414524 window.close()
415525 </script>
416- </body>` , userID )
526+ </body>` , bindingProof )
417527 c .Data (200 , "text/html; charset=utf-8" , []byte (html ))
418528 return
419529 }
0 commit comments