Skip to content

Commit

Permalink
Initial JWT authentication implementation
Browse files Browse the repository at this point in the history
Useful, and licensed for OpenFaaS for Enterprises only this
implementation authenicates requests with a JWT signed by the
gateway issuer.

This token can only be obtained through a token exchange.

Signed-off-by: Alex Ellis (OpenFaaS Ltd) <alexellis2@gmail.com>
  • Loading branch information
alexellis committed Jan 26, 2024
1 parent bb7b23f commit d4ccb9e
Show file tree
Hide file tree
Showing 146 changed files with 18,571 additions and 1 deletion.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@ Environmental variables:
| `healthcheck_interval` | Interval (in seconds) for HTTP healthcheck by container orchestrator i.e. kubelet. Used for graceful shutdowns. |
| `http_buffer_req_body` | `http` mode only - buffers request body in memory before forwarding upstream to your template's `upstream_url`. Use if your upstream HTTP server does not accept `Transfer-Encoding: chunked`, for example WSGI tends to require this setting. Default: `false` |
| `http_upstream_url` | `http` mode only - where to forward requests i.e. `http://127.0.0.1:5000` |
| `jwt_auth` | For OpenFaaS for Enterprises customers only. When set to `true`, the watchdog will require a JWT token to be passed as a Bearer token in the Authorization header. This token can only be obtained through the OpenFaaS gateway using a token exchange using the `http://gateway.openfaas:8080` address as the authority. |
| `jwt_auth_debug` | Print out debug messages from the JWT authentication process. |
| `jwt_auth_local` | When set to `true`, the watchdog will attempt to validate the JWT token using a port-forwarded or local gateway running at `http://127.0.0.1:8080` instead of attempting to reach it via an in-cluster service name. |
| `log_buffer_size` | The amount of bytes to read from stderr/stdout for log lines. When exceeded, the user will see an "bufio.Scanner: token too long" error. The default value is `bufio.MaxScanTokenSize` |
| `max_inflight` | Limit the maximum number of requests in flight, and return a HTTP status 429 when exceeded |
| `mode` | The mode which of-watchdog operates in, Default `streaming` [see doc](#3-streaming-fork-modestreaming---default). Options are [http](#1-http-modehttp), [serialising fork](#2-serializing-fork-modeserializing), [streaming fork](#3-streaming-fork-modestreaming---default), [static](#4-static-modestatic) |
Expand Down
8 changes: 7 additions & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ type WatchdogConfig struct {
// ReadyEndpoint is the custom readiness path for the watchdog. When non-empty
// the /_/ready endpoint with proxy the request to this path.
ReadyEndpoint string

// JWTAuthentication enables JWT authentication for the watchdog
// using the OpenFaaS gateway as the issuer.
JWTAuthentication bool
}

// Process returns a string for the process and a slice for the arguments from the FunctionProcess.
Expand Down Expand Up @@ -159,6 +163,8 @@ func New(env []string) (WatchdogConfig, error) {
return c, fmt.Errorf(`provide a "function_process" or "fprocess" environmental variable for your function`)
}

c.JWTAuthentication = getBool(envMap, "jwt_auth")

return c, nil
}

Expand Down Expand Up @@ -215,7 +221,7 @@ func getInt(env map[string]string, key string, defaultValue int) int {
}

func getBool(env map[string]string, key string) bool {
if env[key] == "true" {
if env[key] == "true" || env[key] == "1" {
return true
}

Expand Down
181 changes: 181 additions & 0 deletions executor/jwt_authenticator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package executor

import (
"crypto"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"time"

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

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

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

var authority = "https://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"
}

jwtAuthDebug := false
if val, ok := os.LookupEnv("jwt_auth_debug"); ok && val == "true" || val == "1" {
jwtAuthDebug = true
}

config, err := getConfig(authority)
if err != nil {
return nil, err
}

if jwtAuthDebug {
log.Printf("[JWT Auth] Issuer: %s\tJWKS URI: %s", config.Issuer, config.JWKSURI)
}

keyset, err := getKeyset(config.JWKSURI)
if err != nil {
return nil, err
}

if jwtAuthDebug {
for _, key := range keyset.Keys {
log.Printf("[JWT Auth] Key: %s", key.KeyID)
}
}

issuer := config.Issuer

return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
st := time.Now()
for _, key := range keyset.Keys {
log.Printf("%s: %v", issuer, key.KeyID)
}

var bearer string
if v := r.Header.Get("Authorization"); v != "" {
bearer = strings.TrimPrefix(v, "Bearer ")
}

if bearer == "" {
http.Error(w, "Bearer must be present in Authorization header", http.StatusUnauthorized)
log.Printf("%s %s - %d ACCESS DENIED - (%s)", r.Method, r.URL.Path, http.StatusUnauthorized, time.Since(st).Round(time.Millisecond))
return
}

mapClaims := jwt.MapClaims{}

token, err := jwt.ParseWithClaims(bearer, &mapClaims, 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"])
}

kid, ok := token.Header["kid"].(string)
if !ok {
return nil, fmt.Errorf("invalid kid: %v", token.Header["kid"])
}
var key *jwk.KeySpec
for _, k := range keyset.Keys {
if k.KeyID == kid {
key = &k
break
}
}

if key == nil {
return nil, fmt.Errorf("invalid kid: %s", kid)
}
return key.Key.(crypto.PublicKey), nil
})
if err != nil {
http.Error(w, fmt.Sprintf("failed to parse JWT token: %s", err), http.StatusUnauthorized)

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

if !token.Valid {
http.Error(w, fmt.Sprintf("invalid JWT token: %s", bearer), http.StatusUnauthorized)

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

next.ServeHTTP(w, r)
}), nil
}

