@@ -18,8 +18,6 @@ import (
18
18
"testing"
19
19
"time"
20
20
21
- "github.com/coder/coder/v2/codersdk"
22
-
23
21
"github.com/coreos/go-oidc/v3/oidc"
24
22
"github.com/go-chi/chi/v5"
25
23
"github.com/go-jose/go-jose/v3"
@@ -33,8 +31,11 @@ import (
33
31
"cdr.dev/slog"
34
32
"cdr.dev/slog/sloggers/slogtest"
35
33
"github.com/coder/coder/v2/coderd"
34
+ "github.com/coder/coder/v2/codersdk"
36
35
)
37
36
37
+ // FakeIDP is a functional OIDC provider.
38
+ // It only supports 1 OIDC client.
38
39
type FakeIDP struct {
39
40
issuer string
40
41
key * rsa.PrivateKey
@@ -47,6 +48,8 @@ type FakeIDP struct {
47
48
clientSecret string
48
49
logger slog.Logger
49
50
51
+ // These maps are used to control the state of the IDP.
52
+ // That is the various access tokens, refresh tokens, states, etc.
50
53
codeToStateMap * SyncMap [string , string ]
51
54
// Token -> Email
52
55
accessTokens * SyncMap [string , string ]
@@ -68,18 +71,23 @@ type FakeIDP struct {
68
71
69
72
type FakeIDPOpt func (idp * FakeIDP )
70
73
74
+ // WithRefreshHook is called when a refresh token is used. The email is
75
+ // the email of the user that is being refreshed assuming the claims are correct.
71
76
func WithRefreshHook (hook func (email string ) error ) func (* FakeIDP ) {
72
77
return func (f * FakeIDP ) {
73
78
f .hookOnRefresh = hook
74
79
}
75
80
}
76
81
82
+ // WithLogging is optional, but will log some HTTP calls made to the IDP.
77
83
func WithLogging (t testing.TB , options * slogtest.Options ) func (* FakeIDP ) {
78
84
return func (f * FakeIDP ) {
79
85
f .logger = slogtest .Make (t , options )
80
86
}
81
87
}
82
88
89
+ // WithStaticUserInfo is optional, but will return the same user info for
90
+ // every user on the /userinfo endpoint.
83
91
func WithStaticUserInfo (info jwt.MapClaims ) func (* FakeIDP ) {
84
92
return func (f * FakeIDP ) {
85
93
f .hookUserInfo = func (_ string ) jwt.MapClaims {
@@ -94,6 +102,7 @@ func WithDynamicUserInfo(userInfoFunc func(email string) jwt.MapClaims) func(*Fa
94
102
}
95
103
}
96
104
105
+ // WithServing makes the IDP run an actual http server.
97
106
func WithServing () func (* FakeIDP ) {
98
107
return func (f * FakeIDP ) {
99
108
f .serve = true
@@ -218,6 +227,8 @@ func (f *FakeIDP) AttemptLogin(t testing.TB, client *codersdk.Client, idTokenCla
218
227
219
228
// LoginClient reuses the context of the passed in client. This means the same
220
229
// cookies will be used. This should be an unauthenticated client in most cases.
230
+ //
231
+ // This is a niche case, but it is needed for testing ConvertLoginType.
221
232
func (f * FakeIDP ) LoginClient (t testing.TB , client * codersdk.Client , idTokenClaims jwt.MapClaims , opts ... func (r * http.Request )) (* codersdk.Client , * http.Response ) {
222
233
t .Helper ()
223
234
@@ -268,7 +279,10 @@ func (f *FakeIDP) LoginClient(t testing.TB, client *codersdk.Client, idTokenClai
268
279
}
269
280
270
281
// OIDCCallback will emulate the IDP redirecting back to the Coder callback.
271
- // This is helpful if no Coderd exists.
282
+ // This is helpful if no Coderd exists because the IDP needs to redirect to
283
+ // something.
284
+ // Essentially this is used to fake the Coderd side of the exchange.
285
+ // The flow starts at the user hitting the OIDC login page.
272
286
func (f * FakeIDP ) OIDCCallback (t testing.TB , state string , idTokenClaims jwt.MapClaims ) (* http.Response , error ) {
273
287
t .Helper ()
274
288
f .stateToIDTokenClaims .Store (state , idTokenClaims )
@@ -320,6 +334,7 @@ func (f *FakeIDP) newRefreshTokens(email string) string {
320
334
return refreshToken
321
335
}
322
336
337
+ // authenticateBearerTokenRequest enforces the access token is valid.
323
338
func (f * FakeIDP ) authenticateBearerTokenRequest (t testing.TB , req * http.Request ) (string , error ) {
324
339
t .Helper ()
325
340
@@ -332,6 +347,7 @@ func (f *FakeIDP) authenticateBearerTokenRequest(t testing.TB, req *http.Request
332
347
return token , nil
333
348
}
334
349
350
+ // authenticateOIDClientRequest enforces the client_id and client_secret are valid.
335
351
func (f * FakeIDP ) authenticateOIDClientRequest (t testing.TB , req * http.Request ) (url.Values , error ) {
336
352
t .Helper ()
337
353
@@ -353,6 +369,7 @@ func (f *FakeIDP) authenticateOIDClientRequest(t testing.TB, req *http.Request)
353
369
return values , nil
354
370
}
355
371
372
+ // encodeClaims is a helper func to convert claims to a valid JWT.
356
373
func (f * FakeIDP ) encodeClaims (t testing.TB , claims jwt.MapClaims ) string {
357
374
t .Helper ()
358
375
@@ -374,6 +391,7 @@ func (f *FakeIDP) encodeClaims(t testing.TB, claims jwt.MapClaims) string {
374
391
return signed
375
392
}
376
393
394
+ // httpHandler is the IDP http server.
377
395
func (f * FakeIDP ) httpHandler (t testing.TB ) http.Handler {
378
396
t .Helper ()
379
397
@@ -572,12 +590,12 @@ func (f *FakeIDP) httpHandler(t testing.TB) http.Handler {
572
590
return mux
573
591
}
574
592
575
- // HTTPClient runs the IDP in memory and returns an http.Client that can be used
576
- // to make requests to the IDP. All requests are handled in memory, and no network
577
- // requests are made.
593
+ // HTTPClient does nothing if IsServing is used.
578
594
//
579
- // If a request is not to the IDP, then the passed in client will be used.
580
- // If no client is passed in, then any regular network requests will fail.
595
+ // If IsServing is not used, then it will return a client that will make requests
596
+ // to the IDP all in memory. If a request is not to the IDP, then the passed in
597
+ // client will be used. If no client is passed in, then any regular network
598
+ // requests will fail.
581
599
func (f * FakeIDP ) HTTPClient (rest * http.Client ) * http.Client {
582
600
if f .serve {
583
601
if rest == nil || rest .Transport == nil {
@@ -619,31 +637,40 @@ func (f *FakeIDP) RefreshUsed(refreshToken string) bool {
619
637
return used
620
638
}
621
639
640
+ // UpdateRefreshClaims allows the caller to change what claims are returned
641
+ // for a given refresh token. By default, all refreshes use the same claims as
642
+ // the original IDToken issuance.
622
643
func (f * FakeIDP ) UpdateRefreshClaims (refreshToken string , claims jwt.MapClaims ) {
623
644
f .refreshIDTokenClaims .Store (refreshToken , claims )
624
645
}
625
646
647
+ // SetRedirect is required for the IDP to know where to redirect and call
648
+ // Coderd.
626
649
func (f * FakeIDP ) SetRedirect (t testing.TB , url string ) {
627
650
t .Helper ()
628
651
629
652
f .cfg .RedirectURL = url
630
653
}
631
654
655
+ // SetCoderdCallback is optional and only works if not using the IsServing.
656
+ // It will setup a fake "Coderd" for the IDP to call when the IDP redirects
657
+ // back after authenticating.
632
658
func (f * FakeIDP ) SetCoderdCallback (callback func (req * http.Request ) (* http.Response , error )) {
659
+ if f .serve {
660
+ panic ("cannot set callback handler when using 'WithServing'. Must implement an actual 'Coderd'" )
661
+ }
633
662
f .fakeCoderd = callback
634
663
}
635
664
636
665
func (f * FakeIDP ) SetCoderdCallbackHandler (handler http.HandlerFunc ) {
637
- if f .serve {
638
- panic ("cannot set callback handler when using 'WithServing'. Must implement an actual 'Coderd'" )
639
- }
640
- f .fakeCoderd = func (req * http.Request ) (* http.Response , error ) {
666
+ f .SetCoderdCallback (func (req * http.Request ) (* http.Response , error ) {
641
667
resp := httptest .NewRecorder ()
642
668
handler .ServeHTTP (resp , req )
643
669
return resp .Result (), nil
644
- }
670
+ })
645
671
}
646
672
673
+ // OIDCConfig returns the OIDC config to use for Coderd.
647
674
func (f * FakeIDP ) OIDCConfig (t testing.TB , scopes []string , opts ... func (cfg * coderd.OIDCConfig )) * coderd.OIDCConfig {
648
675
t .Helper ()
649
676
if len (scopes ) == 0 {
0 commit comments