Skip to content

Commit

Permalink
cache token in file
Browse files Browse the repository at this point in the history
  • Loading branch information
szh committed Dec 15, 2022
1 parent d408e03 commit e678fa2
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 10 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

### Added
- Added support for Conjur's LDAP authenticator
[cyberark/conjur-api-go#141](https://github.com/cyberark/conjur-api-go/pull/141)
- Added support for Conjur's OIDC authenticator
[cyberark/conjur-api-go#144](https://github.com/cyberark/conjur-api-go/pull/144)

### Removed
- Remove all usage of Conjur v4
[cyberark/conjur-api-go#139](https://github.com/cyberark/conjur-api-go/pull/139)
Expand Down
65 changes: 64 additions & 1 deletion conjurapi/authn.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ package conjurapi

import (
"encoding/base64"
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"

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

Expand All @@ -23,6 +27,14 @@ type OidcProviderResponse struct {
func (c *Client) RefreshToken() (err error) {
var token *authn.AuthnToken

// Fetch cached conjur access token if using OIDC
if c.GetConfig().AuthnType == "oidc" {
token := c.readCachedAccessToken()
if token != nil {
c.authToken = token
}
}

if c.NeedsTokenRefresh() {
var tokenBytes []byte
tokenBytes, err = c.authenticator.RefreshToken()
Expand Down Expand Up @@ -137,7 +149,13 @@ func (c *Client) OidcAuthenticate(code, nonce, code_verifier string) ([]byte, er
return nil, err
}

return response.DataResponse(res)
resp, err := response.DataResponse(res)

if err == nil {
c.cacheAccessToken(resp)
}

return resp, err
}

func (c *Client) ListOidcProviders() ([]OidcProviderResponse, error) {
Expand Down Expand Up @@ -212,3 +230,48 @@ func (c *Client) rotateAPIKey(roleID string) (*http.Response, error) {

return c.SubmitRequest(req)
}

func (c *Client) oidcTokenPath() string {
oidcTokenPath := c.GetConfig().OidcTokenPath
if oidcTokenPath == "" {
oidcTokenPath = defaultOidcTokenPath
}
return oidcTokenPath
}

// Caches the conjur access token. We only cache this for OIDC since we don't have access
// to the Conjur API key and this is the only credential we can save.
// TODO: Perhaps .netrc storage should be moved to the conjur-api-go repository. At that point we could store
// the access token there as we do with the API key.
func (c *Client) cacheAccessToken(token []byte) error {
if token == nil {
return nil
}

oidcTokenPath := c.oidcTokenPath()

// Ensure the directory exists
_, err := os.Stat(oidcTokenPath)
if err != nil && errors.Is(err, os.ErrNotExist) {
dir := filepath.Dir(oidcTokenPath)
os.MkdirAll(dir, os.ModePerm)
}
err = os.WriteFile(oidcTokenPath, token, 0600)
if err != nil {
logging.ApiLog.Debugf("Failed to write access token to %s: %s", oidcTokenPath, err)
}
return nil
}

// Fetches the cached conjur access token. We only do this for OIDC since we don't have access
// to the Conjur API key and this is the only credential we can save.
func (c *Client) readCachedAccessToken() *authn.AuthnToken {
if contents, err := os.ReadFile(c.oidcTokenPath()); err == nil {
token, err := authn.NewToken(contents)
if err == nil {
token.FromJSON(contents)
return token
}
}
return nil
}
1 change: 0 additions & 1 deletion conjurapi/authn/oidc_authenticator.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ type OidcAuthenticator struct {

func (a *OidcAuthenticator) RefreshToken() ([]byte, error) {
return a.Authenticate(a.Code, a.Nonce, a.CodeVerifier)
//TODO: We need to save the JWT in the CLI
}

func (a *OidcAuthenticator) NeedsTokenRefresh() bool {
Expand Down
95 changes: 95 additions & 0 deletions conjurapi/authn_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package conjurapi

import (
"io"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"

"github.com/cyberark/conjur-api-go/conjurapi/authn"
Expand Down Expand Up @@ -145,3 +150,93 @@ func runRotateUserAPIKeyAssertions(t *testing.T, tc rotateUserAPIKeyTestCase, co
_, err = conjur.Authenticate(authn.LoginPair{Login: tc.login, APIKey: string(userAPIKey)})
assert.NoError(t, err)
}

func TestClient_OidcAuthenticate(t *testing.T) {
testCases := []struct {
name string
tokenPath string
expectFileWriteError bool
}{
{
name: "Caches token to default location",
tokenPath: "",
},
{
name: "Caches token to custom location",
tokenPath: t.TempDir() + "/tmp/test-token",
},
{
// Writing this file will fail but there should be no error returned from OidcAuthenticate()
name: "Caches token to custom location with trailing slash",
tokenPath: t.TempDir() + "/tmp/test-token/",
expectFileWriteError: true,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ts, client := setupTestOidcClient(tc.tokenPath)
defer ts.Close()

token, err := client.OidcAuthenticate("code", "nonce", "code-verifier")

assert.NoError(t, err)
assert.Equal(t, "test-token", string(token))

if tc.expectFileWriteError {
// File writing should have failed, so the token should not have been cached
// We just want to test that there was no error returned from OidcAuthenticate()
return
}

// Check that token was cached to the correct location
var tokenPath string
if tc.tokenPath == "" {
tokenPath = defaultOidcTokenPath
} else {
tokenPath = tc.tokenPath
}

// Check file permissions
fileInfo, err := os.Stat(tokenPath)
assert.NoError(t, err)
assert.Equal(t, os.FileMode(0600), fileInfo.Mode().Perm())

// Check file contents
tokenFile, err := os.Open(tokenPath)
assert.NoError(t, err)

tokenBytes, err := io.ReadAll(tokenFile)
assert.NoError(t, err)
assert.Equal(t, "test-token", string(tokenBytes))

// Cleanup
os.Remove(tokenPath)
})
}
}

func setupTestOidcClient(tokenPath string) (*httptest.Server, *Client) {
mockConjurServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Listen for the authenticate endpoint and return a test token
if strings.HasSuffix(r.URL.Path, "/authn-oidc/test-provider/cucumber/authenticate") {
w.WriteHeader(http.StatusOK)
w.Write([]byte("test-token"))
} else {
w.WriteHeader(http.StatusNotFound)
}
}))

client := &Client{
config: Config{
Account: "cucumber",
ApplianceURL: mockConjurServer.URL,
AuthnType: "oidc",
ServiceID: "test-provider",
OidcTokenPath: tokenPath,
},
httpClient: &http.Client{},
}

return mockConjurServer, client
}
17 changes: 16 additions & 1 deletion conjurapi/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ func NewClientFromOidcCode(config Config, code, nonce, code_verifier string) (*C
config,
authenticator,
)
authenticator.Authenticate = client.OidcAuthenticate
if err != nil {
authenticator.Authenticate = client.OidcAuthenticate
}
return client, err
}

Expand Down Expand Up @@ -189,6 +191,19 @@ func NewClientFromEnvironment(config Config) (*Client, error) {
return NewClientFromKey(config, *loginPair)
}

if config.AuthnType == "oidc" {
client, err := NewClientFromOidcCode(config, "", "", "")
if err != nil {
return nil, err
}
token := client.readCachedAccessToken()
if token != nil && !token.ShouldRefresh() {
return client, nil
}

return nil, fmt.Errorf("No valid OIDC token found. Please login again.")
}

return nil, fmt.Errorf("Environment variables and machine identity files satisfying at least one authentication strategy must be present!")
}

Expand Down
16 changes: 9 additions & 7 deletions conjurapi/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,17 @@ import (
)

var supportedAuthnTypes = []string{"authn", "ldap", "oidc"}
var defaultOidcTokenPath = os.ExpandEnv("$HOME/.conjur/oidc_token")

type Config struct {
Account string `yaml:"account,omitempty"`
ApplianceURL string `yaml:"appliance_url,omitempty"`
NetRCPath string `yaml:"netrc_path,omitempty"`
SSLCert string `yaml:"-"`
SSLCertPath string `yaml:"cert_file,omitempty"`
AuthnType string `yaml:"authn_type,omitempty"`
ServiceID string `yaml:"service_id,omitempty"`
Account string `yaml:"account,omitempty"`
ApplianceURL string `yaml:"appliance_url,omitempty"`
NetRCPath string `yaml:"netrc_path,omitempty"`
SSLCert string `yaml:"-"`
SSLCertPath string `yaml:"cert_file,omitempty"`
AuthnType string `yaml:"authn_type,omitempty"`
ServiceID string `yaml:"service_id,omitempty"`
OidcTokenPath string `yaml:"oidc_token_path,omitempty"`
}

func (c *Config) IsHttps() bool {
Expand Down

0 comments on commit e678fa2

Please sign in to comment.