Ctrl K
ComunidadGuías…

coderd/oauth2_error_compliance_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package coderd_test

import (
	"encoding/json"
	"fmt"
	"net/http"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/coder/coder/v2/coderd/coderdtest"
	"github.com/coder/coder/v2/coderd/oauth2provider/oauth2providertest"
	"github.com/coder/coder/v2/codersdk"
	"github.com/coder/coder/v2/testutil"
)

// OAuth2ErrorResponse represents RFC-compliant OAuth2 error responses
type OAuth2ErrorResponse struct {
	Error            string `json:"error"`
	ErrorDescription string `json:"error_description,omitempty"`
	ErrorURI         string `json:"error_uri,omitempty"`
}

// TestOAuth2ErrorResponseFormat tests that OAuth2 error responses follow proper RFC format
func TestOAuth2ErrorResponseFormat(t *testing.T) {
	t.Parallel()

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

		client := coderdtest.New(t, nil)
		_ = coderdtest.CreateFirstUser(t, client)
		oauth2providertest.EnableDCR(t, client)
		ctx := testutil.Context(t, testutil.WaitLong)

		// Make a request that will definitely fail
		req := codersdk.OAuth2ClientRegistrationRequest{
			// Missing required redirect_uris
		}

		_, err := client.PostOAuth2ClientRegistration(ctx, req)
		require.Error(t, err)

		// Check that it's an HTTP error with JSON content type
		var httpErr *codersdk.Error
		require.ErrorAs(t, err, &httpErr)

		// The error should be a 400 status for invalid client metadata
		require.Equal(t, http.StatusBadRequest, httpErr.StatusCode())
	})
}

