Skip to content

Commit

Permalink
Verify oidc token against extended expiration time (#12218)
Browse files Browse the repository at this point in the history
* Allow 10 minutes of grace period before the token expire

* test

* Split token and claims verification
Added function for checking extended expiration time

* Use extended expiration when standard expiration was to short

* Mask raw token in debug logs
Configure extended expiration time through flag

* Align with the code structure and test update

revert to Token

align with Token

* Accept ClaimsReader interface in ValidateClaims instead a concrete type
Check if expiration timestamp is in the future
Test VerifyExtendedExpiration method

* cleanup TokenProcessor tests

---------

Co-authored-by: Patryk Dobrowolski <[email protected]>
  • Loading branch information
dekiel and akiioto authored Oct 25, 2024
1 parent 15340cc commit 15339bf
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 67 deletions.
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

0 comments on commit 15339bf

Please sign in to comment.