@@ -19,12 +19,19 @@ import (
1919 "context"
2020 "io"
2121 "net/http"
22+ "net/http/httptest"
2223 "testing"
24+ "time"
2325
26+ conf "github.com/chainloop-dev/chainloop/app/controlplane/internal/conf/controlplane/config/v1"
2427 "github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/attjwtmiddleware"
28+ "github.com/chainloop-dev/chainloop/app/controlplane/pkg/jwt/user"
29+ "github.com/chainloop-dev/chainloop/pkg/cache"
2530 "github.com/go-kratos/kratos/v2/log"
2631 "github.com/go-kratos/kratos/v2/transport"
32+ "github.com/golang-jwt/jwt/v5"
2733 "github.com/stretchr/testify/assert"
34+ "github.com/stretchr/testify/require"
2835)
2936
3037var emptyHandler = func (_ context.Context , _ interface {}) (interface {}, error ) { return nil , nil }
@@ -137,3 +144,52 @@ func TestAttestationAPITokenProvider(t *testing.T) {
137144 })
138145 }
139146}
147+
148+ // newExpiredUserToken builds a Chainloop user token whose expiration is in the
149+ // past, signed with the given key.
150+ func newExpiredUserToken (t * testing.T , key string ) string {
151+ t .Helper ()
152+ token := jwt .NewWithClaims (user .SigningMethod , jwt.RegisteredClaims {
153+ Audience : jwt.ClaimStrings {user .Audience },
154+ ExpiresAt : jwt .NewNumericDate (time .Now ().Add (- 1 * time .Hour )),
155+ IssuedAt : jwt .NewNumericDate (time .Now ().Add (- 2 * time .Hour )),
156+ })
157+ signed , err := token .SignedString ([]byte (key ))
158+ require .NoError (t , err )
159+ return signed
160+ }
161+
162+ // TestExpiredTokenWithFederatedProvider ensures that an expired Chainloop token
163+ // surfaces the proper "token has expired" error even when the federated
164+ // provider is enabled. Otherwise the expiry is masked by the federated path
165+ // (e.g. "no issuers configured"), leaving the user with a misleading message.
166+ func TestExpiredTokenWithFederatedProvider (t * testing.T ) {
167+ // Federated endpoint that mimics the masking behaviour: it rejects the
168+ // expired token with an unrelated authorization error.
169+ federatedSrv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , _ * http.Request ) {
170+ w .WriteHeader (http .StatusUnauthorized )
171+ _ , _ = w .Write ([]byte (`{"code":1,"message":"no issuers configured"}` ))
172+ }))
173+ defer federatedSrv .Close ()
174+
175+ claimsCache , err := cache.New [* jwt.MapClaims ](cache .WithTTL (time .Minute ))
176+ require .NoError (t , err )
177+
178+ logger := log .NewStdLogger (io .Discard )
179+ header := newTokenHeader ("Authorization" , "Bearer " + newExpiredUserToken (t , signingKey ))
180+ ctx := transport .NewServerContext (context .Background (), & mockTransport {reqHeader : header })
181+
182+ m := attjwtmiddleware .WithJWTMulti (logger ,
183+ attjwtmiddleware .NewRobotAccountProvider (signingKey ),
184+ attjwtmiddleware .NewAPITokenProvider (signingKey ),
185+ attjwtmiddleware .NewUserTokenProvider (signingKey ),
186+ attjwtmiddleware .WithFederatedProvider (& conf.FederatedAuthentication {Enabled : true , Url : federatedSrv .URL }),
187+ attjwtmiddleware .WithClaimsCache (claimsCache ),
188+ )
189+
190+ _ , err = m (emptyHandler )(ctx , nil )
191+ require .Error (t , err )
192+ // The expiry must be reported, not the masked federated error.
193+ assert .ErrorContains (t , err , "JWT token has expired" )
194+ assert .NotContains (t , err .Error (), "no issuers configured" )
195+ }
0 commit comments