Skip to content

Commit

Permalink
Validate auth permissions in function claims
Browse files Browse the repository at this point in the history
Compare the auth permissions in the function claim with the function
name and namespace the verify if the provided JWT token is authorized
to invoke the function.

Signed-off-by: Han Verstraete (OpenFaaS Ltd) <han@openfaas.com>
  • Loading branch information
welteki authored and alexellis committed Jun 17, 2024
1 parent 2bd6101 commit f6cb3ed
Show file tree
Hide file tree
Showing 44 changed files with 1,943 additions and 1,246 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ LDFLAGS := "-s -w -X main.Version=$(GIT_VERSION) -X main.GitCommit=$(GIT_COMMIT)
SERVER?=ghcr.io
OWNER?=openfaas
IMG_NAME?=of-watchdog
TAG?=latest
TAG?=$(GIT_VERSION)

export GOFLAGS=-mod=vendor

Expand Down
123 changes: 113 additions & 10 deletions executor/jwt_authenticator.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@ import (
"log"
"net/http"
"os"
"regexp"
"strings"
"time"

"github.com/rakutentech/jwk-go/jwk"

"github.com/golang-jwt/jwt/v4"
"github.com/golang-jwt/jwt/v5"
)

func NewJWTAuthMiddleware(next http.Handler) (http.Handler, error) {

var authority = "http://gateway.openfaas:8080/.well-known/openid-configuration"
if v, ok := os.LookupEnv("jwt_auth_local"); ok && (v == "true" || v == "1") {
authority = "http://127.0.0.1:8000/.well-known/openid-configuration"
Expand Down Expand Up @@ -50,6 +50,15 @@ func NewJWTAuthMiddleware(next http.Handler) (http.Handler, error) {

issuer := config.Issuer

namespace, err := getFnNamespace()
if err != nil {
return nil, fmt.Errorf("failed to get function namespace: %s", err)
}
name, err := getFnName()
if err != nil {
return nil, fmt.Errorf("failed to get function name: %s", err)
}

return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
st := time.Now()
for _, key := range keyset.Keys {
Expand All @@ -67,15 +76,18 @@ func NewJWTAuthMiddleware(next http.Handler) (http.Handler, error) {
return
}

mapClaims := jwt.MapClaims{}
parseOptions := []jwt.ParserOption{
jwt.WithIssuer(issuer),
// The OpenFaaS gateway is the expected audience but we can use the issuer url
// since the gateway is also the issuer of function tokens and thus has the same url.
jwt.WithAudience(issuer),
jwt.WithLeeway(time.Second * 1),
}

token, err := jwt.ParseWithClaims(bearer, &mapClaims, func(token *jwt.Token) (interface{}, error) {
functionClaims := FunctionClaims{}
token, err := jwt.ParseWithClaims(bearer, &functionClaims, func(token *jwt.Token) (interface{}, error) {
if jwtAuthDebug {
log.Printf("[JWT Auth] Token: audience: %v\tissuer: %v", mapClaims["aud"], mapClaims["iss"])
}

if mapClaims["iss"] != issuer {
return nil, fmt.Errorf("invalid issuer: %s", mapClaims["iss"])
log.Printf("[JWT Auth] Token: audience: %v\tissuer: %v", functionClaims.Audience, functionClaims.Issuer)
}

kid, ok := token.Header["kid"].(string)
Expand All @@ -94,7 +106,7 @@ func NewJWTAuthMiddleware(next http.Handler) (http.Handler, error) {
return nil, fmt.Errorf("invalid kid: %s", kid)
}
return key.Key.(crypto.PublicKey), nil
})
}, parseOptions...)
if err != nil {
http.Error(w, fmt.Sprintf("failed to parse JWT token: %s", err), http.StatusUnauthorized)

Expand All @@ -109,6 +121,13 @@ func NewJWTAuthMiddleware(next http.Handler) (http.Handler, error) {
return
}

if !isAuthorized(functionClaims.Authentication, namespace, name) {
http.Error(w, "insufficient permissions", http.StatusForbidden)

log.Printf("%s %s - %d ACCESS DENIED - (%s)", r.Method, r.URL.Path, http.StatusForbidden, time.Since(st).Round(time.Millisecond))
return
}

next.ServeHTTP(w, r)
}), nil
}
Expand Down Expand Up @@ -179,3 +198,87 @@ type OpenIDConfiguration struct {
Issuer string `json:"issuer"`
JWKSURI string `json:"jwks_uri"`
}

func getFnName() (string, error) {
name, ok := os.LookupEnv("OPENFAAS_NAME")
if !ok || len(name) == 0 {
return "", fmt.Errorf("env variable 'OPENFAAS_NAME' not set")
}

return name, nil
}

func getFnNamespace() (string, error) {
nsVal, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace")
if err != nil {
return "", err
}
return string(nsVal), nil
}

type FunctionClaims struct {
jwt.RegisteredClaims

Authentication AuthPermissions `json:"function"`
}

type AuthPermissions struct {
Permissions []string `json:"permissions"`
Audience []string `json:"audience,omitempty"`
}

func isAuthorized(auth AuthPermissions, namespace, fn string) bool {
functionRef := fmt.Sprintf("%s:%s", namespace, fn)

return matchResource(auth.Audience, functionRef, false) &&
matchResource(auth.Permissions, functionRef, true)
}

// matchResources checks if ref matches one of the resources.
// The function will return true if a match is found.
// If required is false, this function will return true if a match is found or the resource list is empty.
func matchResource(resources []string, ref string, req bool) bool {
if !req {
if len(resources) == 0 {
return true
}
}

for _, res := range resources {
if res == "*" {
return true
}

if matchString(res, ref) {
return true
}
}

return false
}

func matchString(pattern string, value string) bool {
if len(pattern) > 0 {
result, _ := regexp.MatchString(wildCardToRegexp(pattern), value)
return result
}

return pattern == value
}

// wildCardToRegexp converts a wildcard pattern to a regular expression pattern.
func wildCardToRegexp(pattern string) string {
var result strings.Builder
for i, literal := range strings.Split(pattern, "*") {

// Replace * with .*
if i > 0 {
result.WriteString(".*")
}

// Quote any regular expression meta characters in the
// literal text.
result.WriteString(regexp.QuoteMeta(literal))
}
return result.String()
}
186 changes: 186 additions & 0 deletions executor/jwt_authenticator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package executor

import (
"testing"
)

func Test_isAuthorized(t *testing.T) {
tests := []struct {
name string
want bool
permissions AuthPermissions
namespace string
function string
}{
{
name: "deny empty permission list",
want: false,
permissions: AuthPermissions{
Permissions: []string{},
},
namespace: "staging",
function: "env",
},
{
name: "allow empty audience list",
want: true,
permissions: AuthPermissions{
Permissions: []string{"staging:env"},
},
namespace: "staging",
function: "env",
},
{
name: "allow cluster wildcard",
want: true,
permissions: AuthPermissions{
Permissions: []string{"*"},
},
namespace: "staging",
function: "figlet",
},
{
name: "allow function wildcard",
want: true,
permissions: AuthPermissions{
Permissions: []string{"dev:*"},
},
namespace: "dev",
function: "figlet",
},
{
name: "allow namespace wildcard",
want: true,
permissions: AuthPermissions{
Permissions: []string{"*:env"},
},
namespace: "openfaas-fn",
function: "env",
},
{
name: "allow function",
want: true,
permissions: AuthPermissions{
Permissions: []string{"openfaas-fn:env"},
},
namespace: "openfaas-fn",
function: "env",
},
{
name: "deny function",
want: false,
permissions: AuthPermissions{
Permissions: []string{"openfaas-fn:env"},
},
namespace: "openfaas-fn",
function: "figlet",
},
{
name: "deny namespace",
want: false,
permissions: AuthPermissions{
Permissions: []string{"openfaas-fn:*"},
},
namespace: "staging",
function: "env",
},
{
name: "deny namespace wildcard",
want: false,
permissions: AuthPermissions{
Permissions: []string{"*:figlet"},
},
namespace: "staging",
function: "env",
},
{
name: "multiple permissions allow function",
want: true,
permissions: AuthPermissions{
Permissions: []string{"openfaas-fn:*", "staging:env"},
},
namespace: "staging",
function: "env",
},
{
name: "multiple permissions deny function",
want: false,
permissions: AuthPermissions{
Permissions: []string{"openfaas-fn:figlet", "staging-*:env"},
},
namespace: "staging",
function: "env",
},
{
name: "allow audience",
want: true,
permissions: AuthPermissions{
Permissions: []string{"openfaas-fn:*"},
Audience: []string{"openfaas-fn:env"},
},
namespace: "openfaas-fn",
function: "env",
},
{
name: "deny audience",
want: false,
permissions: AuthPermissions{
Permissions: []string{"openfaas-fn:*"},
Audience: []string{"openfaas-fn:env"},
},
namespace: "openfaas-fn",
function: "figlet",
},
{
name: "allow audience function wildcard",
want: true,
permissions: AuthPermissions{
Permissions: []string{"openfaas-fn:figlet"},
Audience: []string{"openfaas-fn:*"},
},
namespace: "openfaas-fn",
function: "figlet",
},
{
name: "deny audience function wildcard",
want: false,
permissions: AuthPermissions{
Permissions: []string{"openfaas-fn:figlet", "dev:env"},
Audience: []string{"openfaas-fn:*"},
},
namespace: "dev",
function: "env",
},
{
name: "deny audience namespace wildcard",
want: false,
permissions: AuthPermissions{
Permissions: []string{"openfaas-fn:*", "dev:*"},
Audience: []string{"*:env"},
},
namespace: "dev",
function: "figlet",
},
{
name: "allow audience namespace wildcard",
want: true,
permissions: AuthPermissions{
Permissions: []string{"openfaas-fn:*", "dev:*"},
Audience: []string{"*:env"},
},
namespace: "openfaas-fn",
function: "env",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
want := test.want
got := isAuthorized(test.permissions, test.namespace, test.function)

if want != got {
t.Errorf("want: %t, got: %t", want, got)
}
})
}
}
6 changes: 1 addition & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.21

require (
github.com/docker/go-units v0.5.0
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/openfaas/faas-middleware v1.2.3
github.com/openfaas/faas-provider v0.25.3
github.com/prometheus/client_golang v1.19.0
Expand All @@ -22,10 +22,6 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
golang.org/x/crypto v0.22.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
)
Loading

0 comments on commit f6cb3ed

Please sign in to comment.