mirror of https://github.com/coder/coder.git
681 lines
24 KiB
Go
681 lines
24 KiB
Go
package coderd_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-github/v43/github"
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/oauth2"
|
|
|
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
|
"github.com/coder/coder/v2/coderd/coderdtest/oidctest"
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
|
"github.com/coder/coder/v2/coderd/database/dbtime"
|
|
"github.com/coder/coder/v2/coderd/externalauth"
|
|
"github.com/coder/coder/v2/coderd/httpapi"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/coder/v2/codersdk/agentsdk"
|
|
"github.com/coder/coder/v2/provisioner/echo"
|
|
"github.com/coder/coder/v2/testutil"
|
|
)
|
|
|
|
func TestExternalAuthByID(t *testing.T) {
|
|
t.Parallel()
|
|
t.Run("Unauthenticated", func(t *testing.T) {
|
|
t.Parallel()
|
|
const providerID = "fake-github"
|
|
fake := oidctest.NewFakeIDP(t, oidctest.WithServing())
|
|
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
ExternalAuthConfigs: []*externalauth.Config{
|
|
fake.ExternalAuthConfig(t, providerID, nil, func(cfg *externalauth.Config) {
|
|
cfg.Type = codersdk.EnhancedExternalAuthProviderGitHub.String()
|
|
}),
|
|
},
|
|
})
|
|
coderdtest.CreateFirstUser(t, client)
|
|
auth, err := client.ExternalAuthByID(context.Background(), providerID)
|
|
require.NoError(t, err)
|
|
require.False(t, auth.Authenticated)
|
|
})
|
|
t.Run("AuthenticatedNoUser", func(t *testing.T) {
|
|
// Ensures that a provider that can't obtain a user can
|
|
// still return that the provider is authenticated.
|
|
t.Parallel()
|
|
const providerID = "fake-azure"
|
|
fake := oidctest.NewFakeIDP(t, oidctest.WithServing())
|
|
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
ExternalAuthConfigs: []*externalauth.Config{
|
|
// AzureDevops doesn't have a user endpoint!
|
|
fake.ExternalAuthConfig(t, providerID, nil, func(cfg *externalauth.Config) {
|
|
cfg.Type = codersdk.EnhancedExternalAuthProviderAzureDevops.String()
|
|
}),
|
|
},
|
|
})
|
|
|
|
coderdtest.CreateFirstUser(t, client)
|
|
fake.ExternalLogin(t, client)
|
|
|
|
auth, err := client.ExternalAuthByID(context.Background(), providerID)
|
|
require.NoError(t, err)
|
|
require.True(t, auth.Authenticated)
|
|
})
|
|
t.Run("AuthenticatedWithUser", func(t *testing.T) {
|
|
t.Parallel()
|
|
const providerID = "fake-github"
|
|
fake := oidctest.NewFakeIDP(t, oidctest.WithServing())
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
ExternalAuthConfigs: []*externalauth.Config{
|
|
fake.ExternalAuthConfig(t, providerID, &oidctest.ExternalAuthConfigOptions{
|
|
ValidatePayload: func(_ string) interface{} {
|
|
return github.User{
|
|
Login: github.String("kyle"),
|
|
AvatarURL: github.String("https://avatars.githubusercontent.com/u/12345678?v=4"),
|
|
}
|
|
},
|
|
}, func(cfg *externalauth.Config) {
|
|
cfg.Type = codersdk.EnhancedExternalAuthProviderGitHub.String()
|
|
}),
|
|
},
|
|
})
|
|
|
|
coderdtest.CreateFirstUser(t, client)
|
|
// Login to external auth provider
|
|
fake.ExternalLogin(t, client)
|
|
|
|
auth, err := client.ExternalAuthByID(context.Background(), providerID)
|
|
require.NoError(t, err)
|
|
require.True(t, auth.Authenticated)
|
|
require.NotNil(t, auth.User)
|
|
require.Equal(t, "kyle", auth.User.Login)
|
|
})
|
|
t.Run("AuthenticatedWithInstalls", func(t *testing.T) {
|
|
t.Parallel()
|
|
const providerID = "fake-github"
|
|
fake := oidctest.NewFakeIDP(t, oidctest.WithServing())
|
|
|
|
// routes includes a route for /install that returns a list of installations
|
|
routes := (&oidctest.ExternalAuthConfigOptions{
|
|
ValidatePayload: func(_ string) interface{} {
|
|
return github.User{
|
|
Login: github.String("kyle"),
|
|
AvatarURL: github.String("https://avatars.githubusercontent.com/u/12345678?v=4"),
|
|
}
|
|
},
|
|
}).AddRoute("/installs", func(_ string, rw http.ResponseWriter, r *http.Request) {
|
|
httpapi.Write(r.Context(), rw, http.StatusOK, struct {
|
|
Installations []github.Installation `json:"installations"`
|
|
}{
|
|
Installations: []github.Installation{{
|
|
ID: github.Int64(12345678),
|
|
Account: &github.User{
|
|
Login: github.String("coder"),
|
|
},
|
|
}},
|
|
})
|
|
})
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
ExternalAuthConfigs: []*externalauth.Config{
|
|
fake.ExternalAuthConfig(t, providerID, routes, func(cfg *externalauth.Config) {
|
|
cfg.AppInstallationsURL = strings.TrimSuffix(cfg.ValidateURL, "/") + "/installs"
|
|
cfg.Type = codersdk.EnhancedExternalAuthProviderGitHub.String()
|
|
}),
|
|
},
|
|
})
|
|
|
|
coderdtest.CreateFirstUser(t, client)
|
|
fake.ExternalLogin(t, client)
|
|
|
|
auth, err := client.ExternalAuthByID(context.Background(), providerID)
|
|
require.NoError(t, err)
|
|
require.True(t, auth.Authenticated)
|
|
require.NotNil(t, auth.User)
|
|
require.Equal(t, "kyle", auth.User.Login)
|
|
require.NotNil(t, auth.AppInstallations)
|
|
require.Len(t, auth.AppInstallations, 1)
|
|
})
|
|
}
|
|
|
|
// TestExternalAuthManagement is for testing the apis interacting with
|
|
// external auths from the user perspective. We assume the external auth
|
|
// will always work, so we can test the managing apis like unlinking and
|
|
// listing.
|
|
func TestExternalAuthManagement(t *testing.T) {
|
|
t.Parallel()
|
|
t.Run("ListProviders", func(t *testing.T) {
|
|
t.Parallel()
|
|
const githubID = "fake-github"
|
|
const gitlabID = "fake-gitlab"
|
|
|
|
github := oidctest.NewFakeIDP(t, oidctest.WithServing())
|
|
gitlab := oidctest.NewFakeIDP(t, oidctest.WithServing())
|
|
|
|
owner := coderdtest.New(t, &coderdtest.Options{
|
|
ExternalAuthConfigs: []*externalauth.Config{
|
|
github.ExternalAuthConfig(t, githubID, nil, func(cfg *externalauth.Config) {
|
|
cfg.Type = codersdk.EnhancedExternalAuthProviderGitHub.String()
|
|
}),
|
|
gitlab.ExternalAuthConfig(t, gitlabID, nil, func(cfg *externalauth.Config) {
|
|
cfg.Type = codersdk.EnhancedExternalAuthProviderGitLab.String()
|
|
}),
|
|
},
|
|
})
|
|
ownerUser := coderdtest.CreateFirstUser(t, owner)
|
|
// Just a regular user
|
|
client, _ := coderdtest.CreateAnotherUser(t, owner, ownerUser.OrganizationID)
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
|
|
// List auths without any links.
|
|
list, err := client.ListExternalAuths(ctx)
|
|
require.NoError(t, err)
|
|
require.Len(t, list.Providers, 2)
|
|
require.Len(t, list.Links, 0)
|
|
|
|
// Log into github
|
|
github.ExternalLogin(t, client)
|
|
|
|
list, err = client.ListExternalAuths(ctx)
|
|
require.NoError(t, err)
|
|
require.Len(t, list.Providers, 2)
|
|
require.Len(t, list.Links, 1)
|
|
require.Equal(t, list.Links[0].ProviderID, githubID)
|
|
|
|
// Unlink
|
|
err = client.UnlinkExternalAuthByID(ctx, githubID)
|
|
require.NoError(t, err)
|
|
|
|
list, err = client.ListExternalAuths(ctx)
|
|
require.NoError(t, err)
|
|
require.Len(t, list.Providers, 2)
|
|
require.Len(t, list.Links, 0)
|
|
})
|
|
t.Run("RefreshAllProviders", func(t *testing.T) {
|
|
t.Parallel()
|
|
const githubID = "fake-github"
|
|
const gitlabID = "fake-gitlab"
|
|
|
|
githubCalled := false
|
|
githubApp := oidctest.NewFakeIDP(t, oidctest.WithServing(), oidctest.WithRefresh(func(email string) error {
|
|
githubCalled = true
|
|
return nil
|
|
}))
|
|
gitlabCalled := false
|
|
gitlab := oidctest.NewFakeIDP(t, oidctest.WithServing(), oidctest.WithRefresh(func(email string) error {
|
|
gitlabCalled = true
|
|
return nil
|
|
}))
|
|
|
|
owner, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
|
|
ExternalAuthConfigs: []*externalauth.Config{
|
|
githubApp.ExternalAuthConfig(t, githubID, nil, func(cfg *externalauth.Config) {
|
|
cfg.Type = codersdk.EnhancedExternalAuthProviderGitHub.String()
|
|
}),
|
|
gitlab.ExternalAuthConfig(t, gitlabID, nil, func(cfg *externalauth.Config) {
|
|
cfg.Type = codersdk.EnhancedExternalAuthProviderGitLab.String()
|
|
}),
|
|
},
|
|
})
|
|
ownerUser := coderdtest.CreateFirstUser(t, owner)
|
|
// Just a regular user
|
|
client, user := coderdtest.CreateAnotherUser(t, owner, ownerUser.OrganizationID)
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
|
|
// Log into github & gitlab
|
|
githubApp.ExternalLogin(t, client)
|
|
gitlab.ExternalLogin(t, client)
|
|
|
|
links, err := db.GetExternalAuthLinksByUserID(
|
|
dbauthz.As(ctx, coderdtest.AuthzUserSubject(user, ownerUser.OrganizationID)), user.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, links, 2)
|
|
|
|
// Expire the links
|
|
for _, l := range links {
|
|
_, err := db.UpdateExternalAuthLink(dbauthz.As(ctx, coderdtest.AuthzUserSubject(user, ownerUser.OrganizationID)), database.UpdateExternalAuthLinkParams{
|
|
ProviderID: l.ProviderID,
|
|
UserID: l.UserID,
|
|
UpdatedAt: dbtime.Now(),
|
|
OAuthAccessToken: l.OAuthAccessToken,
|
|
OAuthRefreshToken: l.OAuthRefreshToken,
|
|
OAuthExpiry: time.Now().Add(time.Hour * -1),
|
|
OAuthExtra: l.OAuthExtra,
|
|
})
|
|
require.NoErrorf(t, err, "expire key for %s", l.ProviderID)
|
|
}
|
|
|
|
list, err := client.ListExternalAuths(ctx)
|
|
require.NoError(t, err)
|
|
require.Len(t, list.Links, 2)
|
|
require.True(t, githubCalled, "github should be refreshed")
|
|
require.True(t, gitlabCalled, "gitlab should be refreshed")
|
|
})
|
|
}
|
|
|
|
func TestExternalAuthDevice(t *testing.T) {
|
|
t.Parallel()
|
|
// This is an example test on how to do device auth flow using our fake idp.
|
|
t.Run("WithFakeIDP", func(t *testing.T) {
|
|
t.Parallel()
|
|
fake := oidctest.NewFakeIDP(t, oidctest.WithServing())
|
|
externalID := "fake-idp"
|
|
cfg := fake.ExternalAuthConfig(t, externalID, &oidctest.ExternalAuthConfigOptions{
|
|
UseDeviceAuth: true,
|
|
})
|
|
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
ExternalAuthConfigs: []*externalauth.Config{cfg},
|
|
})
|
|
coderdtest.CreateFirstUser(t, client)
|
|
// Login!
|
|
fake.DeviceLogin(t, client, externalID)
|
|
|
|
extAuth, err := client.ExternalAuthByID(context.Background(), externalID)
|
|
require.NoError(t, err)
|
|
require.True(t, extAuth.Authenticated)
|
|
})
|
|
|
|
t.Run("NotSupported", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
ExternalAuthConfigs: []*externalauth.Config{{
|
|
ID: "test",
|
|
}},
|
|
})
|
|
coderdtest.CreateFirstUser(t, client)
|
|
_, err := client.ExternalAuthDeviceByID(context.Background(), "test")
|
|
var sdkErr *codersdk.Error
|
|
require.ErrorAs(t, err, &sdkErr)
|
|
require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
|
|
})
|
|
t.Run("FetchCode", func(t *testing.T) {
|
|
t.Parallel()
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
httpapi.Write(r.Context(), w, http.StatusOK, codersdk.ExternalAuthDevice{
|
|
UserCode: "hey",
|
|
})
|
|
}))
|
|
defer srv.Close()
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
ExternalAuthConfigs: []*externalauth.Config{{
|
|
ID: "test",
|
|
DeviceAuth: &externalauth.DeviceAuth{
|
|
ClientID: "test",
|
|
CodeURL: srv.URL,
|
|
Scopes: []string{"repo"},
|
|
},
|
|
}},
|
|
})
|
|
coderdtest.CreateFirstUser(t, client)
|
|
device, err := client.ExternalAuthDeviceByID(context.Background(), "test")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "hey", device.UserCode)
|
|
})
|
|
t.Run("ExchangeCode", func(t *testing.T) {
|
|
t.Parallel()
|
|
resp := externalauth.ExchangeDeviceCodeResponse{
|
|
Error: "authorization_pending",
|
|
}
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
httpapi.Write(r.Context(), w, http.StatusOK, resp)
|
|
}))
|
|
defer srv.Close()
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
ExternalAuthConfigs: []*externalauth.Config{{
|
|
ID: "test",
|
|
DeviceAuth: &externalauth.DeviceAuth{
|
|
ClientID: "test",
|
|
TokenURL: srv.URL,
|
|
Scopes: []string{"repo"},
|
|
},
|
|
}},
|
|
})
|
|
coderdtest.CreateFirstUser(t, client)
|
|
err := client.ExternalAuthDeviceExchange(context.Background(), "test", codersdk.ExternalAuthDeviceExchange{
|
|
DeviceCode: "hey",
|
|
})
|
|
var sdkErr *codersdk.Error
|
|
require.ErrorAs(t, err, &sdkErr)
|
|
require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
|
|
require.Equal(t, "authorization_pending", sdkErr.Detail)
|
|
|
|
resp = externalauth.ExchangeDeviceCodeResponse{
|
|
AccessToken: "hey",
|
|
}
|
|
|
|
err = client.ExternalAuthDeviceExchange(context.Background(), "test", codersdk.ExternalAuthDeviceExchange{
|
|
DeviceCode: "hey",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
auth, err := client.ExternalAuthByID(context.Background(), "test")
|
|
require.NoError(t, err)
|
|
require.True(t, auth.Authenticated)
|
|
})
|
|
t.Run("TooManyRequests", func(t *testing.T) {
|
|
t.Parallel()
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusTooManyRequests)
|
|
// Github returns an html payload for this error.
|
|
_, _ = w.Write([]byte(`Please wait a few minutes before you try again`))
|
|
}))
|
|
defer srv.Close()
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
ExternalAuthConfigs: []*externalauth.Config{{
|
|
ID: "test",
|
|
DeviceAuth: &externalauth.DeviceAuth{
|
|
ClientID: "test",
|
|
CodeURL: srv.URL,
|
|
Scopes: []string{"repo"},
|
|
},
|
|
}},
|
|
})
|
|
coderdtest.CreateFirstUser(t, client)
|
|
_, err := client.ExternalAuthDeviceByID(context.Background(), "test")
|
|
require.ErrorContains(t, err, "rate limit hit")
|
|
})
|
|
|
|
// If we forget to add the accept header, we get a form encoded body instead.
|
|
t.Run("FormEncodedBody", func(t *testing.T) {
|
|
t.Parallel()
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/x-www-form-urlencoded")
|
|
_, _ = w.Write([]byte(url.Values{"access_token": {"hey"}}.Encode()))
|
|
}))
|
|
defer srv.Close()
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
ExternalAuthConfigs: []*externalauth.Config{{
|
|
ID: "test",
|
|
DeviceAuth: &externalauth.DeviceAuth{
|
|
ClientID: "test",
|
|
CodeURL: srv.URL,
|
|
Scopes: []string{"repo"},
|
|
},
|
|
}},
|
|
})
|
|
coderdtest.CreateFirstUser(t, client)
|
|
_, err := client.ExternalAuthDeviceByID(context.Background(), "test")
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, "is form-url encoded")
|
|
})
|
|
}
|
|
|
|
// nolint:bodyclose
|
|
func TestExternalAuthCallback(t *testing.T) {
|
|
t.Parallel()
|
|
t.Run("NoMatchingConfig", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
IncludeProvisionerDaemon: true,
|
|
ExternalAuthConfigs: []*externalauth.Config{},
|
|
})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
authToken := uuid.NewString()
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionPlan: echo.PlanComplete,
|
|
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
|
|
})
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
agentClient := agentsdk.New(client.URL)
|
|
agentClient.SetSessionToken(authToken)
|
|
_, err := agentClient.ExternalAuth(context.Background(), agentsdk.ExternalAuthRequest{
|
|
Match: "github.com",
|
|
})
|
|
var apiError *codersdk.Error
|
|
require.ErrorAs(t, err, &apiError)
|
|
require.Equal(t, http.StatusNotFound, apiError.StatusCode())
|
|
})
|
|
t.Run("ReturnsURL", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
IncludeProvisionerDaemon: true,
|
|
ExternalAuthConfigs: []*externalauth.Config{{
|
|
InstrumentedOAuth2Config: &testutil.OAuth2Config{},
|
|
ID: "github",
|
|
Regex: regexp.MustCompile(`github\.com`),
|
|
Type: codersdk.EnhancedExternalAuthProviderGitHub.String(),
|
|
}},
|
|
})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
authToken := uuid.NewString()
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionPlan: echo.PlanComplete,
|
|
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
|
|
})
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
agentClient := agentsdk.New(client.URL)
|
|
agentClient.SetSessionToken(authToken)
|
|
token, err := agentClient.ExternalAuth(context.Background(), agentsdk.ExternalAuthRequest{
|
|
Match: "github.com/asd/asd",
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, strings.HasSuffix(token.URL, fmt.Sprintf("/external-auth/%s", "github")), token.URL)
|
|
})
|
|
t.Run("UnauthorizedCallback", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
IncludeProvisionerDaemon: true,
|
|
ExternalAuthConfigs: []*externalauth.Config{{
|
|
InstrumentedOAuth2Config: &testutil.OAuth2Config{},
|
|
ID: "github",
|
|
Regex: regexp.MustCompile(`github\.com`),
|
|
Type: codersdk.EnhancedExternalAuthProviderGitHub.String(),
|
|
}},
|
|
})
|
|
resp := coderdtest.RequestExternalAuthCallback(t, "github", client)
|
|
require.Equal(t, http.StatusSeeOther, resp.StatusCode)
|
|
})
|
|
t.Run("AuthorizedCallback", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
IncludeProvisionerDaemon: true,
|
|
ExternalAuthConfigs: []*externalauth.Config{{
|
|
InstrumentedOAuth2Config: &testutil.OAuth2Config{},
|
|
ID: "github",
|
|
Regex: regexp.MustCompile(`github\.com`),
|
|
Type: codersdk.EnhancedExternalAuthProviderGitHub.String(),
|
|
}},
|
|
})
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
resp := coderdtest.RequestExternalAuthCallback(t, "github", client)
|
|
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
|
location, err := resp.Location()
|
|
require.NoError(t, err)
|
|
require.Equal(t, "/external-auth/github", location.Path)
|
|
|
|
// Callback again to simulate updating the token.
|
|
resp = coderdtest.RequestExternalAuthCallback(t, "github", client)
|
|
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
|
})
|
|
t.Run("ValidateURL", func(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
|
|
srv := httptest.NewServer(nil)
|
|
defer srv.Close()
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
IncludeProvisionerDaemon: true,
|
|
ExternalAuthConfigs: []*externalauth.Config{{
|
|
ValidateURL: srv.URL,
|
|
InstrumentedOAuth2Config: &testutil.OAuth2Config{},
|
|
ID: "github",
|
|
Regex: regexp.MustCompile(`github\.com`),
|
|
Type: codersdk.EnhancedExternalAuthProviderGitHub.String(),
|
|
}},
|
|
})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
authToken := uuid.NewString()
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionPlan: echo.PlanComplete,
|
|
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
|
|
})
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
agentClient := agentsdk.New(client.URL)
|
|
agentClient.SetSessionToken(authToken)
|
|
|
|
resp := coderdtest.RequestExternalAuthCallback(t, "github", client)
|
|
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
|
|
|
// If the validation URL says unauthorized, the callback
|
|
// URL to re-authenticate should be returned.
|
|
srv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
})
|
|
res, err := agentClient.ExternalAuth(ctx, agentsdk.ExternalAuthRequest{
|
|
Match: "github.com/asd/asd",
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, res.URL)
|
|
|
|
// If the validation URL gives a non-OK status code, this
|
|
// should be treated as an internal server error.
|
|
srv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
w.Write([]byte("Something went wrong!"))
|
|
})
|
|
_, err = agentClient.ExternalAuth(ctx, agentsdk.ExternalAuthRequest{
|
|
Match: "github.com/asd/asd",
|
|
})
|
|
var apiError *codersdk.Error
|
|
require.ErrorAs(t, err, &apiError)
|
|
require.Equal(t, http.StatusInternalServerError, apiError.StatusCode())
|
|
require.Equal(t, "validate external auth token: status 403: body: Something went wrong!", apiError.Detail)
|
|
})
|
|
|
|
t.Run("ExpiredNoRefresh", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
IncludeProvisionerDaemon: true,
|
|
ExternalAuthConfigs: []*externalauth.Config{{
|
|
InstrumentedOAuth2Config: &testutil.OAuth2Config{
|
|
Token: &oauth2.Token{
|
|
AccessToken: "token",
|
|
RefreshToken: "something",
|
|
Expiry: dbtime.Now().Add(-time.Hour),
|
|
},
|
|
},
|
|
ID: "github",
|
|
Regex: regexp.MustCompile(`github\.com`),
|
|
Type: codersdk.EnhancedExternalAuthProviderGitHub.String(),
|
|
NoRefresh: true,
|
|
}},
|
|
})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
authToken := uuid.NewString()
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionPlan: echo.PlanComplete,
|
|
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
|
|
})
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
agentClient := agentsdk.New(client.URL)
|
|
agentClient.SetSessionToken(authToken)
|
|
|
|
token, err := agentClient.ExternalAuth(context.Background(), agentsdk.ExternalAuthRequest{
|
|
Match: "github.com/asd/asd",
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, token.URL)
|
|
|
|
// In the configuration, we set our OAuth provider
|
|
// to return an expired token. Coder consumes this
|
|
// and stores it.
|
|
resp := coderdtest.RequestExternalAuthCallback(t, "github", client)
|
|
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
|
|
|
// Because the token is expired and `NoRefresh` is specified,
|
|
// a redirect URL should be returned again.
|
|
token, err = agentClient.ExternalAuth(context.Background(), agentsdk.ExternalAuthRequest{
|
|
Match: "github.com/asd/asd",
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, token.URL)
|
|
})
|
|
|
|
t.Run("FullFlow", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
IncludeProvisionerDaemon: true,
|
|
ExternalAuthConfigs: []*externalauth.Config{{
|
|
InstrumentedOAuth2Config: &testutil.OAuth2Config{},
|
|
ID: "github",
|
|
Regex: regexp.MustCompile(`github\.com`),
|
|
Type: codersdk.EnhancedExternalAuthProviderGitHub.String(),
|
|
}},
|
|
})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
authToken := uuid.NewString()
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionPlan: echo.PlanComplete,
|
|
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
|
|
})
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
agentClient := agentsdk.New(client.URL)
|
|
agentClient.SetSessionToken(authToken)
|
|
|
|
token, err := agentClient.ExternalAuth(context.Background(), agentsdk.ExternalAuthRequest{
|
|
Match: "github.com/asd/asd",
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, token.URL)
|
|
|
|
// Start waiting for the token callback...
|
|
tokenChan := make(chan agentsdk.ExternalAuthResponse, 1)
|
|
go func() {
|
|
token, err := agentClient.ExternalAuth(context.Background(), agentsdk.ExternalAuthRequest{
|
|
Match: "github.com/asd/asd",
|
|
Listen: true,
|
|
})
|
|
assert.NoError(t, err)
|
|
tokenChan <- token
|
|
}()
|
|
|
|
time.Sleep(250 * time.Millisecond)
|
|
|
|
resp := coderdtest.RequestExternalAuthCallback(t, "github", client)
|
|
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
|
token = <-tokenChan
|
|
require.Equal(t, "access_token", token.Username)
|
|
|
|
token, err = agentClient.ExternalAuth(context.Background(), agentsdk.ExternalAuthRequest{
|
|
Match: "github.com/asd/asd",
|
|
})
|
|
require.NoError(t, err)
|
|
})
|
|
}
|