Skip to content

Commit

Permalink
Merge pull request #12 from Conjur-Enterprise/refactor-jwt
Browse files Browse the repository at this point in the history
CNJR-4190: Refactor JWT authentication to use standard config options
  • Loading branch information
szh authored and GitHub Enterprise committed May 21, 2024
2 parents ecfc8c7 + f465403 commit 8e510b0
Show file tree
Hide file tree
Showing 12 changed files with 380 additions and 121 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

## [0.12.0] - 2024-05-05

### Changed
- Reworked JWT authentication to use standard configuration options (CNJR-4190)

## [0.11.4] - 2024-05-09

### Security
Expand Down
16 changes: 8 additions & 8 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
ARG FROM_IMAGE="golang:1.22"
FROM ${FROM_IMAGE}
MAINTAINER Conjur Inc.
LABEL maintainer="CyberArk Software Ltd."

CMD /bin/bash
EXPOSE 8080

RUN apt update -y && \
apt install -y bash \
gcc \
git \
jq \
less \
libc-dev
RUN apt-get update -y && \
apt-get install -y bash \
gcc \
git \
jq \
less \
libc-dev

RUN go install github.com/jstemmer/go-junit-report@latest && \
go install github.com/axw/gocov/gocov@latest && \
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ questions, please contact us on [Discourse](https://discuss.cyberarkcommons.org/

The `conjur-api-go` has been tested against the following Go versions:

- 1.20
- 1.21
- 1.22

Expand Down Expand Up @@ -191,6 +192,6 @@ guide][contrib].

## License

Copyright (c) 2022 CyberArk Software Ltd. All rights reserved.
Copyright (c) 2022-2024 CyberArk Software Ltd. All rights reserved.

This repository is licensed under Apache License 2.0 - see [`LICENSE`](LICENSE) for more details.
20 changes: 20 additions & 0 deletions conjurapi/authn.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,26 @@ func (c *Client) OidcAuthenticate(code, nonce, code_verifier string) ([]byte, er
return resp, err
}

func (c *Client) JWTAuthenticate(jwt, hostID string) ([]byte, error) {
req, err := c.JWTAuthenticateRequest(jwt, hostID)
if err != nil {
return nil, err
}

res, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}

resp, err := response.DataResponse(res)

if err == nil && c.storage != nil {
c.storage.StoreAuthnToken(resp)
}

return resp, err
}

