@@ -24,11 +24,19 @@ package connection
24
24
25
25
import (
26
26
"context"
27
+ "encoding/base64"
28
+ "encoding/json"
29
+ "fmt"
27
30
"net/http"
31
+ "strings"
32
+ "time"
28
33
)
29
34
30
35
func NewJWTAuthWrapper (username , password string ) Wrapper {
31
- return WrapAuthentication (func (ctx context.Context , conn Connection ) (authentication Authentication , err error ) {
36
+ var token string
37
+ var expiry time.Time
38
+
39
+ refresh := func (ctx context.Context , conn Connection ) error {
32
40
url := NewUrl ("_open" , "auth" )
33
41
34
42
var data jwtOpenResponse
@@ -40,15 +48,26 @@ func NewJWTAuthWrapper(username, password string) Wrapper {
40
48
41
49
resp , err := CallPost (ctx , conn , url , & data , j )
42
50
if err != nil {
43
- return nil , err
51
+ return err
52
+ }
53
+ if resp .Code () != http .StatusOK {
54
+ return NewError (resp .Code (), "unexpected code" )
44
55
}
45
56
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" )
57
+ token = data .Token
58
+ expiry , _ = parseJWTExpiry (token ) // ignore error, just fallback to immediate refresh next time
59
+ return nil
60
+ }
61
+
62
+ return WrapAuthentication (func (ctx context.Context , conn Connection ) (Authentication , error ) {
63
+ // First time fetch
64
+ if token == "" || time .Now ().After (expiry ) {
65
+ if err := refresh (ctx , conn ); err != nil {
66
+ return nil , err
67
+ }
51
68
}
69
+
70
+ return NewHeaderAuth ("Authorization" , "bearer %s" , token ), nil
52
71
})
53
72
}
54
73
@@ -59,5 +78,68 @@ type jwtOpenRequest struct {
59
78
60
79
type jwtOpenResponse struct {
61
80
Token string `json:"jwt"`
81
+ ExpiresIn int `json:"expires_in,omitempty"`
62
82
MustChangePassword bool `json:"must_change_password,omitempty"`
63
83
}
84
+
85
+ func parseJWTExpiry (token string ) (time.Time , error ) {
86
+ parts := strings .Split (token , "." )
87
+ if len (parts ) < 2 {
88
+ return time.Time {}, fmt .Errorf ("invalid JWT format" )
89
+ }
90
+
91
+ payload , err := base64 .RawURLEncoding .DecodeString (parts [1 ])
92
+ if err != nil {
93
+ return time.Time {}, err
94
+ }
95
+
96
+ var claims struct {
97
+ Exp int64 `json:"exp"`
98
+ }
99
+ if err := json .Unmarshal (payload , & claims ); err != nil {
100
+ return time.Time {}, err
101
+ }
102
+
103
+ return time .Unix (claims .Exp , 0 ), nil
104
+ }
105
+
106
+ func NewSSOAuthWrapper (initialToken string ) Wrapper {
107
+ var token = initialToken
108
+ var expiry time.Time
109
+
110
+ setToken := func (newToken string ) {
111
+ token = newToken
112
+ expiry , _ = parseJWTExpiry (newToken )
113
+ }
114
+
115
+ // If we already have a token (from an SSO login), parse expiry now
116
+ if token != "" {
117
+ setToken (token )
118
+ }
119
+
120
+ return WrapAuthentication (func (ctx context.Context , conn Connection ) (Authentication , error ) {
121
+ // No token yet or expired — let caller know they must login via SSO
122
+ if token == "" || time .Now ().After (expiry ) {
123
+ // Try a call to _open/auth just to see if server sends 307
124
+ url := NewUrl ("_open" , "auth" )
125
+ var data jwtOpenResponse
126
+
127
+ resp , err := CallPost (ctx , conn , url , & data , nil )
128
+ if err != nil {
129
+ return nil , err
130
+ }
131
+
132
+ switch resp .Code () {
133
+ case http .StatusOK :
134
+ setToken (data .Token )
135
+ case http .StatusTemporaryRedirect :
136
+ loc := resp .Header ("Location" )
137
+ return nil , fmt .Errorf ("SSO redirect: please authenticate via browser at %s" , loc )
138
+ default :
139
+ return nil , NewError (resp .Code (), "unexpected code" )
140
+ }
141
+ }
142
+
143
+ return NewHeaderAuth ("Authorization" , "bearer %s" , token ), nil
144
+ })
145
+ }
0 commit comments