// TestOAuth2RegistrationErrorCodes tests all RFC 7591 error codes
func TestOAuth2RegistrationErrorCodes(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name          string
		req           codersdk.OAuth2ClientRegistrationRequest
		expectedError string
		expectedCode  int
	}{
		{
			name: "InvalidClientMetadata_NoRedirectURIs",
			req: codersdk.OAuth2ClientRegistrationRequest{
				ClientName: fmt.Sprintf("test-client-%d", time.Now().UnixNano()),
				// Missing required redirect_uris
			},
			expectedError: "invalid_client_metadata",
			expectedCode:  http.StatusBadRequest,
		},
		{
			name: "InvalidClientMetadata_InvalidRedirectURI",
			req: codersdk.OAuth2ClientRegistrationRequest{
				RedirectURIs: []string{"not-a-valid-uri"},
				ClientName:   fmt.Sprintf("test-client-%d", time.Now().UnixNano()),
			},
			expectedError: "invalid_client_metadata",
			expectedCode:  http.StatusBadRequest,
		},
		{
			name: "InvalidClientMetadata_RedirectURIWithFragment",
			req: codersdk.OAuth2ClientRegistrationRequest{
				RedirectURIs: []string{"https://example.com/callback#fragment"},
				ClientName:   fmt.Sprintf("test-client-%d", time.Now().UnixNano()),
			},
			expectedError: "invalid_client_metadata",
			expectedCode:  http.StatusBadRequest,
		},
		{
			name: "InvalidClientMetadata_HTTPRedirectForNonLocalhost",
			req: codersdk.OAuth2ClientRegistrationRequest{
				RedirectURIs: []string{"http://example.com/callback"}, // HTTP for non-localhost
				ClientName:   fmt.Sprintf("test-client-%d", time.Now().UnixNano()),
			},
			expectedError: "invalid_client_metadata",
			expectedCode:  http.StatusBadRequest,
		},
		{
			name: "InvalidClientMetadata_UnsupportedGrantType",
			req: codersdk.OAuth2ClientRegistrationRequest{
				RedirectURIs: []string{"https://example.com/callback"},
				ClientName:   fmt.Sprintf("test-client-%d", time.Now().UnixNano()),
				GrantTypes:   []codersdk.OAuth2ProviderGrantType{"unsupported_grant_type"},
			},
			expectedError: "invalid_client_metadata",
			expectedCode:  http.StatusBadRequest,
		},
		{
			name: "InvalidClientMetadata_UnsupportedResponseType",
			req: codersdk.OAuth2ClientRegistrationRequest{
				RedirectURIs:  []string{"https://example.com/callback"},
				ClientName:    fmt.Sprintf("test-client-%d", time.Now().UnixNano()),
				ResponseTypes: []codersdk.OAuth2ProviderResponseType{"unsupported_response_type"},
			},
			expectedError: "invalid_client_metadata",
			expectedCode:  http.StatusBadRequest,
		},
		{
			name: "InvalidClientMetadata_UnsupportedAuthMethod",
			req: codersdk.OAuth2ClientRegistrationRequest{
				RedirectURIs:            []string{"https://example.com/callback"},
				ClientName:              fmt.Sprintf("test-client-%d", time.Now().UnixNano()),
				TokenEndpointAuthMethod: "unsupported_auth_method",
			},
			expectedError: "invalid_client_metadata",
			expectedCode:  http.StatusBadRequest,
		},
		{
			name: "InvalidClientMetadata_InvalidClientURI",
			req: codersdk.OAuth2ClientRegistrationRequest{
				RedirectURIs: []string{"https://example.com/callback"},
				ClientName:   fmt.Sprintf("test-client-%d", time.Now().UnixNano()),
				ClientURI:    "not-a-valid-uri",
			},
			expectedError: "invalid_client_metadata",
			expectedCode:  http.StatusBadRequest,
		},
		{
			name: "InvalidClientMetadata_InvalidLogoURI",
			req: codersdk.OAuth2ClientRegistrationRequest{
				RedirectURIs: []string{"https://example.com/callback"},
				ClientName:   fmt.Sprintf("test-client-%d", time.Now().UnixNano()),
				LogoURI:      "not-a-valid-uri",
			},
			expectedError: "invalid_client_metadata",
			expectedCode:  http.StatusBadRequest,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()

			client := coderdtest.New(t, nil)
			_ = coderdtest.CreateFirstUser(t, client)
			oauth2providertest.EnableDCR(t, client)
			ctx := testutil.Context(t, testutil.WaitLong)

			// Create a copy of the request with a unique client name
			req := test.req
			if req.ClientName != "" {
				req.ClientName = fmt.Sprintf("%s-%d", req.ClientName, time.Now().UnixNano())
			}

			_, err := client.PostOAuth2ClientRegistration(ctx, req)
			require.Error(t, err)

			// Validate error format and status code
			var httpErr *codersdk.Error
			require.ErrorAs(t, err, &httpErr)
			require.Equal(t, test.expectedCode, httpErr.StatusCode())

			// For now, just verify we get an error with the expected status code
			// The specific error message format can be verified in other ways
			require.True(t, httpErr.StatusCode() >= 400)
		})
	}
}

