Skip to content

feat: add oauth2 token exchange #11778

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 35 commits into from
Feb 20, 2024
Merged
Changes from 1 commit
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
363005f
Extract auth code helper
code-asher Jan 20, 2024
7000a38
Add static OAuth2 authorize page
code-asher Jan 20, 2024
dc7a246
Add URL query validator
code-asher Jan 20, 2024
48fe155
Allow multiple required query params
code-asher Jan 20, 2024
a4a1dd5
Check that required query params are non-zero
code-asher Jan 23, 2024
586d47d
Refactor test apps
code-asher Jan 20, 2024
d8d2674
Add OAuth2 app filtering by user
code-asher Jan 20, 2024
b23f114
Allow fetching app with query param and form value
code-asher Jan 20, 2024
e6a97bc
Add OAuth2 auth, token, and revoke routes
code-asher Jan 20, 2024
ba95d1b
make gen
code-asher Jan 20, 2024
cbce34a
Delete some oidc helper comments
code-asher Feb 6, 2024
89b281d
s/Required/RequiredNotEmpty
code-asher Feb 6, 2024
6e3940d
No URL for me
code-asher Feb 6, 2024
7b132e4
Move redirect check into query parser
code-asher Feb 7, 2024
035092a
Return err on invalid params
code-asher Feb 7, 2024
3edbc35
Use correct godoc style
code-asher Feb 7, 2024
761c57d
Use userpassword.Hash
code-asher Feb 7, 2024
4769af9
Add comments to code timeout
code-asher Feb 7, 2024
30679d7
Comment on blank origin
code-asher Feb 7, 2024
c06218e
Pass the whole app to db2sdk
code-asher Feb 7, 2024
24e643c
Fix a racy context
code-asher Feb 9, 2024
de037d3
Extract state from authURL
code-asher Feb 9, 2024
19fa030
Implement refresh grant
code-asher Feb 10, 2024
10ab1c9
Fix verbiage in app PUT
code-asher Feb 10, 2024
22ff6c8
Remove extra period
code-asher Feb 10, 2024
cae4e61
Fix test race
code-asher Feb 10, 2024
d854f4d
Apparently browsers do not always set origin
code-asher Feb 13, 2024
9aba07f
Mention redirect URL must be a subset
code-asher Feb 16, 2024
4b975dc
Remove redundant comment
code-asher Feb 16, 2024
5b6a096
Clarify revoke operates on an authorized user
code-asher Feb 16, 2024
f583398
Move cURL comment
code-asher Feb 16, 2024
0161b10
Add error when referer is blank
code-asher Feb 16, 2024
c86e65a
Comment app secret struct
code-asher Feb 16, 2024
2dba243
Use var block
code-asher Feb 16, 2024
cedf7f2
Add error for unhandled grant types
code-asher Feb 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Refactor test apps
They are named now so their purpose is clearer.

Also, getting apps can be done as a user so do that instead.
  • Loading branch information
code-asher committed Feb 10, 2024
commit 586d47dd86a1bf985779fef5c9d548d183f2ce23
130 changes: 71 additions & 59 deletions enterprise/coderd/oauth2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
"github.com/coder/coder/v2/testutil"
)

func TestOAuthApps(t *testing.T) {
func TestOAuth2ProviderApps(t *testing.T) {
t.Parallel()

t.Run("Validation", func(t *testing.T) {
Expand Down Expand Up @@ -162,71 +162,62 @@ func TestOAuthApps(t *testing.T) {
t.Run("DeleteNonExisting", func(t *testing.T) {
t.Parallel()

client, _ := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{
client, owner := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureOAuth2Provider: 1,
},
}})
another, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)

ctx := testutil.Context(t, testutil.WaitLong)

//nolint:gocritic // OAauth2 app management requires owner permission.
_, err := client.OAuth2ProviderApp(ctx, uuid.New())
_, err := another.OAuth2ProviderApp(ctx, uuid.New())
require.Error(t, err)
})

t.Run("OK", func(t *testing.T) {
t.Parallel()

client, _ := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{
client, owner := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureOAuth2Provider: 1,
},
}})
another, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)

ctx := testutil.Context(t, testutil.WaitLong)

// No apps yet.
//nolint:gocritic // OAauth2 app management requires owner permission.
apps, err := client.OAuth2ProviderApps(ctx)
apps, err := another.OAuth2ProviderApps(ctx)
require.NoError(t, err)
require.Len(t, apps, 0)

