Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

acl: JWT as SSO auth method #15897

Merged
merged 9 commits into from
Mar 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/15897.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
acl: New auth-method type: JWT
```
1 change: 1 addition & 0 deletions .semgrep/rpc_endpoint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ rules:
- pattern-not: 'structs.ACLListAuthMethodsRPCMethod'
- pattern-not: 'structs.ACLOIDCAuthURLRPCMethod'
- pattern-not: 'structs.ACLOIDCCompleteAuthRPCMethod'
- pattern-not: 'structs.ACLLoginRPCMethod'
- pattern-not: '"CSIPlugin.Get"'
- pattern-not: '"CSIPlugin.List"'
- pattern-not: '"Status.Leader"'
Expand Down
191 changes: 175 additions & 16 deletions api/acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package api
import (
"encoding/json"
"errors"
"fmt"
"time"
)

Expand Down Expand Up @@ -443,18 +444,33 @@ func (a *ACLBindingRules) Get(bindingRuleID string, q *QueryOptions) (*ACLBindin
}

// ACLOIDC is used to query the ACL OIDC endpoints.
//
// Deprecated: ACLOIDC is deprecated, use ACLAuth instead.
type ACLOIDC struct {
client *Client
ACLAuth
}

// ACLOIDC returns a new handle on the ACL auth-methods API client.
//
// Deprecated: c.ACLOIDC() is deprecated, use c.ACLAuth() instead.
func (c *Client) ACLOIDC() *ACLOIDC {
return &ACLOIDC{client: c}
}

// ACLAuth is used to query the ACL auth endpoints.
type ACLAuth struct {
client *Client
}

// ACLAuth returns a new handle on the ACL auth-methods API client.
func (c *Client) ACLAuth() *ACLAuth {
return &ACLAuth{client: c}
}

// GetAuthURL generates the OIDC provider authentication URL. This URL should
// be visited in order to sign in to the provider.
func (a *ACLOIDC) GetAuthURL(req *ACLOIDCAuthURLRequest, q *WriteOptions) (*ACLOIDCAuthURLResponse, *WriteMeta, error) {
func (a *ACLAuth) GetAuthURL(req *ACLOIDCAuthURLRequest, q *WriteOptions) (*ACLOIDCAuthURLResponse, *WriteMeta, error) {
var resp ACLOIDCAuthURLResponse
wm, err := a.client.put("/v1/acl/oidc/auth-url", req, &resp, q)
if err != nil {
Expand All @@ -465,7 +481,7 @@ func (a *ACLOIDC) GetAuthURL(req *ACLOIDCAuthURLRequest, q *WriteOptions) (*ACLO

// CompleteAuth exchanges the OIDC provider token for a Nomad token with the
// appropriate claims attached.
func (a *ACLOIDC) CompleteAuth(req *ACLOIDCCompleteAuthRequest, q *WriteOptions) (*ACLToken, *WriteMeta, error) {
func (a *ACLAuth) CompleteAuth(req *ACLOIDCCompleteAuthRequest, q *WriteOptions) (*ACLToken, *WriteMeta, error) {
var resp ACLToken
wm, err := a.client.put("/v1/acl/oidc/complete-auth", req, &resp, q)
if err != nil {
Expand All @@ -474,6 +490,17 @@ func (a *ACLOIDC) CompleteAuth(req *ACLOIDCCompleteAuthRequest, q *WriteOptions)
return &resp, wm, nil
}

// Login exchanges the third party token for a Nomad token with the appropriate
// claims attached.
func (a *ACLAuth) Login(req *ACLLoginRequest, q *WriteOptions) (*ACLToken, *WriteMeta, error) {
var resp ACLToken
wm, err := a.client.put("/v1/acl/login", req, &resp, q)
if err != nil {
return nil, nil, err
}
return &resp, wm, nil
}

// ACLPolicyListStub is used to for listing ACL policies
type ACLPolicyListStub struct {
Name string
Expand Down Expand Up @@ -740,20 +767,6 @@ type ACLAuthMethod struct {
ModifyIndex uint64
}

// ACLAuthMethodConfig is used to store configuration of an auth method.
type ACLAuthMethodConfig struct {
OIDCDiscoveryURL string
OIDCClientID string
OIDCClientSecret string
OIDCScopes []string
BoundAudiences []string
AllowedRedirectURIs []string
DiscoveryCaPem []string
SigningAlgs []string
ClaimMappings map[string]string
ListClaimMappings map[string]string
}

// MarshalJSON implements the json.Marshaler interface and allows
// ACLAuthMethod.MaxTokenTTL to be marshaled correctly.
func (m *ACLAuthMethod) MarshalJSON() ([]byte, error) {
Expand Down Expand Up @@ -793,6 +806,138 @@ func (m *ACLAuthMethod) UnmarshalJSON(data []byte) error {
return nil
}

// ACLAuthMethodConfig is used to store configuration of an auth method.
type ACLAuthMethodConfig struct {
// A list of PEM-encoded public keys to use to authenticate signatures
// locally
JWTValidationPubKeys []string
// JSON Web Key Sets url for authenticating signatures
JWKSURL string
// The OIDC Discovery URL, without any .well-known component (base path)
OIDCDiscoveryURL string
// The OAuth Client ID configured with the OIDC provider
OIDCClientID string
// The OAuth Client Secret configured with the OIDC provider
OIDCClientSecret string
// List of OIDC scopes
OIDCScopes []string
// List of auth claims that are valid for login
BoundAudiences []string
// The value against which to match the iss claim in a JWT
BoundIssuer []string
// A list of allowed values for redirect_uri
AllowedRedirectURIs []string
// PEM encoded CA certs for use by the TLS client used to talk with the
// OIDC Discovery URL.
DiscoveryCaPem []string
// PEM encoded CA cert for use by the TLS client used to talk with the JWKS
// URL
JWKSCACert string
// A list of supported signing algorithms
SigningAlgs []string
// Duration in seconds of leeway when validating expiration of a token to
// account for clock skew
ExpirationLeeway time.Duration
// Duration in seconds of leeway when validating not before values of a
// token to account for clock skew.
NotBeforeLeeway time.Duration
// Duration in seconds of leeway when validating all claims to account for
// clock skew.
ClockSkewLeeway time.Duration
// Mappings of claims (key) that will be copied to a metadata field
// (value).
ClaimMappings map[string]string
ListClaimMappings map[string]string
}

// MarshalJSON implements the json.Marshaler interface and allows
// time.Duration fields to be marshaled correctly.
func (c *ACLAuthMethodConfig) MarshalJSON() ([]byte, error) {
type Alias ACLAuthMethodConfig
exported := &struct {
ExpirationLeeway string
NotBeforeLeeway string
ClockSkewLeeway string
*Alias
}{
ExpirationLeeway: c.ExpirationLeeway.String(),
NotBeforeLeeway: c.NotBeforeLeeway.String(),
ClockSkewLeeway: c.ClockSkewLeeway.String(),
Alias: (*Alias)(c),
}
if c.ExpirationLeeway == 0 {
exported.ExpirationLeeway = ""
}
if c.NotBeforeLeeway == 0 {
exported.NotBeforeLeeway = ""
}
if c.ClockSkewLeeway == 0 {
exported.ClockSkewLeeway = ""
}
return json.Marshal(exported)
}

// UnmarshalJSON implements the json.Unmarshaler interface and allows
// time.Duration fields to be unmarshalled correctly.
func (c *ACLAuthMethodConfig) UnmarshalJSON(data []byte) error {
type Alias ACLAuthMethodConfig
aux := &struct {
ExpirationLeeway any
NotBeforeLeeway any
ClockSkewLeeway any
*Alias
}{
Alias: (*Alias)(c),
}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
var err error
if aux.ExpirationLeeway != nil {
switch v := aux.ExpirationLeeway.(type) {
case string:
if v != "" {
if c.ExpirationLeeway, err = time.ParseDuration(v); err != nil {
return err
}
}
case float64:
c.ExpirationLeeway = time.Duration(v)
default:
return fmt.Errorf("unexpected ExpirationLeeway type: %v", v)
}
}
if aux.NotBeforeLeeway != nil {
switch v := aux.NotBeforeLeeway.(type) {
case string:
if v != "" {
if c.NotBeforeLeeway, err = time.ParseDuration(v); err != nil {
return err
}
}
case float64:
c.NotBeforeLeeway = time.Duration(v)
default:
return fmt.Errorf("unexpected NotBeforeLeeway type: %v", v)
}
}
if aux.ClockSkewLeeway != nil {
switch v := aux.ClockSkewLeeway.(type) {
case string:
if v != "" {
if c.ClockSkewLeeway, err = time.ParseDuration(v); err != nil {
return err
}
}
case float64:
c.ClockSkewLeeway = time.Duration(v)
default:
return fmt.Errorf("unexpected ClockSkewLeeway type: %v", v)
}
}
return nil
}

// ACLAuthMethodListStub is the stub object returned when performing a listing
// of ACL auth-methods. It is intentionally minimal due to the unauthenticated
// nature of the list endpoint.
Expand All @@ -818,6 +963,10 @@ const (
// ACLAuthMethodTypeOIDC the ACLAuthMethod.Type and represents an
// auth-method which uses the OIDC protocol.
ACLAuthMethodTypeOIDC = "OIDC"

// ACLAuthMethodTypeJWT the ACLAuthMethod.Type and represents an auth-method
// which uses the JWT type.
ACLAuthMethodTypeJWT = "JWT"
)

// ACLBindingRule contains a direct relation to an ACLAuthMethod and represents
Expand Down Expand Up @@ -947,3 +1096,13 @@ type ACLOIDCCompleteAuthRequest struct {
// required parameter.
RedirectURI string
}

// ACLLoginRequest is the request object to begin auth with an external bearer
// token provider.
type ACLLoginRequest struct {
// AuthMethodName is the name of the auth method being used to login. This
// is a required parameter.
AuthMethodName string
// LoginToken is the token used to login. This is a required parameter.
LoginToken string
}
7 changes: 7 additions & 0 deletions command/acl_auth_method.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,21 @@ func formatAuthMethod(authMethod *api.ACLAuthMethod) string {

func formatAuthMethodConfig(config *api.ACLAuthMethodConfig) string {
out := []string{
fmt.Sprintf("JWT Validation Public Keys|%s", strings.Join(config.JWTValidationPubKeys, ",")),
fmt.Sprintf("JWKS URL|%s", config.JWKSURL),
fmt.Sprintf("OIDC Discovery URL|%s", config.OIDCDiscoveryURL),
fmt.Sprintf("OIDC Client ID|%s", config.OIDCClientID),
fmt.Sprintf("OIDC Client Secret|%s", config.OIDCClientSecret),
fmt.Sprintf("OIDC Scopes|%s", strings.Join(config.OIDCScopes, ",")),
fmt.Sprintf("Bound audiences|%s", strings.Join(config.BoundAudiences, ",")),
fmt.Sprintf("Bound issuer|%s", strings.Join(config.BoundIssuer, ",")),
fmt.Sprintf("Allowed redirects URIs|%s", strings.Join(config.AllowedRedirectURIs, ",")),
fmt.Sprintf("Discovery CA pem|%s", strings.Join(config.DiscoveryCaPem, ",")),
fmt.Sprintf("JWKS CA cert|%s", config.JWKSCACert),
fmt.Sprintf("Signing algorithms|%s", strings.Join(config.SigningAlgs, ",")),
fmt.Sprintf("Expiration Leeway|%s", config.ExpirationLeeway.String()),
fmt.Sprintf("NotBefore Leeway|%s", config.NotBeforeLeeway.String()),
fmt.Sprintf("ClockSkew Leeway|%s", config.ClockSkewLeeway.String()),
fmt.Sprintf("Claim mappings|%s", strings.Join(formatMap(config.ClaimMappings), "; ")),
fmt.Sprintf("List claim mappings|%s", strings.Join(formatMap(config.ListClaimMappings), "; ")),
}
Expand Down
9 changes: 4 additions & 5 deletions command/acl_auth_method_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,7 @@ ACL Auth Method Create Options:
between 1-128 characters and is a required parameter.

-type
Sets the type of the auth method. Currently the only supported type is
'OIDC'.
Sets the type of the auth method. Supported types are 'OIDC' and 'JWT'.

-max-token-ttl
Sets the duration of time all tokens created by this auth method should be
Expand Down Expand Up @@ -83,7 +82,7 @@ func (a *ACLAuthMethodCreateCommand) AutocompleteFlags() complete.Flags {
return mergeAutocompleteFlags(a.Meta.AutocompleteFlags(FlagSetClient),
complete.Flags{
"-name": complete.PredictAnything,
"-type": complete.PredictSet("OIDC"),
"-type": complete.PredictSet("OIDC", "JWT"),
"-max-token-ttl": complete.PredictAnything,
"-token-locality": complete.PredictSet("local", "global"),
"-default": complete.PredictSet("true", "false"),
Expand Down Expand Up @@ -140,8 +139,8 @@ func (a *ACLAuthMethodCreateCommand) Run(args []string) int {
a.Ui.Error("Max token TTL must be set to a value between min and max TTL configured for the server.")
return 1
}
if strings.ToUpper(a.methodType) != "OIDC" {
a.Ui.Error("ACL auth method type must be set to 'OIDC'")
if !slices.Contains([]string{"OIDC", "JWT"}, strings.ToUpper(a.methodType)) {
a.Ui.Error("ACL auth method type must be set to 'OIDC' or 'JWT'")
return 1
}
if len(a.config) == 0 {
Expand Down
2 changes: 1 addition & 1 deletion command/acl_auth_method_create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func TestACLAuthMethodCreateCommand_Run(t *testing.T) {
args := []string{
"-address=" + url, "-token=" + rootACLToken.SecretID, "-name=acl-auth-method-cli-test",
"-type=OIDC", "-token-locality=global", "-default=true", "-max-token-ttl=3600s",
"-config={\"OIDCDiscoveryURL\":\"http://example.com\"}",
"-config={\"OIDCDiscoveryURL\":\"http://example.com\", \"ExpirationLeeway\": \"1h\"}",
}
must.Eq(t, 0, cmd.Run(args))
s := ui.OutputWriter.String()
Expand Down
9 changes: 4 additions & 5 deletions command/acl_auth_method_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,7 @@ General Options:
ACL Auth Method Update Options:

-type
Updates the type of the auth method. Currently the only supported type is
'OIDC'.
Updates the type of the auth method. Supported types are 'OIDC' and 'JWT'.

-max-token-ttl
Updates the duration of time all tokens created by this auth method should be
Expand Down Expand Up @@ -79,7 +78,7 @@ ACL Auth Method Update Options:
func (a *ACLAuthMethodUpdateCommand) AutocompleteFlags() complete.Flags {
return mergeAutocompleteFlags(a.Meta.AutocompleteFlags(FlagSetClient),
complete.Flags{
"-type": complete.PredictSet("OIDC"),
"-type": complete.PredictSet("OIDC", "JWT"),
"-max-token-ttl": complete.PredictAnything,
"-token-locality": complete.PredictSet("local", "global"),
"-default": complete.PredictSet("true", "false"),
Expand Down Expand Up @@ -161,8 +160,8 @@ func (a *ACLAuthMethodUpdateCommand) Run(args []string) int {
}

if slices.Contains(setFlags, "type") {
if strings.ToLower(a.methodType) != "oidc" {
a.Ui.Error("ACL auth method type must be set to 'OIDC'")
if !slices.Contains([]string{"OIDC", "JWT"}, strings.ToUpper(a.methodType)) {
a.Ui.Error("ACL auth method type must be set to 'OIDC' or 'JWT'")
return 1
}
updatedMethod.Type = a.methodType
Expand Down
21 changes: 20 additions & 1 deletion command/agent/acl_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -867,10 +867,29 @@ func (s *HTTPServer) ACLOIDCCompleteAuthRequest(resp http.ResponseWriter, req *h
return nil, CodedError(http.StatusBadRequest, err.Error())
}

var out structs.ACLOIDCCompleteAuthResponse
var out structs.ACLLoginResponse
if err := s.agent.RPC(structs.ACLOIDCCompleteAuthRPCMethod, &args, &out); err != nil {
return nil, err
}
setIndex(resp, out.Index)
return out.ACLToken, nil
}

// ACLLoginRequest performs a non-interactive authentication request
func (s *HTTPServer) ACLLoginRequest(resp http.ResponseWriter, req *http.Request) (any, error) {
// The endpoint only supports PUT or POST requests.
if req.Method != http.MethodPost && req.Method != http.MethodPut {
return nil, CodedError(http.StatusMethodNotAllowed, ErrInvalidMethod)
}
var args structs.ACLLoginRequest
s.parseWriteRequest(req, &args.WriteRequest)
if err := decodeBody(req, &args); err != nil {
return nil, CodedError(http.StatusBadRequest, err.Error())
}
var out structs.ACLLoginResponse
if err := s.agent.RPC(structs.ACLLoginRPCMethod, &args, &out); err != nil {
return nil, err
}
setIndex(resp, out.Index)
return out.ACLToken, nil
}
Loading