func (c *Client) ListOidcProviders() ([]OidcProvider, error) {
req, err := c.ListOidcProvidersRequest()
if err != nil {
Expand Down
63 changes: 63 additions & 0 deletions conjurapi/authn/jwt_authenticator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package authn

import (
"fmt"
"os"

"github.com/cyberark/conjur-api-go/conjurapi/logging"
)

type JWTAuthenticator struct {
JWT string
JWTFilePath string
HostID string
Authenticate func(jwt, hostId string) ([]byte, error)
}

const k8sJWTPath = "/var/run/secrets/kubernetes.io/serviceaccount/token"

func (a *JWTAuthenticator) RefreshToken() ([]byte, error) {
err := a.RefreshJWT()
if err != nil {
return nil, fmt.Errorf("Failed to refresh JWT: %v", err)
}
return a.Authenticate(a.JWT, a.HostID)
}

func (a *JWTAuthenticator) NeedsTokenRefresh() bool {
return false
}

func (a *JWTAuthenticator) RefreshJWT() error {
// If a JWT token is already set or retrieved, do nothing.
if a.JWT != "" {
logging.ApiLog.Debugf("Using stored JWT")
return nil
}

// If a token file path is provided, read the JWT token from the file.
// Otherwise, read the token from the default Kubernetes service account path.
var jwtFilePath string
if a.JWTFilePath != "" {
logging.ApiLog.Debugf("Reading JWT from %s", a.JWTFilePath)
jwtFilePath = a.JWTFilePath
} else {
jwtFilePath = k8sJWTPath
logging.ApiLog.Debugf("No JWT file path set. Attempting to ready JWT from %s", jwtFilePath)
}

token, err := readJWTFromFile(jwtFilePath)
if err != nil {
return err
}
a.JWT = token
return nil
}

func readJWTFromFile(filePath string) (string, error) {
bytes, err := os.ReadFile(filePath)
if err != nil {
return "", err
}
return string(bytes), nil
}
105 changes: 105 additions & 0 deletions conjurapi/authn/jwt_authenticator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package authn

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
)

func TestJWTAuthenticator_RefreshToken(t *testing.T) {
// Test that the RefreshToken method calls the Authenticate method
t.Run("Calls Authenticate with stored JWT", func(t *testing.T) {
authenticator := JWTAuthenticator{
Authenticate: func(jwt, hostid string) ([]byte, error) {
assert.Equal(t, "jwt", jwt)
assert.Equal(t, "", hostid)
return []byte("token"), nil
},
JWT: "jwt",
}

token, err := authenticator.RefreshToken()

assert.NoError(t, err)
assert.Equal(t, []byte("token"), token)
})

t.Run("Calls Authenticate with JWT from file", func(t *testing.T) {
tempDir := t.TempDir()
err := os.WriteFile(filepath.Join(tempDir, "jwt"), []byte("jwt-content"), 0600)
assert.NoError(t, err)

authenticator := JWTAuthenticator{
Authenticate: func(jwt, hostid string) ([]byte, error) {
assert.Equal(t, "jwt-content", jwt)
assert.Equal(t, "host-id", hostid)
return []byte("token"), nil
},
JWTFilePath: filepath.Join(tempDir, "jwt"),
HostID: "host-id",
}

token, err := authenticator.RefreshToken()
assert.NoError(t, err)
assert.Equal(t, []byte("token"), token)
})

t.Run("Defaults to Kubernetes service account path", func(t *testing.T) {
authenticator := JWTAuthenticator{
Authenticate: func(jwt, hostid string) ([]byte, error) {
assert.Equal(t, "k8s-jwt-content", jwt)
assert.Equal(t, "", hostid)
return []byte("token"), nil
},
}

// Note: this may fail when not running in a container
err := os.MkdirAll(filepath.Dir(k8sJWTPath), 0755)
assert.NoError(t, err)
err = os.WriteFile(k8sJWTPath, []byte("k8s-jwt-content"), 0600)
assert.NoError(t, err)

token, err := authenticator.RefreshToken()
assert.NoError(t, err)
assert.Equal(t, []byte("token"), token)

t.Cleanup(func() {
os.Remove(k8sJWTPath)
})
})

t.Run("Returns error when Authenticate fails", func(t *testing.T) {
authenticator := JWTAuthenticator{
Authenticate: func(jwt, hostid string) ([]byte, error) {
return nil, assert.AnError
},
}

token, err := authenticator.RefreshToken()
assert.Error(t, err)
assert.Nil(t, token)
})

t.Run("Returns error when no JWT provided", func(t *testing.T) {
authenticator := JWTAuthenticator{
Authenticate: func(jwt, hostid string) ([]byte, error) {
return nil, nil
},
}

token, err := authenticator.RefreshToken()
assert.ErrorContains(t, err, "Failed to refresh JWT")
assert.Nil(t, token)
})
}

func TestJWTAuthenticator_NeedsTokenRefresh(t *testing.T) {
t.Run("Returns false", func(t *testing.T) {
// Test that the NeedsTokenRefresh method always returns false
authenticator := JWTAuthenticator{}

assert.False(t, authenticator.NeedsTokenRefresh())
})
}
15 changes: 15 additions & 0 deletions conjurapi/authn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,18 @@ func TestClient_Login(t *testing.T) {
assert.Contains(t, string(contents), client.GetConfig().ApplianceURL+"/authn-oidc/test-service-id")
assert.Contains(t, string(contents), "test-token-oidc")
})

t.Run("JWT authentication", func(t *testing.T) {
ts, client := setupTestClient(t)
defer ts.Close()

client.config.AuthnType = "jwt"
client.config.ServiceID = "test-service-id"

token, err := client.JWTAuthenticate("jwt", "")
assert.NoError(t, err)
assert.Equal(t, "test-token-jwt", string(token))
})
}

func TestClient_AuthenticateReader(t *testing.T) {
Expand Down Expand Up @@ -632,6 +644,9 @@ func setupTestClient(t *testing.T) (*httptest.Server, *Client) {
} else if strings.HasSuffix(r.URL.Path, "/authn-oidc/test-service-id/cucumber/authenticate") {
w.WriteHeader(http.StatusOK)
w.Write([]byte("test-token-oidc"))
} else if strings.HasSuffix(r.URL.Path, "/authn-jwt/test-service-id/cucumber/authenticate") {
w.WriteHeader(http.StatusOK)
w.Write([]byte("test-token-jwt"))
} else if strings.HasSuffix(r.URL.Path, "/authn-oidc/cucumber/providers") {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`[{"service_id": "test-service-id"}]`))
Expand Down
Loading

0 comments on commit 8e510b0

Please sign in to comment.