diff --git a/auth/clientcredentials/connectrpc.go b/auth/clientcredentials/connectrpc.go index 7d559a8..e2b63f4 100644 --- a/auth/clientcredentials/connectrpc.go +++ b/auth/clientcredentials/connectrpc.go @@ -129,6 +129,10 @@ func (i *Interceptor) requireScope(ctx context.Context, headers http.Header, req if err != nil { return nil, connectInternalError(ctx, i.logger, err, "unable to validate token") } + if result.UserID != "" { + return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("a user-scoped token is not allowed")) + } + span.SetAttributes( attribute.String("client_id", result.ClientID), attribute.String("token_expires_at", result.ExpiresAt.String()), diff --git a/auth/clientcredentials/connectrpc_test.go b/auth/clientcredentials/connectrpc_test.go index 5348963..612be2f 100644 --- a/auth/clientcredentials/connectrpc_test.go +++ b/auth/clientcredentials/connectrpc_test.go @@ -88,6 +88,16 @@ func TestInterceptor(t *testing.T) { // client credentials token wantError: autogold.Expect("permission_denied: permission denied"), wantLogs: autogold.Expect([]string{}), + }, { + name: "SAT token with userID rejected", + token: &sams.IntrospectTokenResponse{ + Active: true, + ClientID: "test-client", + UserID: "test-user", + Scopes: scopes.Scopes{"profile"}, + }, + wantError: autogold.Expect("unauthenticated: a user-scoped token is not allowed"), + wantLogs: autogold.Expect([]string{}), }} { t.Run(tc.name, func(t *testing.T) { logger, exportLogs := logtest.Captured(t) diff --git a/auth/clientcredentials/http.go b/auth/clientcredentials/http.go index d88ebf9..0c60672 100644 --- a/auth/clientcredentials/http.go +++ b/auth/clientcredentials/http.go @@ -68,6 +68,14 @@ func (a *HTTPAuthenticator) RequireScopes(requiredScopes scopes.Scopes, next htt return } + if result.UserID != "" { + logger.Warn("attempt to authenticate using SAMS token with user ID", + log.String("client", result.ClientID), + log.String("userID", result.UserID)) + http.Error(w, "Forbidden: User tokens not allowed", http.StatusForbidden) + return + } + // Check for our required scope. for _, required := range requiredScopes { if !result.Scopes.Match(required) { diff --git a/auth/clientcredentials/http_test.go b/auth/clientcredentials/http_test.go index b0f0e87..553d21b 100644 --- a/auth/clientcredentials/http_test.go +++ b/auth/clientcredentials/http_test.go @@ -52,6 +52,16 @@ func TestHTTPAuthenticator(t *testing.T) { }, wantError: autogold.Expect("Forbidden: Missing required scope\n"), wantLogs: autogold.Expect([]string{"attempt to authenticate using SAMS token without required scope"}), + }, { + name: "SAT token with userID rejected", + token: &sams.IntrospectTokenResponse{ + Active: true, + ClientID: "test-client", + UserID: "test-user", + Scopes: scopes.Scopes{"profile"}, + }, + wantError: autogold.Expect("Forbidden: User tokens not allowed\n"), + wantLogs: autogold.Expect([]string{"attempt to authenticate using SAMS token with user ID"}), }} { t.Run(tc.name, func(t *testing.T) { logger, exportLogs := logtest.Captured(t)