Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit5087f7b

Browse files
authored
chore: improve fake IDP script (#11602)
* chore: testIDP using static defaults for easier reuse
1 parentf915bdf commit5087f7b

File tree

5 files changed

+204
-80
lines changed

5 files changed

+204
-80
lines changed

‎cmd/testidp/main.go‎

Lines changed: 0 additions & 58 deletions
This file was deleted.

‎coderd/coderdtest/oidctest/idp.go‎

Lines changed: 92 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ import (
3939
"github.com/coder/coder/v2/codersdk"
4040
)
4141

42+
typetokenstruct {
43+
issued time.Time
44+
emailstring
45+
exp time.Time
46+
}
47+
4248
// FakeIDP is a functional OIDC provider.
4349
// It only supports 1 OIDC client.
4450
typeFakeIDPstruct {
@@ -65,7 +71,7 @@ type FakeIDP struct {
6571
// That is the various access tokens, refresh tokens, states, etc.
6672
codeToStateMap*syncmap.Map[string,string]
6773
// Token -> Email
68-
accessTokens*syncmap.Map[string,string]
74+
accessTokens*syncmap.Map[string,token]
6975
// Refresh Token -> Email
7076
refreshTokensUsed*syncmap.Map[string,bool]
7177
refreshTokens*syncmap.Map[string,string]
@@ -89,7 +95,8 @@ type FakeIDP struct {
8995
hookAuthenticateClientfunc(t testing.TB,req*http.Request) (url.Values,error)
9096
servebool
9197
// optional middlewares
92-
middlewares chi.Middlewares
98+
middlewares chi.Middlewares
99+
defaultExpire time.Duration
93100
}
94101

95102
funcStatusError(codeint,errerror)error {
@@ -134,6 +141,23 @@ func WithRefresh(hook func(email string) error) func(*FakeIDP) {
134141
}
135142
}
136143

144+
funcWithDefaultExpire(d time.Duration)func(*FakeIDP) {
145+
returnfunc(f*FakeIDP) {
146+
f.defaultExpire=d
147+
}
148+
}
149+
150+
funcWithStaticCredentials(id,secretstring)func(*FakeIDP) {
151+
returnfunc(f*FakeIDP) {
152+
ifid!="" {
153+
f.clientID=id
154+
}
155+
ifsecret!="" {
156+
f.clientSecret=secret
157+
}
158+
}
159+
}
160+
137161
// WithExtra returns extra fields that be accessed on the returned Oauth Token.
138162
// These extra fields can override the default fields (id_token, access_token, etc).
139163
funcWithMutateToken(mutateTokenfunc(tokenmap[string]interface{}))func(*FakeIDP) {
@@ -155,6 +179,12 @@ func WithLogging(t testing.TB, options *slogtest.Options) func(*FakeIDP) {
155179
}
156180
}
157181

182+
funcWithLogger(logger slog.Logger)func(*FakeIDP) {
183+
returnfunc(f*FakeIDP) {
184+
f.logger=logger
185+
}
186+
}
187+
158188
// WithStaticUserInfo is optional, but will return the same user info for
159189
// every user on the /userinfo endpoint.
160190
funcWithStaticUserInfo(info jwt.MapClaims)func(*FakeIDP) {
@@ -211,14 +241,15 @@ func NewFakeIDP(t testing.TB, opts ...FakeIDPOpt) *FakeIDP {
211241
clientSecret:uuid.NewString(),
212242
logger:slog.Make(),
213243
codeToStateMap:syncmap.New[string,string](),
214-
accessTokens:syncmap.New[string,string](),
244+
accessTokens:syncmap.New[string,token](),
215245
refreshTokens:syncmap.New[string,string](),
216246
refreshTokensUsed:syncmap.New[string,bool](),
217247
stateToIDTokenClaims:syncmap.New[string, jwt.MapClaims](),
218248
refreshIDTokenClaims:syncmap.New[string, jwt.MapClaims](),
219249
hookOnRefresh:func(_string)error {returnnil },
220250
hookUserInfo:func(emailstring) (jwt.MapClaims,error) {return jwt.MapClaims{},nil },
221251
hookValidRedirectURL:func(redirectURLstring)error {returnnil },
252+
defaultExpire:time.Minute*5,
222253
}
223254

224255
for_,opt:=rangeopts {
@@ -265,15 +296,31 @@ func (f *FakeIDP) updateIssuerURL(t testing.TB, issuer string) {
265296
Algorithms: []string{
266297
"RS256",
267298
},
299+
ExternalAuthURL:u.ResolveReference(&url.URL{Path:"/external-auth-validate/user"}).String(),
268300
}
269301
}
270302

271303
// realServer turns the FakeIDP into a real http server.
272304
func (f*FakeIDP)realServer(t testing.TB)*httptest.Server {
273305
t.Helper()
274306

307+
srvURL:="localhost:0"
308+
issURL,err:=url.Parse(f.issuer)
309+
iferr==nil {
310+
ifissURL.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+
275318
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+
277324
srv.Config.BaseContext=func(_ net.Listener) context.Context {
278325
returnctx
279326
}
@@ -495,6 +542,8 @@ type ProviderJSON struct {
495542
JWKSURLstring`json:"jwks_uri"`
496543
UserInfoURLstring`json:"userinfo_endpoint"`
497544
Algorithms []string`json:"id_token_signing_alg_values_supported"`
545+
// This is custom
546+
ExternalAuthURLstring`json:"external_auth_url"`
498547
}
499548

500549
// newCode enforces the code exchanged is actually a valid code
@@ -507,9 +556,13 @@ func (f *FakeIDP) newCode(state string) string {
507556

508557
// newToken enforces the access token exchanged is actually a valid access token
509558
// created by the IDP.
510-
func (f*FakeIDP)newToken(emailstring)string {
559+
func (f*FakeIDP)newToken(emailstring,expires time.Time)string {
511560
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+
})
513566
returnaccessToken
514567
}
515568

@@ -525,10 +578,15 @@ func (f *FakeIDP) authenticateBearerTokenRequest(t testing.TB, req *http.Request
525578

526579
auth:=req.Header.Get("Authorization")
527580
token:=strings.TrimPrefix(auth,"Bearer ")
528-
_,ok:=f.accessTokens.Load(token)
581+
authToken,ok:=f.accessTokens.Load(token)
529582
if!ok {
530583
return"",xerrors.New("invalid access token")
531584
}
585+
586+
if!authToken.exp.IsZero()&&authToken.exp.Before(time.Now()) {
587+
return"",xerrors.New("access token expired")
588+
}
589+
532590
returntoken,nil
533591
}
534592

@@ -653,7 +711,8 @@ func (f *FakeIDP) httpHandler(t testing.TB) http.Handler {
653711
mux.Handle(tokenPath,http.HandlerFunc(func(rw http.ResponseWriter,r*http.Request) {
654712
values,err:=f.authenticateOIDCClientRequest(t,r)
655713
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")),
657716
slog.F("values",values.Encode()),
658717
)
659718
iferr!=nil {
@@ -731,15 +790,15 @@ func (f *FakeIDP) httpHandler(t testing.TB) http.Handler {
731790
return
732791
}
733792

734-
exp:=time.Now().Add(time.Minute*5)
793+
exp:=time.Now().Add(f.defaultExpire)
735794
claims["exp"]=exp.UnixMilli()
736795
email:=getEmail(claims)
737796
refreshToken:=f.newRefreshTokens(email)
738797
token:=map[string]interface{}{
739-
"access_token":f.newToken(email),
798+
"access_token":f.newToken(email,exp),
740799
"refresh_token":refreshToken,
741800
"token_type":"Bearer",
742-
"expires_in":int64((time.Minute*5).Seconds()),
801+
"expires_in":int64((f.defaultExpire).Seconds()),
743802
"id_token":f.encodeClaims(t,claims),
744803
}
745804
iff.hookMutateToken!=nil {
@@ -754,25 +813,31 @@ func (f *FakeIDP) httpHandler(t testing.TB) http.Handler {
754813

755814
validateMW:=func(rw http.ResponseWriter,r*http.Request) (emailstring,okbool) {
756815
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-
)
761816
iferr!=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)
763818
return"",false
764819
}
765820

766-
email,ok=f.accessTokens.Load(token)
821+
authToken,ok:=f.accessTokens.Load(token)
767822
if!ok {
768823
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)
770830
return"",false
771831
}
772-
returnemail,true
832+
833+
returnauthToken.email,true
773834
}
774835
mux.Handle(userInfoPath,http.HandlerFunc(func(rw http.ResponseWriter,r*http.Request) {
775836
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+
)
776841
if!ok {
777842
return
778843
}
@@ -790,6 +855,10 @@ func (f *FakeIDP) httpHandler(t testing.TB) http.Handler {
790855
// should be strict, and this one needs to handle sub routes.
791856
mux.Mount("/external-auth-validate/",http.HandlerFunc(func(rw http.ResponseWriter,r*http.Request) {
792857
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+
)
793862
if!ok {
794863
return
795864
}
@@ -941,7 +1010,7 @@ func (f *FakeIDP) ExternalAuthConfig(t testing.TB, id string, custom *ExternalAu
9411010
}
9421011
f.externalProviderID=id
9431012
f.externalAuthValidate=func(emailstring,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")
9451014
switchnewPath {
9461015
// /user is ALWAYS supported under the `/` path too.
9471016
case"/user","/","":
@@ -965,18 +1034,20 @@ func (f *FakeIDP) ExternalAuthConfig(t testing.TB, id string, custom *ExternalAu
9651034
}
9661035
instrumentF:=promoauth.NewFactory(prometheus.NewRegistry())
9671036
cfg:=&externalauth.Config{
1037+
DisplayName:id,
9681038
InstrumentedOAuth2Config:instrumentF.New(f.clientID,f.OIDCConfig(t,nil)),
9691039
ID:id,
9701040
// No defaults for these fields by omitting the type
9711041
Type:"",
9721042
DisplayIcon:f.WellknownConfig().UserInfoURL,
9731043
// Omit the /user for the validate so we can easily append to it when modifying
9741044
// 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(),
9761046
}
9771047
for_,opt:=rangeopts {
9781048
opt(cfg)
9791049
}
1050+
f.updateIssuerURL(t,f.issuer)
9801051
returncfg
9811052
}
9821053

‎coderd/externalauth_test.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ func TestExternalAuthByID(t *testing.T) {
126126
client:=coderdtest.New(t,&coderdtest.Options{
127127
ExternalAuthConfigs: []*externalauth.Config{
128128
fake.ExternalAuthConfig(t,providerID,routes,func(cfg*externalauth.Config) {
129-
cfg.AppInstallationsURL=cfg.ValidateURL+"/installs"
129+
cfg.AppInstallationsURL=strings.TrimSuffix(cfg.ValidateURL,"/")+"/installs"
130130
cfg.Type=codersdk.EnhancedExternalAuthProviderGitHub.String()
131131
}),
132132
},
File renamed without changes.

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp