Skip to content

Commit b89c367

Browse files
committed
chore: add OAuth2 device flow test scripts
Change-Id: Ic232851727e683ab3d8b7ce970c505588da2f827 Signed-off-by: Thomas Kosiewski <tk@coder.com>
1 parent c7c7f3d commit b89c367

File tree

20 files changed

+762
-77
lines changed

20 files changed

+762
-77
lines changed

coderd/database/dbauthz/dbauthz.go

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,7 @@ var (
399399
rbac.ResourceProvisionerJobs.Type: {policy.ActionRead, policy.ActionUpdate, policy.ActionCreate},
400400
rbac.ResourceOauth2App.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
401401
rbac.ResourceOauth2AppSecret.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
402+
rbac.ResourceOauth2AppCodeToken.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
402403
}),
403404
Org: map[string][]rbac.Permission{},
404405
User: []rbac.Permission{},
@@ -1324,6 +1325,14 @@ func (q *querier) CleanTailnetTunnels(ctx context.Context) error {
13241325
return q.db.CleanTailnetTunnels(ctx)
13251326
}
13261327

1328+
func (q *querier) ConsumeOAuth2ProviderAppCodeByPrefix(ctx context.Context, secretPrefix []byte) (database.OAuth2ProviderAppCode, error) {
1329+
return updateWithReturn(q.log, q.auth, q.db.GetOAuth2ProviderAppCodeByPrefix, q.db.ConsumeOAuth2ProviderAppCodeByPrefix)(ctx, secretPrefix)
1330+
}
1331+
1332+
func (q *querier) ConsumeOAuth2ProviderDeviceCodeByPrefix(ctx context.Context, deviceCodePrefix string) (database.OAuth2ProviderDeviceCode, error) {
1333+
return updateWithReturn(q.log, q.auth, q.db.GetOAuth2ProviderDeviceCodeByPrefix, q.db.ConsumeOAuth2ProviderDeviceCodeByPrefix)(ctx, deviceCodePrefix)
1334+
}
1335+
13271336
func (q *querier) CountAuditLogs(ctx context.Context, arg database.CountAuditLogsParams) (int64, error) {
13281337
// Shortcut if the user is an owner. The SQL filter is noticeable,
13291338
// and this is an easy win for owners. Which is the common case.
@@ -2301,8 +2310,8 @@ func (q *querier) GetOAuth2ProviderDeviceCodeByUserCode(ctx context.Context, use
23012310
}
23022311

23032312
func (q *querier) GetOAuth2ProviderDeviceCodesByClientID(ctx context.Context, clientID uuid.UUID) ([]database.OAuth2ProviderDeviceCode, error) {
2304-
// This requires access to read the OAuth2 app
2305-
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceOauth2App); err != nil {
2313+
// This requires access to read OAuth2 app code tokens
2314+
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceOauth2AppCodeToken); err != nil {
23062315
return []database.OAuth2ProviderDeviceCode{}, err
23072316
}
23082317
return q.db.GetOAuth2ProviderDeviceCodesByClientID(ctx, clientID)
@@ -3752,8 +3761,8 @@ func (q *querier) InsertOAuth2ProviderAppToken(ctx context.Context, arg database
37523761
}
37533762

37543763
func (q *querier) InsertOAuth2ProviderDeviceCode(ctx context.Context, arg database.InsertOAuth2ProviderDeviceCodeParams) (database.OAuth2ProviderDeviceCode, error) {
3755-
// Creating device codes requires OAuth2 app access
3756-
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceOauth2App); err != nil {
3764+
// Creating device codes requires OAuth2 app code token creation access
3765+
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceOauth2AppCodeToken); err != nil {
37573766
return database.OAuth2ProviderDeviceCode{}, err
37583767
}
37593768
return q.db.InsertOAuth2ProviderDeviceCode(ctx, arg)
@@ -4432,13 +4441,10 @@ func (q *querier) UpdateOAuth2ProviderAppSecretByID(ctx context.Context, arg dat
44324441
}
44334442

44344443
func (q *querier) UpdateOAuth2ProviderDeviceCodeAuthorization(ctx context.Context, arg database.UpdateOAuth2ProviderDeviceCodeAuthorizationParams) (database.OAuth2ProviderDeviceCode, error) {
4435-
// Verify the user is authenticated for device code authorization
4436-
_, ok := ActorFromContext(ctx)
4437-
if !ok {
4438-
return database.OAuth2ProviderDeviceCode{}, ErrNoActor
4444+
fetch := func(ctx context.Context, arg database.UpdateOAuth2ProviderDeviceCodeAuthorizationParams) (database.OAuth2ProviderDeviceCode, error) {
4445+
return q.db.GetOAuth2ProviderDeviceCodeByID(ctx, arg.ID)
44394446
}
4440-
4441-
return q.db.UpdateOAuth2ProviderDeviceCodeAuthorization(ctx, arg)
4447+
return updateWithReturn(q.log, q.auth, fetch, q.db.UpdateOAuth2ProviderDeviceCodeAuthorization)(ctx, arg)
44424448
}
44434449

44444450
func (q *querier) UpdateOrganization(ctx context.Context, arg database.UpdateOrganizationParams) (database.Organization, error) {

coderd/database/dbauthz/dbauthz_test.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5423,6 +5423,19 @@ func (s *MethodTestSuite) TestOAuth2ProviderAppCodes() {
54235423
UserID: user.ID,
54245424
}).Asserts(rbac.ResourceOauth2AppCodeToken.WithOwner(user.ID.String()), policy.ActionDelete)
54255425
}))
5426+
s.Run("ConsumeOAuth2ProviderAppCodeByPrefix", s.Subtest(func(db database.Store, check *expects) {
5427+
user := dbgen.User(s.T(), db, database.User{})
5428+
app := dbgen.OAuth2ProviderApp(s.T(), db, database.OAuth2ProviderApp{})
5429+
// Use unique prefix to avoid test isolation issues
5430+
uniquePrefix := fmt.Sprintf("prefix-%s-%d", s.T().Name(), time.Now().UnixNano())
5431+
code := dbgen.OAuth2ProviderAppCode(s.T(), db, database.OAuth2ProviderAppCode{
5432+
SecretPrefix: []byte(uniquePrefix),
5433+
UserID: user.ID,
5434+
AppID: app.ID,
5435+
ExpiresAt: time.Now().Add(24 * time.Hour), // Extended expiry for test stability
5436+
})
5437+
check.Args(code.SecretPrefix).Asserts(code, policy.ActionUpdate).Returns(code)
5438+
}))
54265439
}
54275440

