Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions auth/auth_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package auth

import (
"crypto/sha256"
"encoding/json"
"fmt"
)

// AuthConfig combines auth scheme and credentials for a tool.
// This is passed to tools that require authentication.
type AuthConfig struct {
// AuthScheme defines how the API expects authentication.
AuthScheme AuthScheme `json:"authScheme"`
// RawAuthCredential is the initial credential (e.g., client_id/secret).
RawAuthCredential *AuthCredential `json:"rawAuthCredential,omitempty"`
// ExchangedAuthCredential is the processed credential (e.g., access_token).
ExchangedAuthCredential *AuthCredential `json:"exchangedAuthCredential,omitempty"`
// CredentialKey is a unique key for persisting this credential.
CredentialKey string `json:"credentialKey,omitempty"`
}

// NewAuthConfig creates a new AuthConfig with the given scheme and credential.
// If credentialKey is empty, it will be generated automatically.
func NewAuthConfig(scheme AuthScheme, credential *AuthCredential) (*AuthConfig, error) {
cfg := &AuthConfig{
AuthScheme: scheme,
RawAuthCredential: credential,
}
if cfg.CredentialKey == "" {
key, err := cfg.generateCredentialKey()
if err != nil {
return nil, fmt.Errorf("generate credential key: %w", err)
}
cfg.CredentialKey = key
}
return cfg, nil
}

// generateCredentialKey creates a unique key based on auth scheme and credential.
func (c *AuthConfig) generateCredentialKey() (string, error) {
var schemePart, credPart string
if c.AuthScheme != nil {
schemeJSON, err := json.Marshal(c.AuthScheme)
Comment thread
cpunion marked this conversation as resolved.
Outdated
if err != nil {
return "", fmt.Errorf("marshal auth scheme: %w", err)
}
schemeType := c.AuthScheme.GetType()
h := sha256.Sum256(schemeJSON)
schemePart = fmt.Sprintf("%s_%x", schemeType, h[:8])
}
if c.RawAuthCredential != nil {
credJSON, err := json.Marshal(c.RawAuthCredential)
if err != nil {
return "", fmt.Errorf("marshal auth credential: %w", err)
}
h := sha256.Sum256(credJSON)
credPart = fmt.Sprintf("%s_%x", c.RawAuthCredential.AuthType, h[:8])
}
return fmt.Sprintf("adk_%s_%s", schemePart, credPart), nil
}

// Copy creates a deep copy of the AuthConfig.
func (c *AuthConfig) Copy() *AuthConfig {
if c == nil {
return nil
}
return &AuthConfig{
AuthScheme: c.AuthScheme, // AuthScheme is typically immutable
RawAuthCredential: c.RawAuthCredential.Copy(),
ExchangedAuthCredential: c.ExchangedAuthCredential.Copy(),
CredentialKey: c.CredentialKey,
}
}
180 changes: 180 additions & 0 deletions auth/auth_config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package auth

import (
"errors"
"strings"
"testing"

"github.com/google/go-cmp/cmp"
)

func TestNewAuthConfig(t *testing.T) {
scheme := &OAuth2Scheme{
Flows: &OAuthFlows{
AuthorizationCode: &OAuthFlowAuthorizationCode{
AuthorizationURL: "https://example.com/auth",
TokenURL: "https://example.com/token",
},
},
}
cred := &AuthCredential{
AuthType: AuthCredentialTypeOAuth2,
OAuth2: &OAuth2Auth{
ClientID: "client-id",
ClientSecret: "client-secret",
},
}

cfg, err := NewAuthConfig(scheme, cred)
if err != nil {
t.Fatalf("NewAuthConfig() error = %v", err)
}

if cfg.AuthScheme != scheme {
t.Error("AuthScheme not set correctly")
}
if cfg.RawAuthCredential != cred {
t.Error("RawAuthCredential not set correctly")
}
if cfg.CredentialKey == "" {
t.Error("CredentialKey should be auto-generated")
}
if !strings.HasPrefix(cfg.CredentialKey, "adk_") {
t.Errorf("CredentialKey = %q, want prefix 'adk_'", cfg.CredentialKey)
}
}

