Skip to content

Commit

Permalink
Add config to chained validator (#692)
Browse files Browse the repository at this point in the history
* Add config to chained validator

* Add tests with mock oidc server

* simplify config, remove cors middleware

* go mod tidy

* remove IssuerURL

* better log statements

* better flow
  • Loading branch information
Richard87 authored Oct 14, 2024
1 parent 236cc87 commit 6a70301
Show file tree
Hide file tree
Showing 13 changed files with 161 additions and 191 deletions.
9 changes: 4 additions & 5 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,18 @@
"RADIX_CONTAINER_REGISTRY":"radixdev.azurecr.io",
"PIPELINE_IMG_TAG": "master-latest",
"TEKTON_IMG_TAG": "main-latest",
"RADIX_CLUSTER_TYPE": "development",
"RADIX_DNS_ZONE": "dev.radix.equinor.com",
"RADIX_CLUSTERNAME": "weekly-24",
"RADIX_ACTIVE_CLUSTER_EGRESS_IPS": "104.45.84.1",
"REQUIRE_APP_CONFIGURATION_ITEM": "true",
"REQUIRE_APP_AD_GROUPS": "true",
"RADIX_ENVIRONMENT":"qa",
"PROMETHEUS_URL":"http://localhost:9091",
"RADIX_APP":"radix-api",
"LOG_LEVEL":"info",
"LOG_PRETTY":"true",
"OIDC_AUDIENCE": "6dae42f8-4368-4678-94ff-3960e28e3630",
"OIDC_ISSUER": "https://sts.windows.net/3aa4a235-b6e2-48d5-9195-7fcf05b459b0/"
"OIDC_AZURE_ISSUER": "https://sts.windows.net/3aa4a235-b6e2-48d5-9195-7fcf05b459b0/",
"OIDC_AZURE_AUDIENCE": "6dae42f8-4368-4678-94ff-3960e28e3630",
"OIDC_KUBERNETES_ISSUER": "https://northeurope.oic.prod-aks.azure.com/3aa4a235-b6e2-48d5-9195-7fcf05b459b0/79c72762-ce8d-4874-90cd-a119a5050503/",
"OIDC_KUBERNETES_AUDIENCE": "https://northeurope.oic.prod-aks.azure.com/3aa4a235-b6e2-48d5-9195-7fcf05b459b0/79c72762-ce8d-4874-90cd-a119a5050503/"
}
}
]
Expand Down
56 changes: 0 additions & 56 deletions api/middleware/cors/cors.go

This file was deleted.

19 changes: 15 additions & 4 deletions api/utils/token/azure_principal.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package token