func getKeyset(uri string) (jwk.KeySpecSet, error) {
var set jwk.KeySpecSet
req, err := http.NewRequest(http.MethodGet, uri, nil)
if err != nil {
return set, err
}

req.Header.Add("User-Agent", "openfaas-watchdog")

res, err := http.DefaultClient.Do(req)
if err != nil {
return set, err
}

var body []byte

if res.Body != nil {
defer res.Body.Close()
body, _ = io.ReadAll(res.Body)
}

if res.StatusCode != http.StatusOK {
return set, fmt.Errorf("failed to get keyset from %s, status code: %d, body: %s", uri, res.StatusCode, string(body))
}

if err := json.Unmarshal(body, &set); err != nil {
return set, err
}

return set, nil
}

func getConfig(jwksURL string) (OpenIDConfiguration, error) {
var config OpenIDConfiguration

req, err := http.NewRequest(http.MethodGet, jwksURL, nil)
if err != nil {
return config, err
}

res, err := http.DefaultClient.Do(req)
if err != nil {
return config, err
}

var body []byte
if res.Body != nil {
defer res.Body.Close()
body, _ = io.ReadAll(res.Body)
}

if res.StatusCode != http.StatusOK {
return config, fmt.Errorf("failed to get config from %s, status code: %d, body: %s", jwksURL, res.StatusCode, string(body))
}

if err := json.Unmarshal(body, &config); err != nil {
return config, err
}

return config, nil
}

type OpenIDConfiguration struct {
Issuer string `json:"issuer"`
JWKSURI string `json:"jwks_uri"`
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ go 1.21

require (
github.com/docker/go-units v0.5.0
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/openfaas/faas-middleware v1.2.3
github.com/openfaas/faas-provider v0.25.2
github.com/prometheus/client_golang v1.18.0
github.com/rakutentech/jwk-go v1.1.3
)

require (
Expand All @@ -24,5 +26,6 @@ require (
github.com/gorilla/mux v1.8.0 // 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.0.0-20200323165209-0ec3e9974c59 // indirect
google.golang.org/protobuf v1.31.0 // indirect
)
26 changes: 26 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
Expand All @@ -14,10 +17,15 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
github.com/openfaas/faas-middleware v1.2.3 h1:nRib38/i5eNdUTTKA7ILgO/Xns5zVorCO6lIBjr2xA0=
github.com/openfaas/faas-middleware v1.2.3/go.mod h1:pMyWe0SP0zuzIj2on1pmRkZAjGIS+uRk2mp3N6LSlDI=
github.com/openfaas/faas-provider v0.25.1 h1:7Ryxj5Lf7mPBpNPFLO92jqvsrNUIyZBHutoOgp6MH5Q=
Expand All @@ -40,16 +48,34 @@ github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwa
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/rakutentech/jwk-go v1.1.3 h1:PiLwepKyUaW+QFG3ki78DIO2+b4IVK3nMhlxM70zrQ4=
github.com/rakutentech/jwk-go v1.1.3/go.mod h1:LtzSv4/+Iti1nnNeVQiP6l5cI74GBStbhyXCYvgPZFk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 h1:3zb4D3T4G8jdExgVU/95+vQXfpEPiMdCaZgmGVxjNHM=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
10 changes: 10 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,15 @@ func main() {
baseFunctionHandler := buildRequestHandler(watchdogConfig, watchdogConfig.PrefixLogs)
requestHandler := baseFunctionHandler

if watchdogConfig.JWTAuthentication {
handler, err := executor.NewJWTAuthMiddleware(baseFunctionHandler)
if err != nil {
log.Fatalf("Error creating JWTAuthMiddleware: %s", err.Error())
}
requestHandler = handler

}

var limit limiter.Limiter
if watchdogConfig.MaxInflight > 0 {
requestLimiter := limiter.NewConcurrencyLimiter(requestHandler, watchdogConfig.MaxInflight)
Expand Down Expand Up @@ -115,6 +124,7 @@ func main() {
watchdogConfig.ExecTimeout,
watchdogConfig.HealthcheckInterval)

log.Printf("JWT Auth: %v\n", watchdogConfig.JWTAuthentication)
log.Printf("Listening on port: %d\n", watchdogConfig.TCPPort)

listenUntilShutdown(s,
Expand Down
4 changes: 4 additions & 0 deletions vendor/github.com/golang-jwt/jwt/v4/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions vendor/github.com/golang-jwt/jwt/v4/LICENSE

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 22 additions & 0 deletions vendor/github.com/golang-jwt/jwt/v4/MIGRATION_GUIDE.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit d4ccb9e

Please sign in to comment.