// Should be able to add apps.
expected := []codersdk.OAuth2ProviderApp{}
for i := 0; i < 5; i++ {
postReq := codersdk.PostOAuth2ProviderAppRequest{
Name: "foo-" + strconv.Itoa(i),
CallbackURL: "http://" + strconv.Itoa(i) + ".localhost:3000",
}
//nolint:gocritic // OAauth2 app management requires owner permission.
app, err := client.PostOAuth2ProviderApp(ctx, postReq)
require.NoError(t, err)
require.Equal(t, postReq.Name, app.Name)
require.Equal(t, postReq.CallbackURL, app.CallbackURL)
expected = append(expected, app)
expected := generateApps(ctx, t, client, "get-apps")
expectedOrder := []codersdk.OAuth2ProviderApp{
expected.Default, expected.NoPort, expected.Subdomain,
expected.Extra[0], expected.Extra[1],
}

// Should get all the apps now.
//nolint:gocritic // OAauth2 app management requires owner permission.
apps, err = client.OAuth2ProviderApps(ctx)
apps, err = another.OAuth2ProviderApps(ctx)
require.NoError(t, err)
require.Len(t, apps, 5)
require.Equal(t, expected, apps)
require.Equal(t, expectedOrder, apps)

// Should be able to keep the same name when updating.
req := codersdk.PutOAuth2ProviderAppRequest{
Name: expected[0].Name,
Name: expected.Default.Name,
CallbackURL: "http://coder.com",
Icon: "test",
}
//nolint:gocritic // OAauth2 app management requires owner permission.
newApp, err := client.PutOAuth2ProviderApp(ctx, expected[0].ID, req)
newApp, err := client.PutOAuth2ProviderApp(ctx, expected.Default.ID, req)
require.NoError(t, err)
require.Equal(t, req.Name, newApp.Name)
require.Equal(t, req.CallbackURL, newApp.CallbackURL)
require.Equal(t, req.Icon, newApp.Icon)
require.Equal(t, expected[0].ID, newApp.ID)
require.Equal(t, expected.Default.ID, newApp.ID)

// Should be able to update name.
req = codersdk.PutOAuth2ProviderAppRequest{
Expand All @@ -235,34 +226,33 @@ func TestOAuthApps(t *testing.T) {
Icon: "test",
}
//nolint:gocritic // OAauth2 app management requires owner permission.
newApp, err = client.PutOAuth2ProviderApp(ctx, expected[0].ID, req)
newApp, err = client.PutOAuth2ProviderApp(ctx, expected.Default.ID, req)
require.NoError(t, err)
require.Equal(t, req.Name, newApp.Name)
require.Equal(t, req.CallbackURL, newApp.CallbackURL)
require.Equal(t, req.Icon, newApp.Icon)
require.Equal(t, expected[0].ID, newApp.ID)
require.Equal(t, expected.Default.ID, newApp.ID)

// Should be able to get a single app.
//nolint:gocritic // OAauth2 app management requires owner permission.
got, err := client.OAuth2ProviderApp(ctx, expected[0].ID)
got, err := another.OAuth2ProviderApp(ctx, expected.Default.ID)
require.NoError(t, err)
require.Equal(t, newApp, got)

// Should be able to delete an app.
//nolint:gocritic // OAauth2 app management requires owner permission.
err = client.DeleteOAuth2ProviderApp(ctx, expected[0].ID)
err = client.DeleteOAuth2ProviderApp(ctx, expected.Default.ID)
require.NoError(t, err)

// Should show the new count.
//nolint:gocritic // OAauth2 app management requires owner permission.
newApps, err := client.OAuth2ProviderApps(ctx)
newApps, err := another.OAuth2ProviderApps(ctx)
require.NoError(t, err)
require.Len(t, newApps, 4)
require.Equal(t, expected[1:], newApps)

require.Equal(t, expectedOrder[1:], newApps)
})
}