func TestAuthConfig_generateCredentialKey_Deterministic(t *testing.T) {
scheme := &APIKeyScheme{
In: APIKeyInHeader,
Name: "X-API-Key",
}
cred := &AuthCredential{
AuthType: AuthCredentialTypeAPIKey,
APIKey: "test-key",
}

cfg1, err := NewAuthConfig(scheme, cred)
if err != nil {
t.Fatalf("NewAuthConfig() error = %v", err)
}
cfg2, err := NewAuthConfig(scheme, cred)
if err != nil {
t.Fatalf("NewAuthConfig() error = %v", err)
}

if cfg1.CredentialKey != cfg2.CredentialKey {
t.Errorf("generateCredentialKey not deterministic: %q != %q", cfg1.CredentialKey, cfg2.CredentialKey)
}
}

func TestAuthConfig_generateCredentialKey_Different(t *testing.T) {
scheme := &APIKeyScheme{
In: APIKeyInHeader,
Name: "X-API-Key",
}
cred1 := &AuthCredential{
AuthType: AuthCredentialTypeAPIKey,
APIKey: "key-1",
}
cred2 := &AuthCredential{
AuthType: AuthCredentialTypeAPIKey,
APIKey: "key-2",
}

cfg1, err := NewAuthConfig(scheme, cred1)
if err != nil {
t.Fatalf("NewAuthConfig() error = %v", err)
}
cfg2, err := NewAuthConfig(scheme, cred2)
if err != nil {
t.Fatalf("NewAuthConfig() error = %v", err)
}

if cfg1.CredentialKey == cfg2.CredentialKey {
t.Error("Different credentials should produce different keys")
}
}

func TestAuthConfig_Copy_Nil(t *testing.T) {
var cfg *AuthConfig
got := cfg.Copy()
if got != nil {
t.Errorf("Copy() of nil = %v, want nil", got)
}
}

func TestAuthConfig_Copy(t *testing.T) {
scheme := &HTTPScheme{
Scheme: "bearer",
BearerFormat: "JWT",
}
cfg := &AuthConfig{
AuthScheme: scheme,
RawAuthCredential: &AuthCredential{
AuthType: AuthCredentialTypeHTTP,
HTTP: &HTTPAuth{
Scheme: "bearer",
Credentials: &HTTPCredentials{
Token: "raw-token",
},
},
},
ExchangedAuthCredential: &AuthCredential{
AuthType: AuthCredentialTypeHTTP,
HTTP: &HTTPAuth{
Scheme: "bearer",
Credentials: &HTTPCredentials{
Token: "exchanged-token",
},
},
},
CredentialKey: "adk_test_key",
}

got := cfg.Copy()

if got == cfg {
t.Error("Copy() returned same pointer")
}
if got.RawAuthCredential == cfg.RawAuthCredential {
t.Error("Copy() returned same RawAuthCredential pointer")
}
if got.ExchangedAuthCredential == cfg.ExchangedAuthCredential {
t.Error("Copy() returned same ExchangedAuthCredential pointer")
}
if diff := cmp.Diff(cfg, got); diff != "" {
t.Errorf("Copy() mismatch (-want +got):\n%s", diff)
}
}

func TestNewAuthConfig_MarshalError(t *testing.T) {
scheme := &badScheme{}
if _, err := NewAuthConfig(scheme, nil); err == nil {
t.Fatal("NewAuthConfig() did not return error for unmarshalable scheme")
}
}

type badScheme struct{}

func (b *badScheme) GetType() SecuritySchemeType {
return SecuritySchemeType("bad")
}

func (b *badScheme) MarshalJSON() ([]byte, error) {
return nil, errors.New("cannot marshal")
}
Loading