@@ -24,11 +24,20 @@ package connection
2424
2525import (
2626 "context"
27+ "encoding/base64"
28+ "encoding/json"
29+ "fmt"
30+ "log"
2731 "net/http"
32+ "strings"
33+ "time"
2834)
2935
3036func NewJWTAuthWrapper (username , password string ) Wrapper {
31- return WrapAuthentication (func (ctx context.Context , conn Connection ) (authentication Authentication , err error ) {
37+ var token string
38+ var expiry time.Time
39+
40+ refresh := func (ctx context.Context , conn Connection ) error {
3241 url := NewUrl ("_open" , "auth" )
3342
3443 var data jwtOpenResponse
@@ -40,15 +49,31 @@ func NewJWTAuthWrapper(username, password string) Wrapper {
4049
4150 resp , err := CallPost (ctx , conn , url , & data , j )
4251 if err != nil {
43- return nil , err
52+ return err
53+ }
54+ if resp .Code () != http .StatusOK {
55+ return NewError (resp .Code (), "unexpected code" )
56+ }
57+
58+ token = data .Token
59+ expiry , err = parseJWTExpiry (token )
60+ if err != nil {
61+ // Log for visibility but don't break functionality
62+ log .Printf ("failed to parse JWT expiry: %v" , err )
63+ expiry = time .Now ().Add (1 * time .Minute ) // fallback, so it will refresh immediately next time
4464 }
65+ return nil
66+ }
4567
46- switch resp .Code () {
47- case http .StatusOK :
48- return NewHeaderAuth ("Authorization" , "bearer %s" , data .Token ), nil
49- default :
50- return nil , NewError (resp .Code (), "unexpected code" )
68+ return WrapAuthentication (func (ctx context.Context , conn Connection ) (Authentication , error ) {
69+ // First time fetch
70+ if token == "" || time .Now ().After (expiry ) {
71+ if err := refresh (ctx , conn ); err != nil {
72+ return nil , err
73+ }
5174 }
75+
76+ return NewHeaderAuth ("Authorization" , "bearer %s" , token ), nil
5277 })
5378}
5479
@@ -59,5 +84,77 @@ type jwtOpenRequest struct {
5984
6085type jwtOpenResponse struct {
6186 Token string `json:"jwt"`
87+ ExpiresIn int `json:"expires_in,omitempty"`
6288 MustChangePassword bool `json:"must_change_password,omitempty"`
6389}
90+
91+ func parseJWTExpiry (token string ) (time.Time , error ) {
92+ parts := strings .Split (token , "." )
93+ if len (parts ) < 2 {
94+ return time.Time {}, fmt .Errorf ("invalid JWT format" )
95+ }
96+
97+ payload , err := base64 .RawURLEncoding .DecodeString (parts [1 ])
98+ if err != nil {
99+ return time.Time {}, err
100+ }
101+
102+ var claims struct {
103+ Exp int64 `json:"exp"`
104+ }
105+ if err := json .Unmarshal (payload , & claims ); err != nil {
106+ return time.Time {}, err
107+ }
108+
109+ return time .Unix (claims .Exp , 0 ), nil
110+ }
111+
112+ func NewSSOAuthWrapper (initialToken string ) Wrapper {
113+ var token = initialToken
114+ var expiry time.Time
115+ // setToken updates the current JWT and its expiry time.
116+ // If expiry parsing fails, we log the error and fall back to a short 1-minute lifetime.
117+ // This ensures the token will be refreshed soon without breaking functionality.
118+ setToken := func (newToken string ) {
119+ token = newToken
120+ expiryTime , err := parseJWTExpiry (newToken )
121+ if err != nil {
122+ // Log for visibility but don't break functionality
123+ log .Printf ("failed to parse JWT expiry: %v" , err )
124+ expiry = time .Now ().Add (1 * time .Minute ) // fallback, so it will refresh immediately next time
125+ } else {
126+ expiry = expiryTime
127+ }
128+ }
129+
130+ // If we already have a token (from an SSO login), parse expiry now
131+ if token != "" {
132+ setToken (token )
133+ }
134+
135+ return WrapAuthentication (func (ctx context.Context , conn Connection ) (Authentication , error ) {
136+ // No token yet or expired — let caller know they must login via SSO
137+ if token == "" || time .Now ().After (expiry ) {
138+ // Try a call to _open/auth just to see if server sends 307
139+ url := NewUrl ("_open" , "auth" )
140+ var data jwtOpenResponse
141+ // Intentionally passing nil: in SSO mode, /_open/auth expects no body
142+ resp , err := CallPost (ctx , conn , url , & data , nil )
143+ if err != nil {
144+ return nil , err
145+ }
146+
147+ switch resp .Code () {
148+ case http .StatusOK :
149+ setToken (data .Token )
150+ case http .StatusTemporaryRedirect :
151+ loc := resp .Header ("Location" )
152+ return nil , fmt .Errorf ("SSO redirect: please authenticate via browser at %s" , loc )
153+ default :
154+ return nil , NewError (resp .Code (), "unexpected code" )
155+ }
156+ }
157+
158+ return NewHeaderAuth ("Authorization" , "bearer %s" , token ), nil
159+ })
160+ }
0 commit comments