@@ -39,6 +39,12 @@ import (
39
39
"github.com/coder/coder/v2/codersdk"
40
40
)
41
41
42
+ type token struct {
43
+ issued time.Time
44
+ email string
45
+ exp time.Time
46
+ }
47
+
42
48
// FakeIDP is a functional OIDC provider.
43
49
// It only supports 1 OIDC client.
44
50
type FakeIDP struct {
@@ -65,7 +71,7 @@ type FakeIDP struct {
65
71
// That is the various access tokens, refresh tokens, states, etc.
66
72
codeToStateMap * syncmap.Map [string , string ]
67
73
// Token -> Email
68
- accessTokens * syncmap.Map [string , string ]
74
+ accessTokens * syncmap.Map [string , token ]
69
75
// Refresh Token -> Email
70
76
refreshTokensUsed * syncmap.Map [string , bool ]
71
77
refreshTokens * syncmap.Map [string , string ]
@@ -89,7 +95,8 @@ type FakeIDP struct {
89
95
hookAuthenticateClient func (t testing.TB , req * http.Request ) (url.Values , error )
90
96
serve bool
91
97
// optional middlewares
92
- middlewares chi.Middlewares
98
+ middlewares chi.Middlewares
99
+ defaultExpire time.Duration
93
100
}
94
101
95
102
func StatusError (code int , err error ) error {
@@ -134,6 +141,23 @@ func WithRefresh(hook func(email string) error) func(*FakeIDP) {
134
141
}
135
142
}
136
143
144
+ func WithDefaultExpire (d time.Duration ) func (* FakeIDP ) {
145
+ return func (f * FakeIDP ) {
146
+ f .defaultExpire = d
147
+ }
148
+ }
149
+
150
+ func WithStaticCredentials (id , secret string ) func (* FakeIDP ) {
151
+ return func (f * FakeIDP ) {
152
+ if id != "" {
153
+ f .clientID = id
154
+ }
155
+ if secret != "" {
156
+ f .clientSecret = secret
157
+ }
158
+ }
159
+ }
160
+
137
161
// WithExtra returns extra fields that be accessed on the returned Oauth Token.
138
162
// These extra fields can override the default fields (id_token, access_token, etc).
139
163
func WithMutateToken (mutateToken func (token map [string ]interface {})) func (* FakeIDP ) {
@@ -155,6 +179,12 @@ func WithLogging(t testing.TB, options *slogtest.Options) func(*FakeIDP) {
155
179
}
156
180
}
157
181
182
+ func WithLogger (logger slog.Logger ) func (* FakeIDP ) {
183
+ return func (f * FakeIDP ) {
184
+ f .logger = logger
185
+ }
186
+ }
187
+
158
188
// WithStaticUserInfo is optional, but will return the same user info for
159
189
// every user on the /userinfo endpoint.
160
190
func WithStaticUserInfo (info jwt.MapClaims ) func (* FakeIDP ) {
@@ -211,14 +241,15 @@ func NewFakeIDP(t testing.TB, opts ...FakeIDPOpt) *FakeIDP {
211
241
clientSecret : uuid .NewString (),
212
242
logger : slog .Make (),
213
243
codeToStateMap : syncmap .New [string , string ](),
214
- accessTokens : syncmap .New [string , string ](),
244
+ accessTokens : syncmap .New [string , token ](),
215
245
refreshTokens : syncmap .New [string , string ](),
216
246
refreshTokensUsed : syncmap .New [string , bool ](),
217
247
stateToIDTokenClaims : syncmap .New [string , jwt.MapClaims ](),
218
248
refreshIDTokenClaims : syncmap .New [string , jwt.MapClaims ](),
219
249
hookOnRefresh : func (_ string ) error { return nil },
220
250
hookUserInfo : func (email string ) (jwt.MapClaims , error ) { return jwt.MapClaims {}, nil },
221
251
hookValidRedirectURL : func (redirectURL string ) error { return nil },
252
+ defaultExpire : time .Minute * 5 ,
222
253
}
223
254
224
255
for _ , opt := range opts {
@@ -265,15 +296,31 @@ func (f *FakeIDP) updateIssuerURL(t testing.TB, issuer string) {
265
296
Algorithms : []string {
266
297
"RS256" ,
267
298
},
299
+ ExternalAuthURL : u .ResolveReference (& url.URL {Path : "/external-auth-validate/user" }).String (),
268
300
}
269
301
}
270
302
271
303
// realServer turns the FakeIDP into a real http server.
272
304
func (f * FakeIDP ) realServer (t testing.TB ) * httptest.Server {
273
305
t .Helper ()
274
306
307
+ srvURL := "localhost:0"
308
+ issURL , err := url .Parse (f .issuer )
309
+ if err == nil {
310
+ if issURL .Hostname () == "localhost" || issURL .Hostname () == "127.0.0.1" {
311
+ srvURL = issURL .Host
312
+ }
313
+ }
314
+
315
+ l , err := net .Listen ("tcp" , srvURL )
316
+ require .NoError (t , err , "failed to create listener" )
317
+
275
318
ctx , cancel := context .WithCancel (context .Background ())
276
- srv := httptest .NewUnstartedServer (f .handler )
319
+ srv := & httptest.Server {
320
+ Listener : l ,
321
+ Config : & http.Server {Handler : f .handler , ReadHeaderTimeout : time .Second * 5 },
322
+ }
323
+
277
324
srv .Config .BaseContext = func (_ net.Listener ) context.Context {
278
325
return ctx
279
326
}
@@ -495,6 +542,8 @@ type ProviderJSON struct {
495
542
JWKSURL string `json:"jwks_uri"`
496
543
UserInfoURL string `json:"userinfo_endpoint"`
497
544
Algorithms []string `json:"id_token_signing_alg_values_supported"`
545
+ // This is custom
546
+ ExternalAuthURL string `json:"external_auth_url"`
498
547
}
499
548
500
549
// newCode enforces the code exchanged is actually a valid code
@@ -507,9 +556,13 @@ func (f *FakeIDP) newCode(state string) string {
507
556
508
557
// newToken enforces the access token exchanged is actually a valid access token
509
558
// created by the IDP.
510
- func (f * FakeIDP ) newToken (email string ) string {
559
+ func (f * FakeIDP ) newToken (email string , expires time. Time ) string {
511
560
accessToken := uuid .NewString ()
512
- f .accessTokens .Store (accessToken , email )
561
+ f .accessTokens .Store (accessToken , token {
562
+ issued : time .Now (),
563
+ email : email ,
564
+ exp : expires ,
565
+ })
513
566
return accessToken
514
567
}
515
568
@@ -525,10 +578,15 @@ func (f *FakeIDP) authenticateBearerTokenRequest(t testing.TB, req *http.Request
525
578
526
579
auth := req .Header .Get ("Authorization" )
527
580
token := strings .TrimPrefix (auth , "Bearer " )
528
- _ , ok := f .accessTokens .Load (token )
581
+ authToken , ok := f .accessTokens .Load (token )
529
582
if ! ok {
530
583
return "" , xerrors .New ("invalid access token" )
531
584
}
585
+
586
+ if ! authToken .exp .IsZero () && authToken .exp .Before (time .Now ()) {
587
+ return "" , xerrors .New ("access token expired" )
588
+ }
589
+
532
590
return token , nil
533
591
}
534
592
@@ -653,7 +711,8 @@ func (f *FakeIDP) httpHandler(t testing.TB) http.Handler {
653
711
mux .Handle (tokenPath , http .HandlerFunc (func (rw http.ResponseWriter , r * http.Request ) {
654
712
values , err := f .authenticateOIDCClientRequest (t , r )
655
713
f .logger .Info (r .Context (), "http idp call token" ,
656
- slog .Error (err ),
714
+ slog .F ("valid" , err == nil ),
715
+ slog .F ("grant_type" , values .Get ("grant_type" )),
657
716
slog .F ("values" , values .Encode ()),
658
717
)
659
718
if err != nil {
@@ -731,15 +790,15 @@ func (f *FakeIDP) httpHandler(t testing.TB) http.Handler {
731
790
return
732
791
}
733
792
734
- exp := time .Now ().Add (time . Minute * 5 )
793
+ exp := time .Now ().Add (f . defaultExpire )
735
794
claims ["exp" ] = exp .UnixMilli ()
736
795
email := getEmail (claims )
737
796
refreshToken := f .newRefreshTokens (email )
738
797
token := map [string ]interface {}{
739
- "access_token" : f .newToken (email ),
798
+ "access_token" : f .newToken (email , exp ),
740
799
"refresh_token" : refreshToken ,
741
800
"token_type" : "Bearer" ,
742
- "expires_in" : int64 ((time . Minute * 5 ).Seconds ()),
801
+ "expires_in" : int64 ((f . defaultExpire ).Seconds ()),
743
802
"id_token" : f .encodeClaims (t , claims ),
744
803
}
745
804
if f .hookMutateToken != nil {
@@ -754,25 +813,31 @@ func (f *FakeIDP) httpHandler(t testing.TB) http.Handler {
754
813
755
814
validateMW := func (rw http.ResponseWriter , r * http.Request ) (email string , ok bool ) {
756
815
token , err := f .authenticateBearerTokenRequest (t , r )
757
- f .logger .Info (r .Context (), "http call idp user info" ,
758
- slog .Error (err ),
759
- slog .F ("url" , r .URL .String ()),
760
- )
761
816
if err != nil {
762
- http .Error (rw , fmt .Sprintf ("invalid user info request: %s" , err .Error ()), http .StatusBadRequest )
817
+ http .Error (rw , fmt .Sprintf ("invalid user info request: %s" , err .Error ()), http .StatusUnauthorized )
763
818
return "" , false
764
819
}
765
820
766
- email , ok = f .accessTokens .Load (token )
821
+ authToken , ok : = f .accessTokens .Load (token )
767
822
if ! ok {
768
823
t .Errorf ("access token user for user_info has no email to indicate which user" )
769
- http .Error (rw , "invalid access token, missing user info" , http .StatusBadRequest )
824
+ http .Error (rw , "invalid access token, missing user info" , http .StatusUnauthorized )
825
+ return "" , false
826
+ }
827
+
828
+ if ! authToken .exp .IsZero () && authToken .exp .Before (time .Now ()) {
829
+ http .Error (rw , "auth token expired" , http .StatusUnauthorized )
770
830
return "" , false
771
831
}
772
- return email , true
832
+
833
+ return authToken .email , true
773
834
}
774
835
mux .Handle (userInfoPath , http .HandlerFunc (func (rw http.ResponseWriter , r * http.Request ) {
775
836
email , ok := validateMW (rw , r )
837
+ f .logger .Info (r .Context (), "http userinfo endpoint" ,
838
+ slog .F ("valid" , ok ),
839
+ slog .F ("email" , email ),
840
+ )
776
841
if ! ok {
777
842
return
778
843
}
@@ -790,6 +855,10 @@ func (f *FakeIDP) httpHandler(t testing.TB) http.Handler {
790
855
// should be strict, and this one needs to handle sub routes.
791
856
mux .Mount ("/external-auth-validate/" , http .HandlerFunc (func (rw http.ResponseWriter , r * http.Request ) {
792
857
email , ok := validateMW (rw , r )
858
+ f .logger .Info (r .Context (), "http external auth validate" ,
859
+ slog .F ("valid" , ok ),
860
+ slog .F ("email" , email ),
861
+ )
793
862
if ! ok {
794
863
return
795
864
}
@@ -941,7 +1010,7 @@ func (f *FakeIDP) ExternalAuthConfig(t testing.TB, id string, custom *ExternalAu
941
1010
}
942
1011
f .externalProviderID = id
943
1012
f .externalAuthValidate = func (email string , rw http.ResponseWriter , r * http.Request ) {
944
- newPath := strings .TrimPrefix (r .URL .Path , fmt . Sprintf ( "/external-auth-validate/%s" , id ) )
1013
+ newPath := strings .TrimPrefix (r .URL .Path , "/external-auth-validate" )
945
1014
switch newPath {
946
1015
// /user is ALWAYS supported under the `/` path too.
947
1016
case "/user" , "/" , "" :
@@ -965,18 +1034,20 @@ func (f *FakeIDP) ExternalAuthConfig(t testing.TB, id string, custom *ExternalAu
965
1034
}
966
1035
instrumentF := promoauth .NewFactory (prometheus .NewRegistry ())
967
1036
cfg := & externalauth.Config {
1037
+ DisplayName : id ,
968
1038
InstrumentedOAuth2Config : instrumentF .New (f .clientID , f .OIDCConfig (t , nil )),
969
1039
ID : id ,
970
1040
// No defaults for these fields by omitting the type
971
1041
Type : "" ,
972
1042
DisplayIcon : f .WellknownConfig ().UserInfoURL ,
973
1043
// Omit the /user for the validate so we can easily append to it when modifying
974
1044
// the cfg for advanced tests.
975
- ValidateURL : f .issuerURL .ResolveReference (& url.URL {Path : fmt . Sprintf ( "/external-auth-validate/%s" , id ) }).String (),
1045
+ ValidateURL : f .issuerURL .ResolveReference (& url.URL {Path : "/external-auth-validate/" }).String (),
976
1046
}
977
1047
for _ , opt := range opts {
978
1048
opt (cfg )
979
1049
}
1050
+ f .updateIssuerURL (t , f .issuer )
980
1051
return cfg
981
1052
}
982
1053
0 commit comments