@@ -3,8 +3,11 @@ package provider
33import (
44 "context"
55 "crypto/tls"
6+ "encoding/json"
67 "fmt"
8+ "io"
79 "net/http"
10+ "net/url"
811 "strings"
912 "sync"
1013 "time"
@@ -54,13 +57,17 @@ type HMACValidator struct {
5457
5558// OIDCValidator validates JWT tokens using OIDC/JWKS (Okta, Google, Azure)
5659type OIDCValidator struct {
57- verifier * oidc.IDTokenVerifier
58- provider * oidc.Provider
59- audience string
60- TokenValidators []func (claims jwt.MapClaims ) error
61- logger Logger
60+ verifier * oidc.IDTokenVerifier
61+ provider * oidc.Provider
62+ audience string
63+ providerName string
64+ skipAudienceCheck bool
65+ TokenValidators []func (claims jwt.MapClaims ) error
66+ logger Logger
6267}
6368
69+ var googleTokenInfoURL = "https://oauth2.googleapis.com/tokeninfo"
70+
6471// Initialize sets up the HMAC validator with JWT secret and audience
6572func (v * HMACValidator ) Initialize (cfg * Config ) error {
6673 v .secretOnce .Do (func () {
@@ -173,6 +180,8 @@ func (v *OIDCValidator) Initialize(cfg *Config) error {
173180 v .logger = & noOpLogger {}
174181 }
175182 v .audience = cfg .Audience
183+ v .providerName = strings .ToLower (cfg .Provider )
184+ v .skipAudienceCheck = cfg .SkipAudienceCheck
176185
177186 // Use standard library context with timeout
178187 ctx , cancel := context .WithTimeout (context .Background (), 30 * time .Second )
@@ -226,14 +235,22 @@ func (v *OIDCValidator) Initialize(cfg *Config) error {
226235func (v * OIDCValidator ) ValidateToken (ctx context.Context , tokenString string ) (* User , error ) {
227236 // Remove Bearer prefix if present
228237 tokenString = strings .TrimPrefix (tokenString , "Bearer " )
238+ tokenString = strings .TrimSpace (tokenString )
229239
230240 // Use incoming context with timeout for OIDC provider call
231241 ctx , cancel := context .WithTimeout (ctx , 10 * time .Second )
232242 defer cancel ()
233243
244+ if v .providerName == "google" && ! looksLikeJWT (tokenString ) {
245+ return v .validateGoogleOpaqueToken (ctx , tokenString )
246+ }
247+
234248 // go-oidc handles RSA signature validation, JWKS fetching, and key rotation
235249 idToken , err := v .verifier .Verify (ctx , tokenString )
236250 if err != nil {
251+ if v .providerName == "google" && isMalformedJWTError (err ) {
252+ return v .validateGoogleOpaqueToken (ctx , tokenString )
253+ }
237254 return nil , fmt .Errorf ("token verification failed: %w" , err )
238255 }
239256
@@ -277,6 +294,79 @@ func (v *OIDCValidator) ValidateToken(ctx context.Context, tokenString string) (
277294 }, nil
278295}
279296
297+ func (v * OIDCValidator ) validateGoogleOpaqueToken (ctx context.Context , tokenString string ) (* User , error ) {
298+ endpoint := fmt .Sprintf ("%s?access_token=%s" , googleTokenInfoURL , url .QueryEscape (tokenString ))
299+
300+ req , err := http .NewRequestWithContext (ctx , http .MethodGet , endpoint , nil )
301+ if err != nil {
302+ return nil , fmt .Errorf ("failed to create google tokeninfo request: %w" , err )
303+ }
304+
305+ client := & http.Client {Timeout : 10 * time .Second }
306+ resp , err := client .Do (req )
307+ if err != nil {
308+ return nil , fmt .Errorf ("google tokeninfo request failed: %w" , err )
309+ }
310+ defer func () { _ = resp .Body .Close () }()
311+
312+ body , err := io .ReadAll (resp .Body )
313+ if err != nil {
314+ return nil , fmt .Errorf ("failed reading google tokeninfo response: %w" , err )
315+ }
316+
317+ if resp .StatusCode != http .StatusOK {
318+ return nil , fmt .Errorf ("google tokeninfo validation failed: status %d" , resp .StatusCode )
319+ }
320+
321+ var claims map [string ]interface {}
322+ if err := json .Unmarshal (body , & claims ); err != nil {
323+ return nil , fmt .Errorf ("failed parsing google tokeninfo response: %w" , err )
324+ }
325+
326+ return v .userFromGoogleTokenInfoClaims (claims )
327+ }
328+
329+ func (v * OIDCValidator ) userFromGoogleTokenInfoClaims (claims map [string ]interface {}) (* User , error ) {
330+ aud , _ := claims ["aud" ].(string )
331+ if ! v .skipAudienceCheck {
332+ if aud == "" {
333+ return nil , fmt .Errorf ("missing audience claim" )
334+ }
335+ if aud != v .audience {
336+ return nil , fmt .Errorf ("invalid audience: expected %s, got %s" , v .audience , aud )
337+ }
338+ }
339+
340+ subject , _ := claims ["sub" ].(string )
341+ if subject == "" {
342+ return nil , fmt .Errorf ("missing subject in token" )
343+ }
344+
345+ email , _ := claims ["email" ].(string )
346+ username := email
347+ if username == "" {
348+ username = subject
349+ }
350+
351+ return & User {
352+ Subject : subject ,
353+ Username : username ,
354+ Email : email ,
355+ }, nil
356+ }
357+
358+ func looksLikeJWT (token string ) bool {
359+ return strings .Count (token , "." ) == 2
360+ }
361+
362+ func isMalformedJWTError (err error ) bool {
363+ if err == nil {
364+ return false
365+ }
366+ msg := err .Error ()
367+ return strings .Contains (msg , "malformed jwt" ) || strings .Contains (msg , "compact JWS format must have three parts" )
368+ }
369+
280370// validateAudience validates the audience claim matches the expected value for OIDC tokens
281371func (v * OIDCValidator ) validateAudience (claims jwt.MapClaims ) error {
282372 // Extract audience claim (can be string or []string)
0 commit comments