11package main
22
33import (
4+ "context"
5+ "crypto/tls"
6+ "crypto/x509"
47 "encoding/base64"
58 "encoding/json"
69 "flag"
@@ -13,10 +16,13 @@ import (
1316 "os"
1417 "path"
1518 "strings"
19+ "sync"
1620 "time"
1721
18- "github.com/golang-jwt/jwt/v5"
1922 "github.com/gorilla/securecookie"
23+ "github.com/lestrrat-go/httprc/v3"
24+ "github.com/lestrrat-go/jwx/v3/jwk"
25+ "github.com/lestrrat-go/jwx/v3/jwt"
2026)
2127
2228/* ----------------------------- flags ------------------------------------ */
2632 cookieName , cookieSecretB64 string
2733 cookieSecure bool
2834 cookieRefresh time.Duration
29- tokenCheckURL string
35+ jwksURL string
36+ saTokenPath string
37+ saCACertPath string
3038)
3139
3240func init () {
@@ -38,7 +46,70 @@ func init() {
3846 flag .StringVar (& cookieSecretB64 , "cookie-secret" , "" , "Base64-encoded cookie secret" )
3947 flag .BoolVar (& cookieSecure , "cookie-secure" , false , "Set Secure flag on cookie" )
4048 flag .DurationVar (& cookieRefresh , "cookie-refresh" , 0 , "Cookie refresh interval (e.g. 1h)" )
41- flag .StringVar (& tokenCheckURL , "token-check-url" , "" , "URL for external token validation" )
49+ flag .StringVar (& jwksURL , "jwks-url" , "https://kubernetes.default.svc/openid/v1/jwks" , "JWKS URL for token verification" )
50+ flag .StringVar (& saTokenPath , "sa-token-path" , "/var/run/secrets/kubernetes.io/serviceaccount/token" , "Path to service account token" )
51+ flag .StringVar (& saCACertPath , "sa-ca-cert-path" , "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" , "Path to service account CA certificate" )
52+
53+ flag .Parse ()
54+
55+ // Initialize jwkCache
56+ ctx := context .Background ()
57+ // Load CA certificate
58+ caCert , err := os .ReadFile (saCACertPath )
59+ if err != nil {
60+ jwkCacheErr := fmt .Errorf ("failed to read CA cert: %w" , err )
61+ panic (jwkCacheErr )
62+ }
63+ caCertPool := x509 .NewCertPool ()
64+ if ! caCertPool .AppendCertsFromPEM (caCert ) {
65+ jwkCacheErr := fmt .Errorf ("failed to parse CA cert" )
66+ panic (jwkCacheErr )
67+ }
68+
69+ // Create transport with SA token injection
70+ transport := & saTokenTransport {
71+ base : & http.Transport {
72+ TLSClientConfig : & tls.Config {
73+ RootCAs : caCertPool ,
74+ },
75+ },
76+ tokenPath : saTokenPath ,
77+ }
78+ transport .startRefresh (ctx , 5 * time .Minute )
79+
80+ httpClient := & http.Client {
81+ Transport : transport ,
82+ Timeout : 10 * time .Second ,
83+ }
84+
85+ // Create httprc client with custom HTTP client
86+ httprcClient := httprc .NewClient (
87+ httprc .WithHTTPClient (httpClient ),
88+ )
89+
90+ // Create JWK cache
91+ jwkCache , err = jwk .NewCache (ctx , httprcClient )
92+ if err != nil {
93+ jwkCacheErr := fmt .Errorf ("failed to create JWK cache: %w" , err )
94+ panic (jwkCacheErr )
95+ }
96+
97+ // Register the JWKS URL with refresh settings
98+ if err := jwkCache .Register (ctx , jwksURL ,
99+ jwk .WithMinInterval (5 * time .Minute ),
100+ jwk .WithMaxInterval (15 * time .Minute ),
101+ ); err != nil {
102+ jwkCacheErr := fmt .Errorf ("failed to register JWKS URL: %w" , err )
103+ panic (jwkCacheErr )
104+ }
105+
106+ // Perform initial fetch to ensure the JWKS is available
107+ if _ , err := jwkCache .Refresh (ctx , jwksURL ); err != nil {
108+ jwkCacheErr := fmt .Errorf ("failed to fetch initial JWKS: %w" , err )
109+ panic (jwkCacheErr )
110+ }
111+
112+ log .Printf ("JWK cache initialized with JWKS URL: %s" , jwksURL )
42113}
43114
44115/* ----------------------------- templates -------------------------------- */
@@ -117,42 +188,94 @@ var loginTmpl = template.Must(template.New("login").Parse(`
117188</body>
118189</html>` ))
119190
191+ /* ----------------------------- JWK cache -------------------------------- */
192+
193+ var (
194+ jwkCache * jwk.Cache
195+ )
196+
197+ // saTokenTransport adds the service account token to requests and refreshes it periodically.
198+ type saTokenTransport struct {
199+ base http.RoundTripper
200+ tokenPath string
201+ mu sync.RWMutex
202+ token string
203+ }
204+
205+ func (t * saTokenTransport ) RoundTrip (req * http.Request ) (* http.Response , error ) {
206+ t .mu .RLock ()
207+ token := t .token
208+ t .mu .RUnlock ()
209+
210+ if token != "" {
211+ req = req .Clone (req .Context ())
212+ req .Header .Set ("Authorization" , "Bearer " + token )
213+ }
214+ return t .base .RoundTrip (req )
215+ }
216+
217+ func (t * saTokenTransport ) refreshToken () {
218+ data , err := os .ReadFile (t .tokenPath )
219+ if err != nil {
220+ log .Printf ("warning: failed to read SA token: %v" , err )
221+ return
222+ }
223+ t .mu .Lock ()
224+ t .token = string (data )
225+ t .mu .Unlock ()
226+ }
227+
228+ func (t * saTokenTransport ) startRefresh (ctx context.Context , interval time.Duration ) {
229+ t .refreshToken ()
230+ go func () {
231+ ticker := time .NewTicker (interval )
232+ defer ticker .Stop ()
233+ for {
234+ select {
235+ case <- ctx .Done ():
236+ return
237+ case <- ticker .C :
238+ t .refreshToken ()
239+ }
240+ }
241+ }()
242+ }
243+
120244/* ----------------------------- helpers ---------------------------------- */
121245
122- func decodeJWT (raw string ) jwt.MapClaims {
246+ // verifyAndParseJWT verifies the token signature and returns the parsed token.
247+ func verifyAndParseJWT (ctx context.Context , raw string ) (jwt.Token , error ) {
123248 if raw == "" {
124- return jwt. MapClaims {}
249+ return nil , fmt . Errorf ( "empty token" )
125250 }
126- tkn , _ , err := new (jwt.Parser ).ParseUnverified (raw , jwt.MapClaims {})
127- if err != nil || tkn == nil {
128- return jwt.MapClaims {}
251+
252+ keySet , err := jwkCache .Lookup (ctx , jwksURL )
253+ if err != nil {
254+ return nil , fmt .Errorf ("failed to get JWKS: %w" , err )
129255 }
130- if c , ok := tkn .Claims .(jwt.MapClaims ); ok {
131- return c
256+
257+ token , err := jwt .Parse ([]byte (raw ), jwt .WithKeySet (keySet ))
258+ if err != nil {
259+ return nil , fmt .Errorf ("failed to verify token: %w" , err )
132260 }
133- return jwt.MapClaims {}
261+
262+ return token , nil
134263}
135264
136- func externalTokenCheck (raw string ) error {
137- if tokenCheckURL == "" {
265+ // getClaim extracts a claim value from a verified token.
266+ func getClaim (token jwt.Token , key string ) any {
267+ if token == nil {
138268 return nil
139269 }
140- req , _ := http .NewRequest (http .MethodGet , tokenCheckURL , nil )
141- req .Header .Set ("Authorization" , "Bearer " + raw )
142- cli := & http.Client {Timeout : 5 * time .Second }
143- resp , err := cli .Do (req )
144- if err != nil {
145- return err
146- }
147- resp .Body .Close ()
148- if resp .StatusCode != http .StatusOK {
149- return fmt .Errorf ("status %d" , resp .StatusCode )
270+ var val any
271+ if err := token .Get (key , & val ); err != nil {
272+ return nil
150273 }
151- return nil
274+ return val
152275}
153276
154277func encodeSession (sc * securecookie.SecureCookie , token string , exp , issued int64 ) (string , error ) {
155- v := map [string ]interface {} {
278+ v := map [string ]any {
156279 "access_token" : token ,
157280 "expires" : exp ,
158281 "issued" : issued ,
@@ -166,7 +289,6 @@ func encodeSession(sc *securecookie.SecureCookie, token string, exp, issued int6
166289/* ----------------------------- main ------------------------------------- */
167290
168291func main () {
169- flag .Parse ()
170292 if upstream == "" {
171293 log .Fatal ("--upstream is required" )
172294 }
@@ -214,7 +336,11 @@ func main() {
214336 }{Action : signIn , Err : "Token required" })
215337 return
216338 }
217- if err := externalTokenCheck (token ); err != nil {
339+
340+ // Verify token signature using JWKS
341+ verifiedToken , err := verifyAndParseJWT (r .Context (), token )
342+ if err != nil {
343+ log .Printf ("token verification failed: %v" , err )
218344 _ = loginTmpl .Execute (w , struct {
219345 Action string
220346 Err string
@@ -223,9 +349,8 @@ func main() {
223349 }
224350
225351 exp := time .Now ().Add (24 * time .Hour ).Unix ()
226- claims := decodeJWT (token )
227- if v , ok := claims ["exp" ].(float64 ); ok {
228- exp = int64 (v )
352+ if expTime , ok := verifiedToken .Expiration (); ok && ! expTime .IsZero () {
353+ exp = expTime .Unix ()
229354 }
230355 session , _ := encodeSession (sc , token , exp , time .Now ().Unix ())
231356 http .SetCookie (w , & http.Cookie {
@@ -264,7 +389,7 @@ func main() {
264389 return
265390 }
266391 var token string
267- var sess map [string ]interface {}
392+ var sess map [string ]any
268393 if sc != nil {
269394 if err := sc .Decode (cookieName , c .Value , & sess ); err != nil {
270395 http .Error (w , "unauthorized" , http .StatusUnauthorized )
@@ -273,19 +398,25 @@ func main() {
273398 token , _ = sess ["access_token" ].(string )
274399 } else {
275400 token = c .Value
276- sess = map [string ]interface {} {
401+ sess = map [string ]any {
277402 "expires" : time .Now ().Add (24 * time .Hour ).Unix (),
278403 "issued" : time .Now ().Unix (),
279404 }
280405 }
281- claims := decodeJWT (token )
282406
283- out := map [string ]interface {}{
407+ // Re-verify the token to ensure it's still valid
408+ verifiedToken , err := verifyAndParseJWT (r .Context (), token )
409+ if err != nil {
410+ http .Error (w , "unauthorized" , http .StatusUnauthorized )
411+ return
412+ }
413+
414+ out := map [string ]any {
284415 "token" : token ,
285- "sub" : claims [ "sub" ] ,
286- "email" : claims [ "email" ] ,
287- "preferred_username" : claims [ "preferred_username" ] ,
288- "groups" : claims [ "groups" ] ,
416+ "sub" : getClaim ( verifiedToken , "sub" ) ,
417+ "email" : getClaim ( verifiedToken , "email" ) ,
418+ "preferred_username" : getClaim ( verifiedToken , "preferred_username" ) ,
419+ "groups" : getClaim ( verifiedToken , "groups" ) ,
289420 "expires" : sess ["expires" ],
290421 "issued" : sess ["issued" ],
291422 "cookie_refresh_enable" : cookieRefresh > 0 ,
@@ -303,7 +434,7 @@ func main() {
303434 return
304435 }
305436 var token string
306- var sess map [string ]interface {}
437+ var sess map [string ]any
307438 if sc != nil {
308439 if err := sc .Decode (cookieName , c .Value , & sess ); err != nil {
309440 http .Redirect (w , r , signIn , http .StatusFound )
@@ -312,7 +443,7 @@ func main() {
312443 token , _ = sess ["access_token" ].(string )
313444 } else {
314445 token = c .Value
315- sess = map [string ]interface {} {
446+ sess = map [string ]any {
316447 "expires" : time .Now ().Add (24 * time .Hour ).Unix (),
317448 "issued" : time .Now ().Unix (),
318449 }
0 commit comments