// TestOAuth2ManagementErrorCodes tests all RFC 7592 error codes
func TestOAuth2ManagementErrorCodes(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name             string
		useWrongClientID bool
		useWrongToken    bool
		useEmptyToken    bool
		expectedError    string
		expectedCode     int
	}{
		{
			name:          "InvalidToken_WrongToken",
			useWrongToken: true,
			expectedError: "invalid_token",
			expectedCode:  http.StatusUnauthorized,
		},
		{
			name:          "InvalidToken_EmptyToken",
			useEmptyToken: true,
			expectedError: "invalid_token",
			expectedCode:  http.StatusUnauthorized,
		},
		{
			name:             "InvalidClient_WrongClientID",
			useWrongClientID: true,
			expectedError:    "invalid_token",
			expectedCode:     http.StatusUnauthorized,
		},
		// Skip empty client ID test as it causes routing issues
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()

			client := coderdtest.New(t, nil)
			_ = coderdtest.CreateFirstUser(t, client)
			oauth2providertest.EnableDCR(t, client)
			ctx := testutil.Context(t, testutil.WaitLong)

			// First register a valid client to use for management tests
			clientName := fmt.Sprintf("test-client-%d", time.Now().UnixNano())
			regReq := codersdk.OAuth2ClientRegistrationRequest{
				RedirectURIs: []string{"https://example.com/callback"},
				ClientName:   clientName,
			}
			regResp, err := client.PostOAuth2ClientRegistration(ctx, regReq)
			require.NoError(t, err)

			// Determine clientID and token based on test configuration
			var clientID, token string
			switch {
			case test.useWrongClientID:
				clientID = "550e8400-e29b-41d4-a716-446655440000" // Valid UUID format but non-existent
				token = regResp.RegistrationAccessToken
			case test.useWrongToken:
				clientID = regResp.ClientID
				token = "invalid-token"
			case test.useEmptyToken:
				clientID = regResp.ClientID
				token = ""
			default:
				clientID = regResp.ClientID
				token = regResp.RegistrationAccessToken
			}

			// Test GET client configuration
			_, err = client.GetOAuth2ClientConfiguration(ctx, clientID, token)
			require.Error(t, err)

			var httpErr *codersdk.Error
			require.ErrorAs(t, err, &httpErr)
			require.Equal(t, test.expectedCode, httpErr.StatusCode())
			// Verify we get an appropriate error status code
			require.True(t, httpErr.StatusCode() >= 400)

			// Test PUT client configuration (except for empty client ID which causes routing issues)
			if clientID != "" {
				updateReq := codersdk.OAuth2ClientRegistrationRequest{
					RedirectURIs: []string{"https://updated.example.com/callback"},
					ClientName:   clientName + "-updated",
				}
				_, err = client.PutOAuth2ClientConfiguration(ctx, clientID, token, updateReq)
				require.Error(t, err)

				require.ErrorAs(t, err, &httpErr)
				require.Equal(t, test.expectedCode, httpErr.StatusCode())
				require.True(t, httpErr.StatusCode() >= 400)

				// Test DELETE client configuration
				err = client.DeleteOAuth2ClientConfiguration(ctx, clientID, token)
				require.Error(t, err)

				require.ErrorAs(t, err, &httpErr)
				require.Equal(t, test.expectedCode, httpErr.StatusCode())
				require.True(t, httpErr.StatusCode() >= 400)
			}
		})
	}
}

// TestOAuth2ErrorResponseStructure tests the JSON structure of error responses
func TestOAuth2ErrorResponseStructure(t *testing.T) {
	t.Parallel()

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

		client := coderdtest.New(t, nil)
		_ = coderdtest.CreateFirstUser(t, client)
		oauth2providertest.EnableDCR(t, client)
		ctx := testutil.Context(t, testutil.WaitLong)

		// Make a request that will generate an error
		req := codersdk.OAuth2ClientRegistrationRequest{
			RedirectURIs: []string{"invalid-uri"},
			ClientName:   fmt.Sprintf("test-client-%d", time.Now().UnixNano()),
		}

		_, err := client.PostOAuth2ClientRegistration(ctx, req)
		require.Error(t, err)

		// Validate that the error contains the expected OAuth2 error structure
		var httpErr *codersdk.Error
		require.ErrorAs(t, err, &httpErr)

		// The error should be a 400 status for invalid client metadata
		require.Equal(t, http.StatusBadRequest, httpErr.StatusCode())

		// Should have error details
		require.NotEmpty(t, httpErr.Message)
	})

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

		client := coderdtest.New(t, nil)
		_ = coderdtest.CreateFirstUser(t, client)
		ctx := testutil.Context(t, testutil.WaitLong)

		// Try to access a client configuration with invalid token - use a valid UUID format
		validUUID := "550e8400-e29b-41d4-a716-446655440000"
		_, err := client.GetOAuth2ClientConfiguration(ctx, validUUID, "invalid-token")
		require.Error(t, err)

		var httpErr *codersdk.Error
		require.ErrorAs(t, err, &httpErr)
		require.Equal(t, http.StatusUnauthorized, httpErr.StatusCode())
	})
}

// TestOAuth2ErrorHTTPHeaders tests that error responses have correct HTTP headers
func TestOAuth2ErrorHTTPHeaders(t *testing.T) {
	t.Parallel()

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

		client := coderdtest.New(t, nil)
		_ = coderdtest.CreateFirstUser(t, client)
		oauth2providertest.EnableDCR(t, client)
		ctx := testutil.Context(t, testutil.WaitLong)

		// Make a request that will fail
		req := codersdk.OAuth2ClientRegistrationRequest{
			// Missing required fields
		}

		_, err := client.PostOAuth2ClientRegistration(ctx, req)
		require.Error(t, err)

		// The error should indicate proper JSON response format
		var httpErr *codersdk.Error
		require.ErrorAs(t, err, &httpErr)
		require.NotEmpty(t, httpErr.Message)
	})
}

