@@ -15,6 +15,7 @@ import (
1515 "testing"
1616 "time"
1717
18+ "github.com/golang-jwt/jwt/v4"
1819 "github.com/google/uuid"
1920 "github.com/prometheus/client_golang/prometheus"
2021 "github.com/stretchr/testify/assert"
@@ -30,6 +31,7 @@ import (
3031 "github.com/coder/coder/v2/coderd"
3132 "github.com/coder/coder/v2/coderd/audit"
3233 "github.com/coder/coder/v2/coderd/coderdtest"
34+ "github.com/coder/coder/v2/coderd/coderdtest/oidctest"
3335 "github.com/coder/coder/v2/coderd/database"
3436 "github.com/coder/coder/v2/coderd/database/dbauthz"
3537 "github.com/coder/coder/v2/coderd/database/dbgen"
@@ -58,6 +60,175 @@ import (
5860 "github.com/coder/serpent"
5961)
6062
63+ // TestTokenIsRefreshedEarly creates a fake OIDC IDP that sets expiration times
64+ // of the token to values that are "near expiration". Expiration being 10minutes
65+ // earlier than it needs to be. The `ObtainOIDCAccessToken` should refresh these
66+ // tokens early.
67+ func TestTokenIsRefreshedEarly (t * testing.T ) {
68+ t .Parallel ()
69+
70+ t .Run ("WithCoderd" , func (t * testing.T ) {
71+ t .Parallel ()
72+ tokenRefreshCount := 0
73+ fake := oidctest .NewFakeIDP (t ,
74+ oidctest .WithServing (),
75+ oidctest .WithDefaultExpire (time .Minute * 8 ),
76+ oidctest .WithRefresh (func (email string ) error {
77+ tokenRefreshCount ++
78+ return nil
79+ }),
80+ )
81+ cfg := fake .OIDCConfig (t , nil , func (cfg * coderd.OIDCConfig ) {
82+ cfg .AllowSignups = true
83+ })
84+ db , ps := dbtestutil .NewDB (t )
85+ owner := coderdtest .New (t , & coderdtest.Options {
86+ OIDCConfig : cfg ,
87+ IncludeProvisionerDaemon : true ,
88+ Database : db ,
89+ Pubsub : ps ,
90+ })
91+ first := coderdtest .CreateFirstUser (t , owner )
92+ version := coderdtest .CreateTemplateVersion (t , owner , first .OrganizationID , nil )
93+ coderdtest .AwaitTemplateVersionJobCompleted (t , owner , version .ID )
94+ template := coderdtest .CreateTemplate (t , owner , first .OrganizationID , version .ID )
95+
96+ // Setup an OIDC user.
97+ client , _ := fake .Login (t , owner , jwt.MapClaims {
98+ 99+ "email_verified" : true ,
100+ "sub" : uuid .NewString (),
101+ })
102+
103+ // Creating a workspace should refresh the oidc early.
104+ tokenRefreshCount = 0
105+ wrk := coderdtest .CreateWorkspace (t , client , template .ID )
106+ coderdtest .AwaitWorkspaceBuildJobCompleted (t , client , wrk .LatestBuild .ID )
107+ require .Equal (t , 1 , tokenRefreshCount )
108+ })
109+ }
110+
111+ //nolint:tparallel,paralleltest // Sub tests need to run sequentially.
112+ func TestTokenIsRefreshedEarlyWithoutCoderd (t * testing.T ) {
113+ t .Parallel ()
114+ tokenRefreshCount := 0
115+ fake := oidctest .NewFakeIDP (t ,
116+ oidctest .WithServing (),
117+ oidctest .WithDefaultExpire (time .Minute * 8 ),
118+ oidctest .WithRefresh (func (email string ) error {
119+ tokenRefreshCount ++
120+ return nil
121+ }),
122+ )
123+ cfg := fake .OIDCConfig (t , nil )
124+
125+ // Fetch a valid token from the fake OIDC provider
126+ token , err := fake .GenerateAuthenticatedToken (jwt.MapClaims {
127+ 128+ "email_verified" : true ,
129+ "sub" : uuid .NewString (),
130+ })
131+ require .NoError (t , err )
132+
133+ db , _ := dbtestutil .NewDB (t )
134+ user := dbgen .User (t , db , database.User {})
135+ dbgen .UserLink (t , db , database.UserLink {
136+ UserID : user .ID ,
137+ LoginType : database .LoginTypeOIDC ,
138+ LinkedID : "foo" ,
139+ OAuthAccessToken : token .AccessToken ,
140+ OAuthRefreshToken : token .RefreshToken ,
141+ // The oauth expiry does not really matter, since each test will manually control
142+ // this value.
143+ OAuthExpiry : dbtime .Now ().Add (time .Hour ),
144+ })
145+
146+ setLinkExpiration := func (t * testing.T , exp time.Time ) database.UserLink {
147+ ctx := testutil .Context (t , testutil .WaitShort )
148+ links , err := db .GetUserLinksByUserID (ctx , user .ID )
149+ require .NoError (t , err )
150+ require .Len (t , links , 1 )
151+ link := links [0 ]
152+
153+ newLink , err := db .UpdateUserLink (ctx , database.UpdateUserLinkParams {
154+ OAuthAccessToken : link .OAuthAccessToken ,
155+ OAuthAccessTokenKeyID : link .OAuthAccessTokenKeyID ,
156+ OAuthRefreshToken : link .OAuthRefreshToken ,
157+ OAuthRefreshTokenKeyID : link .OAuthRefreshTokenKeyID ,
158+ OAuthExpiry : exp ,
159+ Claims : link .Claims ,
160+ UserID : link .UserID ,
161+ LoginType : link .LoginType ,
162+ })
163+ require .NoError (t , err )
164+ return newLink
165+ }
166+
167+ for _ , c := range []struct {
168+ name string
169+ // expires is a function to return a more up to date "now".
170+ // Because the oauth library is calling `time.Now()`, we cannot use
171+ // mocked clocks.
172+ expires func () time.Time
173+ refreshExpected bool
174+ }{
175+ {
176+ name : "ZeroExpiry" ,
177+ expires : func () time.Time { return time.Time {} },
178+ refreshExpected : false ,
179+ },
180+ {
181+ name : "LongExpired" ,
182+ expires : func () time.Time { return dbtime .Now ().Add (- time .Hour ) },
183+ refreshExpected : true ,
184+ },
185+ {
186+ name : "EdgeExpired" ,
187+ expires : func () time.Time { return dbtime .Now ().Add (- time .Minute * 10 ) },
188+ refreshExpected : true ,
189+ },
190+ {
191+ name : "RecentExpired" ,
192+ expires : func () time.Time { return dbtime .Now ().Add (- time .Second * - 1 ) },
193+ refreshExpected : true ,
194+ },
195+
196+ {
197+ name : "Future" ,
198+ expires : func () time.Time { return dbtime .Now ().Add (time .Hour ) },
199+ refreshExpected : false ,
200+ },
201+ {
202+ name : "FutureWithinRefreshWindow" ,
203+ expires : func () time.Time { return dbtime .Now ().Add (time .Minute * 8 ) },
204+ refreshExpected : true ,
205+ },
206+ } {
207+ t .Run (c .name , func (t * testing.T ) {
208+ ctx := testutil .Context (t , testutil .WaitShort )
209+ oldLink := setLinkExpiration (t , c .expires ())
210+ tokenRefreshCount = 0
211+ _ , err := provisionerdserver .ObtainOIDCAccessToken (ctx , testutil .Logger (t ), db , cfg , user .ID )
212+ require .NoError (t , err )
213+ links , err := db .GetUserLinksByUserID (ctx , user .ID )
214+ require .NoError (t , err )
215+ require .Len (t , links , 1 )
216+ newLink := links [0 ]
217+
218+ if c .refreshExpected {
219+ require .Equal (t , 1 , tokenRefreshCount )
220+
221+ require .NotEqual (t , oldLink .OAuthAccessToken , newLink .OAuthAccessToken )
222+ require .NotEqual (t , oldLink .OAuthRefreshToken , newLink .OAuthRefreshToken )
223+ } else {
224+ require .Equal (t , 0 , tokenRefreshCount )
225+ require .Equal (t , oldLink .OAuthAccessToken , newLink .OAuthAccessToken )
226+ require .Equal (t , oldLink .OAuthRefreshToken , newLink .OAuthRefreshToken )
227+ }
228+ })
229+ }
230+ }
231+
61232func testTemplateScheduleStore () * atomic.Pointer [schedule.TemplateScheduleStore ] {
62233 poitr := & atomic.Pointer [schedule.TemplateScheduleStore ]{}
63234 store := schedule .NewAGPLTemplateScheduleStore ()
0 commit comments