@@ -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