@@ -2,8 +2,10 @@ package server
22
33import (
44 "bytes"
5+ "context"
56 "encoding/base64"
67 "encoding/json"
8+ "errors"
79 "log/slog"
810 "net/http"
911 "net/http/httptest"
@@ -16,6 +18,7 @@ import (
1618 "github.com/stretchr/testify/assert"
1719 "github.com/stretchr/testify/require"
1820
21+ "github.com/dexidp/dex/connector"
1922 "github.com/dexidp/dex/server/internal"
2023 "github.com/dexidp/dex/storage"
2124)
@@ -276,17 +279,17 @@ func TestRefreshTokenAuthTime(t *testing.T) {
276279 mockRefreshTokenTestStorage (t , s .storage , false )
277280
278281 if tc .createUserIdentity {
279- // The mock connector returns UserID "0-385-28089-0" on Refresh,
280- // so the UserIdentity must use that ID to be found by handleRefreshToken .
282+ // UserIdentity must match the refresh token's Claims. UserID ("1")
283+ // because updateRefreshToken looks it up by that ID.
281284 err := s .storage .CreateUserIdentity (t .Context (), storage.UserIdentity {
282- UserID : "0-385-28089-0 " ,
285+ UserID : "1 " ,
283286 ConnectorID : "test" ,
284287 Claims : storage.Claims {
285- UserID : "0-385-28089-0 " ,
286- Username : "Kilgore Trout " ,
287- Email : "kilgore@kilgore.trout " ,
288+ UserID : "1 " ,
289+ Username : "jane " ,
290+ Email : "jane.doe@example.com " ,
288291 EmailVerified : true ,
289- Groups : []string {"authors " },
292+ Groups : []string {"a" , "b " },
290293 },
291294 CreatedAt : loginTime ,
292295 LastLogin : loginTime ,
@@ -345,6 +348,145 @@ func TestRefreshTokenAuthTime(t *testing.T) {
345348 }
346349}
347350
351+ // failingRefreshConnector implements connector.CallbackConnector and connector.RefreshConnector
352+ // but always returns an error on Refresh, proving that the upstream is not contacted.
353+ type failingRefreshConnector struct {
354+ identity connector.Identity
355+ }
356+
357+ func (f * failingRefreshConnector ) LoginURL (_ connector.Scopes , callbackURL , state string ) (string , []byte , error ) {
358+ u , _ := url .Parse (callbackURL )
359+ v := u .Query ()
360+ v .Set ("state" , state )
361+ u .RawQuery = v .Encode ()
362+ return u .String (), nil , nil
363+ }
364+
365+ func (f * failingRefreshConnector ) HandleCallback (_ connector.Scopes , _ []byte , _ * http.Request ) (connector.Identity , error ) {
366+ return f .identity , nil
367+ }
368+
369+ func (f * failingRefreshConnector ) Refresh (_ context.Context , _ connector.Scopes , _ connector.Identity ) (connector.Identity , error ) {
370+ return connector.Identity {}, errors .New ("upstream: refresh token expired" )
371+ }
372+
373+ func TestRefreshDisconnectsUpstreamWhenSessionsEnabled (t * testing.T ) {
374+ t0 := time .Now ().UTC ().Round (time .Second )
375+ loginTime := t0 .Add (- 10 * time .Minute )
376+
377+ tests := []struct {
378+ name string
379+ sessionsEnabled bool
380+ createUserIdentity bool
381+ wantOK bool
382+ }{
383+ {
384+ name : "sessions enabled - uses user identity, skips upstream" ,
385+ sessionsEnabled : true ,
386+ createUserIdentity : true ,
387+ wantOK : true ,
388+ },
389+ {
390+ name : "sessions enabled without user identity - fails" ,
391+ sessionsEnabled : true ,
392+ createUserIdentity : false ,
393+ wantOK : false ,
394+ },
395+ {
396+ name : "sessions disabled - upstream failure returns error" ,
397+ sessionsEnabled : false ,
398+ createUserIdentity : false ,
399+ wantOK : false ,
400+ },
401+ }
402+
403+ for _ , tc := range tests {
404+ t .Run (tc .name , func (t * testing.T ) {
405+ httpServer , s := newTestServer (t , func (c * Config ) {
406+ c .Now = func () time.Time { return t0 }
407+ })
408+ defer httpServer .Close ()
409+
410+ if tc .sessionsEnabled {
411+ s .sessionConfig = & SessionConfig {
412+ CookieName : "dex_session" ,
413+ AbsoluteLifetime : 24 * time .Hour ,
414+ }
415+ }
416+
417+ mockRefreshTokenTestStorage (t , s .storage , false )
418+
419+ // Replace the connector with one that always fails on Refresh.
420+ // When sessions are enabled this connector should never be called;
421+ // when sessions are disabled, the failure proves the error path works.
422+ s .mu .Lock ()
423+ s .connectors ["test" ] = Connector {
424+ Connector : & failingRefreshConnector {
425+ identity : connector.Identity {
426+ UserID : "0-385-28089-0" ,
427+ Username : "Kilgore Trout" ,
428+ Email : "kilgore@kilgore.trout" ,
429+ },
430+ },
431+ }
432+ s .mu .Unlock ()
433+
434+ if tc .createUserIdentity {
435+ err := s .storage .CreateUserIdentity (t .Context (), storage.UserIdentity {
436+ UserID : "1" ,
437+ ConnectorID : "test" ,
438+ Claims : storage.Claims {
439+ UserID : "1" ,
440+ Username : "jane" ,
441+ Email : "jane.doe@example.com" ,
442+ EmailVerified : true ,
443+ Groups : []string {"a" , "b" },
444+ },
445+ CreatedAt : loginTime ,
446+ LastLogin : loginTime ,
447+ })
448+ require .NoError (t , err )
449+ }
450+
451+ u , err := url .Parse (s .issuerURL .String ())
452+ require .NoError (t , err )
453+
454+ tokenData , err := internal .Marshal (& internal.RefreshToken {RefreshId : "test" , Token : "bar" })
455+ require .NoError (t , err )
456+
457+ u .Path = path .Join (u .Path , "/token" )
458+ v := url.Values {}
459+ v .Add ("grant_type" , "refresh_token" )
460+ v .Add ("refresh_token" , tokenData )
461+
462+ req , _ := http .NewRequest ("POST" , u .String (), bytes .NewBufferString (v .Encode ()))
463+ req .Header .Set ("Content-Type" , "application/x-www-form-urlencoded; param=value" )
464+ req .SetBasicAuth ("test" , "barfoo" )
465+
466+ rr := httptest .NewRecorder ()
467+ s .ServeHTTP (rr , req )
468+
469+ if tc .wantOK {
470+ require .Equal (t , http .StatusOK , rr .Code , "body: %s" , rr .Body .String ())
471+
472+ var resp struct {
473+ IDToken string `json:"id_token"`
474+ }
475+ err = json .Unmarshal (rr .Body .Bytes (), & resp )
476+ require .NoError (t , err )
477+
478+ // Verify the returned claims match UserIdentity, not the connector.
479+ claims := decodeJWTClaims (t , resp .IDToken )
480+ assert .Equal (t , "jane.doe@example.com" , claims ["email" ])
481+ assert .Equal (t , "jane" , claims ["name" ])
482+ } else {
483+ require .NotEqual (t , http .StatusOK , rr .Code ,
484+ "expected error when sessions disabled or user identity missing" )
485+ }
486+ })
487+ }
488+ }
489+
348490func TestRefreshTokenPolicy (t * testing.T ) {
349491 lastTime := time .Now ()
350492 l := slog .New (slog .DiscardHandler )
0 commit comments