@@ -24,11 +24,20 @@ package connection
24
24
25
25
import (
26
26
"context"
27
+ "encoding/base64"
28
+ "encoding/json"
29
+ "fmt"
30
+ "log"
27
31
"net/http"
32
+ "strings"
33
+ "time"
28
34
)
29
35
30
36
func 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 {
32
41
url := NewUrl ("_open" , "auth" )
33
42
34
43
var data jwtOpenResponse
@@ -40,15 +49,31 @@ func NewJWTAuthWrapper(username, password string) Wrapper {
40
49
41
50
resp , err := CallPost (ctx , conn , url , & data , j )
42
51
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
44
64
}
65
+ return nil
66
+ }
45
67
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
+ }
51
74
}
75
+
76
+ return NewHeaderAuth ("Authorization" , "bearer %s" , token ), nil
52
77
})
53
78
}
54
79
@@ -59,5 +84,77 @@ type jwtOpenRequest struct {
59
84
60
85
type jwtOpenResponse struct {
61
86
Token string `json:"jwt"`
87
+ ExpiresIn int `json:"expires_in,omitempty"`
62
88
MustChangePassword bool `json:"must_change_password,omitempty"`
63
89
}
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