func TestOAuthAppSecrets(t *testing.T) {
func TestOAuth2ProviderAppSecrets(t *testing.T) {
t.Parallel()

client, _ := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{
Expand All @@ -274,27 +264,15 @@ func TestOAuthAppSecrets(t *testing.T) {
topCtx := testutil.Context(t, testutil.WaitLong)

// Make some apps.
//nolint:gocritic // OAauth2 app management requires owner permission.
app1, err := client.PostOAuth2ProviderApp(topCtx, codersdk.PostOAuth2ProviderAppRequest{
Name: "razzle-dazzle",
CallbackURL: "http://localhost",
})
require.NoError(t, err)

//nolint:gocritic // OAauth2 app management requires owner permission.
app2, err := client.PostOAuth2ProviderApp(topCtx, codersdk.PostOAuth2ProviderAppRequest{
Name: "razzle-dazzle-the-sequel",
CallbackURL: "http://localhost",
})
require.NoError(t, err)
apps := generateApps(topCtx, t, client, "app-secrets")

t.Run("DeleteNonExisting", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)

// Should not be able to create secrets for a non-existent app.
//nolint:gocritic // OAauth2 app management requires owner permission.
_, err = client.OAuth2ProviderAppSecrets(ctx, uuid.New())
_, err := client.OAuth2ProviderAppSecrets(ctx, uuid.New())
require.Error(t, err)

// Should not be able to delete non-existing secrets when there is no app.
Expand All @@ -304,16 +282,16 @@ func TestOAuthAppSecrets(t *testing.T) {

// Should not be able to delete non-existing secrets when the app exists.
//nolint:gocritic // OAauth2 app management requires owner permission.
err = client.DeleteOAuth2ProviderAppSecret(ctx, app1.ID, uuid.New())
err = client.DeleteOAuth2ProviderAppSecret(ctx, apps.Default.ID, uuid.New())
require.Error(t, err)

// Should not be able to delete an existing secret with the wrong app ID.
//nolint:gocritic // OAauth2 app management requires owner permission.
secret, err := client.PostOAuth2ProviderAppSecret(ctx, app2.ID)
secret, err := client.PostOAuth2ProviderAppSecret(ctx, apps.NoPort.ID)
require.NoError(t, err)

//nolint:gocritic // OAauth2 app management requires owner permission.
err = client.DeleteOAuth2ProviderAppSecret(ctx, app1.ID, secret.ID)
err = client.DeleteOAuth2ProviderAppSecret(ctx, apps.Default.ID, secret.ID)
require.Error(t, err)
})

Expand All @@ -323,26 +301,26 @@ func TestOAuthAppSecrets(t *testing.T) {

// No secrets yet.
//nolint:gocritic // OAauth2 app management requires owner permission.
secrets, err := client.OAuth2ProviderAppSecrets(ctx, app1.ID)
secrets, err := client.OAuth2ProviderAppSecrets(ctx, apps.Default.ID)
require.NoError(t, err)
require.Len(t, secrets, 0)

// Should be able to create secrets.
for i := 0; i < 5; i++ {
//nolint:gocritic // OAauth2 app management requires owner permission.
secret, err := client.PostOAuth2ProviderAppSecret(ctx, app1.ID)
secret, err := client.PostOAuth2ProviderAppSecret(ctx, apps.Default.ID)
require.NoError(t, err)
require.NotEmpty(t, secret.ClientSecretFull)
require.True(t, len(secret.ClientSecretFull) > 6)

//nolint:gocritic // OAauth2 app management requires owner permission.
_, err = client.PostOAuth2ProviderAppSecret(ctx, app2.ID)
_, err = client.PostOAuth2ProviderAppSecret(ctx, apps.NoPort.ID)
require.NoError(t, err)
}

// Should get secrets now, but only for the one app.
//nolint:gocritic // OAauth2 app management requires owner permission.
secrets, err = client.OAuth2ProviderAppSecrets(ctx, app1.ID)
secrets, err = client.OAuth2ProviderAppSecrets(ctx, apps.Default.ID)
require.NoError(t, err)
require.Len(t, secrets, 5)
for _, secret := range secrets {
Expand All @@ -351,19 +329,53 @@ func TestOAuthAppSecrets(t *testing.T) {

// Should be able to delete a secret.
//nolint:gocritic // OAauth2 app management requires owner permission.
err = client.DeleteOAuth2ProviderAppSecret(ctx, app1.ID, secrets[0].ID)
err = client.DeleteOAuth2ProviderAppSecret(ctx, apps.Default.ID, secrets[0].ID)
require.NoError(t, err)
secrets, err = client.OAuth2ProviderAppSecrets(ctx, app1.ID)
secrets, err = client.OAuth2ProviderAppSecrets(ctx, apps.Default.ID)
require.NoError(t, err)
require.Len(t, secrets, 4)

// No secrets once the app is deleted.
//nolint:gocritic // OAauth2 app management requires owner permission.
err = client.DeleteOAuth2ProviderApp(ctx, app1.ID)
err = client.DeleteOAuth2ProviderApp(ctx, apps.Default.ID)
require.NoError(t, err)

//nolint:gocritic // OAauth2 app management requires owner permission.
_, err = client.OAuth2ProviderAppSecrets(ctx, app1.ID)
_, err = client.OAuth2ProviderAppSecrets(ctx, apps.Default.ID)
require.Error(t, err)
})
}

type provisionedApps struct {
Default codersdk.OAuth2ProviderApp
NoPort codersdk.OAuth2ProviderApp
Subdomain codersdk.OAuth2ProviderApp
// For sorting purposes these are included. You will likely never touch them.
Extra []codersdk.OAuth2ProviderApp
}

func generateApps(ctx context.Context, t *testing.T, client *codersdk.Client, suffix string) provisionedApps {
create := func(name, callback string) codersdk.OAuth2ProviderApp {
name = fmt.Sprintf("%s-%s", name, suffix)
//nolint:gocritic // OAauth2 app management requires owner permission.
app, err := client.PostOAuth2ProviderApp(ctx, codersdk.PostOAuth2ProviderAppRequest{
Name: name,
CallbackURL: callback,
Icon: "",
})
require.NoError(t, err)
require.Equal(t, name, app.Name)
require.Equal(t, callback, app.CallbackURL)
return app
}

return provisionedApps{
Default: create("razzle-dazzle-a", "http://localhost1:8080/foo/bar"),
NoPort: create("razzle-dazzle-b", "http://localhost2"),
Subdomain: create("razzle-dazzle-z", "http://30.localhost:3000"),
Extra: []codersdk.OAuth2ProviderApp{
create("second-to-last", "http://20.localhost:3000"),
create("woo-10", "http://10.localhost:3000"),
},
}
}