Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OIDC to use GCPLogging #12216

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all 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
39 changes: 23 additions & 16 deletions cmd/oidc-token-verifier/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,22 @@ package main
import (
"fmt"
"os"
"strings"

"github.com/kyma-project/test-infra/pkg/logging"
gcpLogging "github.com/kyma-project/test-infra/pkg/gcp/logging"
tioidc "github.com/kyma-project/test-infra/pkg/oidc"
"github.com/spf13/cobra"
"go.uber.org/zap"
"golang.org/x/net/context"
)

// Cobra root command for the OIDC claim extractor
// Path: cmd/oidc-token-verifier/main.go

type Logger interface {
logging.StructuredLoggerInterface
logging.WithLoggerInterface
type LoggerInterface interface {
Debugw(msg string, keysAndValues ...string)
Infow(msg string, keysAndValues ...string)
Errorw(msg string, keysAndValues ...string)
Errorf(msg string, args ...interface{})
}

type options struct {
Expand Down Expand Up @@ -80,7 +82,7 @@ func init() {
// isTokenProvided checks if the token flag is set.
// If not, check if AUTHORIZATION environment variable is set.
// If neither is set, return an error.
func isTokenProvided(logger Logger, opts *options) error {
func isTokenProvided(logger tioidc.LoggerInterface, opts *options) error {
if opts.token == "" {
logger.Infow("Token flag not provided, checking for AUTHORIZATION environment variable")
opts.token = os.Getenv("AUTHORIZATION")
Expand All @@ -103,26 +105,32 @@ func isTokenProvided(logger Logger, opts *options) error {
// It uses OIDC discovery to get the identity provider public keys.
func (opts *options) extractClaims() error {
var (
zapLogger *zap.Logger
err error
logger tioidc.LoggerInterface
err error
)
if opts.debug {
zapLogger, err = zap.NewDevelopment()
} else {
zapLogger, err = zap.NewProduction()

ctx := context.Background()

loggingClient, err := gcpLogging.NewClient(ctx)
if err != nil {
return err
}

logger, err = loggingClient.NewLogger(
gcpLogging.WithTrace("OIDC Token Verifier"),
)
if err != nil {
return err
}
logger := zapLogger.Sugar()

// Now you can use logger with your functions
err = isTokenProvided(logger, opts)
if err != nil {
return err
}

// Print used options values.
logger.Infow("Using the following trusted workflows", "trusted-workflows", opts.trustedWorkflows)
logger.Infow("Using the following trusted workflows", "trusted-workflows", strings.Join(opts.trustedWorkflows, ","))
logger.Infow("Using the following client ID", "client-id", opts.clientID)
logger.Infow("Using the following public key path", "public-key-path", opts.publicKeyPath)
logger.Infow("Using the following new public keys environment variable", "new-keys-var", opts.newPublicKeysVarName)
Expand All @@ -134,7 +142,7 @@ func (opts *options) extractClaims() error {
if err != nil {
return err
}
logger.Infow("Verifier config created", "config", verifyConfig)
logger.Infow("Verifier config created", "config", fmt.Sprintf("%+v", verifyConfig))

// Create a new token processor
// It reads issuer from the token and verifies if the issuer is trusted.
Expand All @@ -147,7 +155,6 @@ func (opts *options) extractClaims() error {
logger.Infow("Token processor created for trusted issuer", "issuer", tokenProcessor.Issuer())
fmt.Printf("GITHUB_URL=%s\n", tokenProcessor.GetIssuer().GetGithubURL())

ctx := context.Background()
// Create a new provider using OIDC discovery to get the public keys.
// It uses the issuer from the token to get the OIDC discovery endpoint.
provider, err := tioidc.NewProviderFromDiscovery(ctx, logger, tokenProcessor.Issuer())
Expand Down
97 changes: 71 additions & 26 deletions pkg/oidc/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,24 @@
package oidc

import (
"context"
"errors"
"fmt"
"strconv"
"time"

"github.com/coreos/go-oidc/v3/oidc"
"github.com/go-jose/go-jose/v4"
"github.com/go-jose/go-jose/v4/jwt"
"github.com/kyma-project/test-infra/pkg/logging"
"golang.org/x/net/context"
)

type LoggerInterface interface {
Debugw(msg string, keysAndValues ...string)
Infow(msg string, keysAndValues ...string)
Errorw(msg string, keysAndValues ...string)
Errorf(msg string, args ...interface{})
}

var (
// SupportedSigningAlgorithms is a list of supported oidc token signing algorithms.
SupportedSigningAlgorithms = []string{"RS256"}
Expand Down Expand Up @@ -67,11 +75,6 @@ type ClaimsInterface interface {
ValidateExpectations(Issuer) error
}

type LoggerInterface interface {
logging.StructuredLoggerInterface
logging.WithLoggerInterface
}

// Issuer is the OIDC issuer.
// Name is the human-readable name of the issuer.
// IssuerURL is the OIDC discovery issuer endpoint.
Expand Down Expand Up @@ -171,11 +174,11 @@ func NewVerifierConfig(logger LoggerInterface, clientID string, options ...Verif

logger.Debugw("Created Verifier config with default values",
"clientID", clientID,
"SkipClientIDCheck", verifierConfig.SkipClientIDCheck,
"SkipExpiryCheck", verifierConfig.SkipExpiryCheck,
"SkipIssuerCheck", verifierConfig.SkipIssuerCheck,
"InsecureSkipSignatureCheck", verifierConfig.InsecureSkipSignatureCheck,
"SupportedSigningAlgs", verifierConfig.SupportedSigningAlgs,
"SkipClientIDCheck", strconv.FormatBool(verifierConfig.SkipClientIDCheck),
"SkipExpiryCheck", strconv.FormatBool(verifierConfig.SkipExpiryCheck),
"SkipIssuerCheck", strconv.FormatBool(verifierConfig.SkipIssuerCheck),
"InsecureSkipSignatureCheck", strconv.FormatBool(verifierConfig.InsecureSkipSignatureCheck),
"SupportedSigningAlgs", fmt.Sprintf("%v", verifierConfig.SupportedSigningAlgs),
)
logger.Debugw("Applying VerifierConfigOptions")
for _, option := range options {
Expand All @@ -186,11 +189,11 @@ func NewVerifierConfig(logger LoggerInterface, clientID string, options ...Verif
}
logger.Debugw("Applied all VerifierConfigOptions",
"clientID", clientID,
"SkipClientIDCheck", verifierConfig.SkipClientIDCheck,
"SkipExpiryCheck", verifierConfig.SkipExpiryCheck,
"SkipIssuerCheck", verifierConfig.SkipIssuerCheck,
"InsecureSkipSignatureCheck", verifierConfig.InsecureSkipSignatureCheck,
"SupportedSigningAlgs", verifierConfig.SupportedSigningAlgs,
"SkipClientIDCheck", strconv.FormatBool(verifierConfig.SkipClientIDCheck),
"SkipExpiryCheck", strconv.FormatBool(verifierConfig.SkipExpiryCheck),
"SkipIssuerCheck", strconv.FormatBool(verifierConfig.SkipIssuerCheck),
"InsecureSkipSignatureCheck", strconv.FormatBool(verifierConfig.InsecureSkipSignatureCheck),
"SupportedSigningAlgs", fmt.Sprintf("%v", verifierConfig.SupportedSigningAlgs),
)
return verifierConfig, nil
}
Expand Down Expand Up @@ -231,7 +234,8 @@ func (claims *Claims) ValidateExpectations(issuer Issuer) error {
logger := claims.LoggerInterface
logger.Debugw("Validating job_workflow_ref claim against expected value", "job_workflow_ref", claims.JobWorkflowRef, "expected", issuer.ExpectedJobWorkflowRef)
if claims.JobWorkflowRef != issuer.ExpectedJobWorkflowRef {
return fmt.Errorf("job_workflow_ref claim expected value validation failed, expected: %s, provided: %s", claims.JobWorkflowRef, issuer.ExpectedJobWorkflowRef)
logger.Errorw("Job workflow ref validation failed", "expected", issuer.ExpectedJobWorkflowRef, "provided", claims.JobWorkflowRef)
return fmt.Errorf("job_workflow_ref claim expected value validation failed, expected: %s, provided: %s", issuer.ExpectedJobWorkflowRef, claims.JobWorkflowRef)
}
logger.Debugw("Claims validated successfully")
return nil
Expand Down Expand Up @@ -289,11 +293,11 @@ func NewTokenProcessor(
tokenProcessor.verifierConfig = config
logger.Debugw("Added Verifier config to token processor",
"clientID", config.ClientID,
"SkipClientIDCheck", config.SkipClientIDCheck,
"SkipExpiryCheck", config.SkipExpiryCheck,
"SkipIssuerCheck", config.SkipIssuerCheck,
"InsecureSkipSignatureCheck", config.InsecureSkipSignatureCheck,
"SupportedSigningAlgs", config.SupportedSigningAlgs,
"SkipClientIDCheck", strconv.FormatBool(config.SkipClientIDCheck),
"SkipExpiryCheck", strconv.FormatBool(config.SkipExpiryCheck),
"SkipIssuerCheck", strconv.FormatBool(config.SkipIssuerCheck),
"InsecureSkipSignatureCheck", strconv.FormatBool(config.InsecureSkipSignatureCheck),
"SupportedSigningAlgs", fmt.Sprintf("%v", config.SupportedSigningAlgs),
)
if tokenProcessor.verifierConfig.ClientID == "" {
return TokenProcessor{}, errors.New("verifierConfig clientID is empty")
Expand All @@ -310,7 +314,7 @@ func NewTokenProcessor(
return TokenProcessor{}, err
}
tokenProcessor.issuer = trustedIssuer
logger.Debugw("Added trusted issuer to TokenProcessor", "issuer", tokenProcessor.issuer)
logger.Debugw("Added trusted issuer to TokenProcessor", "issuer", fmt.Sprintf("%+v", tokenProcessor.issuer))

logger.Debugw("Applying TokenProcessorOptions")
for _, option := range options {
Expand All @@ -321,7 +325,7 @@ func NewTokenProcessor(
}
logger.Debugw("Applied all TokenProcessorOptions")

logger.Debugw("Created token processor", "issuer", tokenProcessor.issuer)
logger.Debugw("Created token processor", "issuer", fmt.Sprintf("%+v", tokenProcessor.issuer))
return tokenProcessor, nil
}

Expand Down Expand Up @@ -360,7 +364,7 @@ func (tokenProcessor *TokenProcessor) tokenIssuer(signAlgorithm []string) (strin
func (tokenProcessor *TokenProcessor) isTrustedIssuer(issuer string, trustedIssuers map[string]Issuer) (Issuer, error) {
logger := tokenProcessor.logger
logger.Debugw("Checking if issuer is trusted", "issuer", issuer)
logger.Debugw("Trusted issuers", "trustedIssuers", trustedIssuers)
logger.Debugw("Trusted issuers", "trustedIssuers", fmt.Sprintf("%+v", trustedIssuers))
if trustedIssuer, exists := trustedIssuers[issuer]; exists {
logger.Debugw("Issuer is trusted", "issuer", issuer)
return trustedIssuer, nil
Expand All @@ -382,18 +386,59 @@ func (tokenProcessor *TokenProcessor) Issuer() string {
func (tokenProcessor *TokenProcessor) VerifyAndExtractClaims(ctx context.Context, verifier TokenVerifierInterface, claims ClaimsInterface) error {
logger := tokenProcessor.logger
token, err := verifier.Verify(ctx, tokenProcessor.rawToken)
var tokenExpiryError *oidc.TokenExpiredError
if errors.As(err, &tokenExpiryError) {
token, err = tokenProcessor.handleExpiredToken(ctx, tokenExpiryError, logger, err)
}
if err != nil {
logger.Errorf("Failed to verify token", "error", err)
return fmt.Errorf("failed to verify token: %w", err)
}

logger.Debugw("Getting claims from token")
err = token.Claims(claims)
if err != nil {
logger.Errorf("Failed to get claims from token", "error", err)
return fmt.Errorf("failed to get claims from token: %w", err)
}
logger.Debugw("Got claims from token", "claims", fmt.Sprintf("%+v", claims))
err = claims.ValidateExpectations(tokenProcessor.issuer)
if err != nil {
logger.Errorf("Failed to validate claims", "error", err)
return fmt.Errorf("failed to validate claims: %w", err)
}
logger.Infow("Token verified and claims extracted successfully")
return nil
}

func (tokenProcessor *TokenProcessor) handleExpiredToken(ctx context.Context, tokenExpiryError *oidc.TokenExpiredError, logger LoggerInterface, err error) (Token, error) {
expiryTime := tokenExpiryError.Expiry
now := time.Now()
elapsed := now.Sub(expiryTime)
gracePeriod := 10 * time.Minute
if elapsed <= gracePeriod {
logger.Infow("Token expired recently, attempting to verify without expiry check", "elapsed", elapsed.String())
newVerifierConfig := tokenProcessor.verifierConfig
newVerifierConfig.SkipExpiryCheck = true

provider, err := NewProviderFromDiscovery(ctx, logger, tokenProcessor.issuer.IssuerURL)
if err != nil {
logger.Errorw("Failed to create provider", "error", err.Error())
return Token{}, fmt.Errorf("failed to create provider: %w", err)
}

newVerifier := provider.NewVerifier(logger, newVerifierConfig)
token, err := newVerifier.Verify(ctx, tokenProcessor.rawToken)

if err != nil {
logger.Errorw("Failed to verify token after skipping expiry check", "error", err.Error())
return Token{}, fmt.Errorf("failed to verify token after skipping expiry check: %w", err)
}

logger.Debugw("Token verified successfully after skipping expiry check")
return token, nil
} else {
logger.Errorw("Token expired too long ago", "elapsed", elapsed.String(), "gracePeriod", gracePeriod.String())
return Token{}, fmt.Errorf("token expired more than %v ago: %w", gracePeriod, err)
}
}
23 changes: 12 additions & 11 deletions pkg/oidc/oidc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@ package oidc_test
import (
"errors"
"fmt"

// "time"

// "fmt"
"os"

"github.com/coreos/go-oidc/v3/oidc"
Expand All @@ -16,15 +12,22 @@ import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/stretchr/testify/mock"
"go.uber.org/zap"
"golang.org/x/net/context"
)

// MockLogger implements the LoggerInterface for testing purposes
type MockLogger struct{}

func (l *MockLogger) Debugw(msg string, keysAndValues ...string) {}
func (l *MockLogger) Infow(msg string, keysAndValues ...string) {}
func (l *MockLogger) Errorw(msg string, keysAndValues ...string) {}
func (l *MockLogger) Errorf(msg string, keysAndValues ...interface{}) {}

var _ = Describe("OIDC", func() {
var (
err error
ctx context.Context
logger *zap.SugaredLogger
logger tioidc.LoggerInterface
trustedIssuers map[string]tioidc.Issuer
rawToken []byte
verifierConfig tioidc.VerifierConfig
Expand All @@ -33,10 +36,7 @@ var _ = Describe("OIDC", func() {
)

BeforeEach(func() {
l, err := zap.NewDevelopment()
Expect(err).NotTo(HaveOccurred())

logger = l.Sugar()
logger = &MockLogger{}
clientID = "testClientID"
})

Expand Down Expand Up @@ -243,7 +243,8 @@ var _ = Describe("OIDC", func() {

// Verify
Expect(err).To(HaveOccurred())
Expect(err).To(MatchError("failed to validate claims: job_workflow_ref claim expected value validation failed, expected: kyma-project/test-infra/.github/workflows/unexpected.yml@refs/heads/main, provided: kyma-project/test-infra/.github/workflows/verify-oidc-token.yml@refs/heads/main"))
Expect(err).To(MatchError("failed to validate claims: job_workflow_ref claim expected value validation failed, expected: kyma-project/test-infra/.github/workflows/verify-oidc-token.yml@refs/heads/main, provided: kyma-project/test-infra/.github/workflows/unexpected.yml@refs/heads/main"))

})
It("should return an error when token was not verified", func() {
verifier.On("Verify", mock.AnythingOfType("backgroundCtx"), string(rawToken)).Return(token, fmt.Errorf("token validation failed"))
Expand Down
Loading