Skip to content

Commit 5087f7b

Browse files
authored
chore: improve fake IDP script (#11602)
* chore: testIDP using static defaults for easier reuse
1 parent f915bdf commit 5087f7b

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+
type token struct {
43+
issued time.Time
44+
email string
45+
exp time.Time
46+
}
47+
4248
// FakeIDP is a functional OIDC provider.
4349
// It only supports 1 OIDC client.
4450
type FakeIDP struct {
@@ -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
hookAuthenticateClient func(t testing.TB, req *http.Request) (url.Values, error)
9096
serve bool
9197
// optional middlewares
92-
middlewares chi.Middlewares
98+
middlewares chi.Middlewares
99+
defaultExpire time.Duration
93100
}
94101

95102
func StatusError(code int, err error) error {
@@ -134,6 +141,23 @@ func WithRefresh(hook func(email string) error) func(*FakeIDP) {
134141
}
135142
}
136143

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+
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
func WithMutateToken(mutateToken func(token map[string]interface{})) func(*FakeIDP) {
@@ -155,6 +179,12 @@ func WithLogging(t testing.TB, options *slogtest.Options) func(*FakeIDP) {
155179
}
156180
}
157181

182+
func WithLogger(logger slog.Logger) func(*FakeIDP) {
183+
return func(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
func WithStaticUserInfo(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 { return nil },
220250
hookUserInfo: func(email string) (jwt.MapClaims, error) { return jwt.MapClaims{}, nil },
221251
hookValidRedirectURL: func(redirectURL string) error { return nil },
252+
defaultExpire: time.Minute * 5,
222253
}
223254

224255
for _, opt := range opts {
@@ -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+
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+
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
return ctx
279326
}
@@ -495,6 +542,8 @@ type ProviderJSON struct {
495542
JWKSURL string `json:"jwks_uri"`
496543
UserInfoURL string `json:"userinfo_endpoint"`
497544
Algorithms []string `json:"id_token_signing_alg_values_supported"`
545+
// This is custom
546+
ExternalAuthURL string `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(email string) string {
559+
func (f *FakeIDP) newToken(email string, 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
return accessToken
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
return token, 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
if err != 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
if f.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) (email string, ok bool) {
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
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)
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-
return email, true
832+
833+
return authToken.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(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")
9451014
switch newPath {
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 := range opts {
9781048
opt(cfg)
9791049
}
1050+
f.updateIssuerURL(t, f.issuer)
9801051
return cfg
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy