-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #12 from Conjur-Enterprise/refactor-jwt
CNJR-4190: Refactor JWT authentication to use standard config options
- Loading branch information
Showing
12 changed files
with
380 additions
and
121 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
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 | ||
} |
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,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()) | ||
}) | ||
} |
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
Oops, something went wrong.