-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
acl: RPC endpoints for JWT auth (#15918)
- Loading branch information
1 parent
9c050e2
commit 519c746
Showing
22 changed files
with
877 additions
and
73 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
package oidc | ||
package auth | ||
|
||
import ( | ||
"encoding/json" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
package oidc | ||
package auth | ||
|
||
import ( | ||
"testing" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
package oidc | ||
package auth | ||
|
||
import ( | ||
"github.com/hashicorp/nomad/nomad/structs" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
package jwt | ||
|
||
import ( | ||
"context" | ||
"crypto" | ||
"fmt" | ||
"time" | ||
|
||
"github.com/armon/go-metrics" | ||
"github.com/hashicorp/cap/jwt" | ||
"golang.org/x/exp/slices" | ||
|
||
"github.com/hashicorp/nomad/helper" | ||
"github.com/hashicorp/nomad/nomad/structs" | ||
) | ||
|
||
// Validate performs token signature verification and JWT header validation, | ||
// and returns a list of claims or an error in case any validation or signature | ||
// verification fails. | ||
func Validate(ctx context.Context, token string, methodConf *structs.ACLAuthMethodConfig) (map[string]any, error) { | ||
var ( | ||
keySet jwt.KeySet | ||
err error | ||
) | ||
|
||
// JWT validation can happen in 3 ways: | ||
// - via embedded public keys, locally | ||
// - via JWKS | ||
// - or via OIDC provider | ||
if len(methodConf.JWTValidationPubKeys) != 0 { | ||
keySet, err = usingStaticKeys(methodConf.JWTValidationPubKeys) | ||
if err != nil { | ||
return nil, err | ||
} | ||
} else if methodConf.JWKSURL != "" { | ||
keySet, err = usingJWKS(ctx, methodConf.JWKSURL, methodConf.JWKSCACert) | ||
if err != nil { | ||
return nil, err | ||
} | ||
} else if methodConf.OIDCDiscoveryURL != "" { | ||
keySet, err = usingOIDC(ctx, methodConf.OIDCDiscoveryURL, methodConf.DiscoveryCaPem) | ||
if err != nil { | ||
return nil, err | ||
} | ||
} | ||
|
||
// SigningAlgs field is a string, we need to convert it to a type the go-jwt | ||
// accepts in order to validate. | ||
toAlgFn := func(m string) jwt.Alg { return jwt.Alg(m) } | ||
algorithms := helper.ConvertSlice(methodConf.SigningAlgs, toAlgFn) | ||
|
||
expected := jwt.Expected{ | ||
Audiences: methodConf.BoundAudiences, | ||
SigningAlgorithms: algorithms, | ||
NotBeforeLeeway: methodConf.NotBeforeLeeway, | ||
ExpirationLeeway: methodConf.ExpirationLeeway, | ||
ClockSkewLeeway: methodConf.ClockSkewLeeway, | ||
} | ||
|
||
validator, err := jwt.NewValidator(keySet) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
claims, err := validator.Validate(ctx, token, expected) | ||
if err != nil { | ||
return nil, fmt.Errorf("unable to verify signature of JWT token: %v", err) | ||
} | ||
|
||
// validate issuer manually, because we allow users to specify an array | ||
if len(methodConf.BoundIssuer) > 0 { | ||
if _, ok := claims["iss"]; !ok { | ||
return nil, fmt.Errorf( | ||
"auth method specifies BoundIssuers but the provided token does not contain issuer information", | ||
) | ||
} | ||
if iss, ok := claims["iss"].(string); !ok { | ||
return nil, fmt.Errorf("unable to read iss property of provided token") | ||
} else if !slices.Contains(methodConf.BoundIssuer, iss) { | ||
return nil, fmt.Errorf("invalid JWT issuer: %v", claims["iss"]) | ||
} | ||
} | ||
|
||
return claims, nil | ||
} | ||
|
||
func usingStaticKeys(keys []string) (jwt.KeySet, error) { | ||
var parsedKeys []crypto.PublicKey | ||
for _, v := range keys { | ||
key, err := jwt.ParsePublicKeyPEM([]byte(v)) | ||
parsedKeys = append(parsedKeys, key) | ||
if err != nil { | ||
return nil, fmt.Errorf("unable to parse public key for JWT auth: %v", err) | ||
} | ||
} | ||
return jwt.NewStaticKeySet(parsedKeys) | ||
} | ||
|
||
func usingJWKS(ctx context.Context, jwksurl, jwkscapem string) (jwt.KeySet, error) { | ||
// Measure the JWKS endpoint performance. | ||
defer metrics.MeasureSince([]string{"nomad", "acl", "jwt", "jwks"}, time.Now()) | ||
|
||
keySet, err := jwt.NewJSONWebKeySet(ctx, jwksurl, jwkscapem) | ||
if err != nil { | ||
return nil, fmt.Errorf("unable to get validation keys from JWKS: %v", err) | ||
} | ||
return keySet, nil | ||
} | ||
|
||
func usingOIDC(ctx context.Context, oidcurl string, oidccapem []string) (jwt.KeySet, error) { | ||
// Measure the OIDC endpoint performance. | ||
defer metrics.MeasureSince([]string{"nomad", "acl", "jwt", "oidc_jwt"}, time.Now()) | ||
|
||
// TODO why do we have DiscoverCaPem as an array but JWKSCaPem as a single string? | ||
pem := "" | ||
if len(oidccapem) > 0 { | ||
pem = oidccapem[0] | ||
} | ||
|
||
keySet, err := jwt.NewOIDCDiscoveryKeySet(ctx, oidcurl, pem) | ||
if err != nil { | ||
return nil, fmt.Errorf("unable to get validation keys from OIDC provider: %v", err) | ||
} | ||
return keySet, nil | ||
} |
Oops, something went wrong.