54285441
func (s *MethodTestSuite) TestOAuth2ProviderAppTokens() {
@@ -5498,6 +5511,110 @@ func (s *MethodTestSuite) TestOAuth2ProviderAppTokens() {
54985511
}))
54995512
}
55005513

5514+
func (s *MethodTestSuite) TestOAuth2ProviderDeviceCodes() {
5515+
s.Run("InsertOAuth2ProviderDeviceCode", s.Subtest(func(db database.Store, check *expects) {
5516+
app := dbgen.OAuth2ProviderApp(s.T(), db, database.OAuth2ProviderApp{})
5517+
check.Args(database.InsertOAuth2ProviderDeviceCodeParams{
5518+
ClientID: app.ID,
5519+
}).Asserts(rbac.ResourceOauth2AppCodeToken, policy.ActionCreate)
5520+
}))
5521+
s.Run("GetOAuth2ProviderDeviceCodeByID", s.Subtest(func(db database.Store, check *expects) {
5522+
app := dbgen.OAuth2ProviderApp(s.T(), db, database.OAuth2ProviderApp{})
5523+
deviceCode, err := db.InsertOAuth2ProviderDeviceCode(context.Background(), database.InsertOAuth2ProviderDeviceCodeParams{
5524+
ClientID: app.ID,
5525+
DeviceCodePrefix: "test-prefix",
5526+
UserCode: "TEST1234",
5527+
VerificationUri: "http://example.com/device",
5528+
})
5529+
require.NoError(s.T(), err)
5530+
check.Args(deviceCode.ID).Asserts(deviceCode, policy.ActionRead).Returns(deviceCode)
5531+
}))
5532+
s.Run("GetOAuth2ProviderDeviceCodeByPrefix", s.Subtest(func(db database.Store, check *expects) {
5533+
app := dbgen.OAuth2ProviderApp(s.T(), db, database.OAuth2ProviderApp{})
5534+
deviceCode, err := db.InsertOAuth2ProviderDeviceCode(context.Background(), database.InsertOAuth2ProviderDeviceCodeParams{
5535+
ClientID: app.ID,
5536+
DeviceCodePrefix: "test-prefix",
5537+
UserCode: "TEST1234",
5538+
VerificationUri: "http://example.com/device",
5539+
})
5540+
require.NoError(s.T(), err)
5541+
check.Args(deviceCode.DeviceCodePrefix).Asserts(deviceCode, policy.ActionRead).Returns(deviceCode)
5542+
}))
5543+
s.Run("GetOAuth2ProviderDeviceCodeByUserCode", s.Subtest(func(db database.Store, check *expects) {
5544+
app := dbgen.OAuth2ProviderApp(s.T(), db, database.OAuth2ProviderApp{})
5545+
deviceCode, err := db.InsertOAuth2ProviderDeviceCode(context.Background(), database.InsertOAuth2ProviderDeviceCodeParams{
5546+
ClientID: app.ID,
5547+
DeviceCodePrefix: "test-prefix",
5548+
UserCode: "TEST1234",
5549+
VerificationUri: "http://example.com/device",
5550+
})
5551+
require.NoError(s.T(), err)
5552+
check.Args(deviceCode.UserCode).Asserts(deviceCode, policy.ActionRead).Returns(deviceCode)
5553+
}))
5554+
s.Run("GetOAuth2ProviderDeviceCodesByClientID", s.Subtest(func(db database.Store, check *expects) {
5555+
app := dbgen.OAuth2ProviderApp(s.T(), db, database.OAuth2ProviderApp{})
5556+
deviceCode, err := db.InsertOAuth2ProviderDeviceCode(context.Background(), database.InsertOAuth2ProviderDeviceCodeParams{
5557+
ClientID: app.ID,
5558+
DeviceCodePrefix: "test-prefix",
5559+
UserCode: "TEST1234",
5560+
VerificationUri: "http://example.com/device",
5561+
})
5562+
require.NoError(s.T(), err)
5563+
check.Args(app.ID).Asserts(rbac.ResourceOauth2AppCodeToken, policy.ActionRead).Returns([]database.OAuth2ProviderDeviceCode{deviceCode})
5564+
}))
5565+
s.Run("ConsumeOAuth2ProviderDeviceCodeByPrefix", s.Subtest(func(db database.Store, check *expects) {
5566+
app := dbgen.OAuth2ProviderApp(s.T(), db, database.OAuth2ProviderApp{})
5567+
user := dbgen.User(s.T(), db, database.User{})
5568+
// Use unique identifiers to avoid test isolation issues
5569+
uniquePrefix := fmt.Sprintf("dev-prefix-%s-%d", s.T().Name(), time.Now().UnixNano())
5570+
uniqueUserCode := fmt.Sprintf("USER%s-%d", s.T().Name(), time.Now().UnixNano())
5571+
// Create device code using dbgen (now available!)
5572+
deviceCode := dbgen.OAuth2ProviderDeviceCode(s.T(), db, database.OAuth2ProviderDeviceCode{
5573+
DeviceCodePrefix: uniquePrefix,
5574+
UserCode: uniqueUserCode,
5575+
ClientID: app.ID,
5576+
ExpiresAt: time.Now().Add(24 * time.Hour), // Extended expiry for test stability
5577+
})
5578+
// Authorize the device code so it can be consumed
5579+
deviceCode, err := db.UpdateOAuth2ProviderDeviceCodeAuthorization(s.T().Context(), database.UpdateOAuth2ProviderDeviceCodeAuthorizationParams{
5580+
ID: deviceCode.ID,
5581+
UserID: uuid.NullUUID{UUID: user.ID, Valid: true},
5582+
Status: database.OAuth2DeviceStatusAuthorized,
5583+
})
5584+
require.NoError(s.T(), err)
5585+
require.Equal(s.T(), database.OAuth2DeviceStatusAuthorized, deviceCode.Status)
5586+
check.Args(uniquePrefix).Asserts(deviceCode, policy.ActionUpdate).Returns(deviceCode)
5587+
}))
5588+
s.Run("UpdateOAuth2ProviderDeviceCodeAuthorization", s.Subtest(func(db database.Store, check *expects) {
5589+
app := dbgen.OAuth2ProviderApp(s.T(), db, database.OAuth2ProviderApp{})
5590+
user := dbgen.User(s.T(), db, database.User{})
5591+
// Create device code using dbgen
5592+
deviceCode := dbgen.OAuth2ProviderDeviceCode(s.T(), db, database.OAuth2ProviderDeviceCode{
5593+
ClientID: app.ID,
5594+
})
5595+
require.Equal(s.T(), database.OAuth2DeviceStatusPending, deviceCode.Status)
5596+
check.Args(database.UpdateOAuth2ProviderDeviceCodeAuthorizationParams{
5597+
ID: deviceCode.ID,
5598+
UserID: uuid.NullUUID{UUID: user.ID, Valid: true},
5599+
Status: database.OAuth2DeviceStatusAuthorized,
5600+
}).Asserts(deviceCode, policy.ActionUpdate)
5601+
}))
5602+
s.Run("DeleteOAuth2ProviderDeviceCodeByID", s.Subtest(func(db database.Store, check *expects) {
5603+
app := dbgen.OAuth2ProviderApp(s.T(), db, database.OAuth2ProviderApp{})
5604+
deviceCode, err := db.InsertOAuth2ProviderDeviceCode(context.Background(), database.InsertOAuth2ProviderDeviceCodeParams{
5605+
ClientID: app.ID,
5606+
DeviceCodePrefix: "test-prefix",
5607+
UserCode: "TEST1234",
5608+
VerificationUri: "http://example.com/device",
5609+
})
5610+
require.NoError(s.T(), err)
5611+
check.Args(deviceCode.ID).Asserts(deviceCode, policy.ActionDelete)
5612+
}))
5613+
s.Run("DeleteExpiredOAuth2ProviderDeviceCodes", s.Subtest(func(db database.Store, check *expects) {
5614+
check.Args().Asserts(rbac.ResourceSystem, policy.ActionDelete)
5615+
}))
5616+
}
5617+
55015618
func (s *MethodTestSuite) TestResourcesMonitor() {
55025619
createAgent := func(t *testing.T, db database.Store) (database.WorkspaceAgent, database.WorkspaceTable) {
55035620
t.Helper()

coderd/database/dbgen/dbgen.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1199,7 +1199,7 @@ func OAuth2ProviderAppCode(t testing.TB, db database.Store, seed database.OAuth2
11991199
code, err := db.InsertOAuth2ProviderAppCode(genCtx, database.InsertOAuth2ProviderAppCodeParams{
12001200
ID: takeFirst(seed.ID, uuid.New()),
12011201
CreatedAt: takeFirst(seed.CreatedAt, dbtime.Now()),
1202-
ExpiresAt: takeFirst(seed.CreatedAt, dbtime.Now()),
1202+
ExpiresAt: takeFirst(seed.ExpiresAt, dbtime.Now().Add(24*time.Hour)),
12031203
SecretPrefix: takeFirstSlice(seed.SecretPrefix, []byte("prefix")),
12041204
HashedSecret: takeFirstSlice(seed.HashedSecret, []byte("hashed-secret")),
12051205
AppID: takeFirst(seed.AppID, uuid.New()),
@@ -1216,7 +1216,7 @@ func OAuth2ProviderAppToken(t testing.TB, db database.Store, seed database.OAuth
12161216
token, err := db.InsertOAuth2ProviderAppToken(genCtx, database.InsertOAuth2ProviderAppTokenParams{
12171217
ID: takeFirst(seed.ID, uuid.New()),
12181218
CreatedAt: takeFirst(seed.CreatedAt, dbtime.Now()),
1219-
ExpiresAt: takeFirst(seed.CreatedAt, dbtime.Now()),
1219+
ExpiresAt: takeFirst(seed.ExpiresAt, dbtime.Now().Add(24*time.Hour)),
12201220
HashPrefix: takeFirstSlice(seed.HashPrefix, []byte("prefix")),
12211221
RefreshHash: takeFirstSlice(seed.RefreshHash, []byte("hashed-secret")),
12221222
AppSecretID: takeFirst(seed.AppSecretID, uuid.New()),
@@ -1228,6 +1228,25 @@ func OAuth2ProviderAppToken(t testing.TB, db database.Store, seed database.OAuth
12281228
return token
12291229
}
12301230

1231+
func OAuth2ProviderDeviceCode(t testing.TB, db database.Store, seed database.OAuth2ProviderDeviceCode) database.OAuth2ProviderDeviceCode {
1232+
deviceCode, err := db.InsertOAuth2ProviderDeviceCode(genCtx, database.InsertOAuth2ProviderDeviceCodeParams{
1233+
ID: takeFirst(seed.ID, uuid.New()),
1234+
CreatedAt: takeFirst(seed.CreatedAt, dbtime.Now()),
1235+
ExpiresAt: takeFirst(seed.ExpiresAt, dbtime.Now().Add(24*time.Hour)),
1236+
DeviceCodeHash: takeFirstSlice(seed.DeviceCodeHash, []byte("device-hash")),
1237+
DeviceCodePrefix: takeFirst(seed.DeviceCodePrefix, testutil.GetRandomName(t)),
1238+
UserCode: takeFirst(seed.UserCode, testutil.GetRandomName(t)),
1239+
ClientID: takeFirst(seed.ClientID, uuid.New()),
1240+
VerificationUri: takeFirst(seed.VerificationUri, "https://example.com/device"),
1241+
VerificationUriComplete: seed.VerificationUriComplete,
1242+
Scope: seed.Scope,
1243+
ResourceUri: seed.ResourceUri,
1244+
PollingInterval: takeFirst(seed.PollingInterval, 5),
1245+
})
1246+
require.NoError(t, err, "insert oauth2 device code")
1247+
return deviceCode
1248+
}
1249+
12311250
func WorkspaceAgentMemoryResourceMonitor(t testing.TB, db database.Store, seed database.WorkspaceAgentMemoryResourceMonitor) database.WorkspaceAgentMemoryResourceMonitor {
12321251
monitor, err := db.InsertMemoryResourceMonitor(genCtx, database.InsertMemoryResourceMonitorParams{
12331252
AgentID: takeFirst(seed.AgentID, uuid.New()),

coderd/database/dbmetrics/querymetrics.go

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/dbmock/dbmock.go

Lines changed: 30 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
INSERT INTO oauth2_provider_device_codes (
2+
id, created_at, expires_at, device_code_hash, device_code_prefix,
3+
user_code, client_id, user_id, status, verification_uri,
4+
verification_uri_complete, scope, resource_uri, polling_interval
5+
) VALUES (
6+
'c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11',
7+
'2023-06-15 10:23:54+00',
8+
'2023-06-15 10:33:54+00',
9+
CAST('abcdefg123' AS bytea),
10+
'abcdefg1',
11+
'ABCD1234',
12+
'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11',
13+
'0ed9befc-4911-4ccf-a8e2-559bf72daa94',
14+
'pending',
15+
'http://coder.com/oauth2/device',
16+
'http://coder.com/oauth2/device?user_code=ABCD1234',
17+
'read:user',
18+
'http://coder.com/api',
19+
5
20+
);

coderd/database/querier.go

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)