Skip to content

Commit

Permalink
Add JWT flow
Browse files Browse the repository at this point in the history
  • Loading branch information
tzheleznyak committed Dec 26, 2021
1 parent b5ff748 commit 82cfcc7
Show file tree
Hide file tree
Showing 16 changed files with 697 additions and 59 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

### Added
- Add support for tracing with OpenTelemetry. This adds a new function to the authenticator, `AuthenticateWithContext`. The existing funtion, `Authenticate()` is deprecated and will be removed in a future upddate. [cyberark/conjur-authn-k8s-client#423](https://github.com/cyberark/conjur-authn-k8s-client/pull/423)
- Authn JWT flow support [cyberark/conjur-authn-k8s-client#426](https://github.com/cyberark/conjur-authn-k8s-client/pull/426)

### Changed
- The project Golang version is updated from the end-of-life v1.15 to version v1.16. [cyberark/conjur-authn-k8s-client#416](https://github.com/cyberark/conjur-authn-k8s-client/pull/416)
- Reduced default timeout for `waitForFile` from 1s to 50ms. [cyberark/conjur-authn-k8s-client#423](https://github.com/cyberark/conjur-authn-k8s-client/pull/423)
- Instead of getting K8S config object now you get Config Interface using NewConfigFromEnv() and ConfigFromEnv() [cyberark/conjur-authn-k8s-client#425](https://github.com/cyberark/conjur-authn-k8s-client/pull/425)
- Instead of getting K8S authenticator object now you get Authenticator Interface using NewAuthenticator() and NewAuthenticatorWithAccessToken() [cyberark/conjur-authn-k8s-client#425](https://github.com/cyberark/conjur-authn-k8s-client/pull/425)

## [0.22.0] - 2021-09-17
### Added
Expand Down
12 changes: 8 additions & 4 deletions pkg/authenticator/authenticator_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"github.com/cyberark/conjur-authn-k8s-client/pkg/access_token"
"github.com/cyberark/conjur-authn-k8s-client/pkg/access_token/file"
"github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/config"
jwtAuthenticator "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/jwt"
k8sAuthenticator "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/k8s"
"github.com/cyberark/conjur-authn-k8s-client/pkg/log"
)
Expand All @@ -25,9 +26,12 @@ func NewAuthenticatorWithAccessToken(conf config.Configuration, token access_tok
}

func getAuthenticator(conf config.Configuration, token access_token.AccessToken) (Authenticator, error) {
if conf.GetAuthenticationType() == k8sAuthenticator.AuthnType {
k8sCfg := (conf).(*k8sAuthenticator.Config)
return k8sAuthenticator.NewWithAccessToken(*k8sCfg, token)
switch c := conf.(type) {
case *k8sAuthenticator.Config:
return k8sAuthenticator.NewWithAccessToken(*c, token)
case *jwtAuthenticator.Config:
return jwtAuthenticator.NewWithAccessToken(*c, token)
default:
return nil, fmt.Errorf(log.CAKC064)
}
return nil, fmt.Errorf(log.CAKC064)
}
8 changes: 6 additions & 2 deletions pkg/authenticator/common/common_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,12 @@ func (config *Config) LoadConfig(settings map[string]string) {
case "CONJUR_ACCOUNT":
config.Account = value
case "CONJUR_AUTHN_LOGIN":
username, _ := NewUsername(value)
config.Username = username
if len(value) == 0 {
config.Username = nil
} else {
username, _ := NewUsername(value)
config.Username = username
}
case "CONJUR_AUTHN_URL":
config.URL = value
case "CONJUR_SSL_CERTIFICATE":
Expand Down
23 changes: 23 additions & 0 deletions pkg/authenticator/common/validations.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package common
import (
"errors"
"fmt"
"io/ioutil"
"os"
"strconv"

"github.com/cyberark/conjur-authn-k8s-client/pkg/log"
Expand All @@ -25,6 +27,9 @@ func validInt(key, value string) error {
}

func validUsername(key, value string) error {
if len(value) == 0 {
return nil
}
_, err := NewUsername(value)
return err
}
Expand Down Expand Up @@ -53,6 +58,8 @@ func ValidateSetting(key string, value string) error {
return validTimeout(key, value)
case "CONJUR_VERSION":
return validConjurVersion(key, value)
case "JWT_TOKEN_PATH":
return validatePath(value)
default:
return nil
}
Expand All @@ -70,3 +77,19 @@ func ReadSSLCert(settings map[string]string, readFile ReadFileFunc) ([]byte, err
}
return readFile(SSLCertPath)
}

func validatePath(path string) error {
// Check if file already exists
if _, err := os.Stat(path); err == nil {
return nil
}

// Attempt to create the file and delete it right after
var emptyData []byte
if err := ioutil.WriteFile(path, emptyData, 0644); err == nil {
os.Remove(path) // And delete it
return nil
}

return fmt.Errorf(log.CAKC065, path)
}
3 changes: 3 additions & 0 deletions pkg/authenticator/config/configuration_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"os"
"strings"

jwtAuthenticator "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/jwt"
k8sAuthenticator "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/k8s"
"github.com/cyberark/conjur-authn-k8s-client/pkg/log"
)
Expand Down Expand Up @@ -44,6 +45,8 @@ func ConfigFromEnv(readFileFunc common.ReadFileFunc) (Configuration, error) {
func getConfiguration(url string) (Configuration, error) {
if strings.Contains(url, k8sAuthenticator.AuthnType) {
return &k8sAuthenticator.Config{}, nil
} else if strings.Contains(url, jwtAuthenticator.AuthnType) {
return &jwtAuthenticator.Config{}, nil
}
return nil, fmt.Errorf(log.CAKC063, url)
}
Expand Down
55 changes: 45 additions & 10 deletions pkg/authenticator/config/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ func TestValidate(t *testing.T) {
assert errorAssertFunc
}{
{
description: "happy path",
description: "happy path - k8s",
settings: AuthnSettings{
// required variables
"CONJUR_AUTHN_URL": "filepath",
"CONJUR_AUTHN_URL": "authn-k8s",
"CONJUR_ACCOUNT": "testAccount",
"CONJUR_AUTHN_LOGIN": "host",
"CONJUR_AUTHN_LOGIN": "host/myapp",
"MY_POD_NAME": "testPodName",
"MY_POD_NAMESPACE": "testNameSpace",
// correct value types
Expand All @@ -37,15 +37,49 @@ func TestValidate(t *testing.T) {
},
assert: assertEmptyErrorList(),
},
{
description: "happy path - jwt",
settings: AuthnSettings{
// required variables
"CONJUR_AUTHN_URL": "authn-jwt",
"CONJUR_ACCOUNT": "testAccount",
"JWT_TOKEN_PATH": "/tmp/token",
// correct value types
"CONJUR_CLIENT_CERT_RETRY_COUNT_LIMIT": "7",
"CONJUR_TOKEN_TIMEOUT": "6m0s",
"CONTAINER_MODE": "init",
// certificate provided
"CONJUR_SSL_CERTIFICATE": "samplecertificate",
},
assert: assertEmptyErrorList(),
},
{
description: "invalid jwt token path",
settings: AuthnSettings{
// required variables
"CONJUR_AUTHN_URL": "authn-jwt",
"CONJUR_ACCOUNT": "testAccount",
// correct value types
"CONJUR_CLIENT_CERT_RETRY_COUNT_LIMIT": "7",
"CONJUR_TOKEN_TIMEOUT": "6m0s",
"CONTAINER_MODE": "init",
// certificate provided
"CONJUR_SSL_CERTIFICATE": "samplecertificate",
"JWT_TOKEN_PATH": "invalid//path",
},
assert: assertErrorInList(fmt.Errorf(logger.CAKC065, "invalid//path")),
},
{
description: "error raised for missing required setting",
settings: AuthnSettings{},
assert: assertErrorInList(fmt.Errorf(logger.CAKC062, "CONJUR_AUTHN_URL")),
settings: AuthnSettings{
"CONJUR_AUTHN_URL": "authn-jwt",
},
assert: assertErrorInList(fmt.Errorf(logger.CAKC062, "CONJUR_ACCOUNT")),
},
{
description: "error raised for invalid username",
settings: AuthnSettings{
"CONJUR_AUTHN_URL": "filepath",
"CONJUR_AUTHN_URL": "authn-k8s",
"CONJUR_ACCOUNT": "testAccount",
"CONJUR_AUTHN_LOGIN": "bad-username",
"MY_POD_NAME": "testPodName",
Expand All @@ -56,7 +90,7 @@ func TestValidate(t *testing.T) {
{
description: "error raised for invalid retry count limit",
settings: AuthnSettings{
"CONJUR_AUTHN_URL": "filepath",
"CONJUR_AUTHN_URL": "authn-k8s",
"CONJUR_ACCOUNT": "testAccount",
"CONJUR_AUTHN_LOGIN": "host",
"MY_POD_NAME": "testPodName",
Expand All @@ -68,7 +102,7 @@ func TestValidate(t *testing.T) {
{
description: "error raised for invalid timeout",
settings: AuthnSettings{
"CONJUR_AUTHN_URL": "filepath",
"CONJUR_AUTHN_URL": "authn-k8s",
"CONJUR_ACCOUNT": "testAccount",
"CONJUR_AUTHN_LOGIN": "host",
"MY_POD_NAME": "testPodName",
Expand All @@ -81,7 +115,7 @@ func TestValidate(t *testing.T) {
{
description: "error raised for invalid certificate",
settings: AuthnSettings{
"CONJUR_AUTHN_URL": "filepath",
"CONJUR_AUTHN_URL": "authn-k8s",
"CONJUR_ACCOUNT": "testAccount",
"CONJUR_AUTHN_LOGIN": "host",
"MY_POD_NAME": "testPodName",
Expand All @@ -99,7 +133,8 @@ func TestValidate(t *testing.T) {
for _, tc := range TestCases {
t.Run(tc.description, func(t *testing.T) {
// SETUP & EXERCISE
errLogs := tc.settings.validate(&k8s.Config{}, successfulMockReadFile)
configObj, _ := getConfiguration(tc.settings["CONJUR_AUTHN_URL"])
errLogs := tc.settings.validate(configObj, successfulMockReadFile)

// ASSERT
tc.assert(t, errLogs)
Expand Down
128 changes: 128 additions & 0 deletions pkg/authenticator/jwt/authenticator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package jwt

import (
"context"
"crypto/rand"
"crypto/rsa"
"fmt"
"go.opentelemetry.io/otel"
"net/http"
"os"

"github.com/cyberark/conjur-authn-k8s-client/pkg/access_token"
"github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/common"
"github.com/cyberark/conjur-authn-k8s-client/pkg/log"
"github.com/cyberark/conjur-authn-k8s-client/pkg/utils"
"github.com/cyberark/secrets-provider-for-k8s/pkg/trace"
)

// Authenticator contains the configuration and client
// for the authentication connection to Conjur
type Authenticator struct {
client *http.Client
privateKey *rsa.PrivateKey
accessToken access_token.AccessToken
Config *Config
}

// NewWithAccessToken creates a new authenticator instance from a given access token
func NewWithAccessToken(config Config, accessToken access_token.AccessToken) (*Authenticator, error) {
signingKey, err := rsa.GenerateKey(rand.Reader, 1024)
if err != nil {
return nil, log.RecordedError(log.CAKC030, err)
}

client, err := common.NewHTTPSClient(config.Common.SSLCertificate, nil, nil)
if err != nil {
return nil, err
}

return &Authenticator{
client: client,
privateKey: signingKey,
accessToken: accessToken,
Config: &config,
}, nil
}

// Authenticate sends Conjur an authenticate request and writes the response
// to the token file (after decrypting it if needed). It also manages state of
// certificates.
// @deprecated Use AuthenticateWithContext instead
func (auth *Authenticator) Authenticate() error {
return auth.AuthenticateWithContext(context.TODO())
}

func (auth *Authenticator) AuthenticateWithContext(ctx context.Context) error {
log.Info(log.CAKC066)

tr := trace.NewOtelTracer(otel.Tracer("conjur-authn-k8s-client"))
spanCtx, span := tr.Start(ctx, "Authenticate")
defer span.End()

authenticationResponse, err := auth.sendAuthenticationRequest(spanCtx, tr)
if err != nil {
span.RecordErrorAndSetStatus(err)
span.End()
return err
}

err = auth.accessToken.Write(authenticationResponse)
if err != nil {
return err
}

log.Info(log.CAKC035)
return nil
}

// sendAuthenticationRequest reads the JWT token from the file system and sends
// an authentication request to the Conjur server. It also validates the response
// code before returning its body
func (auth *Authenticator) sendAuthenticationRequest(ctx context.Context, tracer trace.Tracer) ([]byte, error) {
var authenticatingIdentity string

_, span := tracer.Start(ctx, "Send authentication request")
defer span.End()

jwtToken, err := loadJWTToken(auth.Config.JWTTokenFilePath)

if err != nil {
return nil, err
}

if auth.Config.Common.Username != nil {
authenticatingIdentity = auth.Config.Common.Username.FullUsername
} else {
authenticatingIdentity = ""
}

req, err := AuthenticateRequest(
auth.Config.Common.URL,
auth.Config.Common.Account,
authenticatingIdentity,
jwtToken,
)

resp, err := auth.client.Do(req)

if err != nil {
return nil, log.RecordedError(log.CAKC027, err)
}

err = utils.ValidateResponse(resp)
if err != nil {
return nil, err
}

return utils.ReadResponseBody(resp)
}

func loadJWTToken(path string) (string, error) {
data, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf(log.CAKC067, path)
}

return string(data), nil
}
Loading

0 comments on commit 82cfcc7

Please sign in to comment.