Files
coder/coderd/userauth_test.go
Spike Curtis 5861e516b9 chore: add standard test logger ignoring db canceled (#15556)
Refactors our use of `slogtest` to instantiate a "standard logger" across most of our tests.  This standard logger incorporates https://github.com/coder/slog/pull/217 to also ignore database query canceled errors by default, which are a source of low-severity flakes.

Any test that has set non-default `slogtest.Options` is left alone. In particular, `coderdtest` defaults to ignoring all errors. We might consider revisiting that decision now that we have better tools to target the really common flaky Error logs on shutdown.
2024-11-18 14:09:22 +04:00

2188 lines
70 KiB
Go

package coderd_test
import (
"context"
"crypto"
"crypto/rand"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/cookiejar"
"net/url"
"strings"
"testing"
"time"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/go-jose/go-jose/v4"
"github.com/golang-jwt/jwt/v4"
"github.com/google/go-github/v43/github"
"github.com/google/uuid"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/v2/coderd"
"github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/coderdtest/oidctest"
"github.com/coder/coder/v2/coderd/cryptokeys"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/jwtutils"
"github.com/coder/coder/v2/coderd/notifications"
"github.com/coder/coder/v2/coderd/notifications/notificationstest"
"github.com/coder/coder/v2/coderd/promoauth"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/cryptorand"
"github.com/coder/coder/v2/testutil"
)
// This test specifically tests logging in with OIDC when an expired
// OIDC session token exists.
// The token refreshing should not happen since we are reauthenticating.
// nolint:bodyclose
func TestOIDCOauthLoginWithExisting(t *testing.T) {
t.Parallel()
fake := oidctest.NewFakeIDP(t,
oidctest.WithRefresh(func(_ string) error {
return xerrors.New("refreshing token should never occur")
}),
oidctest.WithServing(),
)
cfg := fake.OIDCConfig(t, nil, func(cfg *coderd.OIDCConfig) {
cfg.AllowSignups = true
cfg.IgnoreUserInfo = true
})
client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{
OIDCConfig: cfg,
})
const username = "alice"
claims := jwt.MapClaims{
"email": "alice@coder.com",
"email_verified": true,
"preferred_username": username,
}
helper := oidctest.NewLoginHelper(client, fake)
// Signup alice
userClient, _ := helper.Login(t, claims)
// Expire the link. This will force the client to refresh the token.
helper.ExpireOauthToken(t, api.Database, userClient)
// Instead of refreshing, just log in again.
helper.Login(t, claims)
}
func TestUserLogin(t *testing.T) {
t.Parallel()
t.Run("OK", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
anotherClient, anotherUser := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
_, err := anotherClient.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{
Email: anotherUser.Email,
Password: "SomeSecurePassword!",
})
require.NoError(t, err)
})
t.Run("UserDeleted", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
anotherClient, anotherUser := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
client.DeleteUser(context.Background(), anotherUser.ID)
_, err := anotherClient.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{
Email: anotherUser.Email,
Password: "SomeSecurePassword!",
})
require.Error(t, err)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
})
t.Run("LoginTypeNone", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
anotherClient, anotherUser := coderdtest.CreateAnotherUserMutators(t, client, user.OrganizationID, nil, func(r *codersdk.CreateUserRequestWithOrgs) {
r.Password = ""
r.UserLoginType = codersdk.LoginTypeNone
})
_, err := anotherClient.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{
Email: anotherUser.Email,
Password: "SomeSecurePassword!",
})
require.Error(t, err)
})
}
func TestUserAuthMethods(t *testing.T) {
t.Parallel()
t.Run("Password", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
methods, err := client.AuthMethods(ctx)
require.NoError(t, err)
require.True(t, methods.Password.Enabled)
require.False(t, methods.Github.Enabled)
})
t.Run("Github", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{
GithubOAuth2Config: &coderd.GithubOAuth2Config{},
})
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
methods, err := client.AuthMethods(ctx)
require.NoError(t, err)
require.True(t, methods.Password.Enabled)
require.True(t, methods.Github.Enabled)
})
}
// nolint:bodyclose
func TestUserOAuth2Github(t *testing.T) {
t.Parallel()
stateActive := "active"
statePending := "pending"
t.Run("NotInAllowedOrganization", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{
GithubOAuth2Config: &coderd.GithubOAuth2Config{
OAuth2Config: &testutil.OAuth2Config{},
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
return []*github.Membership{{
State: &stateActive,
Organization: &github.Organization{
Login: github.String("kyle"),
},
}}, nil
},
},
})
resp := oauth2Callback(t, client)
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
})
t.Run("NotInAllowedTeam", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{
GithubOAuth2Config: &coderd.GithubOAuth2Config{
AllowOrganizations: []string{"coder"},
AllowTeams: []coderd.GithubOAuth2Team{{"another", "something"}, {"coder", "frontend"}},
OAuth2Config: &testutil.OAuth2Config{},
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
return []*github.Membership{{
State: &stateActive,
Organization: &github.Organization{
Login: github.String("coder"),
},
}}, nil
},
AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) {
return &github.User{
ID: github.Int64(100),
Login: github.String("kyle"),
Name: github.String("Kylium Carbonate"),
}, nil
},
TeamMembership: func(ctx context.Context, client *http.Client, org, team, username string) (*github.Membership, error) {
return nil, xerrors.New("no perms")
},
},
})
resp := oauth2Callback(t, client)
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
})
t.Run("UnverifiedEmail", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{
GithubOAuth2Config: &coderd.GithubOAuth2Config{
OAuth2Config: &testutil.OAuth2Config{},
AllowOrganizations: []string{"coder"},
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
return []*github.Membership{{
State: &stateActive,
Organization: &github.Organization{
Login: github.String("coder"),
},
}}, nil
},
AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) {
return &github.User{}, nil
},
ListEmails: func(ctx context.Context, client *http.Client) ([]*github.UserEmail, error) {
return []*github.UserEmail{{
Email: github.String("testuser@coder.com"),
Verified: github.Bool(false),
}}, nil
},
},
})
_ = coderdtest.CreateFirstUser(t, client)
resp := oauth2Callback(t, client)
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
t.Run("BlockSignups", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{
GithubOAuth2Config: &coderd.GithubOAuth2Config{
OAuth2Config: &testutil.OAuth2Config{},
AllowOrganizations: []string{"coder"},
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
return []*github.Membership{{
State: &stateActive,
Organization: &github.Organization{
Login: github.String("coder"),
},
}}, nil
},
AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) {
return &github.User{
ID: github.Int64(100),
Login: github.String("testuser"),
Name: github.String("The Right Honorable Sir Test McUser"),
}, nil
},
ListEmails: func(ctx context.Context, client *http.Client) ([]*github.UserEmail, error) {
return []*github.UserEmail{{
Email: github.String("testuser@coder.com"),
Verified: github.Bool(true),
Primary: github.Bool(true),
}}, nil
},
},
})
resp := oauth2Callback(t, client)
require.Equal(t, http.StatusForbidden, resp.StatusCode)
})
t.Run("MultiLoginNotAllowed", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{
GithubOAuth2Config: &coderd.GithubOAuth2Config{
OAuth2Config: &testutil.OAuth2Config{},
AllowOrganizations: []string{"coder"},
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
return []*github.Membership{{
State: &stateActive,
Organization: &github.Organization{
Login: github.String("coder"),
},
}}, nil
},
AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) {
return &github.User{
ID: github.Int64(100),
Login: github.String("testuser"),
Name: github.String("The Right Honorable Sir Test McUser"),
}, nil
},
ListEmails: func(ctx context.Context, client *http.Client) ([]*github.UserEmail, error) {
return []*github.UserEmail{{
Email: github.String("testuser@coder.com"),
Verified: github.Bool(true),
Primary: github.Bool(true),
}}, nil
},
},
})
// Creates the first user with login_type 'password'.
_ = coderdtest.CreateFirstUser(t, client)
// Attempting to login should give us a 403 since the user
// already has a login_type of 'password'.
resp := oauth2Callback(t, client)
require.Equal(t, http.StatusForbidden, resp.StatusCode)
})
t.Run("Signup", func(t *testing.T) {
t.Parallel()
auditor := audit.NewMock()
client := coderdtest.New(t, &coderdtest.Options{
Auditor: auditor,
GithubOAuth2Config: &coderd.GithubOAuth2Config{
OAuth2Config: &testutil.OAuth2Config{},
AllowOrganizations: []string{"coder"},
AllowSignups: true,
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
return []*github.Membership{{
State: &stateActive,
Organization: &github.Organization{
Login: github.String("coder"),
},
}}, nil
},
AuthenticatedUser: func(ctx context.Context, _ *http.Client) (*github.User, error) {
return &github.User{
AvatarURL: github.String("/hello-world"),
ID: i64ptr(1234),
Login: github.String("kyle"),
Name: github.String("Kylium Carbonate"),
}, nil
},
ListEmails: func(ctx context.Context, client *http.Client) ([]*github.UserEmail, error) {
return []*github.UserEmail{{
Email: github.String("kyle@coder.com"),
Verified: github.Bool(true),
Primary: github.Bool(true),
}}, nil
},
},
})
numLogs := len(auditor.AuditLogs())
// Validate that attempting to redirect away from the
// site does not work.
maliciousHost := "https://malicious.com"
expectedPath := "/my/path"
resp := oauth2Callback(t, client, func(req *http.Request) {
// Add the cookie to bypass the parsing in httpmw/oauth2.go
req.AddCookie(&http.Cookie{
Name: codersdk.OAuth2RedirectCookie,
Value: maliciousHost + expectedPath,
})
})
numLogs++ // add an audit log for login
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
redirect, err := resp.Location()
require.NoError(t, err)
require.Equal(t, expectedPath, redirect.Path)
require.Equal(t, client.URL.Host, redirect.Host)
require.NotContains(t, redirect.String(), maliciousHost)
client.SetSessionToken(authCookieValue(resp.Cookies()))
user, err := client.User(context.Background(), "me")
require.NoError(t, err)
require.Equal(t, "kyle@coder.com", user.Email)
require.Equal(t, "kyle", user.Username)
require.Equal(t, "Kylium Carbonate", user.Name)
require.Equal(t, "/hello-world", user.AvatarURL)
require.Equal(t, 1, len(user.OrganizationIDs), "in the default org")
require.Len(t, auditor.AuditLogs(), numLogs)
require.NotEqual(t, auditor.AuditLogs()[numLogs-1].UserID, uuid.Nil)
require.Equal(t, database.AuditActionRegister, auditor.AuditLogs()[numLogs-1].Action)
})
t.Run("SignupWeirdName", func(t *testing.T) {
t.Parallel()
auditor := audit.NewMock()
client := coderdtest.New(t, &coderdtest.Options{
Auditor: auditor,
GithubOAuth2Config: &coderd.GithubOAuth2Config{
OAuth2Config: &testutil.OAuth2Config{},
AllowOrganizations: []string{"coder"},
AllowSignups: true,
ListOrganizationMemberships: func(_ context.Context, _ *http.Client) ([]*github.Membership, error) {
return []*github.Membership{{
State: &stateActive,
Organization: &github.Organization{
Login: github.String("coder"),
},
}}, nil
},
AuthenticatedUser: func(_ context.Context, _ *http.Client) (*github.User, error) {
return &github.User{
AvatarURL: github.String("/hello-world"),
ID: i64ptr(1234),
Login: github.String("kyle"),
Name: github.String(" " + strings.Repeat("a", 129) + " "),
}, nil
},
ListEmails: func(_ context.Context, _ *http.Client) ([]*github.UserEmail, error) {
return []*github.UserEmail{{
Email: github.String("kyle@coder.com"),
Verified: github.Bool(true),
Primary: github.Bool(true),
}}, nil
},
},
})
numLogs := len(auditor.AuditLogs())
resp := oauth2Callback(t, client)
numLogs++ // add an audit log for login
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
client.SetSessionToken(authCookieValue(resp.Cookies()))
user, err := client.User(context.Background(), "me")
require.NoError(t, err)
require.Equal(t, "kyle@coder.com", user.Email)
require.Equal(t, "kyle", user.Username)
require.Equal(t, strings.Repeat("a", 128), user.Name)
require.Equal(t, "/hello-world", user.AvatarURL)
require.Equal(t, 1, len(user.OrganizationIDs), "in the default org")
require.Len(t, auditor.AuditLogs(), numLogs)
require.NotEqual(t, auditor.AuditLogs()[numLogs-1].UserID, uuid.Nil)
require.Equal(t, database.AuditActionRegister, auditor.AuditLogs()[numLogs-1].Action)
})
t.Run("SignupAllowedTeam", func(t *testing.T) {
t.Parallel()
auditor := audit.NewMock()
client := coderdtest.New(t, &coderdtest.Options{
Auditor: auditor,
GithubOAuth2Config: &coderd.GithubOAuth2Config{
AllowSignups: true,
AllowOrganizations: []string{"coder"},
AllowTeams: []coderd.GithubOAuth2Team{{"coder", "frontend"}},
OAuth2Config: &testutil.OAuth2Config{},
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
return []*github.Membership{{
State: &stateActive,
Organization: &github.Organization{
Login: github.String("coder"),
},
}}, nil
},
TeamMembership: func(ctx context.Context, client *http.Client, org, team, username string) (*github.Membership, error) {
return &github.Membership{}, nil
},
AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) {
return &github.User{
AvatarURL: github.String("/hello-world"),
ID: github.Int64(100),
Login: github.String("kyle"),
Name: github.String("Kylium Carbonate"),
}, nil
},
ListEmails: func(ctx context.Context, client *http.Client) ([]*github.UserEmail, error) {
return []*github.UserEmail{{
Email: github.String("kyle@coder.com"),
Verified: github.Bool(true),
Primary: github.Bool(true),
}}, nil
},
},
})
numLogs := len(auditor.AuditLogs())
resp := oauth2Callback(t, client)
numLogs++ // add an audit log for login
client.SetSessionToken(authCookieValue(resp.Cookies()))
user, err := client.User(context.Background(), "me")
require.NoError(t, err)
require.Equal(t, "kyle@coder.com", user.Email)
require.Equal(t, "kyle", user.Username)
require.Equal(t, "Kylium Carbonate", user.Name)
require.Equal(t, "/hello-world", user.AvatarURL)
require.Equal(t, 1, len(user.OrganizationIDs), "in the default org")
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
require.Len(t, auditor.AuditLogs(), numLogs)
require.Equal(t, database.AuditActionRegister, auditor.AuditLogs()[numLogs-1].Action)
})
// nolint: dupl
t.Run("SignupAllowedTeamInFirstOrganization", func(t *testing.T) {
t.Parallel()
auditor := audit.NewMock()
client := coderdtest.New(t, &coderdtest.Options{
Auditor: auditor,
GithubOAuth2Config: &coderd.GithubOAuth2Config{
AllowSignups: true,
AllowOrganizations: []string{"coder", "nil"},
AllowTeams: []coderd.GithubOAuth2Team{{"coder", "backend"}},
OAuth2Config: &testutil.OAuth2Config{},
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
return []*github.Membership{
{
State: &stateActive,
Organization: &github.Organization{
Login: github.String("coder"),
},
},
{
State: &stateActive,
Organization: &github.Organization{
Login: github.String("nil"),
},
},
}, nil
},
TeamMembership: func(ctx context.Context, client *http.Client, org, team, username string) (*github.Membership, error) {
return &github.Membership{}, nil
},
AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) {
return &github.User{
ID: github.Int64(100),
Login: github.String("mathias"),
Name: github.String("Mathias Mathias"),
}, nil
},
ListEmails: func(ctx context.Context, client *http.Client) ([]*github.UserEmail, error) {
return []*github.UserEmail{{
Email: github.String("mathias@coder.com"),
Verified: github.Bool(true),
Primary: github.Bool(true),
}}, nil
},
},
})
numLogs := len(auditor.AuditLogs())
resp := oauth2Callback(t, client)
numLogs++ // add an audit log for login
client.SetSessionToken(authCookieValue(resp.Cookies()))
user, err := client.User(context.Background(), "me")
require.NoError(t, err)
require.Equal(t, "mathias@coder.com", user.Email)
require.Equal(t, "mathias", user.Username)
require.Equal(t, "Mathias Mathias", user.Name)
require.Equal(t, 1, len(user.OrganizationIDs), "in the default org")
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
require.Len(t, auditor.AuditLogs(), numLogs)
require.Equal(t, database.AuditActionRegister, auditor.AuditLogs()[numLogs-1].Action)
})
// nolint: dupl
t.Run("SignupAllowedTeamInSecondOrganization", func(t *testing.T) {
t.Parallel()
auditor := audit.NewMock()
client := coderdtest.New(t, &coderdtest.Options{
Auditor: auditor,
GithubOAuth2Config: &coderd.GithubOAuth2Config{
AllowSignups: true,
AllowOrganizations: []string{"coder", "nil"},
AllowTeams: []coderd.GithubOAuth2Team{{"nil", "null"}},
OAuth2Config: &testutil.OAuth2Config{},
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
return []*github.Membership{
{
State: &stateActive,
Organization: &github.Organization{
Login: github.String("coder"),
},
},
{
State: &stateActive,
Organization: &github.Organization{
Login: github.String("nil"),
},
},
}, nil
},
TeamMembership: func(ctx context.Context, client *http.Client, org, team, username string) (*github.Membership, error) {
return &github.Membership{}, nil
},
AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) {
return &github.User{
ID: github.Int64(100),
Login: github.String("mathias"),
Name: github.String("Mathias Mathias"),
}, nil
},
ListEmails: func(ctx context.Context, client *http.Client) ([]*github.UserEmail, error) {
return []*github.UserEmail{{
Email: github.String("mathias@coder.com"),
Verified: github.Bool(true),
Primary: github.Bool(true),
}}, nil
},
},
})
numLogs := len(auditor.AuditLogs())
resp := oauth2Callback(t, client)
numLogs++ // add an audit log for login
client.SetSessionToken(authCookieValue(resp.Cookies()))
user, err := client.User(context.Background(), "me")
require.NoError(t, err)
require.Equal(t, "mathias@coder.com", user.Email)
require.Equal(t, "mathias", user.Username)
require.Equal(t, "Mathias Mathias", user.Name)
require.Equal(t, 1, len(user.OrganizationIDs), "in the default org")
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
require.Len(t, auditor.AuditLogs(), numLogs)
require.Equal(t, database.AuditActionRegister, auditor.AuditLogs()[numLogs-1].Action)
})
t.Run("SignupAllowEveryone", func(t *testing.T) {
t.Parallel()
auditor := audit.NewMock()
client := coderdtest.New(t, &coderdtest.Options{
Auditor: auditor,
GithubOAuth2Config: &coderd.GithubOAuth2Config{
AllowSignups: true,
AllowEveryone: true,
OAuth2Config: &testutil.OAuth2Config{},
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
return []*github.Membership{}, nil
},
TeamMembership: func(ctx context.Context, client *http.Client, org, team, username string) (*github.Membership, error) {
return nil, xerrors.New("no teams")
},
AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) {
return &github.User{
ID: github.Int64(100),
Login: github.String("mathias"),
Name: github.String("Mathias Mathias"),
}, nil
},
ListEmails: func(ctx context.Context, client *http.Client) ([]*github.UserEmail, error) {
return []*github.UserEmail{{
Email: github.String("mathias@coder.com"),
Verified: github.Bool(true),
Primary: github.Bool(true),
}}, nil
},
},
})
numLogs := len(auditor.AuditLogs())
resp := oauth2Callback(t, client)
numLogs++ // add an audit log for login
client.SetSessionToken(authCookieValue(resp.Cookies()))
user, err := client.User(context.Background(), "me")
require.NoError(t, err)
require.Equal(t, "mathias@coder.com", user.Email)
require.Equal(t, "mathias", user.Username)
require.Equal(t, "Mathias Mathias", user.Name)
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
require.Len(t, auditor.AuditLogs(), numLogs)
require.Equal(t, database.AuditActionRegister, auditor.AuditLogs()[numLogs-1].Action)
})
t.Run("SignupReplaceUnderscores", func(t *testing.T) {
t.Parallel()
auditor := audit.NewMock()
client := coderdtest.New(t, &coderdtest.Options{
Auditor: auditor,
GithubOAuth2Config: &coderd.GithubOAuth2Config{
AllowSignups: true,
AllowEveryone: true,
OAuth2Config: &testutil.OAuth2Config{},
ListOrganizationMemberships: func(_ context.Context, _ *http.Client) ([]*github.Membership, error) {
return []*github.Membership{}, nil
},
TeamMembership: func(_ context.Context, _ *http.Client, _, _, _ string) (*github.Membership, error) {
return nil, xerrors.New("no teams")
},
AuthenticatedUser: func(_ context.Context, _ *http.Client) (*github.User, error) {
return &github.User{
ID: github.Int64(100),
Login: github.String("mathias_coder"),
}, nil
},
ListEmails: func(_ context.Context, _ *http.Client) ([]*github.UserEmail, error) {
return []*github.UserEmail{{
Email: github.String("mathias@coder.com"),
Verified: github.Bool(true),
Primary: github.Bool(true),
}}, nil
},
},
})
numLogs := len(auditor.AuditLogs())
resp := oauth2Callback(t, client)
numLogs++ // add an audit log for login
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
require.Len(t, auditor.AuditLogs(), numLogs)
require.Equal(t, database.AuditActionRegister, auditor.AuditLogs()[numLogs-1].Action)
client.SetSessionToken(authCookieValue(resp.Cookies()))
user, err := client.User(context.Background(), "me")
require.NoError(t, err)
require.Equal(t, "mathias-coder", user.Username)
})
t.Run("SignupFailedInactiveInOrg", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{
GithubOAuth2Config: &coderd.GithubOAuth2Config{
AllowSignups: true,
AllowOrganizations: []string{"coder"},
AllowTeams: []coderd.GithubOAuth2Team{{"coder", "frontend"}},
OAuth2Config: &testutil.OAuth2Config{},
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
return []*github.Membership{{
State: &statePending,
Organization: &github.Organization{
Login: github.String("coder"),
},
}}, nil
},
TeamMembership: func(ctx context.Context, client *http.Client, org, team, username string) (*github.Membership, error) {
return &github.Membership{}, nil
},
AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) {
return &github.User{
ID: github.Int64(100),
Login: github.String("kyle"),
Name: github.String("Kylium Carbonate"),
}, nil
},
ListEmails: func(ctx context.Context, client *http.Client) ([]*github.UserEmail, error) {
return []*github.UserEmail{{
Email: github.String("kyle@coder.com"),
Verified: github.Bool(true),
Primary: github.Bool(true),
}}, nil
},
},
})
resp := oauth2Callback(t, client)
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
})
// The bug only is exercised when a deleted user with the same linked_id exists.
// Still related open issues:
// - https://github.com/coder/coder/issues/12116
// - https://github.com/coder/coder/issues/12115
t.Run("ChangedEmail", func(t *testing.T) {
t.Parallel()
fake := oidctest.NewFakeIDP(t,
oidctest.WithServing(),
oidctest.WithCallbackPath("/api/v2/users/oauth2/github/callback"),
)
const ghID = int64(7777)
auditor := audit.NewMock()
coderEmail := &github.UserEmail{
Email: github.String("alice@coder.com"),
Verified: github.Bool(true),
Primary: github.Bool(true),
}
gmailEmail := &github.UserEmail{
Email: github.String("alice@gmail.com"),
Verified: github.Bool(true),
Primary: github.Bool(false),
}
emails := []*github.UserEmail{
gmailEmail,
coderEmail,
}
owner, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
Auditor: auditor,
GithubOAuth2Config: &coderd.GithubOAuth2Config{
AllowSignups: true,
AllowEveryone: true,
OAuth2Config: promoauth.NewFactory(prometheus.NewRegistry()).NewGithub("test-github", fake.OIDCConfig(t, []string{})),
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
return []*github.Membership{}, nil
},
TeamMembership: func(ctx context.Context, client *http.Client, org, team, username string) (*github.Membership, error) {
return nil, xerrors.New("no teams")
},
AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) {
return &github.User{
Login: github.String("alice"),
ID: github.Int64(ghID),
Name: github.String("Alice Liddell"),
}, nil
},
ListEmails: func(ctx context.Context, client *http.Client) ([]*github.UserEmail, error) {
return emails, nil
},
},
})
first := coderdtest.CreateFirstUser(t, owner)
ctx := testutil.Context(t, testutil.WaitLong)
ownerUser, err := owner.User(context.Background(), "me")
require.NoError(t, err)
// Create the user, then delete the user, then create again.
// This causes the email change to fail.
client := codersdk.New(owner.URL)
client, _ = fake.Login(t, client, jwt.MapClaims{})
deleted, err := client.User(ctx, "me")
require.NoError(t, err)
err = owner.DeleteUser(ctx, deleted.ID)
require.NoError(t, err)
// Check no user links for the user
links, err := db.GetUserLinksByUserID(dbauthz.As(ctx, coderdtest.AuthzUserSubject(ownerUser, first.OrganizationID)), deleted.ID)
require.NoError(t, err)
require.Empty(t, links)
// Make sure a user_link cannot be created with a deleted user.
// nolint:gocritic // Unit test
_, err = db.InsertUserLink(dbauthz.AsSystemRestricted(ctx), database.InsertUserLinkParams{
UserID: deleted.ID,
LoginType: "github",
LinkedID: "100",
OAuthAccessToken: "random",
OAuthRefreshToken: "random",
OAuthExpiry: time.Now(),
Claims: database.UserLinkClaims{},
})
require.ErrorContains(t, err, "Cannot create user_link for deleted user")
// Create the user again.
client, _ = fake.Login(t, client, jwt.MapClaims{})
user, err := client.User(ctx, "me")
require.NoError(t, err)
userID := user.ID
require.Equal(t, user.Email, *coderEmail.Email)
// Now the user is registered, let's change their primary email.
coderEmail.Primary = github.Bool(false)
gmailEmail.Primary = github.Bool(true)
client, _ = fake.Login(t, client, jwt.MapClaims{})
user, err = client.User(ctx, "me")
require.NoError(t, err)
require.Equal(t, user.ID, userID, "user_id is different, a new user was likely created")
require.Equal(t, user.Email, *gmailEmail.Email)
// Entirely change emails.
newEmail := "alice@newdomain.com"
emails = []*github.UserEmail{
{
Email: github.String("alice@newdomain.com"),
Primary: github.Bool(true),
Verified: github.Bool(true),
},
}
client, _ = fake.Login(t, client, jwt.MapClaims{})
user, err = client.User(ctx, "me")
require.NoError(t, err)
require.Equal(t, user.ID, userID, "user_id is different, a new user was likely created")
require.Equal(t, user.Email, newEmail)
})
}
// nolint:bodyclose
func TestUserOIDC(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
Name string
IDTokenClaims jwt.MapClaims
UserInfoClaims jwt.MapClaims
AllowSignups bool
EmailDomain []string
AssertUser func(t testing.TB, u codersdk.User)
StatusCode int
AssertResponse func(t testing.TB, resp *http.Response)
IgnoreEmailVerified bool
IgnoreUserInfo bool
}{
{
Name: "EmailOnly",
IDTokenClaims: jwt.MapClaims{
"email": "kyle@kwc.io",
},
AllowSignups: true,
StatusCode: http.StatusOK,
AssertUser: func(t testing.TB, u codersdk.User) {
assert.Equal(t, "kyle", u.Username)
},
},
{
Name: "EmailNotVerified",
IDTokenClaims: jwt.MapClaims{
"email": "kyle@kwc.io",
"email_verified": false,
},
AllowSignups: true,
StatusCode: http.StatusForbidden,
},
{
Name: "EmailNotAString",
IDTokenClaims: jwt.MapClaims{
"email": 3.14159,
"email_verified": false,
},
AllowSignups: true,
StatusCode: http.StatusBadRequest,
},
{
Name: "EmailNotVerifiedIgnored",
IDTokenClaims: jwt.MapClaims{
"email": "kyle@kwc.io",
"email_verified": false,
},
AllowSignups: true,
StatusCode: http.StatusOK,
AssertUser: func(t testing.TB, u codersdk.User) {
assert.Equal(t, u.Username, "kyle")
},
IgnoreEmailVerified: true,
},
{
Name: "NotInRequiredEmailDomain",
IDTokenClaims: jwt.MapClaims{
"email": "kyle@kwc.io",
"email_verified": true,
},
AllowSignups: true,
EmailDomain: []string{
"coder.com",
},
StatusCode: http.StatusForbidden,
},
{
Name: "EmailDomainWithLeadingAt",
IDTokenClaims: jwt.MapClaims{
"email": "cian@coder.com",
"email_verified": true,
},
AllowSignups: true,
EmailDomain: []string{
"@coder.com",
},
StatusCode: http.StatusOK,
},
{
Name: "EmailDomainForbiddenWithLeadingAt",
IDTokenClaims: jwt.MapClaims{
"email": "kyle@kwc.io",
"email_verified": true,
},
AllowSignups: true,
EmailDomain: []string{
"@coder.com",
},
StatusCode: http.StatusForbidden,
},
{
Name: "EmailDomainCaseInsensitive",
IDTokenClaims: jwt.MapClaims{
"email": "kyle@KWC.io",
"email_verified": true,
},
AllowSignups: true,
AssertUser: func(t testing.TB, u codersdk.User) {
assert.Equal(t, u.Username, "kyle")
},
EmailDomain: []string{
"kwc.io",
},
StatusCode: http.StatusOK,
},
{
Name: "EmailDomainSubset",
IDTokenClaims: jwt.MapClaims{
"email": "colin@gmail.com",
"email_verified": true,
},
AllowSignups: true,
EmailDomain: []string{
"mail.com",
},
StatusCode: http.StatusForbidden,
},
{
Name: "EmptyClaims",
IDTokenClaims: jwt.MapClaims{},
AllowSignups: true,
StatusCode: http.StatusBadRequest,
},
{
Name: "NoSignups",
IDTokenClaims: jwt.MapClaims{
"email": "kyle@kwc.io",
"email_verified": true,
},
StatusCode: http.StatusForbidden,
},
{
Name: "UsernameFromEmail",
IDTokenClaims: jwt.MapClaims{
"email": "kyle@kwc.io",
"email_verified": true,
},
AssertUser: func(t testing.TB, u codersdk.User) {
assert.Equal(t, "kyle", u.Username)
},
AllowSignups: true,
StatusCode: http.StatusOK,
},
{
Name: "UsernameFromClaims",
IDTokenClaims: jwt.MapClaims{
"email": "kyle@kwc.io",
"email_verified": true,
"preferred_username": "hotdog",
},
AssertUser: func(t testing.TB, u codersdk.User) {
assert.Equal(t, "hotdog", u.Username)
},
AllowSignups: true,
StatusCode: http.StatusOK,
},
{
Name: "FullNameFromClaims",
IDTokenClaims: jwt.MapClaims{
"email": "kyle@kwc.io",
"email_verified": true,
"name": "Hot Dog",
},
AssertUser: func(t testing.TB, u codersdk.User) {
assert.Equal(t, "Hot Dog", u.Name)
},
AllowSignups: true,
StatusCode: http.StatusOK,
},
{
Name: "InvalidFullNameFromClaims",
IDTokenClaims: jwt.MapClaims{
"email": "kyle@kwc.io",
"email_verified": true,
// Full names must be less or equal to than 128 characters in length.
// However, we should not fail to log someone in if their name is too long.
// Just truncate it.
"name": strings.Repeat("a", 129),
},
AllowSignups: true,
StatusCode: http.StatusOK,
AssertUser: func(t testing.TB, u codersdk.User) {
assert.Equal(t, strings.Repeat("a", 128), u.Name)
},
},
{
Name: "FullNameWhitespace",
IDTokenClaims: jwt.MapClaims{
"email": "kyle@kwc.io",
"email_verified": true,
// Full names must not have leading or trailing whitespace, but this is a
// daft reason to fail a login.
"name": " Bobby Whitespace ",
},
AllowSignups: true,
StatusCode: http.StatusOK,
AssertUser: func(t testing.TB, u codersdk.User) {
assert.Equal(t, "Bobby Whitespace", u.Name)
},
},
{
// Services like Okta return the email as the username:
// https://developer.okta.com/docs/reference/api/oidc/#base-claims-always-present
Name: "UsernameAsEmail",
IDTokenClaims: jwt.MapClaims{
"email": "kyle@kwc.io",
"email_verified": true,
"name": "Kylium Carbonate",
"preferred_username": "kyle@kwc.io",
},
AssertUser: func(t testing.TB, u codersdk.User) {
assert.Equal(t, "kyle", u.Username)
},
AllowSignups: true,
StatusCode: http.StatusOK,
},
{
// See: https://github.com/coder/coder/issues/4472
Name: "UsernameIsEmail",
IDTokenClaims: jwt.MapClaims{
"preferred_username": "kyle@kwc.io",
},
AssertUser: func(t testing.TB, u codersdk.User) {
assert.Equal(t, "kyle", u.Username)
assert.Empty(t, u.Name)
},
AllowSignups: true,
StatusCode: http.StatusOK,
},
{
Name: "WithPicture",
IDTokenClaims: jwt.MapClaims{
"email": "kyle@kwc.io",
"email_verified": true,
"preferred_username": "kyle",
"picture": "/example.png",
},
AssertUser: func(t testing.TB, u codersdk.User) {
assert.Equal(t, "/example.png", u.AvatarURL)
assert.Equal(t, "kyle", u.Username)
},
AllowSignups: true,
StatusCode: http.StatusOK,
},
{
Name: "WithUserInfoClaims",
IDTokenClaims: jwt.MapClaims{
"email": "kyle@kwc.io",
"email_verified": true,
},
UserInfoClaims: jwt.MapClaims{
"preferred_username": "potato",
"picture": "/example.png",
"name": "Kylium Carbonate",
},
AssertUser: func(t testing.TB, u codersdk.User) {
assert.Equal(t, "/example.png", u.AvatarURL)
assert.Equal(t, "Kylium Carbonate", u.Name)
assert.Equal(t, "potato", u.Username)
},
AllowSignups: true,
StatusCode: http.StatusOK,
},
{
Name: "GroupsDoesNothing",
IDTokenClaims: jwt.MapClaims{
"email": "coolin@coder.com",
"groups": []string{"pingpong"},
},
AllowSignups: true,
StatusCode: http.StatusOK,
},
{
Name: "UserInfoOverridesIDTokenClaims",
IDTokenClaims: jwt.MapClaims{
"email": "internaluser@internal.domain",
"email_verified": false,
},
UserInfoClaims: jwt.MapClaims{
"email": "externaluser@external.domain",
"email_verified": true,
"preferred_username": "user",
},
AssertUser: func(t testing.TB, u codersdk.User) {
assert.Equal(t, "user", u.Username)
},
AllowSignups: true,
IgnoreEmailVerified: false,
StatusCode: http.StatusOK,
},
{
Name: "InvalidUserInfo",
IDTokenClaims: jwt.MapClaims{
"email": "internaluser@internal.domain",
"email_verified": false,
},
UserInfoClaims: jwt.MapClaims{
"email": 1,
},
AllowSignups: true,
IgnoreEmailVerified: false,
StatusCode: http.StatusInternalServerError,
},
{
Name: "IgnoreUserInfo",
IDTokenClaims: jwt.MapClaims{
"email": "user@internal.domain",
"email_verified": true,
"name": "User McName",
"preferred_username": "user",
},
UserInfoClaims: jwt.MapClaims{
"email": "user.mcname@external.domain",
"name": "Mr. User McName",
"preferred_username": "Mr. User McName",
},
AssertUser: func(t testing.TB, u codersdk.User) {
assert.Equal(t, "user", u.Username)
assert.Equal(t, "User McName", u.Name)
},
IgnoreUserInfo: true,
AllowSignups: true,
StatusCode: http.StatusOK,
},
{
Name: "HugeIDToken",
IDTokenClaims: inflateClaims(t, jwt.MapClaims{
"email": "user@domain.tld",
"email_verified": true,
}, 65536),
AssertUser: func(t testing.TB, u codersdk.User) {
assert.Equal(t, "user", u.Username)
},
AllowSignups: true,
StatusCode: http.StatusOK,
},
{
Name: "HugeClaims",
IDTokenClaims: jwt.MapClaims{
"email": "user@domain.tld",
"email_verified": true,
},
UserInfoClaims: inflateClaims(t, jwt.MapClaims{}, 65536),
AssertUser: func(t testing.TB, u codersdk.User) {
assert.Equal(t, "user", u.Username)
},
AllowSignups: true,
StatusCode: http.StatusOK,
},
{
Name: "IssuerMismatch",
IDTokenClaims: jwt.MapClaims{
"iss": "https://mismatch.com",
"email": "user@domain.tld",
"email_verified": true,
},
AllowSignups: true,
StatusCode: http.StatusBadRequest,
AssertResponse: func(t testing.TB, resp *http.Response) {
data, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Contains(t, string(data), "id token issued by a different provider")
},
},
} {
tc := tc
t.Run(tc.Name, func(t *testing.T) {
t.Parallel()
fake := oidctest.NewFakeIDP(t,
oidctest.WithRefresh(func(_ string) error {
return xerrors.New("refreshing token should never occur")
}),
oidctest.WithServing(),
oidctest.WithStaticUserInfo(tc.UserInfoClaims),
)
cfg := fake.OIDCConfig(t, nil, func(cfg *coderd.OIDCConfig) {
cfg.AllowSignups = tc.AllowSignups
cfg.EmailDomain = tc.EmailDomain
cfg.IgnoreEmailVerified = tc.IgnoreEmailVerified
cfg.IgnoreUserInfo = tc.IgnoreUserInfo
cfg.NameField = "name"
})
auditor := audit.NewMock()
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
owner := coderdtest.New(t, &coderdtest.Options{
Auditor: auditor,
OIDCConfig: cfg,
Logger: &logger,
})
numLogs := len(auditor.AuditLogs())
client, resp := fake.AttemptLogin(t, owner, tc.IDTokenClaims)
numLogs++ // add an audit log for login
require.Equal(t, tc.StatusCode, resp.StatusCode)
if tc.AssertResponse != nil {
tc.AssertResponse(t, resp)
}
ctx := testutil.Context(t, testutil.WaitShort)
if tc.AssertUser != nil {
user, err := client.User(ctx, "me")
require.NoError(t, err)
tc.AssertUser(t, user)
require.Len(t, auditor.AuditLogs(), numLogs)
require.NotEqual(t, uuid.Nil, auditor.AuditLogs()[numLogs-1].UserID)
require.Equal(t, database.AuditActionRegister, auditor.AuditLogs()[numLogs-1].Action)
require.Equal(t, 1, len(user.OrganizationIDs), "in the default org")
}
})
}
t.Run("OIDCDormancy", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
auditor := audit.NewMock()
fake := oidctest.NewFakeIDP(t,
oidctest.WithRefresh(func(_ string) error {
return xerrors.New("refreshing token should never occur")
}),
oidctest.WithServing(),
)
cfg := fake.OIDCConfig(t, nil, func(cfg *coderd.OIDCConfig) {
cfg.AllowSignups = true
})
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
owner, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
Auditor: auditor,
OIDCConfig: cfg,
Logger: &logger,
})
user := dbgen.User(t, db, database.User{
LoginType: database.LoginTypeOIDC,
Status: database.UserStatusDormant,
})
auditor.ResetLogs()
client, resp := fake.AttemptLogin(t, owner, jwt.MapClaims{
"email": user.Email,
})
require.Equal(t, http.StatusOK, resp.StatusCode)
auditor.Contains(t, database.AuditLog{
ResourceType: database.ResourceTypeUser,
AdditionalFields: json.RawMessage(`{"automatic_actor":"coder","automatic_subsystem":"dormancy"}`),
})
me, err := client.User(ctx, "me")
require.NoError(t, err)
require.Equal(t, codersdk.UserStatusActive, me.Status)
})
t.Run("OIDCConvert", func(t *testing.T) {
t.Parallel()
auditor := audit.NewMock()
fake := oidctest.NewFakeIDP(t,
oidctest.WithRefresh(func(_ string) error {
return xerrors.New("refreshing token should never occur")
}),
oidctest.WithServing(),
)
cfg := fake.OIDCConfig(t, nil, func(cfg *coderd.OIDCConfig) {
cfg.AllowSignups = true
})
client := coderdtest.New(t, &coderdtest.Options{
Auditor: auditor,
OIDCConfig: cfg,
})
owner := coderdtest.CreateFirstUser(t, client)
user, userData := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
require.Equal(t, codersdk.LoginTypePassword, userData.LoginType)
claims := jwt.MapClaims{
"email": userData.Email,
}
var err error
user.HTTPClient.Jar, err = cookiejar.New(nil)
require.NoError(t, err)
user.HTTPClient.Transport = http.DefaultTransport.(*http.Transport).Clone()
ctx := testutil.Context(t, testutil.WaitShort)
convertResponse, err := user.ConvertLoginType(ctx, codersdk.ConvertLoginRequest{
ToType: codersdk.LoginTypeOIDC,
Password: "SomeSecurePassword!",
})
require.NoError(t, err)
_, _ = fake.LoginWithClient(t, user, claims, func(r *http.Request) {
r.URL.RawQuery = url.Values{
"oidc_merge_state": {convertResponse.StateString},
}.Encode()
r.Header.Set(codersdk.SessionTokenHeader, user.SessionToken())
cookies := user.HTTPClient.Jar.Cookies(r.URL)
for _, cookie := range cookies {
r.AddCookie(cookie)
}
})
info, err := client.User(ctx, userData.ID.String())
require.NoError(t, err)
require.Equal(t, codersdk.LoginTypeOIDC, info.LoginType)
})
t.Run("BadJWT", func(t *testing.T) {
t.Parallel()
var (
ctx = testutil.Context(t, testutil.WaitMedium)
logger = testutil.Logger(t)
)
auditor := audit.NewMock()
fake := oidctest.NewFakeIDP(t,
oidctest.WithRefresh(func(_ string) error {
return xerrors.New("refreshing token should never occur")
}),
oidctest.WithServing(),
)
cfg := fake.OIDCConfig(t, nil, func(cfg *coderd.OIDCConfig) {
cfg.AllowSignups = true
})
db, ps := dbtestutil.NewDB(t)
fetcher := &cryptokeys.DBFetcher{
DB: db,
}
kc, err := cryptokeys.NewSigningCache(ctx, logger, fetcher, codersdk.CryptoKeyFeatureOIDCConvert)
require.NoError(t, err)
client := coderdtest.New(t, &coderdtest.Options{
Auditor: auditor,
OIDCConfig: cfg,
Database: db,
Pubsub: ps,
OIDCConvertKeyCache: kc,
})
owner := coderdtest.CreateFirstUser(t, client)
user, userData := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
claims := jwt.MapClaims{
"email": userData.Email,
}
user.HTTPClient.Jar, err = cookiejar.New(nil)
require.NoError(t, err)
user.HTTPClient.Transport = http.DefaultTransport.(*http.Transport).Clone()
convertResponse, err := user.ConvertLoginType(ctx, codersdk.ConvertLoginRequest{
ToType: codersdk.LoginTypeOIDC,
Password: "SomeSecurePassword!",
})
require.NoError(t, err)
// Update the cookie to use a bad signing key. We're asserting the behavior of the scenario
// where a JWT gets minted on an old version of Coder but gets verified on a new version.
_, resp := fake.AttemptLogin(t, user, claims, func(r *http.Request) {
r.URL.RawQuery = url.Values{
"oidc_merge_state": {convertResponse.StateString},
}.Encode()
r.Header.Set(codersdk.SessionTokenHeader, user.SessionToken())
cookies := user.HTTPClient.Jar.Cookies(user.URL)
for i, cookie := range cookies {
if cookie.Name != coderd.OAuthConvertCookieValue {
continue
}
jwt := cookie.Value
var claims coderd.OAuthConvertStateClaims
err := jwtutils.Verify(ctx, kc, jwt, &claims)
require.NoError(t, err)
badJWT := generateBadJWT(t, claims)
cookie.Value = badJWT
cookies[i] = cookie
}
user.HTTPClient.Jar.SetCookies(user.URL, cookies)
for _, cookie := range cookies {
fmt.Printf("cookie: %+v\n", cookie)
r.AddCookie(cookie)
}
})
defer resp.Body.Close()
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
var respErr codersdk.Response
err = json.NewDecoder(resp.Body).Decode(&respErr)
require.NoError(t, err)
require.Contains(t, respErr.Message, "Using an invalid jwt to authorize this action.")
})
t.Run("AlternateUsername", func(t *testing.T) {
t.Parallel()
auditor := audit.NewMock()
fake := oidctest.NewFakeIDP(t,
oidctest.WithRefresh(func(_ string) error {
return xerrors.New("refreshing token should never occur")
}),
oidctest.WithServing(),
)
cfg := fake.OIDCConfig(t, nil, func(cfg *coderd.OIDCConfig) {
cfg.AllowSignups = true
})
client := coderdtest.New(t, &coderdtest.Options{
Auditor: auditor,
OIDCConfig: cfg,
})
numLogs := len(auditor.AuditLogs())
claims := jwt.MapClaims{
"email": "jon@coder.com",
}
userClient, _ := fake.Login(t, client, claims)
numLogs++ // add an audit log for login
ctx := testutil.Context(t, testutil.WaitLong)
user, err := userClient.User(ctx, "me")
require.NoError(t, err)
require.Equal(t, "jon", user.Username)
// Pass a different subject field so that we prompt creating a
// new user
userClient, _ = fake.Login(t, client, jwt.MapClaims{
"email": "jon@example2.com",
"sub": "diff",
})
numLogs++ // add an audit log for login
user, err = userClient.User(ctx, "me")
require.NoError(t, err)
require.True(t, strings.HasPrefix(user.Username, "jon-"), "username %q should have prefix %q", user.Username, "jon-")
require.Len(t, auditor.AuditLogs(), numLogs)
require.Equal(t, database.AuditActionRegister, auditor.AuditLogs()[numLogs-1].Action)
})
t.Run("Disabled", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
oauthURL, err := client.URL.Parse("/api/v2/users/oidc/callback")
require.NoError(t, err)
req, err := http.NewRequestWithContext(context.Background(), "GET", oauthURL.String(), nil)
require.NoError(t, err)
resp, err := client.HTTPClient.Do(req)
require.NoError(t, err)
resp.Body.Close()
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
t.Run("NoIDToken", func(t *testing.T) {
t.Parallel()
fake := oidctest.NewFakeIDP(t,
oidctest.WithRefresh(func(_ string) error {
return xerrors.New("refreshing token should never occur")
}),
oidctest.WithServing(),
)
cfg := fake.OIDCConfig(t, nil, func(cfg *coderd.OIDCConfig) {
cfg.AllowSignups = true
})
client := coderdtest.New(t, &coderdtest.Options{
OIDCConfig: cfg,
})
_, resp := fake.AttemptLogin(t, client, jwt.MapClaims{})
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
t.Run("BadVerify", func(t *testing.T) {
t.Parallel()
badVerifier := oidc.NewVerifier("", &oidc.StaticKeySet{
PublicKeys: []crypto.PublicKey{},
}, &oidc.Config{})
badProvider := &oidc.Provider{}
fake := oidctest.NewFakeIDP(t,
oidctest.WithRefresh(func(_ string) error {
return xerrors.New("refreshing token should never occur")
}),
oidctest.WithServing(),
)
cfg := fake.OIDCConfig(t, nil, func(cfg *coderd.OIDCConfig) {
cfg.AllowSignups = true
cfg.Provider = badProvider
cfg.Verifier = badVerifier
})
client := coderdtest.New(t, &coderdtest.Options{
OIDCConfig: cfg,
})
_, resp := fake.AttemptLogin(t, client, jwt.MapClaims{})
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
t.Run("StripRedirectHost", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
expectedRedirect := "/foo/bar?hello=world&bar=baz"
redirectURL := "https://malicious" + expectedRedirect
callbackPath := fmt.Sprintf("/api/v2/users/oidc/callback?redirect=%s", url.QueryEscape(redirectURL))
fake := oidctest.NewFakeIDP(t,
oidctest.WithRefresh(func(_ string) error {
return xerrors.New("refreshing token should never occur")
}),
oidctest.WithServing(),
oidctest.WithCallbackPath(callbackPath),
)
cfg := fake.OIDCConfig(t, nil, func(cfg *coderd.OIDCConfig) {
cfg.AllowSignups = true
})
client := coderdtest.New(t, &coderdtest.Options{
OIDCConfig: cfg,
})
client.HTTPClient.Transport = http.DefaultTransport
client.HTTPClient.CheckRedirect = func(*http.Request, []*http.Request) error {
return http.ErrUseLastResponse
}
claims := jwt.MapClaims{
"email": "user@example.com",
"email_verified": true,
}
// Perform the login
loginClient, resp := fake.LoginWithClient(t, client, claims)
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
// Get the location from the response
location, err := resp.Location()
require.NoError(t, err)
// Check that the redirect URL has been stripped of its malicious host
require.Equal(t, expectedRedirect, location.RequestURI())
require.Equal(t, client.URL.Host, location.Host)
require.NotContains(t, location.String(), "malicious")
// Verify the user was created
user, err := loginClient.User(ctx, "me")
require.NoError(t, err)
require.Equal(t, "user@example.com", user.Email)
})
}
func TestUserLogout(t *testing.T) {
t.Parallel()
// Create a custom database so it's easier to make scoped tokens for
// testing.
db, pubSub := dbtestutil.NewDB(t)
client := coderdtest.New(t, &coderdtest.Options{
Database: db,
Pubsub: pubSub,
})
firstUser := coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitLong)
// Create a user with built-in auth.
const (
email = "dean.was.here@test.coder.com"
username = "dean"
//nolint:gosec
password = "SomeSecurePassword123!"
)
newUser, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
Email: email,
Username: username,
Password: password,
OrganizationIDs: []uuid.UUID{firstUser.OrganizationID},
})
require.NoError(t, err)
// Log in with basic auth and keep the the session token (but don't use it).
userClient := codersdk.New(client.URL)
loginRes1, err := userClient.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
Email: email,
Password: password,
})
require.NoError(t, err)
// Log in again but actually set the token this time.
loginRes2, err := userClient.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
Email: email,
Password: password,
})
require.NoError(t, err)
userClient.SetSessionToken(loginRes2.SessionToken)
// Add the user's second session token to the list of API keys that should
// be deleted.
shouldBeDeleted := map[string]string{
"user login 2 (logging out with this)": loginRes2.SessionToken,
}
// Add the user's first token, and the admin's session token to the list of
// API keys that should not be deleted.
shouldNotBeDeleted := map[string]string{
"user login 1 (not logging out of)": loginRes1.SessionToken,
"admin login": client.SessionToken(),
}
// Create a few application_connect-scoped API keys that should be deleted.
for i := 0; i < 3; i++ {
key, _ := dbgen.APIKey(t, db, database.APIKey{
UserID: newUser.ID,
Scope: database.APIKeyScopeApplicationConnect,
})
shouldBeDeleted[fmt.Sprintf("application_connect key owned by logout user %d", i)] = key.ID
}
// Create a few application_connect-scoped API keys for the admin user that
// should not be deleted.
for i := 0; i < 3; i++ {
key, _ := dbgen.APIKey(t, db, database.APIKey{
UserID: firstUser.UserID,
Scope: database.APIKeyScopeApplicationConnect,
})
shouldNotBeDeleted[fmt.Sprintf("application_connect key owned by admin user %d", i)] = key.ID
}
// Log out of the new user.
err = userClient.Logout(ctx)
require.NoError(t, err)
// Ensure the new user's session token is no longer valid.
_, err = userClient.User(ctx, codersdk.Me)
require.Error(t, err)
var sdkErr *codersdk.Error
require.ErrorAs(t, err, &sdkErr)
require.Equal(t, http.StatusUnauthorized, sdkErr.StatusCode())
// Check that the deleted keys are gone.
for name, id := range shouldBeDeleted {
id := strings.Split(id, "-")[0]
_, err := db.GetAPIKeyByID(ctx, id)
require.Error(t, err, name)
}
// Check that the other keys are still there.
for name, id := range shouldNotBeDeleted {
id := strings.Split(id, "-")[0]
_, err := db.GetAPIKeyByID(ctx, id)
require.NoError(t, err, name)
}
}
// TestOIDCSkipIssuer verifies coderd can run without checking the issuer url
// in the OIDC exchange. This means the CODER_OIDC_ISSUER_URL does not need
// to match the id_token `iss` field, or the value returned in the well-known
// config.
//
// So this test has:
// - OIDC at http://localhost:<port>
// - well-known config with issuer https://primary.com
// - JWT with issuer https://secondary.com
//
// Without this security check disabled, all three above would have to match.
func TestOIDCSkipIssuer(t *testing.T) {
t.Parallel()
const primaryURLString = "https://primary.com"
const secondaryURLString = "https://secondary.com"
primaryURL := must(url.Parse(primaryURLString))
fake := oidctest.NewFakeIDP(t,
oidctest.WithServing(),
oidctest.WithDefaultIDClaims(jwt.MapClaims{}),
oidctest.WithHookWellKnown(func(r *http.Request, j *oidctest.ProviderJSON) error {
assert.NotEqual(t, r.URL.Host, primaryURL.Host, "request went to wrong host")
j.Issuer = primaryURLString
return nil
}),
)
owner := coderdtest.New(t, &coderdtest.Options{
OIDCConfig: fake.OIDCConfigSkipIssuerChecks(t, nil, func(cfg *coderd.OIDCConfig) {
cfg.AllowSignups = true
}),
})
// User can login and use their token.
ctx := testutil.Context(t, testutil.WaitShort)
//nolint:bodyclose
userClient, _ := fake.Login(t, owner, jwt.MapClaims{
"iss": secondaryURLString,
"email": "alice@coder.com",
})
found, err := userClient.User(ctx, "me")
require.NoError(t, err)
require.Equal(t, found.LoginType, codersdk.LoginTypeOIDC)
}
func TestUserForgotPassword(t *testing.T) {
t.Parallel()
const oldPassword = "SomeSecurePassword!"
const newPassword = "SomeNewSecurePassword!"
requireOneTimePasscodeNotification := func(t *testing.T, notif *notificationstest.FakeNotification, userID uuid.UUID) {
require.Equal(t, notifications.TemplateUserRequestedOneTimePasscode, notif.TemplateID)
require.Equal(t, userID, notif.UserID)
require.Equal(t, 1, len(notif.Targets))
require.Equal(t, userID, notif.Targets[0])
}
requireCanLogin := func(t *testing.T, ctx context.Context, client *codersdk.Client, email string, password string) {
_, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
Email: email,
Password: password,
})
require.NoError(t, err)
}
requireCannotLogin := func(t *testing.T, ctx context.Context, client *codersdk.Client, email string, password string) {
_, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
Email: email,
Password: password,
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
require.Contains(t, apiErr.Message, "Incorrect email or password.")
}
requireRequestOneTimePasscode := func(t *testing.T, ctx context.Context, client *codersdk.Client, notifyEnq *notificationstest.FakeEnqueuer, email string, userID uuid.UUID) string {
notifyEnq.Clear()
err := client.RequestOneTimePasscode(ctx, codersdk.RequestOneTimePasscodeRequest{Email: email})
require.NoError(t, err)
sent := notifyEnq.Sent()
require.Len(t, sent, 1)
requireOneTimePasscodeNotification(t, sent[0], userID)
return sent[0].Labels["one_time_passcode"]
}
requireChangePasswordWithOneTimePasscode := func(t *testing.T, ctx context.Context, client *codersdk.Client, email string, passcode string, password string) {
err := client.ChangePasswordWithOneTimePasscode(ctx, codersdk.ChangePasswordWithOneTimePasscodeRequest{
Email: email,
OneTimePasscode: passcode,
Password: password,
})
require.NoError(t, err)
}
t.Run("CanChangePassword", func(t *testing.T) {
t.Parallel()
notifyEnq := &notificationstest.FakeEnqueuer{}
client := coderdtest.New(t, &coderdtest.Options{
NotificationsEnqueuer: notifyEnq,
})
user := coderdtest.CreateFirstUser(t, client)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
anotherClient, anotherUser := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
// First try to login before changing our password. We expected this to error
// as we haven't change the password yet.
requireCannotLogin(t, ctx, anotherClient, anotherUser.Email, newPassword)
oneTimePasscode := requireRequestOneTimePasscode(t, ctx, anotherClient, notifyEnq, anotherUser.Email, anotherUser.ID)
requireChangePasswordWithOneTimePasscode(t, ctx, anotherClient, anotherUser.Email, oneTimePasscode, newPassword)
requireCanLogin(t, ctx, anotherClient, anotherUser.Email, newPassword)
// We now need to check that the one-time passcode isn't valid.
err := anotherClient.ChangePasswordWithOneTimePasscode(ctx, codersdk.ChangePasswordWithOneTimePasscodeRequest{
Email: anotherUser.Email,
OneTimePasscode: oneTimePasscode,
Password: newPassword + "!",
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
require.Contains(t, apiErr.Message, "Incorrect email or one-time passcode.")
requireCannotLogin(t, ctx, anotherClient, anotherUser.Email, newPassword+"!")
requireCanLogin(t, ctx, anotherClient, anotherUser.Email, newPassword)
})
t.Run("OneTimePasscodeExpires", func(t *testing.T) {
t.Parallel()
const oneTimePasscodeValidityPeriod = 1 * time.Millisecond
notifyEnq := &notificationstest.FakeEnqueuer{}
client := coderdtest.New(t, &coderdtest.Options{
NotificationsEnqueuer: notifyEnq,
OneTimePasscodeValidityPeriod: oneTimePasscodeValidityPeriod,
})
user := coderdtest.CreateFirstUser(t, client)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
anotherClient, anotherUser := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
oneTimePasscode := requireRequestOneTimePasscode(t, ctx, anotherClient, notifyEnq, anotherUser.Email, anotherUser.ID)
// Wait for long enough so that the token expires
time.Sleep(oneTimePasscodeValidityPeriod + 1*time.Millisecond)
// Try to change password with an expired one time passcode.
err := anotherClient.ChangePasswordWithOneTimePasscode(ctx, codersdk.ChangePasswordWithOneTimePasscodeRequest{
Email: anotherUser.Email,
OneTimePasscode: oneTimePasscode,
Password: newPassword,
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
require.Contains(t, apiErr.Message, "Incorrect email or one-time passcode.")
// Ensure that the password was not changed.
requireCannotLogin(t, ctx, anotherClient, anotherUser.Email, newPassword)
requireCanLogin(t, ctx, anotherClient, anotherUser.Email, oldPassword)
})
t.Run("CannotChangePasswordWithoutRequestingOneTimePasscode", func(t *testing.T) {
t.Parallel()
notifyEnq := &notificationstest.FakeEnqueuer{}
client := coderdtest.New(t, &coderdtest.Options{
NotificationsEnqueuer: notifyEnq,
})
user := coderdtest.CreateFirstUser(t, client)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
anotherClient, anotherUser := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
err := anotherClient.ChangePasswordWithOneTimePasscode(ctx, codersdk.ChangePasswordWithOneTimePasscodeRequest{
Email: anotherUser.Email,
OneTimePasscode: uuid.New().String(),
Password: newPassword,
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
require.Contains(t, apiErr.Message, "Incorrect email or one-time passcode")
requireCannotLogin(t, ctx, anotherClient, anotherUser.Email, newPassword)
requireCanLogin(t, ctx, anotherClient, anotherUser.Email, oldPassword)
})
t.Run("CannotChangePasswordWithInvalidOneTimePasscode", func(t *testing.T) {
t.Parallel()
notifyEnq := &notificationstest.FakeEnqueuer{}
client := coderdtest.New(t, &coderdtest.Options{
NotificationsEnqueuer: notifyEnq,
})
user := coderdtest.CreateFirstUser(t, client)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
anotherClient, anotherUser := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
_ = requireRequestOneTimePasscode(t, ctx, anotherClient, notifyEnq, anotherUser.Email, anotherUser.ID)
err := anotherClient.ChangePasswordWithOneTimePasscode(ctx, codersdk.ChangePasswordWithOneTimePasscodeRequest{
Email: anotherUser.Email,
OneTimePasscode: uuid.New().String(), // Use a different UUID to the one expected
Password: newPassword,
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
require.Contains(t, apiErr.Message, "Incorrect email or one-time passcode")
requireCannotLogin(t, ctx, anotherClient, anotherUser.Email, newPassword)
requireCanLogin(t, ctx, anotherClient, anotherUser.Email, oldPassword)
})
t.Run("CannotChangePasswordWithNoOneTimePasscode", func(t *testing.T) {
t.Parallel()
notifyEnq := &notificationstest.FakeEnqueuer{}
client := coderdtest.New(t, &coderdtest.Options{
NotificationsEnqueuer: notifyEnq,
})
user := coderdtest.CreateFirstUser(t, client)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
anotherClient, anotherUser := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
_ = requireRequestOneTimePasscode(t, ctx, anotherClient, notifyEnq, anotherUser.Email, anotherUser.ID)
err := anotherClient.ChangePasswordWithOneTimePasscode(ctx, codersdk.ChangePasswordWithOneTimePasscodeRequest{
Email: anotherUser.Email,
OneTimePasscode: "",
Password: newPassword,
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
require.Contains(t, apiErr.Message, "Validation failed.")
require.Equal(t, 1, len(apiErr.Validations))
require.Equal(t, "one_time_passcode", apiErr.Validations[0].Field)
requireCannotLogin(t, ctx, anotherClient, anotherUser.Email, newPassword)
requireCanLogin(t, ctx, anotherClient, anotherUser.Email, oldPassword)
})
t.Run("CannotChangePasswordWithWeakPassword", func(t *testing.T) {
t.Parallel()
notifyEnq := &notificationstest.FakeEnqueuer{}
client := coderdtest.New(t, &coderdtest.Options{
NotificationsEnqueuer: notifyEnq,
})
user := coderdtest.CreateFirstUser(t, client)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
anotherClient, anotherUser := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
oneTimePasscode := requireRequestOneTimePasscode(t, ctx, anotherClient, notifyEnq, anotherUser.Email, anotherUser.ID)
err := anotherClient.ChangePasswordWithOneTimePasscode(ctx, codersdk.ChangePasswordWithOneTimePasscodeRequest{
Email: anotherUser.Email,
OneTimePasscode: oneTimePasscode,
Password: "notstrong",
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
require.Contains(t, apiErr.Message, "Invalid password.")
require.Equal(t, 1, len(apiErr.Validations))
require.Equal(t, "password", apiErr.Validations[0].Field)
requireCannotLogin(t, ctx, anotherClient, anotherUser.Email, "notstrong")
requireCanLogin(t, ctx, anotherClient, anotherUser.Email, oldPassword)
})
t.Run("CannotChangePasswordOfAnotherUser", func(t *testing.T) {
t.Parallel()
notifyEnq := &notificationstest.FakeEnqueuer{}
client := coderdtest.New(t, &coderdtest.Options{
NotificationsEnqueuer: notifyEnq,
})
user := coderdtest.CreateFirstUser(t, client)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
anotherClient, anotherUser := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
thirdClient, thirdUser := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
// Request a One-Time Passcode for `anotherUser`
oneTimePasscode := requireRequestOneTimePasscode(t, ctx, anotherClient, notifyEnq, anotherUser.Email, anotherUser.ID)
// Ensure we cannot change the password for `thirdUser` with `anotherUser`'s One-Time Passcode.
err := thirdClient.ChangePasswordWithOneTimePasscode(ctx, codersdk.ChangePasswordWithOneTimePasscodeRequest{
Email: thirdUser.Email,
OneTimePasscode: oneTimePasscode,
Password: newPassword,
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
require.Contains(t, apiErr.Message, "Incorrect email or one-time passcode")
requireCannotLogin(t, ctx, thirdClient, thirdUser.Email, newPassword)
requireCanLogin(t, ctx, thirdClient, thirdUser.Email, oldPassword)
requireCanLogin(t, ctx, anotherClient, anotherUser.Email, oldPassword)
})
t.Run("GivenOKResponseWithInvalidEmail", func(t *testing.T) {
t.Parallel()
notifyEnq := &notificationstest.FakeEnqueuer{}
client := coderdtest.New(t, &coderdtest.Options{
NotificationsEnqueuer: notifyEnq,
})
user := coderdtest.CreateFirstUser(t, client)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
anotherClient, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
err := anotherClient.RequestOneTimePasscode(ctx, codersdk.RequestOneTimePasscodeRequest{
Email: "not-a-member@coder.com",
})
require.NoError(t, err)
sent := notifyEnq.Sent()
require.Len(t, notifyEnq.Sent(), 1)
require.NotEqual(t, notifications.TemplateUserRequestedOneTimePasscode, sent[0].TemplateID)
})
}
func oauth2Callback(t *testing.T, client *codersdk.Client, opts ...func(*http.Request)) *http.Response {
client.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
state := "somestate"
oauthURL, err := client.URL.Parse("/api/v2/users/oauth2/github/callback?code=asd&state=" + state)
require.NoError(t, err)
req, err := http.NewRequestWithContext(context.Background(), "GET", oauthURL.String(), nil)
require.NoError(t, err)
for _, opt := range opts {
opt(req)
}
req.AddCookie(&http.Cookie{
Name: codersdk.OAuth2StateCookie,
Value: state,
})
res, err := client.HTTPClient.Do(req)
require.NoError(t, err)
t.Cleanup(func() {
_ = res.Body.Close()
})
return res
}
func i64ptr(i int64) *int64 {
return &i
}
func authCookieValue(cookies []*http.Cookie) string {
for _, cookie := range cookies {
if cookie.Name == codersdk.SessionTokenCookie {
return cookie.Value
}
}
return ""
}
// inflateClaims 'inflates' a jwt.MapClaims from a seed by
// adding a ridiculously large key-value pair of length size.
func inflateClaims(t testing.TB, seed jwt.MapClaims, size int) jwt.MapClaims {
t.Helper()
junk, err := cryptorand.String(size)
require.NoError(t, err)
seed["random_data"] = junk
return seed
}
// generateBadJWT generates a JWT with a random key. It's intended to emulate the old-style JWT's we generated.
func generateBadJWT(t *testing.T, claims interface{}) string {
t.Helper()
var buf [64]byte
_, err := rand.Read(buf[:])
require.NoError(t, err)
signer, err := jose.NewSigner(jose.SigningKey{
Algorithm: jose.HS512,
Key: buf[:],
}, nil)
require.NoError(t, err)
payload, err := json.Marshal(claims)
require.NoError(t, err)
signed, err := signer.Sign(payload)
require.NoError(t, err)
compact, err := signed.CompactSerialize()
require.NoError(t, err)
return compact
}