-
Notifications
You must be signed in to change notification settings - Fork 120
/
token_refresher.go
147 lines (127 loc) · 4.25 KB
/
token_refresher.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
package client
import (
"context"
"errors"
"fmt"
"time"
"connectrpc.com/connect"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/oauth2"
"github.com/akuity/kargo/internal/cli/config"
v1alpha1 "github.com/akuity/kargo/pkg/api/service/v1alpha1"
)
// tokenRefresher is a component that helps to refresh tokens.
type tokenRefresher struct {
// The following behaviors are overridable for testing purposes:
redeemRefreshTokenFn func(
ctx context.Context,
serverAddress string,
refreshToken string,
insecureTLS bool,
) (string, string, error)
saveCLIConfigFn func(cfg config.CLIConfig) error
}
// newTokenRefresher returns a new tokenRefresher.
func newTokenRefresher() *tokenRefresher {
return &tokenRefresher{
redeemRefreshTokenFn: redeemRefreshToken,
saveCLIConfigFn: config.SaveCLIConfig,
}
}
// refreshToken checks the token and refresh token in the provided config. If
// the token is not parsable as a JWT, it will assume the token is not something
// refreshable and return the provided config unmodified. If the token is
// parsable as a JWT and is not expired, it will return the provided config
// unmodified. If the token is expired and no refresh token is available OR TLS
// cert verification is disabled, an error is returned indicating that the user
// must re-authenticate. If a refresh token is available, it will attempt to
// redeem that token and return updated config.
func (t *tokenRefresher) refreshToken(
ctx context.Context,
cfg config.CLIConfig,
insecureTLS bool,
) (config.CLIConfig, error) {
jwtParser := jwt.NewParser(jwt.WithoutClaimsValidation())
var untrustedClaims jwt.RegisteredClaims
if _, _, err :=
jwtParser.ParseUnverified(cfg.BearerToken, &untrustedClaims); err != nil {
// This token isn't a JWT. So it's probably a bearer token for the
// Kubernetes API server. Just return. There's nothing further to do.
return cfg, nil
}
// If we get to here, we're dealing with a JWT. It could have been issued:
//
// 1. Directly by the Kargo API server (in the case of admin)
// 2. By Kargo's OpenID Connect identity provider
// 3. By the Kubernetes cluster's identity provider
// 4. By Kubernetes itself (a service account token, perhaps)
if untrustedClaims.ExpiresAt == nil || time.Now().Before(untrustedClaims.ExpiresAt.Time) {
// Token doesn't expire (possible for case 4) or hasn't yet. There's nothing
// further to do.
return cfg, nil
}
// If we get to here, the token is expired.
if cfg.InsecureSkipTLSVerify || cfg.RefreshToken == "" {
// We don't have a refresh token OR TLS cert verification is disabled. We'll
// prompt the user to re-authenticate.
return cfg, errors.New(
"your token is expired; please use `kargo login` to re-authenticate",
)
}
var err error
if cfg.BearerToken, cfg.RefreshToken, err = t.redeemRefreshTokenFn(
ctx,
cfg.APIAddress,
cfg.RefreshToken,
insecureTLS,
); err != nil {
return cfg, errors.New(
"error refreshing token; please use `kargo login` to re-authenticate",
)
}
// Save and return the updated config
return cfg, t.saveCLIConfigFn(cfg)
}
// redeemRefreshToken redeems the provided refresh token for a new ID token and
// refresh token.
func redeemRefreshToken(
ctx context.Context,
serverAddress string,
refreshToken string,
insecureTLS bool,
) (string, string, error) {
client := GetClient(serverAddress, "", insecureTLS)
res, err := client.GetPublicConfig(
ctx,
connect.NewRequest(&v1alpha1.GetPublicConfigRequest{}),
)
if err != nil {
return "", "", fmt.Errorf("error retrieving public configuration from server: %w", err)
}
if res.Msg.OidcConfig == nil {
return "", "", errors.New("server does not support OpenID Connect")
}
provider, err := oidc.NewProvider(ctx, res.Msg.OidcConfig.IssuerUrl)
if err != nil {
return "", "", fmt.Errorf("error initializing OIDC provider: %w", err)
}
cfg := oauth2.Config{
ClientID: res.Msg.OidcConfig.ClientId,
Endpoint: provider.Endpoint(),
}
token, err := cfg.TokenSource(
ctx,
&oauth2.Token{
RefreshToken: refreshToken,
},
).Token()
if err != nil {
return "", "", err
}
idToken, ok := token.Extra("id_token").(string)
if !ok {
return "", "", errors.New("no id_token in token response")
}
return idToken, token.RefreshToken, nil
}