Skip to content

Commit b118f1f

Browse files
authored
feat: add OAuth client type (#2152)
## Summary Add OAuth 2.1 client type support (public vs confidential) to enable proper client authentication for MCP integrations and lay the foundation for the upcoming token endpoint implementation. ## Why This Matters - MCP Integration: Some MCP clients don't provide client secrets in /token requests. We need to know which clients require secrets vs PKCE-only authentication. - OAuth 2.1 Compliance: Proper distinction between public clients (SPAs, mobile apps) and confidential clients (server apps) as required by the spec. - Token Endpoint Foundation: This client authentication logic will be essential for the upcoming /token endpoint implementation to handle different client types correctly. ## Key Changes ### Database - Added client_type enum ('public', 'confidential') to oauth_clients table - Made client_secret_hash nullable for public clients - Default: 'confidential' for security ### OAuth Client Registration - Support token_endpoint_auth_method parameter in registration - Auto-infer client type: `none` → public, `client_secret_*` → confidential - Priority: explicit client_type > inferred from auth method > default confidential ### Authentication Logic - Public clients: No client secret required, use PKCE - Confidential clients: Client secret required - Updated middleware to enforce type-specific authentication rules - Foundation for /token endpoint: Centralized client auth functions ready for token exchange implementation
1 parent 5318552 commit b118f1f

File tree

9 files changed

+697
-38
lines changed

9 files changed

+697
-38
lines changed

internal/api/middleware.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,9 @@ func (a *API) oauthClientAuth(w http.ResponseWriter, r *http.Request) (context.C
108108
return nil, apierrors.NewInternalServerError("Error validating client credentials").WithInternalError(err)
109109
}
110110

111-
// Validate client secret
112-
if !oauthserver.ValidateClientSecret(clientSecret, client.ClientSecretHash) {
113-
return nil, apierrors.NewBadRequestError(apierrors.ErrorCodeInvalidCredentials, "Invalid client credentials")
111+
// Validate authentication using centralized logic
112+
if err := oauthserver.ValidateClientAuthentication(client, clientSecret); err != nil {
113+
return nil, apierrors.NewBadRequestError(apierrors.ErrorCodeInvalidCredentials, err.Error())
114114
}
115115

116116
// Add authenticated client to context

internal/api/oauthserver/auth.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,12 @@ func ExtractClientCredentials(r *http.Request) (clientID, clientSecret string, e
4141
return "", "", nil
4242
}
4343

44-
// If only one is provided, it's an error
45-
if clientID == "" || clientSecret == "" {
46-
return "", "", errors.New("both client_id and client_secret must be provided")
44+
// For public clients, only client_id is required (client_secret should be empty)
45+
// For confidential clients, both client_id and client_secret are required
46+
// We'll validate this based on the client type in the calling handler
47+
// TODO(cemal) :: this will be validated in detail during the `/token` endpoint implementation
48+
if clientID == "" {
49+
return "", "", errors.New("client_id is required")
4750
}
4851

4952
return clientID, clientSecret, nil
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package oauthserver
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/supabase/auth/internal/models"
7+
)
8+
9+
// InferClientTypeFromAuthMethod infers client type from token_endpoint_auth_method
10+
func InferClientTypeFromAuthMethod(authMethod string) string {
11+
switch authMethod {
12+
case models.TokenEndpointAuthMethodNone:
13+
return models.OAuthServerClientTypePublic
14+
case models.TokenEndpointAuthMethodClientSecretBasic, models.TokenEndpointAuthMethodClientSecretPost:
15+
return models.OAuthServerClientTypeConfidential
16+
default:
17+
return models.OAuthServerClientTypeConfidential // Default to confidential
18+
}
19+
}
20+
21+
// GetValidAuthMethodsForClientType returns the valid authentication methods for a client type
22+
func GetValidAuthMethodsForClientType(clientType string) []string {
23+
switch clientType {
24+
case models.OAuthServerClientTypePublic:
25+
return []string{models.TokenEndpointAuthMethodNone}
26+
case models.OAuthServerClientTypeConfidential:
27+
return []string{
28+
models.TokenEndpointAuthMethodClientSecretBasic,
29+
models.TokenEndpointAuthMethodClientSecretPost,
30+
}
31+
default:
32+
return []string{} // Unknown client type
33+
}
34+
}
35+
36+
// ValidateClientTypeConsistency validates consistency between client_type and token_endpoint_auth_method
37+
func ValidateClientTypeConsistency(clientType, authMethod string) error {
38+
if clientType == "" || authMethod == "" {
39+
return nil // Skip validation if either is not provided
40+
}
41+
42+
expectedClientType := InferClientTypeFromAuthMethod(authMethod)
43+
if clientType != expectedClientType {
44+
return fmt.Errorf("client_type '%s' is inconsistent with token_endpoint_auth_method '%s' (expected client_type '%s')",
45+
clientType, authMethod, expectedClientType)
46+
}
47+
48+
return nil
49+
}
50+
51+
// IsValidAuthMethodForClientType checks if the auth method is valid for the given client type
52+
func IsValidAuthMethodForClientType(clientType, authMethod string) bool {
53+
validMethods := GetValidAuthMethodsForClientType(clientType)
54+
for _, method := range validMethods {
55+
if method == authMethod {
56+
return true
57+
}
58+
}
59+
return false
60+
}
61+
62+
// DetermineClientType determines the final client type using the priority:
63+
// 1. Explicit client_type
64+
// 2. Inferred from token_endpoint_auth_method
65+
// 3. Default to confidential
66+
func DetermineClientType(explicitClientType, authMethod string) string {
67+
// Priority 1: Explicit client_type
68+
if explicitClientType != "" {
69+
return explicitClientType
70+
}
71+
72+
// Priority 2: Infer from token_endpoint_auth_method
73+
if authMethod != "" {
74+
return InferClientTypeFromAuthMethod(authMethod)
75+
}
76+
77+
// Priority 3: Default to confidential
78+
return models.OAuthServerClientTypeConfidential
79+
}
80+
81+
// ValidateClientAuthentication validates client authentication based on client type
82+
func ValidateClientAuthentication(client *models.OAuthServerClient, providedSecret string) error {
83+
if client.IsPublic() {
84+
// Public clients should not provide client secrets
85+
if providedSecret != "" {
86+
return fmt.Errorf("public clients must not provide client_secret")
87+
}
88+
return nil
89+
}
90+
91+
// Confidential clients must provide a valid client secret
92+
if providedSecret == "" {
93+
return fmt.Errorf("confidential clients must provide client_secret")
94+
}
95+
96+
if !ValidateClientSecret(providedSecret, client.ClientSecretHash) {
97+
return fmt.Errorf("invalid client credentials")
98+
}
99+
100+
return nil
101+
}
102+
103+
// GetAllValidAuthMethods returns all supported authentication methods
104+
func GetAllValidAuthMethods() []string {
105+
return []string{
106+
models.TokenEndpointAuthMethodNone,
107+
models.TokenEndpointAuthMethodClientSecretBasic,
108+
models.TokenEndpointAuthMethodClientSecretPost,
109+
}
110+
}

0 commit comments

Comments
 (0)