import (
"context"
"fmt"

"github.com/auth0/go-jwt-middleware/v2/validator"
)
Expand All @@ -19,8 +20,8 @@ func (c *azureClaims) Validate(_ context.Context) error {

type azurePrincipal struct {
token string
claims *validator.ValidatedClaims
azureClaims *azureClaims
claims validator.RegisteredClaims
azureClaims azureClaims
}

func (p *azurePrincipal) Token() string {
Expand All @@ -29,7 +30,13 @@ func (p *azurePrincipal) Token() string {
func (p *azurePrincipal) IsAuthenticated() bool {
return true
}
func (p *azurePrincipal) Id() string { return p.azureClaims.ObjectId }
func (p *azurePrincipal) Id() string {
if p.azureClaims.ObjectId != "" {
return fmt.Sprintf("oid:%s", p.azureClaims.ObjectId)
}

return fmt.Sprintf("sub:%s", p.claims.Subject)
}

func (p *azurePrincipal) Name() string {
if p.azureClaims.Upn != "" {
Expand All @@ -44,5 +51,9 @@ func (p *azurePrincipal) Name() string {
return p.azureClaims.AppId
}

return p.azureClaims.ObjectId
if p.azureClaims.ObjectId != "" {
return p.azureClaims.ObjectId
}

return p.claims.Subject
}
34 changes: 34 additions & 0 deletions api/utils/token/chained_validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package token

import (
"context"
"errors"
"fmt"
)

type ChainedValidator struct{ validators []ValidatorInterface }

var _ ValidatorInterface = &ChainedValidator{}
var errNoValidatorsFound = errors.New("no validators found")

func NewChainedValidator(validators ...ValidatorInterface) *ChainedValidator {
return &ChainedValidator{validators}
}

func (v *ChainedValidator) ValidateToken(ctx context.Context, token string) (TokenPrincipal, error) {
var errs []error

for _, validator := range v.validators {
principal, err := validator.ValidateToken(ctx, token)
if principal != nil {
return principal, nil
}
errs = append(errs, err)
}

if len(errs) > 0 {
return nil, fmt.Errorf("no issuers could validate token: %w", errors.Join(errs...))
}

return nil, errNoValidatorsFound
}
52 changes: 0 additions & 52 deletions api/utils/token/unchecked_principal.go

This file was deleted.

36 changes: 0 additions & 36 deletions api/utils/token/unchecked_validator.go

This file was deleted.

11 changes: 6 additions & 5 deletions api/utils/token/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type TokenPrincipal interface {
}

type ValidatorInterface interface {
// ValidateToken will return a TokenPrincipal object if token payload and signature is validated agains issuer. It will return nil principal and a error if it fails.
ValidateToken(context.Context, string) (TokenPrincipal, error)
}

Expand All @@ -29,8 +30,8 @@ var _ ValidatorInterface = &Validator{}

type KeyFunc func(context.Context) (interface{}, error)

func NewValidator(issuerUrl *url.URL, audience string) (*Validator, error) {
provider := jwks.NewCachingProvider(issuerUrl, 5*time.Hour)
func NewValidator(issuerUrl url.URL, audience string) (*Validator, error) {
provider := jwks.NewCachingProvider(&issuerUrl, 5*time.Hour)

validator, err := validator.New(
provider.KeyFunc,
Expand Down Expand Up @@ -59,11 +60,11 @@ func (v *Validator) ValidateToken(ctx context.Context, token string) (TokenPrinc
return nil, http.ForbiddenError("invalid token")
}

azureClaims, ok := claims.CustomClaims.(*azureClaims)
if !ok {
azClaims, ok := claims.CustomClaims.(*azureClaims)
if !ok || azClaims == nil {
return nil, http.ForbiddenError("invalid azure token")
}

principal := &azurePrincipal{token: token, claims: claims, azureClaims: azureClaims}
principal := &azurePrincipal{token: token, claims: claims.RegisteredClaims, azureClaims: *azClaims}
return principal, nil
}
76 changes: 71 additions & 5 deletions api/utils/token/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,94 @@ import (
"net/url"
"testing"

"github.com/golang-jwt/jwt/v5"
"github.com/oauth2-proxy/mockoidc"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

const (
test_oidc_issuer = "https://radix.equinor.com"
test_oidc_audience = "testaudience"
test_jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IkJCOENlRlZxeWFHckdOdWVoSklpTDRkZmp6dyIsImtpZCI6IkJCOENlRlZxeWFHckdOdWVoSklpTDRkZmp6dyJ9.eyJhdWQiOiIxMjM0NTY3OC0xMjM0LTEyMzQtMTIzNC0xMjM0MjQ1YTJlYzEiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC8xMjM0NTY3OC03NTY1LTIzNDItMjM0Mi0xMjM0MDViNDU5YjAvIiwiaWF0IjoxNTc1MzU1NTA4LCJuYmYiOjE1NzUzNTU1MDgsImV4cCI6MTU3NTM1OTQwOCwiYWNyIjoiMSIsImFpbyI6IjQyYXNkYXMiLCJhbXIiOlsicHdkIl0sImFwcGlkIjoiMTIzNDU2NzgtMTIzNC0xMjM0LTEyMzQtMTIzNDc5MDM5YTkwIiwiYXBwaWRhY3IiOiIwIiwiZmFtaWx5X25hbWUiOiJKb2huIiwiZ2l2ZW5fbmFtZSI6IkRvZSIsImhhc2dyb3VwcyI6InRydWUiLCJpcGFkZHIiOiIxMC4xMC4xMC4xMCIsIm5hbWUiOiJKb2huIERvZSIsIm9pZCI6IjEyMzQ1Njc4LTEyMzQtMTIzNC0xMjM0LTEyMzRmYzhmYTBlYSIsIm9ucHJlbV9zaWQiOiJTLTEtNS0yMS0xMjM0NTY3ODktMTIzNDU2OTc4MC0xMjM0NTY3ODktMTIzNDU2NyIsInNjcCI6InVzZXJfaW1wZXJzb25hdGlvbiIsInN1YiI6IjBoa2JpbEo3MTIzNHpSU3h6eHZiSW1hc2RmZ3N4amI2YXNkZmVOR2FzZGYiLCJ0aWQiOiIxMjM0NTY3OC0xMjM0LTEyMzQtMTIzNC0xMjM0MDViNDU5YjAiLCJ1bmlxdWVfbmFtZSI6Im5vdC1leGlzdGluZy1yYWRpeC1lbWFpbEBlcXVpbm9yLmNvbSIsInVwbiI6Im5vdC1leGlzdGluZy10ZXN0LXJhZGl4LWVtYWlsQGVxdWlub3IuY29tIiwidXRpIjoiQlMxMmFzR2R1RXlyZUVjRGN2aDJBRyIsInZlciI6IjEuMCJ9.EB5z7Mk34NkFPCP8MqaNMo4UeWgNyO4-qEmzOVPxfoBqbgA16Ar4xeONXODwjZn9iD-CwJccusW6GP0xZ_PJHBFpfaJO_tLaP1k0KhT-eaANt112TvDBt0yjHtJg6He6CEDqagREIsH3w1mSm40zWLKGZeRLdnGxnQyKsTmNJ1rFRdY3AyoEgf6-pnJweUt0LaFMKmIJ2HornStm2hjUstBaji_5cSS946zqp4tgrc-RzzDuaQXzqlVL2J22SR2S_Oux_3yw88KmlhEFFP9axNcbjZrzW3L9XWnPT6UzVIaVRaNRSWfqDATg-jeHg4Gm1bp8w0aIqLdDxc9CfFMjuQ"
)

func TestNewValidator(t *testing.T) {
issuer, _ := url.Parse(test_oidc_issuer)
func TestValidUser(t *testing.T) {
issuer := createServer(t)
v, err := NewValidator(issuer, test_oidc_audience)
assert.NoError(t, err)
assert.NotNil(t, v)

principal, err := v.ValidateToken(context.Background(), createUser(t, issuer, test_oidc_audience, "user1"))
require.NoError(t, err)
assert.NotNil(t, principal)
assert.Equal(t, "sub:user1", principal.Id())
assert.Equal(t, "user1", principal.Name())
}

func TestValidateToken(t *testing.T) {
issuer, _ := url.Parse(test_oidc_issuer)
func TestInvalidToken(t *testing.T) {
issuer := createServer(t)
v, err := NewValidator(issuer, test_oidc_audience)
assert.NoError(t, err)
assert.NotNil(t, v)

_, err = v.ValidateToken(context.Background(), createUser(t, issuer, "invalid audience", "user1"))
assert.Error(t, err)
}
func TestValidUserMultipleIssuers(t *testing.T) {
issuer1 := createServer(t)
issuer2 := createServer(t)
v1, err := NewValidator(issuer1, test_oidc_audience)
assert.NoError(t, err)
v2, err := NewValidator(issuer2, test_oidc_audience)
assert.NoError(t, err)
v := NewChainedValidator(v1, v2)

principal, err := v.ValidateToken(context.Background(), createUser(t, issuer2, test_oidc_audience, "user1"))
require.NoError(t, err)
assert.NotNil(t, principal)
assert.Equal(t, "sub:user1", principal.Id())
assert.Equal(t, "user1", principal.Name())

_, err = v.ValidateToken(context.Background(), createUser(t, issuer1, "invalid audience", "user1"))
assert.Error(t, err)

fakeIssuer, err := url.Parse("http://fakeissuer/")
require.NoError(t, err)
_, err = v.ValidateToken(context.Background(), createUser(t, *fakeIssuer, test_oidc_audience, "user1"))
assert.Error(t, err)

_, err = v.ValidateToken(context.Background(), test_jwt)
assert.Error(t, err)
}

func createServer(t *testing.T) url.URL {
m, err := mockoidc.Run()
require.NoError(t, err)
t.Cleanup(func() {
_ = m.Shutdown()
})

issuer, err := url.Parse(m.Issuer())
require.NoError(t, err)

return *issuer
}

func createUser(t *testing.T, issuer url.URL, audience, subject string) string {
user := mockoidc.DefaultUser()
key, err := mockoidc.DefaultKeypair()
require.NoError(t, err)

claims, err := user.Claims([]string{"profile", "email"}, &mockoidc.IDTokenClaims{
RegisteredClaims: &jwt.RegisteredClaims{
Issuer: issuer.String(),
Subject: subject,
Audience: []string{audience},
},
})
require.NoError(t, err)

signJWT, err := key.SignJWT(claims)
require.NoError(t, err)
return signJWT
}
Loading

0 comments on commit 6a70301

Please sign in to comment.