diff --git a/cmd/testidp/README.md b/cmd/testidp/README.md new file mode 100644 index 0000000000000..2dac79af8602b --- /dev/null +++ b/cmd/testidp/README.md @@ -0,0 +1,17 @@ +# How to use + +Start the idp service: + +```bash +$ go run main.go +2024-01-10 16:48:01.415 [info] stdlib: 2024/01/10 10:48:01 IDP Issuer URL http://127.0.0.1:44517 +2024-01-10 16:48:01.415 [info] stdlib: 2024/01/10 10:48:01 Oauth Flags +2024-01-10 16:48:01.415 [info] stdlib: 2024/01/10 10:48:01 --external-auth-providers='[{"type":"fake","client_id":"f2df566b-a1c9-407a-8b75-480db45c6476","client_secret":"55aca4e3-7b94-44b6-9f45-ecb5e81c560d","auth_url":"http://127.0.0.1:44517/oauth2/authorize","token_url":"http://127.0.0.1:44517/oauth2/token","validate_url":"http://127.0.0.1:44517/oauth2/userinfo","scopes":["openid","email","profile"]}]' +2024-01-10 16:48:01.415 [info] stdlib: 2024/01/10 10:48:01 Press Ctrl+C to exit +``` + +Then use the flag into your coderd instance: + +```bash +develop.sh -- --external-auth-providers='[{"type":"fake","client_id":"f2df566b-a1c9-407a-8b75-480db45c6476","client_secret":"55aca4e3-7b94-44b6-9f45-ecb5e81c560d","auth_url":"http://127.0.0.1:44517/oauth2/authorize","token_url":"http://127.0.0.1:44517/oauth2/token","validate_url":"http://127.0.0.1:44517/oauth2/userinfo","scopes":["openid","email","profile"]}]' +``` diff --git a/cmd/testidp/main.go b/cmd/testidp/main.go new file mode 100644 index 0000000000000..fd96d0b84a87e --- /dev/null +++ b/cmd/testidp/main.go @@ -0,0 +1,58 @@ +package main + +import ( + "flag" + "log" + "os" + "os/signal" + "testing" + + "github.com/golang-jwt/jwt/v4" + + "github.com/coder/coder/v2/coderd/coderdtest/oidctest" +) + +func main() { + testing.Init() + _ = flag.Set("test.timeout", "0") + + flag.Parse() + + // This is just a way to run tests outside go test + testing.Main(func(pat, str string) (bool, error) { + return true, nil + }, []testing.InternalTest{ + { + Name: "Run Fake IDP", + F: RunIDP(), + }, + }, nil, nil) +} + +// RunIDP needs the testing.T because our oidctest package requires the +// testing.T. +func RunIDP() func(t *testing.T) { + return func(t *testing.T) { + idp := oidctest.NewFakeIDP(t, + oidctest.WithServing(), + oidctest.WithStaticUserInfo(jwt.MapClaims{}), + oidctest.WithDefaultIDClaims(jwt.MapClaims{}), + ) + id, sec := idp.AppCredentials() + prov := idp.WellknownConfig() + + log.Println("IDP Issuer URL", idp.IssuerURL()) + log.Println("Coderd Flags") + log.Printf(`--external-auth-providers='[{"type":"fake","client_id":"%s","client_secret":"%s","auth_url":"%s","token_url":"%s","validate_url":"%s","scopes":["openid","email","profile"]}]'`, + id, sec, prov.AuthURL, prov.TokenURL, prov.UserInfoURL, + ) + + log.Println("Press Ctrl+C to exit") + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + + // Block until ctl+c + <-c + log.Println("Closing") + } +} diff --git a/coderd/coderdtest/oidctest/idp.go b/coderd/coderdtest/oidctest/idp.go index bb758d60f5d0a..6b6936e3465e7 100644 --- a/coderd/coderdtest/oidctest/idp.go +++ b/coderd/coderdtest/oidctest/idp.go @@ -78,9 +78,12 @@ type FakeIDP struct { // "Authorized Redirect URLs". This can be used to emulate that. hookValidRedirectURL func(redirectURL string) error hookUserInfo func(email string) (jwt.MapClaims, error) - hookMutateToken func(token map[string]interface{}) - fakeCoderd func(req *http.Request) (*http.Response, error) - hookOnRefresh func(email string) error + // defaultIDClaims is if a new client connects and we didn't preset + // some claims. + defaultIDClaims jwt.MapClaims + hookMutateToken func(token map[string]interface{}) + fakeCoderd func(req *http.Request) (*http.Response, error) + hookOnRefresh func(email string) error // Custom authentication for the client. This is useful if you want // to test something like PKI auth vs a client_secret. hookAuthenticateClient func(t testing.TB, req *http.Request) (url.Values, error) @@ -162,6 +165,12 @@ func WithStaticUserInfo(info jwt.MapClaims) func(*FakeIDP) { } } +func WithDefaultIDClaims(claims jwt.MapClaims) func(*FakeIDP) { + return func(f *FakeIDP) { + f.defaultIDClaims = claims + } +} + func WithDynamicUserInfo(userInfoFunc func(email string) (jwt.MapClaims, error)) func(*FakeIDP) { return func(f *FakeIDP) { f.hookUserInfo = userInfoFunc @@ -679,7 +688,7 @@ func (f *FakeIDP) httpHandler(t testing.TB) http.Handler { // Always invalidate the code after it is used. f.codeToStateMap.Delete(code) - idTokenClaims, ok := f.stateToIDTokenClaims.Load(stateStr) + idTokenClaims, ok := f.getClaims(f.stateToIDTokenClaims, stateStr) if !ok { t.Errorf("missing id token claims") http.Error(rw, "missing id token claims", http.StatusBadRequest) @@ -699,7 +708,7 @@ func (f *FakeIDP) httpHandler(t testing.TB) http.Handler { return } - idTokenClaims, ok := f.refreshIDTokenClaims.Load(refreshToken) + idTokenClaims, ok := f.getClaims(f.refreshIDTokenClaims, refreshToken) if !ok { t.Errorf("missing id token claims in refresh") http.Error(rw, "missing id token claims in refresh", http.StatusBadRequest) @@ -971,6 +980,10 @@ func (f *FakeIDP) ExternalAuthConfig(t testing.TB, id string, custom *ExternalAu return cfg } +func (f *FakeIDP) AppCredentials() (clientID string, clientSecret string) { + return f.clientID, f.clientSecret +} + // OIDCConfig returns the OIDC config to use for Coderd. func (f *FakeIDP) OIDCConfig(t testing.TB, scopes []string, opts ...func(cfg *coderd.OIDCConfig)) *coderd.OIDCConfig { t.Helper() @@ -1023,6 +1036,17 @@ func (f *FakeIDP) OIDCConfig(t testing.TB, scopes []string, opts ...func(cfg *co return cfg } +func (f *FakeIDP) getClaims(m *syncmap.Map[string, jwt.MapClaims], key string) (jwt.MapClaims, bool) { + v, ok := m.Load(key) + if !ok { + if f.defaultIDClaims != nil { + return f.defaultIDClaims, true + } + return nil, false + } + return v, true +} + func httpErrorCode(defaultCode int, err error) int { var stautsErr statusHookError status := defaultCode diff --git a/codersdk/deployment.go b/codersdk/deployment.go index c3e2db238e4ef..e0dcc4fe4ded2 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -1790,11 +1790,10 @@ Write out the current server config as YAML to stdout.`, // Env handling is done in cli.ReadGitAuthFromEnvironment Name: "External Auth Providers", Description: "External Authentication providers.", - // We need extra scrutiny to ensure this works, is documented, and - // tested before enabling. - YAML: "externalAuthProviders", - Value: &c.ExternalAuthConfigs, - Hidden: true, + YAML: "externalAuthProviders", + Flag: "external-auth-providers", + Value: &c.ExternalAuthConfigs, + Hidden: true, }, { Name: "Custom wgtunnel Host",