mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
refactor: organize OAuth2 provider tests into dedicated packages (#18747)
# OAuth2 Provider Code Reorganization This PR reorganizes the OAuth2 provider code to improve separation of concerns and maintainability. The changes include: 1. Migrating OAuth2 provider app validation tests from `coderd/oauth2_test.go` to `oauth2provider/provider_test.go` 2. Moving OAuth2 client registration validation tests to `oauth2provider/validation_test.go` 3. Adding new comprehensive test files for metadata and validation edge cases 4. Renaming `OAuth2ProviderAppSecret` to `AppSecret` for better naming consistency 5. Simplifying the main integration test in `oauth2_test.go` to focus on core functionality The PR maintains all existing test coverage while organizing the code more logically, making it easier to understand and maintain the OAuth2 provider implementation. This reorganization will help with future enhancements to the OAuth2 provider functionality.
This commit is contained in:
@ -32,287 +32,27 @@ import (
|
||||
func TestOAuth2ProviderApps(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("Validation", func(t *testing.T) {
|
||||
// NOTE: Unit tests for OAuth2 provider app validation have been migrated to
|
||||
// oauth2provider/provider_test.go for better separation of concerns.
|
||||
// This test function now focuses on integration testing with the full server stack.
|
||||
|
||||
t.Run("IntegrationFlow", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
req codersdk.PostOAuth2ProviderAppRequest
|
||||
}{
|
||||
{
|
||||
name: "NameMissing",
|
||||
req: codersdk.PostOAuth2ProviderAppRequest{
|
||||
CallbackURL: "http://localhost:3000",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NameSpaces",
|
||||
req: codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "foo bar",
|
||||
CallbackURL: "http://localhost:3000",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NameTooLong",
|
||||
req: codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "too loooooooooooooooooooooooooong",
|
||||
CallbackURL: "http://localhost:3000",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "URLMissing",
|
||||
req: codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "foo",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "URLLocalhostNoScheme",
|
||||
req: codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "foo",
|
||||
CallbackURL: "localhost:3000",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "URLNoScheme",
|
||||
req: codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "foo",
|
||||
CallbackURL: "coder.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "URLNoColon",
|
||||
req: codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "foo",
|
||||
CallbackURL: "http//coder",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "URLJustBar",
|
||||
req: codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "foo",
|
||||
CallbackURL: "bar",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "URLPathOnly",
|
||||
req: codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "foo",
|
||||
CallbackURL: "/bar/baz/qux",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "URLJustHttp",
|
||||
req: codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "foo",
|
||||
CallbackURL: "http",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "URLNoHost",
|
||||
req: codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "foo",
|
||||
CallbackURL: "http://",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "URLSpaces",
|
||||
req: codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "foo",
|
||||
CallbackURL: "bar baz qux",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Generate an application for testing PUTs.
|
||||
req := codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: fmt.Sprintf("quark-%d", time.Now().UnixNano()%1000000),
|
||||
CallbackURL: "http://coder.com",
|
||||
}
|
||||
//nolint:gocritic // OAauth2 app management requires owner permission.
|
||||
existingApp, err := client.PostOAuth2ProviderApp(ctx, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
//nolint:gocritic // OAauth2 app management requires owner permission.
|
||||
_, err := client.PostOAuth2ProviderApp(ctx, test.req)
|
||||
require.Error(t, err)
|
||||
|
||||
//nolint:gocritic // OAauth2 app management requires owner permission.
|
||||
_, err = client.PutOAuth2ProviderApp(ctx, existingApp.ID, codersdk.PutOAuth2ProviderAppRequest{
|
||||
Name: test.req.Name,
|
||||
CallbackURL: test.req.CallbackURL,
|
||||
})
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("DeleteNonExisting", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, nil)
|
||||
owner := coderdtest.CreateFirstUser(t, client)
|
||||
another, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
_, err := another.OAuth2ProviderApp(ctx, uuid.New())
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, nil)
|
||||
owner := coderdtest.CreateFirstUser(t, client)
|
||||
another, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
// No apps yet.
|
||||
apps, err := another.OAuth2ProviderApps(ctx, codersdk.OAuth2ProviderAppFilter{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, apps, 0)
|
||||
|
||||
// Should be able to add apps.
|
||||
expected := generateApps(ctx, t, client, "get-apps")
|
||||
expectedOrder := []codersdk.OAuth2ProviderApp{
|
||||
expected.Default, expected.NoPort,
|
||||
expected.Extra[0], expected.Extra[1], expected.Subdomain,
|
||||
}
|
||||
|
||||
// Should get all the apps now.
|
||||
apps, err = another.OAuth2ProviderApps(ctx, codersdk.OAuth2ProviderAppFilter{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, apps, 5)
|
||||
require.Equal(t, expectedOrder, apps)
|
||||
|
||||
// Should be able to keep the same name when updating.
|
||||
req := codersdk.PutOAuth2ProviderAppRequest{
|
||||
Name: expected.Default.Name,
|
||||
CallbackURL: "http://coder.com",
|
||||
Icon: "test",
|
||||
}
|
||||
//nolint:gocritic // OAauth2 app management requires owner permission.
|
||||
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.Default.ID, newApp.ID)
|
||||
|
||||
// Should be able to update name.
|
||||
req = codersdk.PutOAuth2ProviderAppRequest{
|
||||
Name: "new-foo",
|
||||
CallbackURL: "http://coder.com",
|
||||
Icon: "test",
|
||||
}
|
||||
//nolint:gocritic // OAauth2 app management requires owner permission.
|
||||
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.Default.ID, newApp.ID)
|
||||
|
||||
// Should be able to get a single app.
|
||||
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.Default.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should show the new count.
|
||||
newApps, err := another.OAuth2ProviderApps(ctx, codersdk.OAuth2ProviderAppFilter{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, newApps, 4)
|
||||
|
||||
require.Equal(t, expectedOrder[1:], newApps)
|
||||
})
|
||||
|
||||
t.Run("ByUser", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
owner := coderdtest.CreateFirstUser(t, client)
|
||||
another, user := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
_ = generateApps(ctx, t, client, "by-user")
|
||||
apps, err := another.OAuth2ProviderApps(ctx, codersdk.OAuth2ProviderAppFilter{
|
||||
UserID: user.ID,
|
||||
// Test basic app creation and management in integration context
|
||||
//nolint:gocritic // OAuth2 app management requires owner permission.
|
||||
app, err := client.PostOAuth2ProviderApp(ctx, codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: fmt.Sprintf("integration-test-%d", time.Now().UnixNano()%1000000),
|
||||
CallbackURL: "http://localhost:3000",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, apps, 0)
|
||||
})
|
||||
|
||||
t.Run("DuplicateNames", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
// Create multiple OAuth2 apps with the same name to verify RFC 7591 compliance
|
||||
// RFC 7591 allows multiple apps to have the same name
|
||||
appName := fmt.Sprintf("duplicate-name-%d", time.Now().UnixNano()%1000000)
|
||||
|
||||
// Create first app
|
||||
//nolint:gocritic // OAuth2 app management requires owner permission.
|
||||
app1, err := client.PostOAuth2ProviderApp(ctx, codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: appName,
|
||||
CallbackURL: "http://localhost:3001",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, appName, app1.Name)
|
||||
|
||||
// Create second app with the same name
|
||||
//nolint:gocritic // OAuth2 app management requires owner permission.
|
||||
app2, err := client.PostOAuth2ProviderApp(ctx, codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: appName,
|
||||
CallbackURL: "http://localhost:3002",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, appName, app2.Name)
|
||||
|
||||
// Create third app with the same name
|
||||
//nolint:gocritic // OAuth2 app management requires owner permission.
|
||||
app3, err := client.PostOAuth2ProviderApp(ctx, codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: appName,
|
||||
CallbackURL: "http://localhost:3003",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, appName, app3.Name)
|
||||
|
||||
// Verify all apps have different IDs but same name
|
||||
require.NotEqual(t, app1.ID, app2.ID)
|
||||
require.NotEqual(t, app1.ID, app3.ID)
|
||||
require.NotEqual(t, app2.ID, app3.ID)
|
||||
require.Equal(t, app1.Name, app2.Name)
|
||||
require.Equal(t, app1.Name, app3.Name)
|
||||
|
||||
// Verify all apps can be retrieved and have the same name
|
||||
//nolint:gocritic // OAuth2 app management requires owner permission.
|
||||
apps, err := client.OAuth2ProviderApps(ctx, codersdk.OAuth2ProviderAppFilter{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Count apps with our duplicate name
|
||||
duplicateNameCount := 0
|
||||
for _, app := range apps {
|
||||
if app.Name == appName {
|
||||
duplicateNameCount++
|
||||
}
|
||||
}
|
||||
require.Equal(t, 3, duplicateNameCount, "Should have exactly 3 apps with the duplicate name")
|
||||
require.NotEmpty(t, app.ID)
|
||||
require.NotEmpty(t, app.Name)
|
||||
require.Equal(t, "http://localhost:3000", app.CallbackURL)
|
||||
})
|
||||
}
|
||||
|
||||
@ -1796,145 +1536,5 @@ func TestOAuth2RegistrationAccessToken(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestOAuth2ClientRegistrationValidation tests validation of client registration requests
|
||||
func TestOAuth2ClientRegistrationValidation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("ValidURIs", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
validURIs := []string{
|
||||
"https://example.com/callback",
|
||||
"http://localhost:8080/callback",
|
||||
"custom-scheme://app/callback",
|
||||
}
|
||||
|
||||
req := codersdk.OAuth2ClientRegistrationRequest{
|
||||
RedirectURIs: validURIs,
|
||||
ClientName: fmt.Sprintf("valid-uris-client-%d", time.Now().UnixNano()),
|
||||
}
|
||||
|
||||
resp, err := client.PostOAuth2ClientRegistration(ctx, req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, validURIs, resp.RedirectURIs)
|
||||
})
|
||||
|
||||
t.Run("InvalidURIs", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
uris []string
|
||||
}{
|
||||
{
|
||||
name: "InvalidURL",
|
||||
uris: []string{"not-a-url"},
|
||||
},
|
||||
{
|
||||
name: "EmptyFragment",
|
||||
uris: []string{"https://example.com/callback#"},
|
||||
},
|
||||
{
|
||||
name: "Fragment",
|
||||
uris: []string{"https://example.com/callback#fragment"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create new client for each sub-test to avoid shared state issues
|
||||
subClient := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, subClient)
|
||||
subCtx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
req := codersdk.OAuth2ClientRegistrationRequest{
|
||||
RedirectURIs: tc.uris,
|
||||
ClientName: fmt.Sprintf("invalid-uri-client-%s-%d", tc.name, time.Now().UnixNano()),
|
||||
}
|
||||
|
||||
_, err := subClient.PostOAuth2ClientRegistration(subCtx, req)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "invalid_client_metadata")
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ValidGrantTypes", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
req := codersdk.OAuth2ClientRegistrationRequest{
|
||||
RedirectURIs: []string{"https://example.com/callback"},
|
||||
ClientName: fmt.Sprintf("valid-grant-types-client-%d", time.Now().UnixNano()),
|
||||
GrantTypes: []string{"authorization_code", "refresh_token"},
|
||||
}
|
||||
|
||||
resp, err := client.PostOAuth2ClientRegistration(ctx, req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, req.GrantTypes, resp.GrantTypes)
|
||||
})
|
||||
|
||||
t.Run("InvalidGrantTypes", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
req := codersdk.OAuth2ClientRegistrationRequest{
|
||||
RedirectURIs: []string{"https://example.com/callback"},
|
||||
ClientName: fmt.Sprintf("invalid-grant-types-client-%d", time.Now().UnixNano()),
|
||||
GrantTypes: []string{"unsupported_grant"},
|
||||
}
|
||||
|
||||
_, err := client.PostOAuth2ClientRegistration(ctx, req)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "invalid_client_metadata")
|
||||
})
|
||||
|
||||
t.Run("ValidResponseTypes", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
req := codersdk.OAuth2ClientRegistrationRequest{
|
||||
RedirectURIs: []string{"https://example.com/callback"},
|
||||
ClientName: fmt.Sprintf("valid-response-types-client-%d", time.Now().UnixNano()),
|
||||
ResponseTypes: []string{"code"},
|
||||
}
|
||||
|
||||
resp, err := client.PostOAuth2ClientRegistration(ctx, req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, req.ResponseTypes, resp.ResponseTypes)
|
||||
})
|
||||
|
||||
t.Run("InvalidResponseTypes", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
req := codersdk.OAuth2ClientRegistrationRequest{
|
||||
RedirectURIs: []string{"https://example.com/callback"},
|
||||
ClientName: fmt.Sprintf("invalid-response-types-client-%d", time.Now().UnixNano()),
|
||||
ResponseTypes: []string{"token"}, // Not supported
|
||||
}
|
||||
|
||||
_, err := client.PostOAuth2ClientRegistration(ctx, req)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "invalid_client_metadata")
|
||||
})
|
||||
}
|
||||
// NOTE: OAuth2 client registration validation tests have been migrated to
|
||||
// oauth2provider/validation_test.go for better separation of concerns
|
||||
|
86
coderd/oauth2provider/metadata_test.go
Normal file
86
coderd/oauth2provider/metadata_test.go
Normal file
@ -0,0 +1,86 @@
|
||||
package oauth2provider_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
func TestOAuth2AuthorizationServerMetadata(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, nil)
|
||||
serverURL := client.URL
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
// Use a plain HTTP client since this endpoint doesn't require authentication
|
||||
endpoint := serverURL.ResolveReference(&url.URL{Path: "/.well-known/oauth-authorization-server"}).String()
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var metadata codersdk.OAuth2AuthorizationServerMetadata
|
||||
err = json.NewDecoder(resp.Body).Decode(&metadata)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the metadata
|
||||
require.NotEmpty(t, metadata.Issuer)
|
||||
require.NotEmpty(t, metadata.AuthorizationEndpoint)
|
||||
require.NotEmpty(t, metadata.TokenEndpoint)
|
||||
require.Contains(t, metadata.ResponseTypesSupported, "code")
|
||||
require.Contains(t, metadata.GrantTypesSupported, "authorization_code")
|
||||
require.Contains(t, metadata.GrantTypesSupported, "refresh_token")
|
||||
require.Contains(t, metadata.CodeChallengeMethodsSupported, "S256")
|
||||
}
|
||||
|
||||
func TestOAuth2ProtectedResourceMetadata(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, nil)
|
||||
serverURL := client.URL
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
// Use a plain HTTP client since this endpoint doesn't require authentication
|
||||
endpoint := serverURL.ResolveReference(&url.URL{Path: "/.well-known/oauth-protected-resource"}).String()
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var metadata codersdk.OAuth2ProtectedResourceMetadata
|
||||
err = json.NewDecoder(resp.Body).Decode(&metadata)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the metadata
|
||||
require.NotEmpty(t, metadata.Resource)
|
||||
require.NotEmpty(t, metadata.AuthorizationServers)
|
||||
require.Len(t, metadata.AuthorizationServers, 1)
|
||||
require.Equal(t, metadata.Resource, metadata.AuthorizationServers[0])
|
||||
// RFC 6750 bearer tokens are now supported as fallback methods
|
||||
require.Contains(t, metadata.BearerMethodsSupported, "header")
|
||||
require.Contains(t, metadata.BearerMethodsSupported, "query")
|
||||
// ScopesSupported can be empty until scope system is implemented
|
||||
// Empty slice is marshaled as empty array, but can be nil when unmarshaled
|
||||
require.True(t, len(metadata.ScopesSupported) == 0)
|
||||
}
|
453
coderd/oauth2provider/provider_test.go
Normal file
453
coderd/oauth2provider/provider_test.go
Normal file
@ -0,0 +1,453 @@
|
||||
package oauth2provider_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
// TestOAuth2ProviderAppValidation tests validation logic for OAuth2 provider app requests
|
||||
func TestOAuth2ProviderAppValidation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("ValidationErrors", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
req codersdk.PostOAuth2ProviderAppRequest
|
||||
}{
|
||||
{
|
||||
name: "NameMissing",
|
||||
req: codersdk.PostOAuth2ProviderAppRequest{
|
||||
CallbackURL: "http://localhost:3000",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NameSpaces",
|
||||
req: codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "foo bar",
|
||||
CallbackURL: "http://localhost:3000",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NameTooLong",
|
||||
req: codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "too loooooooooooooooooooooooooong",
|
||||
CallbackURL: "http://localhost:3000",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "URLMissing",
|
||||
req: codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "foo",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "URLLocalhostNoScheme",
|
||||
req: codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "foo",
|
||||
CallbackURL: "localhost:3000",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "URLNoScheme",
|
||||
req: codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "foo",
|
||||
CallbackURL: "coder.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "URLNoColon",
|
||||
req: codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "foo",
|
||||
CallbackURL: "http//coder",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "URLJustBar",
|
||||
req: codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "foo",
|
||||
CallbackURL: "bar",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "URLPathOnly",
|
||||
req: codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "foo",
|
||||
CallbackURL: "/bar/baz/qux",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "URLJustHttp",
|
||||
req: codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "foo",
|
||||
CallbackURL: "http",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "URLNoHost",
|
||||
req: codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "foo",
|
||||
CallbackURL: "http://",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "URLSpaces",
|
||||
req: codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: "foo",
|
||||
CallbackURL: "bar baz qux",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCtx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
//nolint:gocritic // OAuth2 app management requires owner permission.
|
||||
_, err := client.PostOAuth2ProviderApp(testCtx, test.req)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("DuplicateNames", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
// Create multiple OAuth2 apps with the same name to verify RFC 7591 compliance
|
||||
// RFC 7591 allows multiple apps to have the same name
|
||||
appName := fmt.Sprintf("duplicate-name-%d", time.Now().UnixNano()%1000000)
|
||||
|
||||
// Create first app
|
||||
//nolint:gocritic // OAuth2 app management requires owner permission.
|
||||
app1, err := client.PostOAuth2ProviderApp(ctx, codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: appName,
|
||||
CallbackURL: "http://localhost:3001",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, appName, app1.Name)
|
||||
|
||||
// Create second app with the same name
|
||||
//nolint:gocritic // OAuth2 app management requires owner permission.
|
||||
app2, err := client.PostOAuth2ProviderApp(ctx, codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: appName,
|
||||
CallbackURL: "http://localhost:3002",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, appName, app2.Name)
|
||||
|
||||
// Create third app with the same name
|
||||
//nolint:gocritic // OAuth2 app management requires owner permission.
|
||||
app3, err := client.PostOAuth2ProviderApp(ctx, codersdk.PostOAuth2ProviderAppRequest{
|
||||
Name: appName,
|
||||
CallbackURL: "http://localhost:3003",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, appName, app3.Name)
|
||||
|
||||
// Verify all apps have different IDs but same name
|
||||
require.NotEqual(t, app1.ID, app2.ID)
|
||||
require.NotEqual(t, app1.ID, app3.ID)
|
||||
require.NotEqual(t, app2.ID, app3.ID)
|
||||
})
|
||||
}
|
||||
|
||||
// TestOAuth2ClientRegistrationValidation tests OAuth2 client registration validation
|
||||
func TestOAuth2ClientRegistrationValidation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("ValidURIs", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
validURIs := []string{
|
||||
"https://example.com/callback",
|
||||
"http://localhost:8080/callback",
|
||||
"custom-scheme://app/callback",
|
||||
}
|
||||
|
||||
req := codersdk.OAuth2ClientRegistrationRequest{
|
||||
RedirectURIs: validURIs,
|
||||
ClientName: fmt.Sprintf("valid-uris-client-%d", time.Now().UnixNano()),
|
||||
}
|
||||
|
||||
resp, err := client.PostOAuth2ClientRegistration(ctx, req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, validURIs, resp.RedirectURIs)
|
||||
})
|
||||
|
||||
t.Run("InvalidURIs", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
uris []string
|
||||
}{
|
||||
{
|
||||
name: "InvalidURL",
|
||||
uris: []string{"not-a-url"},
|
||||
},
|
||||
{
|
||||
name: "EmptyFragment",
|
||||
uris: []string{"https://example.com/callback#"},
|
||||
},
|
||||
{
|
||||
name: "Fragment",
|
||||
uris: []string{"https://example.com/callback#fragment"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create new client for each sub-test to avoid shared state issues
|
||||
subClient := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, subClient)
|
||||
subCtx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
req := codersdk.OAuth2ClientRegistrationRequest{
|
||||
RedirectURIs: tc.uris,
|
||||
ClientName: fmt.Sprintf("invalid-uri-client-%s-%d", tc.name, time.Now().UnixNano()),
|
||||
}
|
||||
|
||||
_, err := subClient.PostOAuth2ClientRegistration(subCtx, req)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "invalid_client_metadata")
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ValidGrantTypes", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
req := codersdk.OAuth2ClientRegistrationRequest{
|
||||
RedirectURIs: []string{"https://example.com/callback"},
|
||||
ClientName: fmt.Sprintf("valid-grant-types-client-%d", time.Now().UnixNano()),
|
||||
GrantTypes: []string{"authorization_code", "refresh_token"},
|
||||
}
|
||||
|
||||
resp, err := client.PostOAuth2ClientRegistration(ctx, req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, req.GrantTypes, resp.GrantTypes)
|
||||
})
|
||||
|
||||
t.Run("InvalidGrantTypes", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
req := codersdk.OAuth2ClientRegistrationRequest{
|
||||
RedirectURIs: []string{"https://example.com/callback"},
|
||||
ClientName: fmt.Sprintf("invalid-grant-types-client-%d", time.Now().UnixNano()),
|
||||
GrantTypes: []string{"unsupported_grant"},
|
||||
}
|
||||
|
||||
_, err := client.PostOAuth2ClientRegistration(ctx, req)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "invalid_client_metadata")
|
||||
})
|
||||
|
||||
t.Run("ValidResponseTypes", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
req := codersdk.OAuth2ClientRegistrationRequest{
|
||||
RedirectURIs: []string{"https://example.com/callback"},
|
||||
ClientName: fmt.Sprintf("valid-response-types-client-%d", time.Now().UnixNano()),
|
||||
ResponseTypes: []string{"code"},
|
||||
}
|
||||
|
||||
resp, err := client.PostOAuth2ClientRegistration(ctx, req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, req.ResponseTypes, resp.ResponseTypes)
|
||||
})
|
||||
|
||||
t.Run("InvalidResponseTypes", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
req := codersdk.OAuth2ClientRegistrationRequest{
|
||||
RedirectURIs: []string{"https://example.com/callback"},
|
||||
ClientName: fmt.Sprintf("invalid-response-types-client-%d", time.Now().UnixNano()),
|
||||
ResponseTypes: []string{"token"}, // Not supported
|
||||
}
|
||||
|
||||
_, err := client.PostOAuth2ClientRegistration(ctx, req)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "invalid_client_metadata")
|
||||
})
|
||||
}
|
||||
|
||||
// TestOAuth2ProviderAppOperations tests basic CRUD operations for OAuth2 provider apps
|
||||
func TestOAuth2ProviderAppOperations(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("DeleteNonExisting", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, nil)
|
||||
owner := coderdtest.CreateFirstUser(t, client)
|
||||
another, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
_, err := another.OAuth2ProviderApp(ctx, uuid.New())
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("BasicOperations", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, nil)
|
||||
owner := coderdtest.CreateFirstUser(t, client)
|
||||
another, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
// No apps yet.
|
||||
apps, err := another.OAuth2ProviderApps(ctx, codersdk.OAuth2ProviderAppFilter{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, apps, 0)
|
||||
|
||||
// Should be able to add apps.
|
||||
expectedApps := generateApps(ctx, t, client, "get-apps")
|
||||
expectedOrder := []codersdk.OAuth2ProviderApp{
|
||||
expectedApps.Default, expectedApps.NoPort,
|
||||
expectedApps.Extra[0], expectedApps.Extra[1], expectedApps.Subdomain,
|
||||
}
|
||||
|
||||
// Should get all the apps now.
|
||||
apps, err = another.OAuth2ProviderApps(ctx, codersdk.OAuth2ProviderAppFilter{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, apps, 5)
|
||||
require.Equal(t, expectedOrder, apps)
|
||||
|
||||
// Should be able to keep the same name when updating.
|
||||
req := codersdk.PutOAuth2ProviderAppRequest{
|
||||
Name: expectedApps.Default.Name,
|
||||
CallbackURL: "http://coder.com",
|
||||
Icon: "test",
|
||||
}
|
||||
//nolint:gocritic // OAuth2 app management requires owner permission.
|
||||
newApp, err := client.PutOAuth2ProviderApp(ctx, expectedApps.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, expectedApps.Default.ID, newApp.ID)
|
||||
|
||||
// Should be able to update name.
|
||||
req = codersdk.PutOAuth2ProviderAppRequest{
|
||||
Name: "new-foo",
|
||||
CallbackURL: "http://coder.com",
|
||||
Icon: "test",
|
||||
}
|
||||
//nolint:gocritic // OAuth2 app management requires owner permission.
|
||||
newApp, err = client.PutOAuth2ProviderApp(ctx, expectedApps.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, expectedApps.Default.ID, newApp.ID)
|
||||
|
||||
// Should be able to get a single app.
|
||||
got, err := another.OAuth2ProviderApp(ctx, expectedApps.Default.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, newApp, got)
|
||||
|
||||
// Should be able to delete an app.
|
||||
//nolint:gocritic // OAuth2 app management requires owner permission.
|
||||
err = client.DeleteOAuth2ProviderApp(ctx, expectedApps.Default.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should show the new count.
|
||||
newApps, err := another.OAuth2ProviderApps(ctx, codersdk.OAuth2ProviderAppFilter{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, newApps, 4)
|
||||
|
||||
require.Equal(t, expectedOrder[1:], newApps)
|
||||
})
|
||||
|
||||
t.Run("ByUser", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
owner := coderdtest.CreateFirstUser(t, client)
|
||||
another, user := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
_ = generateApps(ctx, t, client, "by-user")
|
||||
apps, err := another.OAuth2ProviderApps(ctx, codersdk.OAuth2ProviderAppFilter{
|
||||
UserID: user.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, apps, 0)
|
||||
})
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
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 // OAuth2 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("app-a", "http://localhost1:8080/foo/bar"),
|
||||
NoPort: create("app-b", "http://localhost2"),
|
||||
Subdomain: create("app-z", "http://30.localhost:3000"),
|
||||
Extra: []codersdk.OAuth2ProviderApp{
|
||||
create("app-x", "http://20.localhost:3000"),
|
||||
create("app-y", "http://10.localhost:3000"),
|
||||
},
|
||||
}
|
||||
}
|
782
coderd/oauth2provider/validation_test.go
Normal file
782
coderd/oauth2provider/validation_test.go
Normal file
@ -0,0 +1,782 @@
|
||||
package oauth2provider_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
// TestOAuth2ClientMetadataValidation tests enhanced metadata validation per RFC 7591
|
||||
func TestOAuth2ClientMetadataValidation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("RedirectURIValidation", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
redirectURIs []string
|
||||
expectError bool
|
||||
errorContains string
|
||||
}{
|
||||
{
|
||||
name: "ValidHTTPS",
|
||||
redirectURIs: []string{"https://example.com/callback"},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "ValidLocalhost",
|
||||
redirectURIs: []string{"http://localhost:8080/callback"},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "ValidLocalhostIP",
|
||||
redirectURIs: []string{"http://127.0.0.1:8080/callback"},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "ValidCustomScheme",
|
||||
redirectURIs: []string{"com.example.myapp://auth/callback"},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "InvalidHTTPNonLocalhost",
|
||||
redirectURIs: []string{"http://example.com/callback"},
|
||||
expectError: true,
|
||||
errorContains: "redirect_uri",
|
||||
},
|
||||
{
|
||||
name: "InvalidWithFragment",
|
||||
redirectURIs: []string{"https://example.com/callback#fragment"},
|
||||
expectError: true,
|
||||
errorContains: "fragment",
|
||||
},
|
||||
{
|
||||
name: "InvalidJavaScriptScheme",
|
||||
redirectURIs: []string{"javascript:alert('xss')"},
|
||||
expectError: true,
|
||||
errorContains: "dangerous scheme",
|
||||
},
|
||||
{
|
||||
name: "InvalidDataScheme",
|
||||
redirectURIs: []string{"data:text/html,<script>alert('xss')</script>"},
|
||||
expectError: true,
|
||||
errorContains: "dangerous scheme",
|
||||
},
|
||||
{
|
||||
name: "InvalidFileScheme",
|
||||
redirectURIs: []string{"file:///etc/passwd"},
|
||||
expectError: true,
|
||||
errorContains: "dangerous scheme",
|
||||
},
|
||||
{
|
||||
name: "EmptyString",
|
||||
redirectURIs: []string{""},
|
||||
expectError: true,
|
||||
errorContains: "redirect_uri",
|
||||
},
|
||||
{
|
||||
name: "RelativeURL",
|
||||
redirectURIs: []string{"/callback"},
|
||||
expectError: true,
|
||||
errorContains: "redirect_uri",
|
||||
},
|
||||
{
|
||||
name: "MultipleValid",
|
||||
redirectURIs: []string{"https://example.com/callback", "com.example.app://auth"},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "MixedValidInvalid",
|
||||
redirectURIs: []string{"https://example.com/callback", "http://example.com/callback"},
|
||||
expectError: true,
|
||||
errorContains: "redirect_uri",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
req := codersdk.OAuth2ClientRegistrationRequest{
|
||||
RedirectURIs: test.redirectURIs,
|
||||
ClientName: fmt.Sprintf("test-client-%d", time.Now().UnixNano()),
|
||||
}
|
||||
|
||||
_, err := client.PostOAuth2ClientRegistration(ctx, req)
|
||||
|
||||
if test.expectError {
|
||||
require.Error(t, err)
|
||||
if test.errorContains != "" {
|
||||
require.Contains(t, strings.ToLower(err.Error()), strings.ToLower(test.errorContains))
|
||||
}
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ClientURIValidation", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
clientURI string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "ValidHTTPS",
|
||||
clientURI: "https://example.com",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "ValidHTTPLocalhost",
|
||||
clientURI: "http://localhost:8080",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "ValidWithPath",
|
||||
clientURI: "https://example.com/app",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "ValidWithQuery",
|
||||
clientURI: "https://example.com/app?param=value",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "InvalidNotURL",
|
||||
clientURI: "not-a-url",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "ValidWithFragment",
|
||||
clientURI: "https://example.com#fragment",
|
||||
expectError: false, // Fragments are allowed in client_uri, unlike redirect_uri
|
||||
},
|
||||
{
|
||||
name: "InvalidJavaScript",
|
||||
clientURI: "javascript:alert('xss')",
|
||||
expectError: true, // Only http/https allowed for client_uri
|
||||
},
|
||||
{
|
||||
name: "InvalidFTP",
|
||||
clientURI: "ftp://example.com",
|
||||
expectError: true, // Only http/https allowed for client_uri
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
req := codersdk.OAuth2ClientRegistrationRequest{
|
||||
RedirectURIs: []string{"https://example.com/callback"},
|
||||
ClientName: fmt.Sprintf("test-client-%d", time.Now().UnixNano()),
|
||||
ClientURI: test.clientURI,
|
||||
}
|
||||
|
||||
_, err := client.PostOAuth2ClientRegistration(ctx, req)
|
||||
|
||||
if test.expectError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("LogoURIValidation", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
logoURI string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "ValidHTTPS",
|
||||
logoURI: "https://example.com/logo.png",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "ValidHTTPLocalhost",
|
||||
logoURI: "http://localhost:8080/logo.png",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "ValidWithQuery",
|
||||
logoURI: "https://example.com/logo.png?size=large",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "InvalidNotURL",
|
||||
logoURI: "not-a-url",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "ValidWithFragment",
|
||||
logoURI: "https://example.com/logo.png#fragment",
|
||||
expectError: false, // Fragments are allowed in logo_uri
|
||||
},
|
||||
{
|
||||
name: "InvalidJavaScript",
|
||||
logoURI: "javascript:alert('xss')",
|
||||
expectError: true, // Only http/https allowed for logo_uri
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
req := codersdk.OAuth2ClientRegistrationRequest{
|
||||
RedirectURIs: []string{"https://example.com/callback"},
|
||||
ClientName: fmt.Sprintf("test-client-%d", time.Now().UnixNano()),
|
||||
LogoURI: test.logoURI,
|
||||
}
|
||||
|
||||
_, err := client.PostOAuth2ClientRegistration(ctx, req)
|
||||
|
||||
if test.expectError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GrantTypeValidation", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
grantTypes []string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "DefaultEmpty",
|
||||
grantTypes: []string{},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "ValidAuthorizationCode",
|
||||
grantTypes: []string{"authorization_code"},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "InvalidRefreshTokenAlone",
|
||||
grantTypes: []string{"refresh_token"},
|
||||
expectError: true, // refresh_token requires authorization_code to be present
|
||||
},
|
||||
{
|
||||
name: "ValidMultiple",
|
||||
grantTypes: []string{"authorization_code", "refresh_token"},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "InvalidUnsupported",
|
||||
grantTypes: []string{"client_credentials"},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "InvalidPassword",
|
||||
grantTypes: []string{"password"},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "InvalidImplicit",
|
||||
grantTypes: []string{"implicit"},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "MixedValidInvalid",
|
||||
grantTypes: []string{"authorization_code", "client_credentials"},
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
req := codersdk.OAuth2ClientRegistrationRequest{
|
||||
RedirectURIs: []string{"https://example.com/callback"},
|
||||
ClientName: fmt.Sprintf("test-client-%d", time.Now().UnixNano()),
|
||||
GrantTypes: test.grantTypes,
|
||||
}
|
||||
|
||||
_, err := client.PostOAuth2ClientRegistration(ctx, req)
|
||||
|
||||
if test.expectError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ResponseTypeValidation", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
responseTypes []string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "DefaultEmpty",
|
||||
responseTypes: []string{},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "ValidCode",
|
||||
responseTypes: []string{"code"},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "InvalidToken",
|
||||
responseTypes: []string{"token"},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "InvalidImplicit",
|
||||
responseTypes: []string{"id_token"},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "InvalidMultiple",
|
||||
responseTypes: []string{"code", "token"},
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
req := codersdk.OAuth2ClientRegistrationRequest{
|
||||
RedirectURIs: []string{"https://example.com/callback"},
|
||||
ClientName: fmt.Sprintf("test-client-%d", time.Now().UnixNano()),
|
||||
ResponseTypes: test.responseTypes,
|
||||
}
|
||||
|
||||
_, err := client.PostOAuth2ClientRegistration(ctx, req)
|
||||
|
||||
if test.expectError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("TokenEndpointAuthMethodValidation", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
authMethod string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "DefaultEmpty",
|
||||
authMethod: "",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "ValidClientSecretBasic",
|
||||
authMethod: "client_secret_basic",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "ValidClientSecretPost",
|
||||
authMethod: "client_secret_post",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "ValidNone",
|
||||
authMethod: "none",
|
||||
expectError: false, // "none" is valid for public clients per RFC 7591
|
||||
},
|
||||
{
|
||||
name: "InvalidPrivateKeyJWT",
|
||||
authMethod: "private_key_jwt",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "InvalidClientSecretJWT",
|
||||
authMethod: "client_secret_jwt",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "InvalidCustom",
|
||||
authMethod: "custom_method",
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
req := codersdk.OAuth2ClientRegistrationRequest{
|
||||
RedirectURIs: []string{"https://example.com/callback"},
|
||||
ClientName: fmt.Sprintf("test-client-%d", time.Now().UnixNano()),
|
||||
TokenEndpointAuthMethod: test.authMethod,
|
||||
}
|
||||
|
||||
_, err := client.PostOAuth2ClientRegistration(ctx, req)
|
||||
|
||||
if test.expectError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestOAuth2ClientNameValidation tests client name validation requirements
|
||||
func TestOAuth2ClientNameValidation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
clientName string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "ValidBasic",
|
||||
clientName: "My App",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "ValidWithNumbers",
|
||||
clientName: "My App 2.0",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "ValidWithSpecialChars",
|
||||
clientName: "My-App_v1.0",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "ValidUnicode",
|
||||
clientName: "My App 🚀",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "ValidLong",
|
||||
clientName: strings.Repeat("A", 100),
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "ValidEmpty",
|
||||
clientName: "",
|
||||
expectError: false, // Empty names are allowed, defaults are applied
|
||||
},
|
||||
{
|
||||
name: "ValidWhitespaceOnly",
|
||||
clientName: " ",
|
||||
expectError: false, // Whitespace-only names are allowed
|
||||
},
|
||||
{
|
||||
name: "ValidTooLong",
|
||||
clientName: strings.Repeat("A", 1000),
|
||||
expectError: false, // Very long names are allowed
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
req := codersdk.OAuth2ClientRegistrationRequest{
|
||||
RedirectURIs: []string{"https://example.com/callback"},
|
||||
ClientName: test.clientName,
|
||||
}
|
||||
|
||||
_, err := client.PostOAuth2ClientRegistration(ctx, req)
|
||||
|
||||
if test.expectError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestOAuth2ClientScopeValidation tests scope parameter validation
|
||||
func TestOAuth2ClientScopeValidation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
scope string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "DefaultEmpty",
|
||||
scope: "",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "ValidRead",
|
||||
scope: "read",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "ValidWrite",
|
||||
scope: "write",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "ValidMultiple",
|
||||
scope: "read write",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "ValidOpenID",
|
||||
scope: "openid",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "ValidProfile",
|
||||
scope: "profile",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "ValidEmail",
|
||||
scope: "email",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "ValidCombined",
|
||||
scope: "openid profile email read write",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "InvalidAdmin",
|
||||
scope: "admin",
|
||||
expectError: false, // Admin scope should be allowed but validated during authorization
|
||||
},
|
||||
{
|
||||
name: "ValidCustom",
|
||||
scope: "custom:scope",
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
req := codersdk.OAuth2ClientRegistrationRequest{
|
||||
RedirectURIs: []string{"https://example.com/callback"},
|
||||
ClientName: fmt.Sprintf("test-client-%d", time.Now().UnixNano()),
|
||||
Scope: test.scope,
|
||||
}
|
||||
|
||||
_, err := client.PostOAuth2ClientRegistration(ctx, req)
|
||||
|
||||
if test.expectError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestOAuth2ClientMetadataDefaults tests that default values are properly applied
|
||||
func TestOAuth2ClientMetadataDefaults(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
// Register a minimal client to test defaults
|
||||
req := codersdk.OAuth2ClientRegistrationRequest{
|
||||
RedirectURIs: []string{"https://example.com/callback"},
|
||||
ClientName: fmt.Sprintf("test-client-%d", time.Now().UnixNano()),
|
||||
}
|
||||
|
||||
resp, err := client.PostOAuth2ClientRegistration(ctx, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Get the configuration to check defaults
|
||||
config, err := client.GetOAuth2ClientConfiguration(ctx, resp.ClientID, resp.RegistrationAccessToken)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should default to authorization_code
|
||||
require.Contains(t, config.GrantTypes, "authorization_code")
|
||||
|
||||
// Should default to code
|
||||
require.Contains(t, config.ResponseTypes, "code")
|
||||
|
||||
// Should default to client_secret_basic or client_secret_post
|
||||
require.True(t, config.TokenEndpointAuthMethod == "client_secret_basic" ||
|
||||
config.TokenEndpointAuthMethod == "client_secret_post" ||
|
||||
config.TokenEndpointAuthMethod == "")
|
||||
|
||||
// Client secret should be generated
|
||||
require.NotEmpty(t, resp.ClientSecret)
|
||||
require.Greater(t, len(resp.ClientSecret), 20)
|
||||
|
||||
// Registration access token should be generated
|
||||
require.NotEmpty(t, resp.RegistrationAccessToken)
|
||||
require.Greater(t, len(resp.RegistrationAccessToken), 20)
|
||||
}
|
||||
|
||||
// TestOAuth2ClientMetadataEdgeCases tests edge cases and boundary conditions
|
||||
func TestOAuth2ClientMetadataEdgeCases(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("ExtremelyLongRedirectURI", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
// Create a very long but valid HTTPS URI
|
||||
longPath := strings.Repeat("a", 2000)
|
||||
longURI := "https://example.com/" + longPath
|
||||
|
||||
req := codersdk.OAuth2ClientRegistrationRequest{
|
||||
RedirectURIs: []string{longURI},
|
||||
ClientName: fmt.Sprintf("test-client-%d", time.Now().UnixNano()),
|
||||
}
|
||||
|
||||
_, err := client.PostOAuth2ClientRegistration(ctx, req)
|
||||
// This might be accepted or rejected depending on URI length limits
|
||||
// The test verifies the behavior is consistent
|
||||
if err != nil {
|
||||
require.Contains(t, strings.ToLower(err.Error()), "uri")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ManyRedirectURIs", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
// Test with many redirect URIs
|
||||
redirectURIs := make([]string, 20)
|
||||
for i := 0; i < 20; i++ {
|
||||
redirectURIs[i] = fmt.Sprintf("https://example%d.com/callback", i)
|
||||
}
|
||||
|
||||
req := codersdk.OAuth2ClientRegistrationRequest{
|
||||
RedirectURIs: redirectURIs,
|
||||
ClientName: fmt.Sprintf("test-client-%d", time.Now().UnixNano()),
|
||||
}
|
||||
|
||||
_, err := client.PostOAuth2ClientRegistration(ctx, req)
|
||||
// Should handle multiple redirect URIs gracefully
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("URIWithUnusualPort", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
req := codersdk.OAuth2ClientRegistrationRequest{
|
||||
RedirectURIs: []string{"https://example.com:8443/callback"},
|
||||
ClientName: fmt.Sprintf("test-client-%d", time.Now().UnixNano()),
|
||||
}
|
||||
|
||||
_, err := client.PostOAuth2ClientRegistration(ctx, req)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("URIWithComplexPath", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
req := codersdk.OAuth2ClientRegistrationRequest{
|
||||
RedirectURIs: []string{"https://example.com/path/to/callback?param=value&other=123"},
|
||||
ClientName: fmt.Sprintf("test-client-%d", time.Now().UnixNano()),
|
||||
}
|
||||
|
||||
_, err := client.PostOAuth2ClientRegistration(ctx, req)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("URIWithEncodedCharacters", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
// Test with URL-encoded characters
|
||||
encodedURI := "https://example.com/callback?param=" + url.QueryEscape("value with spaces")
|
||||
|
||||
req := codersdk.OAuth2ClientRegistrationRequest{
|
||||
RedirectURIs: []string{encodedURI},
|
||||
ClientName: fmt.Sprintf("test-client-%d", time.Now().UnixNano()),
|
||||
}
|
||||
|
||||
_, err := client.PostOAuth2ClientRegistration(ctx, req)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user