// TestOAuth2SpecificErrorScenarios tests specific error scenarios from RFC specifications
func TestOAuth2SpecificErrorScenarios(t *testing.T) {
	t.Parallel()

	// Single instance shared across all sub-tests that need a
	// coderd server. Sub-tests that don't need one just ignore it.
	client := coderdtest.New(t, nil)
	_ = coderdtest.CreateFirstUser(t, client)
	oauth2providertest.EnableDCR(t, client)

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

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

		// Test completely empty request
		req := codersdk.OAuth2ClientRegistrationRequest{}
		_, err := client.PostOAuth2ClientRegistration(ctx, req)
		require.Error(t, err)

		var httpErr *codersdk.Error
		require.ErrorAs(t, err, &httpErr)
		require.Equal(t, http.StatusBadRequest, httpErr.StatusCode())
		// Error properly returned with bad request status
	})

	// A malformed body is rejected before validation, so it is the one path on
	// this handler that does not go through req.Validate. It still owes the
	// caller an RFC 7591 error rather than a codersdk.Response, which is the
	// reason the decode is local to the handler rather than httpapi.Read. The
	// body is sent raw because a typed request cannot express invalid JSON.
	t.Run("InvalidJSONStructure", func(t *testing.T) {
		t.Parallel()

		for _, tc := range []struct {
			name string
			body string
			// The decoder's own text, which the response must carry so a client
			// integrator can tell an unterminated body from a mistyped field.
			detail string
		}{
			{
				name:   "UnterminatedObject",
				body:   `{"client_name": "test"`,
				detail: "unexpected EOF",
			},
			{
				name:   "WrongFieldType",
				body:   `{"redirect_uris": "https://example.com/callback"}`,
				detail: "redirect_uris",
			},
		} {
			t.Run(tc.name, func(t *testing.T) {
				t.Parallel()
				ctx := testutil.Context(t, testutil.WaitLong)

				res, err := client.Request(ctx, http.MethodPost, "/oauth2/register",
					strings.NewReader(tc.body))
				require.NoError(t, err)
				defer res.Body.Close()

				require.Equal(t, http.StatusBadRequest, res.StatusCode)

				var errResp OAuth2ErrorResponse
				require.NoError(t, json.NewDecoder(res.Body).Decode(&errResp))
				require.Equal(t, "invalid_request", errResp.Error)
				require.Contains(t, errResp.ErrorDescription, tc.detail)
			})
		}
	})

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

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

		// Test with fields that might not be supported yet
		req := codersdk.OAuth2ClientRegistrationRequest{
			RedirectURIs:            []string{"https://example.com/callback"},
			ClientName:              fmt.Sprintf("test-client-%d", time.Now().UnixNano()),
			TokenEndpointAuthMethod: "private_key_jwt", // Not supported yet
		}

		_, err := client.PostOAuth2ClientRegistration(ctx, req)
		require.Error(t, err)

		var httpErr *codersdk.Error
		require.ErrorAs(t, err, &httpErr)
		require.Equal(t, http.StatusBadRequest, httpErr.StatusCode())
		// Error properly returned with bad request status
	})

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

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

		// Register a client first
		clientName := fmt.Sprintf("test-client-%d", time.Now().UnixNano())
		regReq := codersdk.OAuth2ClientRegistrationRequest{
			RedirectURIs: []string{"https://example.com/callback"},
			ClientName:   clientName,
		}
		regResp, err := client.PostOAuth2ClientRegistration(ctx, regReq)
		require.NoError(t, err)

		// Try to access with completely wrong token format
		_, err = client.GetOAuth2ClientConfiguration(ctx, regResp.ClientID, "malformed-token-format")
		require.Error(t, err)

		var httpErr *codersdk.Error
		require.ErrorAs(t, err, &httpErr)
		require.Equal(t, http.StatusUnauthorized, httpErr.StatusCode())
	})
}