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

Verify oidc token against extended expiration time #12218

Merged
merged 8 commits into from
Oct 25, 2024
Merged
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
55 changes: 39 additions & 16 deletions cmd/oidc-token-verifier/main.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package main

import (
"errors"
"fmt"
"os"

"github.com/coreos/go-oidc/v3/oidc"
"github.com/kyma-project/test-infra/pkg/logging"
tioidc "github.com/kyma-project/test-infra/pkg/oidc"
"github.com/spf13/cobra"
Expand All @@ -20,13 +22,14 @@ type Logger interface {
}

type options struct {
token string
clientID string
outputPath string
publicKeyPath string
newPublicKeysVarName string
trustedWorkflows []string
debug bool
token string
clientID string
outputPath string
publicKeyPath string
newPublicKeysVarName string
trustedWorkflows []string
debug bool
oidcTokenExpirationTime int // OIDC token expiration time in minutes
}

var (
Expand All @@ -53,6 +56,7 @@ func NewRootCmd() *cobra.Command {
rootCmd.PersistentFlags().StringVarP(&opts.clientID, "client-id", "c", "image-builder", "OIDC token client ID, this is used to verify the audience claim in the token. The value should be the same as the audience claim value in the token.")
rootCmd.PersistentFlags().StringVarP(&opts.publicKeyPath, "public-key-path", "p", "", "Path to the cached public keys directory")
rootCmd.PersistentFlags().BoolVarP(&opts.debug, "debug", "d", false, "Enable debug mode")
rootCmd.PersistentFlags().IntVarP(&opts.oidcTokenExpirationTime, "oidc-token-expiration-time", "e", 10, "OIDC token expiration time in minutes")
return rootCmd
}

Expand Down Expand Up @@ -103,8 +107,10 @@ 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
zapLogger *zap.Logger
err error
tokenExpiredError *oidc.TokenExpiredError
token *tioidc.Token
)
if opts.debug {
zapLogger, err = zap.NewDevelopment()
Expand Down Expand Up @@ -161,18 +167,35 @@ func (opts *options) extractClaims() error {
verifier := provider.NewVerifier(logger, verifyConfig)
logger.Infow("New verifier created")

// claims will store the extracted claim values from the token.
claims := tioidc.NewClaims(logger)
// Verifies the token and check if the claims have expected values.
// Verifies custom claim values too.
// Extract the claim values from the token into the claims struct.
// It provides a final result if the token is valid and the claims have expected values.
err = tokenProcessor.VerifyAndExtractClaims(ctx, &verifier, &claims)
// Verify the token
token, err = verifier.Verify(ctx, opts.token)
if errors.As(err, &tokenExpiredError) {
err = verifier.VerifyExtendedExpiration(err.(*oidc.TokenExpiredError).Expiry, opts.oidcTokenExpirationTime)
if err != nil {
return err
}
verifyConfig.SkipExpiryCheck = false
verifierWithoutExpiration := provider.NewVerifier(logger, verifyConfig)
token, err = verifierWithoutExpiration.Verify(ctx, opts.token)
}
if err != nil {
return err
}
logger.Infow("Token verified successfully")

// Create claims
claims := tioidc.NewClaims(logger)
logger.Infow("Verifying token claims")

// Pass the token to ValidateClaims
err = tokenProcessor.ValidateClaims(&claims, token)

if err != nil {
return err
}
logger.Infow("Token claims expectations verified successfully")
logger.Infow("All token checks passed successfully")

return nil
}

Expand Down
62 changes: 46 additions & 16 deletions pkg/oidc/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package oidc
import (
"errors"
"fmt"
"time"

"github.com/coreos/go-oidc/v3/oidc"
"github.com/go-jose/go-jose/v4"
Expand Down Expand Up @@ -64,7 +65,7 @@ type VerifierProvider interface {

type ClaimsInterface interface {
// Validate(jwt.Expected) error
ValidateExpectations(Issuer) error
validateExpectations(Issuer) error
}

type LoggerInterface interface {
Expand Down Expand Up @@ -154,6 +155,13 @@ type TokenVerifier struct {
Logger LoggerInterface
}

func maskToken(token string) string {
if len(token) < 15 {
return "********"
}
return token[:2] + "********" + token[len(token)-2:]
}

// NewVerifierConfig creates a new VerifierConfig.
// It verifies the clientID is not empty.
func NewVerifierConfig(logger LoggerInterface, clientID string, options ...VerifierConfigOption) (VerifierConfig, error) {
Expand Down Expand Up @@ -197,20 +205,39 @@ func NewVerifierConfig(logger LoggerInterface, clientID string, options ...Verif

// Verify verifies the raw OIDC token.
// It returns a Token struct which contains the verified token if successful.
func (tokenVerifier *TokenVerifier) Verify(ctx context.Context, rawToken string) (Token, error) {
func (tokenVerifier *TokenVerifier) Verify(ctx context.Context, rawToken string) (*Token, error) {
logger := tokenVerifier.Logger
logger.Debugw("Verifying token")
logger.Debugw("Got raw token value", "rawToken", rawToken)
logger.Debugw("Got raw token value", "rawToken", maskToken(rawToken))
idToken, err := tokenVerifier.Verifier.Verify(ctx, rawToken)
if err != nil {
token := Token{}
return token, fmt.Errorf("failed to verify token: %w", err)
return &token, fmt.Errorf("failed to verify token: %w", err)
}
logger.Debugw("Token verified successfully")
token := Token{
Token: idToken,
}
return token, nil
return &token, nil
}

// VerifyExtendedExpiration checks the OIDC token expiration timestamp against the provided expiration time.
// It allows to accept tokens after the token original expiration time elapsed.
// The other aspects of the token must be verified separately with expiration check disabled.
func (tokenVerifier *TokenVerifier) VerifyExtendedExpiration(expirationTimestamp time.Time, gracePeriodMinutes int) error {
logger := tokenVerifier.Logger
logger.Debugw("Verifying token expiration time", "expirationTimestamp", expirationTimestamp, "gracePeriodMinutes", gracePeriodMinutes)
now := time.Now()
// check if expirationTimestamp is in the future
if expirationTimestamp.After(now) {
return fmt.Errorf("token expiration time is in the future: %v", expirationTimestamp)
}
elapsed := now.Sub(expirationTimestamp)
gracePeriod := time.Minute
if elapsed <= gracePeriod {
return nil
}
return fmt.Errorf("token expired more than %v ago", gracePeriod)
}

// Claims gets the claims from the token and unmarshal them into the provided claims struct.
Expand All @@ -225,9 +252,9 @@ func NewClaims(logger LoggerInterface) Claims {
}
}

// ValidateExpectations validates the claims against the trusted issuer expected values.
// validateExpectations validates the claims against the trusted issuer expected values.
// It checks audience, issuer, and job_workflow_ref claims.
func (claims *Claims) ValidateExpectations(issuer Issuer) error {
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 {
Expand Down Expand Up @@ -284,7 +311,7 @@ func NewTokenProcessor(
tokenProcessor.logger = logger

tokenProcessor.rawToken = rawToken
logger.Debugw("Added raw token to token processor", "rawToken", rawToken)
logger.Debugw("Added raw token to token processor", "rawToken", maskToken(rawToken))

tokenProcessor.verifierConfig = config
logger.Debugw("Added Verifier config to token processor",
Expand Down Expand Up @@ -375,25 +402,28 @@ func (tokenProcessor *TokenProcessor) Issuer() string {
return tokenProcessor.issuer.IssuerURL
}

// VerifyAndExtractClaims verify and parse the token to get the token claims.
// ValidateClaims verify and parse the token to get the token claims.
// It uses the provided verifier to verify the token signature and expiration time.
// It verifies if the token claims have expected values.
// It unmarshal the claims into the provided claims struct.
func (tokenProcessor *TokenProcessor) VerifyAndExtractClaims(ctx context.Context, verifier TokenVerifierInterface, claims ClaimsInterface) error {
func (tokenProcessor *TokenProcessor) ValidateClaims(claims ClaimsInterface, token ClaimsReader) error {
logger := tokenProcessor.logger
token, err := verifier.Verify(ctx, tokenProcessor.rawToken)
if err != nil {
return fmt.Errorf("failed to verify token: %w", err)

// Ensure that the token is initialized
if token == nil {
return fmt.Errorf("token cannot be nil")
}

logger.Debugw("Getting claims from token")
err = token.Claims(claims)
err := token.Claims(claims)
if err != nil {
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)

err = claims.validateExpectations(tokenProcessor.issuer)
if err != nil {
return fmt.Errorf("failed to validate claims: %w", err)
return fmt.Errorf("expecations validation failed: %w", err)
}
return nil
}
Loading
Loading