From 529d9eed1fb83efa0a5b10b9b53a4d403816e3fb Mon Sep 17 00:00:00 2001 From: Michel Osswald Date: Tue, 9 Jun 2026 11:30:29 +0200 Subject: [PATCH] fix(auth): deduplicate login scopes in one pass Summary This optimizes auth login scope resolution by deduplicating requested scopes in one pass. Before this, resolveLoginScopes in internal/auth/oidc.go rescanned the growing output slice for every requested scope, which made duplicate-heavy scope lists cost more work than needed. Now a seen set is the canonical path: for _, scope := range scopes { if _, ok := seen[scope]; ok { continue } seen[scope] = struct{}{} resolved = append(resolved, scope) } Why This gives kontext-cli a cheaper runtime path for browser login scope assembly: login request -> resolveLoginScopes -> deduplicated ordered scopes This change does not broaden behavior beyond the optimization scope. What changed Optimized login scope deduplication in internal/auth/oidc.go Removed repeated linear rescans while building the scope list Preserved base-scope ordering and first-seen custom scope ordering Updated tests for duplicate custom scopes Verification go test ./internal/auth go test ./internal/guard/judge -run 'TestStartLlamaServerHealthCheckAndStop|TestStartLlamaServerEarlyExitDoesNotWaitForStopTimeout' -count=1 go test ./... go vet ./... git diff --check --- internal/auth/oidc.go | 19 ++++++++----------- internal/auth/oidc_test.go | 17 +++++++++++++++++ 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/internal/auth/oidc.go b/internal/auth/oidc.go index 2c6124b..e835419 100644 --- a/internal/auth/oidc.go +++ b/internal/auth/oidc.go @@ -204,23 +204,20 @@ func resolveLoginScopes(scopes []string) []string { } resolved := append([]string(nil), baseScopes...) + seen := make(map[string]struct{}, len(resolved)+len(scopes)) + for _, scope := range resolved { + seen[scope] = struct{}{} + } for _, scope := range scopes { - if !hasScope(resolved, scope) { - resolved = append(resolved, scope) + if _, ok := seen[scope]; ok { + continue } + seen[scope] = struct{}{} + resolved = append(resolved, scope) } return resolved } -func hasScope(scopes []string, scope string) bool { - for _, existing := range scopes { - if existing == scope { - return true - } - } - return false -} - func applyTokenExtraEmailFallback(session *Session, token *oauth2.Token) { if session.User.Email != "" { return diff --git a/internal/auth/oidc_test.go b/internal/auth/oidc_test.go index 2e1eee7..83244fd 100644 --- a/internal/auth/oidc_test.go +++ b/internal/auth/oidc_test.go @@ -67,6 +67,23 @@ func TestResolveLoginScopesDeduplicatesDefaultScopes(t *testing.T) { } } +func TestResolveLoginScopesDeduplicatesCustomScopesInOrder(t *testing.T) { + t.Parallel() + + input := []string{"gateway:access", "gateway:access", "openid", "custom:read", "custom:read"} + got := resolveLoginScopes(input) + want := []string{ + "openid", + "email", + "profile", + "gateway:access", + "custom:read", + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("resolveLoginScopes(%#v) = %#v, want %#v", input, got, want) + } +} + func TestDecodeJWTClaims(t *testing.T) { t.Parallel()