Skip to content

Commit 5c9a953

Browse files
Add SSO auth token implementation
1 parent 834d0dc commit 5c9a953

File tree

1 file changed

+89
-7
lines changed

1 file changed

+89
-7
lines changed

v2/connection/auth_jwt_impl.go

Lines changed: 89 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,19 @@ package connection
2424

2525
import (
2626
"context"
27+
"encoding/base64"
28+
"encoding/json"
29+
"fmt"
2730
"net/http"
31+
"strings"
32+
"time"
2833
)
2934

3035
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 {
3240
url := NewUrl("_open", "auth")
3341

3442
var data jwtOpenResponse
@@ -40,15 +48,26 @@ func NewJWTAuthWrapper(username, password string) Wrapper {
4048

4149
resp, err := CallPost(ctx, conn, url, &data, j)
4250
if err != nil {
43-
return nil, err
51+
return err
52+
}
53+
if resp.Code() != http.StatusOK {
54+
return NewError(resp.Code(), "unexpected code")
4455
}
4556

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+
}
5168
}
69+
70+
return NewHeaderAuth("Authorization", "bearer %s", token), nil
5271
})
5372
}
5473

@@ -59,5 +78,68 @@ type jwtOpenRequest struct {
5978

6079
type jwtOpenResponse struct {
6180
Token string `json:"jwt"`
81+
ExpiresIn int `json:"expires_in,omitempty"`
6282
MustChangePassword bool `json:"must_change_password,omitempty"`
6383
}
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

Comments
 (0)