From 73e8e4e888dd8c99574cd50b41936bfc3cc8da54 Mon Sep 17 00:00:00 2001
From: Sean <1661003+spbsoluble@users.noreply.github.com>
Date: Thu, 31 Oct 2024 12:56:29 -0700
Subject: [PATCH 01/10] feat: Initial release (#2)
* feat: Initial release
---
.github/config/MODULE.MD | 41 +
.github/config/Makefile | 26 +
.github/config/README.md | 92 +++
.github/config/environments.tf | 35 +
.github/config/providers.tf | 20 +
.github/config/repo.tf | 3 +
.github/config/variables.tf | 38 +
.github/dependabot.yml | 12 +
.github/workflows/go_tests.yml | 53 ++
.../workflows/keyfactor-starter-workflow.yml | 19 +
.gitignore | 139 ++++
Makefile | 80 ++
README.md | 153 +++-
auth_config_schema.yml | 3 +
auth_providers/auth_basic.go | 288 +++++++
auth_providers/auth_basic_test.go | 308 +++++++
auth_providers/auth_core.go | 749 ++++++++++++++++++
auth_providers/auth_core_test.go | 104 +++
auth_providers/auth_oauth.go | 414 ++++++++++
auth_providers/auth_oauth_test.go | 436 ++++++++++
auth_providers/command_config.go | 342 ++++++++
auth_providers/command_config_test.go | 358 +++++++++
go.mod | 22 +
go.sum | 10 +
integration-manifest.json | 11 +
...nt-oidc-lab.eastus2.cloudapp.azure.com.pem | 25 +
lib/config/auth_config_schema.json | 153 ++++
lib/config/basic_auth_config_example.json | 18 +
lib/config/full_auth_config_example.json | 40 +
lib/config/oauth_config_example.json | 18 +
lib/main.go | 146 ++++
lib/test_ca_cert.pem | 18 +
lib/test_chain.pem | 36 +
lib/test_leaf_cert.pem | 18 +
main.go | 168 ++++
pkg/version.go | 21 +
scripts/auth_keycloak.ps1 | 64 ++
scripts/auth_keycloak.sh | 68 ++
tag.sh | 5 +
39 files changed, 4553 insertions(+), 1 deletion(-)
create mode 100644 .github/config/MODULE.MD
create mode 100644 .github/config/Makefile
create mode 100644 .github/config/README.md
create mode 100644 .github/config/environments.tf
create mode 100644 .github/config/providers.tf
create mode 100644 .github/config/repo.tf
create mode 100644 .github/config/variables.tf
create mode 100644 .github/dependabot.yml
create mode 100644 .github/workflows/go_tests.yml
create mode 100644 .github/workflows/keyfactor-starter-workflow.yml
create mode 100644 .gitignore
create mode 100644 Makefile
create mode 100644 auth_config_schema.yml
create mode 100644 auth_providers/auth_basic.go
create mode 100644 auth_providers/auth_basic_test.go
create mode 100644 auth_providers/auth_core.go
create mode 100644 auth_providers/auth_core_test.go
create mode 100644 auth_providers/auth_oauth.go
create mode 100644 auth_providers/auth_oauth_test.go
create mode 100644 auth_providers/command_config.go
create mode 100644 auth_providers/command_config_test.go
create mode 100644 go.mod
create mode 100644 go.sum
create mode 100644 integration-manifest.json
create mode 100644 lib/certs/int-oidc-lab.eastus2.cloudapp.azure.com.pem
create mode 100644 lib/config/auth_config_schema.json
create mode 100644 lib/config/basic_auth_config_example.json
create mode 100644 lib/config/full_auth_config_example.json
create mode 100644 lib/config/oauth_config_example.json
create mode 100644 lib/main.go
create mode 100644 lib/test_ca_cert.pem
create mode 100644 lib/test_chain.pem
create mode 100644 lib/test_leaf_cert.pem
create mode 100644 main.go
create mode 100644 pkg/version.go
create mode 100644 scripts/auth_keycloak.ps1
create mode 100755 scripts/auth_keycloak.sh
create mode 100755 tag.sh
diff --git a/.github/config/MODULE.MD b/.github/config/MODULE.MD
new file mode 100644
index 0000000..4a0f4ab
--- /dev/null
+++ b/.github/config/MODULE.MD
@@ -0,0 +1,41 @@
+## Requirements
+
+| Name | Version |
+|------|---------|
+| [terraform](#requirement\_terraform) | >= 1.0 |
+| [github](#requirement\_github) | >=6.2 |
+
+## Providers
+
+| Name | Version |
+|------|---------|
+| [github](#provider\_github) | 6.3.1 |
+
+## Modules
+
+| Name | Source | Version |
+|------|--------|---------|
+| [keyfactor\_github\_test\_environment\_12\_3\_0\_kc](#module\_keyfactor\_github\_test\_environment\_12\_3\_0\_kc) | git::ssh://git@github.com/Keyfactor/terraform-module-keyfactor-github-test-environment-ad.git | main |
+| [keyfactor\_github\_test\_environment\_ad\_10\_5\_0](#module\_keyfactor\_github\_test\_environment\_ad\_10\_5\_0) | git::ssh://git@github.com/Keyfactor/terraform-module-keyfactor-github-test-environment-ad.git | main |
+
+## Resources
+
+| Name | Type |
+|------|------|
+| [github_repository.repo](https://registry.terraform.io/providers/integrations/github/latest/docs/data-sources/repository) | data source |
+
+## Inputs
+
+| Name | Description | Type | Default | Required |
+|------|-------------|------|---------|:--------:|
+| [keyfactor\_auth\_token\_url\_12\_3\_0\_KC](#input\_keyfactor\_auth\_token\_url\_12\_3\_0\_KC) | The hostname of the KeyCloak instance to authenticate to for a Keyfactor Command access token | `string` | `"https://int-oidc-lab.eastus2.cloudapp.azure.com:8444/realms/Keyfactor/protocol/openid-connect/token"` | no |
+| [keyfactor\_client\_id\_12\_3\_0](#input\_keyfactor\_client\_id\_12\_3\_0) | The client ID to authenticate with the Keyfactor instance using Keycloak client credentials | `string` | n/a | yes |
+| [keyfactor\_client\_secret\_12\_3\_0](#input\_keyfactor\_client\_secret\_12\_3\_0) | The client secret to authenticate with the Keyfactor instance using Keycloak client credentials | `string` | n/a | yes |
+| [keyfactor\_hostname\_10\_5\_0](#input\_keyfactor\_hostname\_10\_5\_0) | The hostname of the Keyfactor instance | `string` | `"integrations1050-lab.kfdelivery.com"` | no |
+| [keyfactor\_hostname\_12\_3\_0\_KC](#input\_keyfactor\_hostname\_12\_3\_0\_KC) | The hostname of the Keyfactor instance | `string` | `"int-oidc-lab.eastus2.cloudapp.azure.com"` | no |
+| [keyfactor\_password\_10\_5\_0](#input\_keyfactor\_password\_10\_5\_0) | The password to authenticate with the Keyfactor instance | `string` | n/a | yes |
+| [keyfactor\_username\_10\_5\_0](#input\_keyfactor\_username\_10\_5\_0) | The username to authenticate with the Keyfactor instance | `string` | n/a | yes |
+
+## Outputs
+
+No outputs.
diff --git a/.github/config/Makefile b/.github/config/Makefile
new file mode 100644
index 0000000..f67d9df
--- /dev/null
+++ b/.github/config/Makefile
@@ -0,0 +1,26 @@
+.DEFAULT_GOAL := help
+
+##@ Utility
+help: ## Display this help
+ @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
+
+deps: ## Install deps for macos
+ @brew install pre-commit tflint terraform terraform-docs
+
+docs: ## Run terraform-docs to update module docs.
+ @terraform-docs markdown . > MODULE.MD
+ @terraform-docs markdown table --output-file README.md --output-mode inject .
+
+lint: ## Run tflint
+ @tflint
+
+validate: ## Run terraform validate
+ @terraform init --upgrade
+ @terraform validate
+
+precommit/add: ## Install pre-commit hook
+ @pre-commit install
+
+precommit/remove: ## Uninstall pre-commit hook
+ @pre-commit uninstall
+
diff --git a/.github/config/README.md b/.github/config/README.md
new file mode 100644
index 0000000..e1a8977
--- /dev/null
+++ b/.github/config/README.md
@@ -0,0 +1,92 @@
+# GitHub Test Environment Setup
+
+This code sets up GitHub environments for testing against Keyfactor Command instances that are configured to use
+Active Directory or Keycloak for authentication.
+
+## Requirements
+
+1. Terraform >= 1.0
+2. GitHub Provider >= 6.2
+3. Keyfactor Command instance(s) configured to use Active Directory or Keycloak for authentication
+4. AD or Keycloak credentials for authenticating to the Keyfactor Command instance(s)
+5. A GitHub token with access and permissions to the repository where the environments will be created
+
+## Adding a new environment
+
+Modify the `environments.tf` file to include the new environment module. The module should be named appropriately.
+Example:
+
+### Active Directory Environment
+
+```hcl
+module "keyfactor_github_test_environment_ad_10_5_0" {
+ source = "git::ssh://git@github.com/Keyfactor/terraform-module-keyfactor-github-test-environment-ad.git?ref=main"
+
+ gh_environment_name = "KFC_10_5_0" # Keyfactor Command 10.5.0 environment using Active Directory(/Basic Auth)
+ gh_repo_name = data.github_repository.repo.name
+ keyfactor_hostname = var.keyfactor_hostname_10_5_0
+ keyfactor_username = var.keyfactor_username_10_5_0
+ keyfactor_password = var.keyfactor_password_10_5_0
+}
+```
+
+### oAuth Client Environment
+
+```hcl
+module "keyfactor_github_test_environment_12_3_0_kc" {
+ source = "git::ssh://git@github.com/Keyfactor/terraform-module-keyfactor-github-test-environment-kc.git?ref=main"
+
+ gh_environment_name = "KFC_12_3_0_KC" # Keyfactor Command 12.3.0 environment using Keycloak
+ gh_repo_name = data.github_repository.repo.name
+ keyfactor_hostname = var.keyfactor_hostname_12_3_0_KC
+ keyfactor_auth_token_url = var.keyfactor_auth_token_url_12_3_0_KC
+ keyfactor_client_id = var.keyfactor_client_id_12_3_0
+ keyfactor_client_secret = var.keyfactor_client_secret_12_3_0
+ keyfactor_tls_skip_verify = true
+}
+```
+
+
+
+## Requirements
+
+| Name | Version |
+|---------------------------------------------------------------------------|---------|
+| [terraform](#requirement\_terraform) | >= 1.0 |
+| [github](#requirement\_github) | >=6.2 |
+
+## Providers
+
+| Name | Version |
+|------------------------------------------------------------|---------|
+| [github](#provider\_github) | 6.3.1 |
+
+## Modules
+
+| Name | Source | Version |
+|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------|---------|
+| [keyfactor\_github\_test\_environment\_12\_3\_0\_kc](#module\_keyfactor\_github\_test\_environment\_12\_3\_0\_kc) | git::ssh://git@github.com/Keyfactor/terraform-module-keyfactor-github-test-environment-ad.git | main |
+| [keyfactor\_github\_test\_environment\_ad\_10\_5\_0](#module\_keyfactor\_github\_test\_environment\_ad\_10\_5\_0) | git::ssh://git@github.com/Keyfactor/terraform-module-keyfactor-github-test-environment-ad.git | main |
+
+## Resources
+
+| Name | Type |
+|---------------------------------------------------------------------------------------------------------------------------|-------------|
+| [github_repository.repo](https://registry.terraform.io/providers/integrations/github/latest/docs/data-sources/repository) | data source |
+
+## Inputs
+
+| Name | Description | Type | Default | Required |
+|--------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------|----------|---------------------------------------------------------------------------------------------------------|:--------:|
+| [keyfactor\_auth\_token\_url\_12\_3\_0\_KC](#input\_keyfactor\_auth\_token\_url\_12\_3\_0\_KC) | The hostname of the KeyCloak instance to authenticate to for a Keyfactor Command access token | `string` | `"https://int-oidc-lab.eastus2.cloudapp.azure.com:8444/realms/Keyfactor/protocol/openid-connect/token"` | no |
+| [keyfactor\_client\_id\_12\_3\_0](#input\_keyfactor\_client\_id\_12\_3\_0) | The client ID to authenticate with the Keyfactor instance using Keycloak client credentials | `string` | n/a | yes |
+| [keyfactor\_client\_secret\_12\_3\_0](#input\_keyfactor\_client\_secret\_12\_3\_0) | The client secret to authenticate with the Keyfactor instance using Keycloak client credentials | `string` | n/a | yes |
+| [keyfactor\_hostname\_10\_5\_0](#input\_keyfactor\_hostname\_10\_5\_0) | The hostname of the Keyfactor instance | `string` | `"integrations1050-lab.kfdelivery.com"` | no |
+| [keyfactor\_hostname\_12\_3\_0\_KC](#input\_keyfactor\_hostname\_12\_3\_0\_KC) | The hostname of the Keyfactor instance | `string` | `"int-oidc-lab.eastus2.cloudapp.azure.com"` | no |
+| [keyfactor\_password\_10\_5\_0](#input\_keyfactor\_password\_10\_5\_0) | The password to authenticate with the Keyfactor instance | `string` | n/a | yes |
+| [keyfactor\_username\_10\_5\_0](#input\_keyfactor\_username\_10\_5\_0) | The username to authenticate with the Keyfactor instance | `string` | n/a | yes |
+
+## Outputs
+
+No outputs.
+
\ No newline at end of file
diff --git a/.github/config/environments.tf b/.github/config/environments.tf
new file mode 100644
index 0000000..d26fab7
--- /dev/null
+++ b/.github/config/environments.tf
@@ -0,0 +1,35 @@
+module "keyfactor_github_test_environment_ad_10_5_0" {
+ source = "git::ssh://git@github.com/Keyfactor/terraform-module-keyfactor-github-test-environment-ad.git?ref=main"
+
+ gh_environment_name = "KFC_10_5_0"
+ gh_repo_name = data.github_repository.repo.name
+ keyfactor_hostname = var.keyfactor_hostname_10_5_0
+ keyfactor_username = var.keyfactor_username_10_5_0
+ keyfactor_password = var.keyfactor_password_10_5_0
+ keyfactor_config_file = base64encode(file("${path.module}/command_config.json"))
+}
+
+# module "keyfactor_github_test_environment_11_5_0_kc" {
+# source = "git::ssh://git@github.com/Keyfactor/terraform-module-keyfactor-github-test-environment-kc.git?ref=main"
+#
+# gh_environment_name = "KFC_11_5_0_KC"
+# gh_repo_name = data.github_repository.repo.name
+# keyfactor_hostname = var.keyfactor_hostname_11_5_0_KC
+# keyfactor_client_id = var.keyfactor_client_id_11_5_0
+# keyfactor_client_secret = var.keyfactor_client_secret_11_5_0
+# keyfactor_auth_hostname = var.keyfactor_auth_hostname_11_5_0_KC
+# keyfactor_tls_skip_verify = true
+# }
+
+module "keyfactor_github_test_environment_12_3_0_kc" {
+ source = "git::ssh://git@github.com/Keyfactor/terraform-module-keyfactor-github-test-environment-ad.git?ref=main"
+
+ gh_environment_name = "KFC_12_3_0_KC"
+ gh_repo_name = data.github_repository.repo.name
+ keyfactor_hostname = var.keyfactor_hostname_12_3_0_KC
+ keyfactor_auth_token_url = var.keyfactor_auth_token_url_12_3_0_KC
+ keyfactor_client_id = var.keyfactor_client_id_12_3_0
+ keyfactor_client_secret = var.keyfactor_client_secret_12_3_0
+ keyfactor_tls_skip_verify = true
+ keyfactor_config_file = base64encode(file("${path.module}/command_config.json"))
+}
diff --git a/.github/config/providers.tf b/.github/config/providers.tf
new file mode 100644
index 0000000..313bfcd
--- /dev/null
+++ b/.github/config/providers.tf
@@ -0,0 +1,20 @@
+terraform {
+ required_version = ">= 1.0"
+ required_providers {
+ github = {
+ source = "integrations/github"
+ version = ">=6.2"
+ }
+ }
+ backend "azurerm" {
+ resource_group_name = "integrations-infra"
+ storage_account_name = "integrationstfstate"
+ container_name = "tfstate"
+ key = "github/repos/keyfactor-auth-client-go.tfstate"
+ }
+}
+
+provider "github" {
+ # Configuration options
+ owner = "Keyfactor"
+}
\ No newline at end of file
diff --git a/.github/config/repo.tf b/.github/config/repo.tf
new file mode 100644
index 0000000..51e9c49
--- /dev/null
+++ b/.github/config/repo.tf
@@ -0,0 +1,3 @@
+data "github_repository" "repo" {
+ name = "keyfactor-auth-client-go"
+}
\ No newline at end of file
diff --git a/.github/config/variables.tf b/.github/config/variables.tf
new file mode 100644
index 0000000..5353f86
--- /dev/null
+++ b/.github/config/variables.tf
@@ -0,0 +1,38 @@
+variable "keyfactor_hostname_10_5_0" {
+ description = "The hostname of the Keyfactor instance"
+ type = string
+ default = "integrations1050-lab.kfdelivery.com"
+}
+
+variable "keyfactor_username_10_5_0" {
+ description = "The username to authenticate with the Keyfactor instance"
+ type = string
+}
+
+variable "keyfactor_password_10_5_0" {
+ description = "The password to authenticate with the Keyfactor instance"
+ type = string
+}
+
+variable "keyfactor_client_id_12_3_0" {
+ description = "The client ID to authenticate with the Keyfactor instance using Keycloak client credentials"
+ type = string
+}
+
+variable "keyfactor_client_secret_12_3_0" {
+ description = "The client secret to authenticate with the Keyfactor instance using Keycloak client credentials"
+ type = string
+}
+
+variable "keyfactor_hostname_12_3_0_KC" {
+ description = "The hostname of the Keyfactor instance"
+ type = string
+ default = "int-oidc-lab.eastus2.cloudapp.azure.com"
+}
+
+variable "keyfactor_auth_token_url_12_3_0_KC" {
+ description = "The hostname of the KeyCloak instance to authenticate to for a Keyfactor Command access token"
+ type = string
+ default = "https://int-oidc-lab.eastus2.cloudapp.azure.com:8444/realms/Keyfactor/protocol/openid-connect/token"
+}
+
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..fa3ed22
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,12 @@
+# See GitHub's documentation for more information on this file:
+# https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "daily"
+ - package-ecosystem: "gomod"
+ directory: "/"
+ schedule:
+ interval: "daily"
\ No newline at end of file
diff --git a/.github/workflows/go_tests.yml b/.github/workflows/go_tests.yml
new file mode 100644
index 0000000..a40e8ba
--- /dev/null
+++ b/.github/workflows/go_tests.yml
@@ -0,0 +1,53 @@
+name: Go Test Workflow
+
+on:
+ push:
+ workflow_dispatch:
+
+jobs:
+ test:
+ name: Run tests
+ runs-on: kf-auth-client-runner-set
+ strategy:
+ matrix:
+ environment: [ "KFC_10_5_0", "KFC_12_3_0_KC"]
+ environment: ${{ matrix.environment }}
+ steps:
+ - name: Check out code
+ uses: actions/checkout@v3
+
+ - name: Set up Go
+ uses: actions/setup-go@v3
+ with:
+ go-version: 1.22
+
+ - name: Get Public IP
+ run: curl -s https://api.ipify.org
+
+ - name: Validate lab cert is present
+ run: |
+ pwd
+ ls -la
+ ls -la lib
+ ls -la lib/certs
+ cat lib/certs/int-oidc-lab.eastus2.cloudapp.azure.com.pem
+
+ - name: Run tests
+ run: |
+ if [ -n "${{ secrets.KEYFACTOR_AUTH_CONFIG_B64 }}" ]; then
+ mkdir -p ~/.keyfactor
+ echo "${{ secrets.KEYFACTOR_AUTH_CONFIG_B64 }}" | base64 --decode > ~/.keyfactor/command_config.json
+ fi
+ go test -v -cover ./auth_providers/...
+ env:
+ KEYFACTOR_PASSWORD: ${{ secrets.KEYFACTOR_PASSWORD }}
+ KEYFACTOR_USERNAME: ${{ secrets.KEYFACTOR_USERNAME }}
+ KEYFACTOR_AUTH_CONFIG_B64: ${{ secrets.KEYFACTOR_AUTH_CONFIG_B64 }}
+ KEYFACTOR_AUTH_CLIENT_ID: ${{ secrets.KEYFACTOR_AUTH_CLIENT_ID }}
+ KEYFACTOR_AUTH_CLIENT_SECRET: ${{ secrets.KEYFACTOR_AUTH_CLIENT_SECRET }}
+ KEYFACTOR_AUTH_TOKEN_URL: ${{ vars.KEYFACTOR_AUTH_TOKEN_URL }}
+ KEYFACTOR_HOSTNAME: ${{ vars.KEYFACTOR_HOSTNAME }}
+ KEYFACTOR_AUTH_HOSTNAME: ${{ vars.KEYFACTOR_AUTH_HOSTNAME }}
+ KEYFACTOR_SKIP_VERIFY: ${{ vars.KEYFACTOR_SKIP_VERIFY }}
+ TEST_KEYFACTOR_AD_AUTH: ${{ vars.TEST_KEYFACTOR_AD_AUTH }}
+ TEST_KEYFACTOR_KC_AUTH: ${{ vars.TEST_KEYFACTOR_KC_AUTH }}
\ No newline at end of file
diff --git a/.github/workflows/keyfactor-starter-workflow.yml b/.github/workflows/keyfactor-starter-workflow.yml
new file mode 100644
index 0000000..01ddd34
--- /dev/null
+++ b/.github/workflows/keyfactor-starter-workflow.yml
@@ -0,0 +1,19 @@
+name: Keyfactor Bootstrap Workflow
+
+on:
+ workflow_dispatch:
+ pull_request:
+ types: [ opened, closed, synchronize, edited, reopened ]
+ push:
+ create:
+ branches:
+ - 'release-*.*'
+
+jobs:
+ call-starter-workflow:
+ uses: keyfactor/actions/.github/workflows/starter.yml@v3
+ secrets:
+ token: ${{ secrets.V2BUILDTOKEN}}
+ APPROVE_README_PUSH: ${{ secrets.APPROVE_README_PUSH}}
+ gpg_key: ${{ secrets.KF_GPG_PRIVATE_KEY }}
+ gpg_pass: ${{ secrets.KF_GPG_PASSPHRASE }}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9ab1d97
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,139 @@
+### JetBrains template
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# AWS User-specific
+.idea/**/aws.xml
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# SonarLint plugin
+.idea/sonarlint/
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### Terraform template
+# Local .terraform directories
+**/.terraform/*
+
+# .tfstate files
+*.tfstate
+*.tfstate.*
+
+# Crash log files
+crash.log
+crash.*.log
+
+# Exclude all .tfvars files, which are likely to contain sensitive data, such as
+# password, private keys, and other secrets. These should not be part of version
+# control as they are data points which are potentially sensitive and subject
+# to change depending on the environment.
+*.tfvars
+*.tfvars.json
+
+# Ignore override files as they are usually used to override resources locally and so
+# are not checked in
+override.tf
+override.tf.json
+*_override.tf
+*_override.tf.json
+
+# Include override files you do wish to add to version control using negated pattern
+# !example_override.tf
+
+# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
+# example: *tfplan*
+
+# Ignore CLI configuration files
+.terraformrc
+terraform.rc
+
+### Go template
+# If you prefer the allow list template instead of the deny list, see community template:
+# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
+#
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, built with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+
+# Dependency directories (remove the comment below to include it)
+# vendor/
+
+# Go workspace file
+go.work
+
+*.env*
\ No newline at end of file
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..fd0b4fb
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,80 @@
+PROVIDER_DIR := $(PWD)
+TEST?=$$(go list ./... | grep -v 'vendor')
+HOSTNAME=keyfactor.com
+GOFMT_FILES := $$(find $(PROVIDER_DIR) -name '*.go' |grep -v vendor)
+NAMESPACE=keyfactor
+WEBSITE_REPO=https://github.com/Keyfactor/keyfactor-auth-client
+NAME=keyfactor-auth-client
+BINARY=${NAME}
+VERSION := $(GITHUB_REF_NAME)
+ifeq ($(VERSION),)
+ VERSION := v1.0.0
+endif
+OS_ARCH := $(shell go env GOOS)_$(shell go env GOARCH)
+BASEDIR := ${HOME}/go/bin
+INSTALLDIR := ${BASEDIR}
+MARKDOWN_FILE := README.md
+TEMP_TOC_FILE := temp_toc.md
+
+
+
+default: build
+
+build: fmt
+ go install
+
+release:
+ GOOS=darwin GOARCH=amd64 go build -o ./bin/${BINARY}_${VERSION}_darwin_amd64 -ldflags "-X 'keyfactor_auth_client/pkg.Version=${VERSION}' -X 'keyfactor_auth_client/pkg.BuildTime=$$(date)' -X 'keyfactor_auth_client/pkg.CommitHash=$$(git rev-parse HEAD)'"
+ GOOS=freebsd GOARCH=386 go build -o ./bin/${BINARY}_${VERSION}_freebsd_386 -ldflags "-X 'keyfactor_auth_client/pkg.Version=${VERSION}' -X 'keyfactor_auth_client/pkg.BuildTime=$$(date)' -X 'keyfactor_auth_client/pkg.CommitHash=$$(git rev-parse HEAD)'"
+ GOOS=freebsd GOARCH=amd64 go build -o ./bin/${BINARY}_${VERSION}_freebsd_amd64 -ldflags "-X 'keyfactor_auth_client/pkg.Version=${VERSION}' -X 'keyfactor_auth_client/pkg.BuildTime=$(date)' -X 'keyfactor_auth_client/pkg.CommitHash=$(git rev-parse HEAD)'"
+ GOOS=freebsd GOARCH=arm go build -o ./bin/${BINARY}_${VERSION}_freebsd_arm -ldflags "-X 'keyfactor_auth_client/pkg.Version=${VERSION}' -X 'keyfactor_auth_client/pkg.BuildTime=$$(date)' -X 'keyfactor_auth_client/pkg.CommitHash=$$(git rev-parse HEAD)'"
+ GOOS=linux GOARCH=386 go build -o ./bin/${BINARY}_${VERSION}_linux_386 -ldflags "-X 'keyfactor_auth_client/pkg.Version=${VERSION}' -X 'keyfactor_auth_client/pkg.BuildTime=$$(date)' -X 'keyfactor_auth_client/pkg.CommitHash=$$(git rev-parse HEAD)'"
+ GOOS=linux GOARCH=amd64 go build -o ./bin/${BINARY}_${VERSION}_linux_amd64 -ldflags "-X 'keyfactor_auth_client/pkg.Version=${VERSION}' -X 'keyfactor_auth_client/pkg.BuildTime=$$(date)' -X 'keyfactor_auth_client/pkg.CommitHash=$$(git rev-parse HEAD)'"
+ GOOS=linux GOARCH=arm go build -o ./bin/${BINARY}_${VERSION}_linux_arm -ldflags "-X 'keyfactor_auth_client/pkg.Version=${VERSION}' -X 'keyfactor_auth_client/pkg.BuildTime=$$(date)' -X 'keyfactor_auth_client/pkg.CommitHash=$$(git rev-parse HEAD)'"
+ GOOS=openbsd GOARCH=386 go build -o ./bin/${BINARY}_${VERSION}_openbsd_386 -ldflags "-X 'keyfactor_auth_client/pkg.Version=${VERSION}' -X 'keyfactor_auth_client/pkg.BuildTime=$$(date)' -X 'keyfactor_auth_client/pkg.CommitHash=$$(git rev-parse HEAD)'"
+ GOOS=openbsd GOARCH=amd64 go build -o ./bin/${BINARY}_${VERSION}_openbsd_amd64 -ldflags "-X 'keyfactor_auth_client/pkg.Version=${VERSION}' -X 'keyfactor_auth_client/pkg.BuildTime=$$(date)' -X 'keyfactor_auth_client/pkg.CommitHash=$$(git rev-parse HEAD)'"
+ GOOS=solaris GOARCH=amd64 go build -o ./bin/${BINARY}_${VERSION}_solaris_amd64 -ldflags "-X 'keyfactor_auth_client/pkg.Version=${VERSION}' -X 'keyfactor_auth_client/pkg.BuildTime=$$(date)' -X 'keyfactor_auth_client/pkg.CommitHash=$$(git rev-parse HEAD)'"
+ GOOS=windows GOARCH=386 go build -o ./bin/${BINARY}_${VERSION}_windows_386 -ldflags "-X 'keyfactor_auth_client/pkg.Version=${VERSION}' -X 'keyfactor_auth_client/pkg.BuildTime=$$(date)' -X 'keyfactor_auth_client/pkg.CommitHash=$$(git rev-parse HEAD)'"
+ GOOS=windows GOARCH=amd64 go build -o ./bin/${BINARY}_${VERSION}_windows_amd64 -ldflags "-X 'keyfactor_auth_client/pkg.Version=${VERSION}' -X 'keyfactor_auth_client/pkg.BuildTime=$$(date)' -X 'keyfactor_auth_client/pkg.CommitHash=$$(git rev-parse HEAD)'"
+
+install: fmt
+ go build -o ${BINARY}
+ rm -rf ${INSTALLDIR}/${BINARY}
+ mkdir -p ${INSTALLDIR}
+ chmod oug+x ${BINARY}
+ cp ${BINARY} ${INSTALLDIR}
+ mkdir -p ${HOME}/.local/bin || true
+ mv ${BINARY} ${HOME}/.local/bin/${BINARY}
+
+vendor:
+ go mod vendor
+
+version:
+ @echo ${VERSION}
+
+setversion:
+ sed -i '' -e 's/VERSION = ".*"/VERSION = "$(VERSION)"/' pkg/version/version.go
+
+test:
+ go test -i $(TEST) || exit 1
+ echo $(TEST) | xargs -t -n4 go test $(TESTARGS) -timeout=30s -parallel=4
+
+fmt:
+ gofmt -w $(GOFMT_FILES)
+
+prerelease: fmt setversion
+ git tag -d $(VERSION) || true
+ git push origin :$(VERSION) || true
+ git tag $(VERSION)
+ git push origin $(VERSION)
+
+check_toc:
+ @grep -q 'TOC_START' $(MARKDOWN_FILE) && echo "TOC already exists." || (echo "TOC not found. Generating..." && $(MAKE) generate_toc)
+
+generate_toc:
+ # check if markdown-toc is installed and if not install it
+ @command -v markdown-toc >/dev/null 2>&1 || (echo "markdown-toc is not installed. Installing..." && npm install -g markdown-toc)
+ markdown-toc -i $(MARKDOWN_FILE) --skip 'Table of Contents'
+
+
+.PHONY: build prerelease release install test fmt vendor version setversion
\ No newline at end of file
diff --git a/README.md b/README.md
index 8e27d92..c92a028 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,152 @@
-# keyfactor-auth-client-go
\ No newline at end of file
+# keyfactor-auth-client-go
+
+Client library for authenticating to Keyfactor Command
+
+
+
+- [Environment Variables](#environment-variables)
+ * [Global](#global)
+ * [Basic Auth](#basic-auth)
+ * [oAuth Client Credentials](#oauth-client-credentials)
+- [Configuration File](#configuration-file)
+ * [Basic Auth](#basic-auth)
+ * [oAuth Client Credentials](#oauth-client-credentials)
+ * [Auth Providers](#auth-providers)
+ + [Azure Id](#azure-id)
+ + [Azure KeyVault](#azure-keyvault)
+- [Test Environment Variables](#test-environment-variables)
+
+
+
+## Environment Variables
+
+### Global
+
+| Name | Description | Default |
+|-------------------------------|-----------------------------------------------------------------------------------------------------------------|----------------------------------------|
+| KEYFACTOR_HOSTNAME | Keyfactor Command hostname without protocol and port | |
+| KEYFACTOR_PORT | Keyfactor Command port | `443` |
+| KEYFACTOR_API_PATH | Keyfactor Command API Path | `KeyfactorAPI` |
+| KEYFACTOR_SKIP_VERIFY | Skip TLS verification when connecting to Keyfactor Command | `false` |
+| KEYFACTOR_CA_CERT | Either a file path or PEM encoded string to a CA certificate to trust when communicating with Keyfactor Command | |
+| KEYFACTOR_CLIENT_TIMEOUT | Timeout for HTTP client requests to Keyfactor Command | `60s` |
+| KEYFACTOR_AUTH_CONFIG_FILE | Path to a JSON file containing the authentication configuration | `$HOME/.keyfactor/command_config.json` |
+| KEYFACTOR_AUTH_CONFIG_PROFILE | Profile to use from the authentication configuration file | `default` |
+
+### Basic Auth
+
+Currently, only Active Directory `Basic` authentication is supported.
+
+| Name | Description | Default |
+|--------------------|---------------------------------------------------------------------------------------------|---------|
+| KEYFACTOR_USERNAME | Active Directory username to authenticate to Keyfactor Command API | |
+| KEYFACTOR_PASSWORD | Password associated with Active Directory username to authenticate to Keyfactor Command API | |
+| KEYFACTOR_DOMAIN | Active Directory domain of user. Can be implied from username if it contains `@` or `\\` | |
+
+### oAuth Client Credentials
+
+| Name | Description | Default |
+|------------------------------|---------------------------------------------------------------------------------------------------------------------------------|----------|
+| KEYFACTOR_AUTH_CLIENT_ID | Keyfactor Auth Client ID | |
+| KEYFACTOR_AUTH_CLIENT_SECRET | Keyfactor Auth Client Secret | |
+| KEYFACTOR_AUTH_TOKEN_URL | URL to request an access token from Keyfactor Auth | |
+| KEYFACTOR_AUTH_SCOPES | Scopes to request when authenticating to Keyfactor Command API | `openid` |
+| KEYFACTOR_AUTH_ACCESS_TOKEN | Access token to use to authenticate to Keyfactor Command API. This can be supplied directly or generated via client credentials | |
+| KEYFACTOR_AUTH_CA_CERT | Either a file path or PEM encoded string to a CA certificate to use when connecting to Keyfactor Auth | |
+
+### Test Environment Variables
+
+These environment variables are used to run go tests. They are not used in the actual client library.
+
+| Name | Description | Default |
+|------------------------|-------------------------------------------------------|---------|
+| TEST_KEYFACTOR_AD_AUTH | Set to `true` to test Active Directory authentication | false |
+| TEST_KEYFACTOR_KC_AUTH | Set to `true` to test Keycloak authentication | false |
+
+## Configuration File
+A JSON or YAML file can be used to store authentication configuration. A configuration file can contain references to
+multiple Keyfactor Command environments and can be referenced by a `profile` name. The `default` profile will be used
+when no profile is specified. Keyfactor tools will look for a config file located at `$HOME/.keyfactor/command_config.json`
+by default. The config file should be structured as follows:
+
+### Basic Auth
+
+#### JSON
+```json
+{
+ "servers": {
+ "default": {
+ "host": "keyfactor.command.kfdelivery.com",
+ "username": "keyfactor",
+ "password": "password",
+ "domain": "command",
+ "api_path": "KeyfactorAPI"
+ },
+ "server2": {
+ "host": "keyfactor2.command.kfdelivery.com",
+ "username": "keyfactor2",
+ "password": "password2",
+ "domain": "command",
+ "api_path": "Keyfactor/API"
+ }
+ }
+}
+```
+
+#### YAML
+```yaml
+servers:
+ default:
+ host: keyfactor.command.kfdelivery.com
+ username: keyfactor
+ password: password
+ domain: command
+ api_path: KeyfactorAPI
+ server2:
+ host: keyfactor2.command.kfdelivery.com
+ username: keyfactor2
+ password: password2
+ domain: command
+ api_path: Keyfactor/API
+```
+
+### oAuth Client Credentials
+
+#### JSON
+```json
+{
+ "servers": {
+ "default": {
+ "host": "keyfactor.command.kfdelivery.com",
+ "token_url": "https://idp.keyfactor.command.kfdelivery.com/oauth2/token",
+ "client_id": "client-id",
+ "client_secret": "client-secret",
+ "api_path": "KeyfactorAPI"
+ },
+ "server2": {
+ "host": "keyfactor.command.kfdelivery.com",
+ "token_url": "https://idp.keyfactor.command.kfdelivery.com/oauth2/token",
+ "client_id": "client-id",
+ "client_secret": "client-secret",
+ "api_path": "KeyfactorAPI"
+ }
+ }
+}
+```
+
+#### YAML
+```yaml
+servers:
+ default:
+ host: keyfactor.command.kfdelivery.com
+ token_url: https://idp.keyfactor.command.kfdelivery.com/oauth2/token
+ client_id: client-id
+ client_secret: client-secret
+ api_path: KeyfactorAPI
+ server2:
+ host: keyfactor.command.kfdelivery.com
+ token_url: https://idp.keyfactor.command.kfdelivery.com/oauth2/token
+ client_id: client-id
+ client_secret: client-secret
+ api_path: KeyfactorAPI
+```
\ No newline at end of file
diff --git a/auth_config_schema.yml b/auth_config_schema.yml
new file mode 100644
index 0000000..f1c5a1e
--- /dev/null
+++ b/auth_config_schema.yml
@@ -0,0 +1,3 @@
+---
+servers:
+ description: The list of servers to authenticate against
\ No newline at end of file
diff --git a/auth_providers/auth_basic.go b/auth_providers/auth_basic.go
new file mode 100644
index 0000000..eb3f58a
--- /dev/null
+++ b/auth_providers/auth_basic.go
@@ -0,0 +1,288 @@
+// Copyright 2024 Keyfactor
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package auth_providers
+
+import (
+ "encoding/base64"
+ "fmt"
+ "net/http"
+ "os"
+ "strings"
+)
+
+const (
+ // EnvKeyfactorUsername is the environment variable for the Keyfactor hostname
+ EnvKeyfactorUsername = "KEYFACTOR_USERNAME"
+
+ // EnvKeyfactorPassword is the environment variable for the Keyfactor password
+ EnvKeyfactorPassword = "KEYFACTOR_PASSWORD"
+
+ // EnvKeyfactorDomain is the environment variable for the Keyfactor domain
+ EnvKeyfactorDomain = "KEYFACTOR_DOMAIN"
+)
+
+// Basic Authenticator
+var _ Authenticator = &BasicAuthAuthenticator{}
+
+// BasicAuthAuthenticator is an Authenticator that uses Basic Auth for authentication.
+type BasicAuthAuthenticator struct {
+ Client *http.Client
+}
+
+// GetHttpClient returns the http client
+func (b *BasicAuthAuthenticator) GetHttpClient() (*http.Client, error) {
+ return b.Client, nil
+}
+
+// CommandAuthConfigBasic represents the base configuration needed for authentication to Keyfactor Command API.
+type CommandAuthConfigBasic struct {
+ // CommandAuthConfig is a reference to the base configuration needed for authentication to Keyfactor Command API
+ CommandAuthConfig
+
+ // Username is the username to be used for authentication to Keyfactor Command API
+ Username string `json:"username,omitempty"`
+
+ // Password is the password to be used for authentication to Keyfactor Command API
+ Password string `json:"password,omitempty"`
+
+ // Domain is the domain of the Active Directory used to authenticate to Keyfactor Command API
+ Domain string `json:"domain,omitempty"`
+}
+
+// NewBasicAuthAuthenticatorBuilder creates a new instance of CommandAuthConfigBasic
+func NewBasicAuthAuthenticatorBuilder() *CommandAuthConfigBasic {
+ return &CommandAuthConfigBasic{}
+}
+
+// WithUsername sets the username for authentication
+func (a *CommandAuthConfigBasic) WithUsername(username string) *CommandAuthConfigBasic {
+ a.Username = username
+ return a
+}
+
+// WithPassword sets the password for authentication
+func (a *CommandAuthConfigBasic) WithPassword(password string) *CommandAuthConfigBasic {
+ a.Password = password
+ return a
+}
+
+// WithDomain sets the domain for authentication
+func (a *CommandAuthConfigBasic) WithDomain(domain string) *CommandAuthConfigBasic {
+ a.Domain = domain
+ return a
+}
+
+// GetHttpClient returns the http client
+func (a *CommandAuthConfigBasic) GetHttpClient() (*http.Client, error) {
+ //validate the configuration
+ cErr := a.ValidateAuthConfig()
+ if cErr != nil {
+ return nil, cErr
+ }
+
+ // Encode the username and password in Base64
+ var auth string
+ if a.Domain != "" {
+ auth = a.Domain + "\\" + a.Username + ":" + a.Password
+ } else {
+ auth = a.Username + ":" + a.Password
+ }
+ encodedAuth := base64.StdEncoding.EncodeToString([]byte(auth))
+
+ // Create a custom RoundTripper
+ transport, tErr := a.CommandAuthConfig.BuildTransport()
+ if tErr != nil {
+ return nil, tErr
+ }
+
+ return &http.Client{
+ Transport: roundTripperFunc(
+ func(req *http.Request) (*http.Response, error) {
+ // Add the Authorization header to the request
+ req.Header.Set("Authorization", "Basic "+encodedAuth)
+
+ // Forward the request to the actual transport
+ return transport.RoundTrip(req)
+ },
+ ),
+ }, nil
+}
+
+// Build creates a new instance of BasicAuthAuthenticator
+func (a *CommandAuthConfigBasic) Build() (Authenticator, error) {
+
+ client, cErr := a.GetHttpClient()
+ if cErr != nil {
+ return nil, cErr
+ }
+ a.HttpClient = client
+
+ return &BasicAuthAuthenticator{Client: client}, nil
+}
+
+// ValidateAuthConfig validates the basic authentication configuration.
+func (a *CommandAuthConfigBasic) ValidateAuthConfig() error {
+ silentLoad := true
+ if a.CommandAuthConfig.ConfigProfile != "" {
+ silentLoad = false
+ } else if a.CommandAuthConfig.ConfigFilePath != "" {
+ silentLoad = false
+ }
+ serverConfig, cErr := a.CommandAuthConfig.LoadConfig(
+ a.CommandAuthConfig.ConfigProfile,
+ a.CommandAuthConfig.ConfigFilePath,
+ silentLoad,
+ )
+ if !silentLoad && cErr != nil {
+ return cErr
+ }
+
+ if a.Username == "" {
+ if username, ok := os.LookupEnv(EnvKeyfactorUsername); ok {
+ a.Username = username
+ } else {
+ if serverConfig != nil && serverConfig.Username != "" {
+ a.Username = serverConfig.Username
+ } else {
+ return fmt.Errorf("username or environment variable %s is required", EnvKeyfactorUsername)
+ }
+ }
+ }
+ if a.Password == "" {
+ if password, ok := os.LookupEnv(EnvKeyfactorPassword); ok {
+ a.Password = password
+ } else {
+ if serverConfig != nil && serverConfig.Password != "" {
+ a.Password = serverConfig.Password
+ } else {
+ return fmt.Errorf("password or environment variable %s is required", EnvKeyfactorPassword)
+ }
+ }
+ }
+
+ domainErr := a.parseUsernameDomain()
+ if domainErr != nil {
+ return domainErr
+
+ }
+
+ if a.Domain == "" {
+ if domain, ok := os.LookupEnv(EnvKeyfactorDomain); ok {
+ a.Domain = domain
+ }
+ }
+
+ return a.CommandAuthConfig.ValidateAuthConfig()
+}
+
+// Authenticate authenticates the request using basic authentication.
+func (a *CommandAuthConfigBasic) Authenticate() error {
+ cErr := a.ValidateAuthConfig()
+ if cErr != nil {
+ return cErr
+ }
+
+ // create oauth Client
+ authy, err := NewBasicAuthAuthenticatorBuilder().
+ WithUsername(a.Username).
+ WithPassword(a.Password).
+ WithDomain(a.Domain).
+ Build()
+
+ if err != nil {
+ return err
+ }
+
+ if authy != nil {
+ bClient, berr := authy.GetHttpClient()
+ if berr != nil {
+ return berr
+ }
+ a.SetClient(bClient)
+ }
+
+ return a.CommandAuthConfig.Authenticate()
+}
+
+// parseUsernameDomain parses the username to extract the domain if it's included in the username.
+// It supports two formats: "username@domain" and "domain\username".
+func (a *CommandAuthConfigBasic) parseUsernameDomain() error {
+ domainErr := fmt.Errorf("domain or environment variable %s is required", EnvKeyfactorDomain)
+ if strings.Contains(a.Username, "@") {
+ dSplit := strings.Split(a.Username, "@")
+ if len(dSplit) != 2 {
+ return domainErr
+ }
+ a.Username = dSplit[0] // remove domain from username
+ a.Domain = dSplit[1]
+ } else if strings.Contains(a.Username, "\\") {
+ dSplit := strings.Split(a.Username, "\\")
+ if len(dSplit) != 2 {
+ return domainErr
+ }
+ a.Domain = dSplit[0]
+ a.Username = dSplit[1] // remove domain from username
+ }
+
+ return nil
+}
+
+// GetServerConfig returns the server configuration
+func (a *CommandAuthConfigBasic) GetServerConfig() *Server {
+ server := Server{
+ Host: a.CommandHostName,
+ Port: a.CommandPort,
+ Username: a.Username,
+ Password: a.Password,
+ Domain: a.Domain,
+ ClientID: "",
+ ClientSecret: "",
+ OAuthTokenUrl: "",
+ APIPath: a.CommandAPIPath,
+ //AuthProvider: AuthProvider{},
+ SkipTLSVerify: a.SkipVerify,
+ CACertPath: a.CommandCACert,
+ AuthType: "basic",
+ }
+ return &server
+}
+
+// Example usage of CommandAuthConfigBasic
+//
+// This example demonstrates how to use CommandAuthConfigBasic to authenticate a user.
+//
+// func ExampleCommandAuthConfigBasic_Authenticate() {
+// authConfig := &CommandAuthConfigBasic{
+// CommandAuthConfig: CommandAuthConfig{
+// ConfigFilePath: "/path/to/config.json",
+// ConfigProfile: "default",
+// CommandHostName: "exampleHost",
+// CommandPort: 443,
+// CommandAPIPath: "/api/v1",
+// CommandCACert: "/path/to/ca-cert.pem",
+// SkipVerify: true,
+// },
+// Username: "exampleUser",
+// Password: "examplePassword",
+// Domain: "exampleDomain",
+// }
+//
+// err := authConfig.Authenticate()
+// if err != nil {
+// fmt.Println("Authentication failed:", err)
+// } else {
+// fmt.Println("Authentication successful")
+// }
+// }
diff --git a/auth_providers/auth_basic_test.go b/auth_providers/auth_basic_test.go
new file mode 100644
index 0000000..5c404ae
--- /dev/null
+++ b/auth_providers/auth_basic_test.go
@@ -0,0 +1,308 @@
+// Copyright 2024 Keyfactor
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package auth_providers_test
+
+import (
+ "fmt"
+ "net/http"
+ "os"
+ "strings"
+ "testing"
+
+ "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers"
+)
+
+func TestBasicAuthAuthenticator_GetHttpClient(t *testing.T) {
+ // Skip test if TEST_KEYFACTOR_KC_AUTH is set to 1 or true
+ if os.Getenv("TEST_KEYFACTOR_KC_AUTH") == "1" || os.Getenv("TEST_KEYFACTOR_KC_AUTH") == "true" {
+ t.Skip("Skipping TestBasicAuthAuthenticator_GetHttpClient")
+ return
+ }
+
+ auth := &auth_providers.BasicAuthAuthenticator{
+ Client: &http.Client{},
+ }
+
+ client, err := auth.GetHttpClient()
+ if err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+
+ if client == nil {
+ t.Fatalf("expected a non-nil http.Client")
+ }
+}
+
+func TestCommandAuthConfigBasic_ValidateAuthConfig(t *testing.T) {
+ // Skip test if TEST_KEYFACTOR_KC_AUTH is set to 1 or true
+ if os.Getenv("TEST_KEYFACTOR_KC_AUTH") == "1" || os.Getenv("TEST_KEYFACTOR_KC_AUTH") == "true" {
+ t.Skip("Skipping TestBasicAuthAuthenticator_GetHttpClient")
+ return
+ }
+ config := &auth_providers.CommandAuthConfigBasic{
+ Username: os.Getenv(auth_providers.EnvKeyfactorUsername),
+ Password: os.Getenv(auth_providers.EnvKeyfactorPassword),
+ }
+
+ err := config.ValidateAuthConfig()
+ if err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+}
+
+func TestCommandAuthConfigBasic_GetHttpClient(t *testing.T) {
+ // Skip test if TEST_KEYFACTOR_KC_AUTH is set to 1 or true
+ if os.Getenv("TEST_KEYFACTOR_KC_AUTH") == "1" || os.Getenv("TEST_KEYFACTOR_KC_AUTH") == "true" {
+ t.Skip("Skipping TestBasicAuthAuthenticator_GetHttpClient")
+ return
+ }
+
+ config := &auth_providers.CommandAuthConfigBasic{
+ Username: os.Getenv(auth_providers.EnvKeyfactorUsername),
+ Password: os.Getenv(auth_providers.EnvKeyfactorPassword),
+ }
+
+ client, err := config.GetHttpClient()
+ if err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+
+ if client == nil {
+ t.Fatalf("expected a non-nil http.Client")
+ }
+}
+
+func TestCommandAuthConfigBasic_Authenticate(t *testing.T) {
+ // Skip test if TEST_KEYFACTOR_KC_AUTH is set to 1 or true
+ if os.Getenv("TEST_KEYFACTOR_KC_AUTH") == "1" || os.Getenv("TEST_KEYFACTOR_KC_AUTH") == "true" {
+ t.Skip("Skipping TestBasicAuthAuthenticator_GetHttpClient")
+ return
+ }
+
+ userHome, hErr := os.UserHomeDir()
+ if hErr != nil {
+ userHome = os.Getenv("HOME")
+ }
+
+ configFilePath := fmt.Sprintf("%s/%s", userHome, auth_providers.DefaultConfigFilePath)
+ configFromFile, cErr := auth_providers.ReadConfigFromJSON(configFilePath)
+ if cErr != nil {
+ t.Errorf("unable to load auth config from file %s: %v", configFilePath, cErr)
+ }
+
+ if configFromFile == nil || configFromFile.Servers == nil {
+ t.Errorf("invalid config file %s", configFilePath)
+ t.FailNow()
+ }
+
+ // Delete the config file
+ t.Logf("Deleting config file: %s", configFilePath)
+ os.Remove(configFilePath)
+ defer func() {
+ // Write the config file back
+ t.Logf("Writing config file: %s", configFilePath)
+ fErr := auth_providers.WriteConfigToJSON(configFilePath, configFromFile)
+ if fErr != nil {
+ t.Errorf("unable to write auth config to file %s: %v", configFilePath, fErr)
+ }
+ }()
+
+ t.Log("Testing Basic Auth with Environmental variables")
+ noParamsConfig := &auth_providers.CommandAuthConfigBasic{}
+ authBasicTest(t, "with complete Environmental variables", false, noParamsConfig)
+
+ t.Log("Testing Basic Auth with invalid config file path")
+ invFilePath := &auth_providers.CommandAuthConfigBasic{}
+ invFilePath.WithConfigFile("invalid-file-path")
+ invalidPathExpectedError := []string{"no such file or directory", "invalid-file-path"}
+ authBasicTest(t, "with invalid config file PATH", true, invFilePath, invalidPathExpectedError...)
+
+ // Environment variables are not set
+ t.Log("Unsetting environment variables")
+ username, password, domain := exportBasicEnvVariables()
+ unsetBasicEnvVariables()
+ defer func() {
+ t.Log("Resetting environment variables")
+ setBasicEnvVariables(username, password, domain)
+ }()
+
+ t.Log("Testing Basic Auth with no Environmental variables")
+ incompleteEnvConfig := &auth_providers.CommandAuthConfigBasic{}
+ incompleteEnvConfigExpectedError := "username or environment variable KEYFACTOR_USERNAME is required"
+ authBasicTest(
+ t,
+ "with incomplete Environmental variables",
+ true,
+ incompleteEnvConfig,
+ incompleteEnvConfigExpectedError,
+ )
+
+ t.Log("Testing auth with only username")
+ usernameOnlyConfig := &auth_providers.CommandAuthConfigBasic{
+ Username: "test-username",
+ }
+ usernameOnlyConfigExceptedError := "password or environment variable KEYFACTOR_PASSWORD is required"
+ authBasicTest(t, "username only", true, usernameOnlyConfig, usernameOnlyConfigExceptedError)
+
+ t.Log("Testing auth with w/ full params variables")
+ fullParamsConfig := &auth_providers.CommandAuthConfigBasic{
+ Username: username,
+ Password: password,
+ Domain: domain,
+ }
+ authBasicTest(t, "w/ full params variables", false, fullParamsConfig)
+
+ t.Log("Testing auth with w/ full params variables")
+ fullParamsinvalidPassConfig := &auth_providers.CommandAuthConfigBasic{
+ Username: username,
+ Password: "invalid-password",
+ Domain: domain,
+ }
+ invalidCredsExpectedError := []string{"401", "Unauthorized", "Access is denied due to invalid credentials"}
+ authBasicTest(t, "w/ full params & invalid pass", true, fullParamsinvalidPassConfig, invalidCredsExpectedError...)
+
+ t.Log("Testing auth with w/ no domain")
+ noDomainConfig := &auth_providers.CommandAuthConfigBasic{
+ Username: username,
+ Password: password,
+ }
+ authBasicTest(t, "w/ no domain", false, noDomainConfig)
+
+ t.Log("Testing auth with w/ no domain and no domain in username")
+ usernameNoDomain := strings.Split(username, "@")[0]
+ t.Logf("Username without domain: %s", usernameNoDomain)
+ usernameNoDomainConfig := &auth_providers.CommandAuthConfigBasic{
+ Username: usernameNoDomain,
+ Password: password,
+ }
+ //TODO: This really SHOULD fail, but it doesn't and the auth header is sent without the domain yet it still authenticates
+ authBasicTest(t, "w/o domain and no domain in username", false, usernameNoDomainConfig)
+
+ // Write the config file back
+ t.Logf("Writing config file: %s", configFilePath)
+ fErr := auth_providers.WriteConfigToJSON(configFilePath, configFromFile)
+ if fErr != nil {
+ t.Errorf("unable to write auth config to file %s: %v", configFilePath, fErr)
+ }
+
+ t.Log("Testing Basic Auth with valid implicit config file")
+ wConfigFile := &auth_providers.CommandAuthConfigBasic{}
+ authBasicTest(t, "with valid implicit config file", false, wConfigFile)
+
+ t.Log("Testing Basic Auth with invalid profile implicit config file")
+ invProfile := &auth_providers.CommandAuthConfigBasic{}
+ invProfile.WithConfigProfile("invalid-profile")
+ expectedError := []string{"profile", "invalid-profile", "not found"}
+ authBasicTest(t, "with invalid profile implicit config file", true, invProfile, expectedError...)
+
+ t.Log("Testing Basic Auth with invalid creds implicit config file")
+ invProfileCreds := &auth_providers.CommandAuthConfigBasic{}
+ invProfileCreds.WithConfigProfile("invalid_username")
+ authBasicTest(t, "with invalid creds implicit config file", true, invProfileCreds, invalidCredsExpectedError...)
+
+ t.Log("Testing Basic Auth with invalid Command host implicit config file")
+ invHostConfig := &auth_providers.CommandAuthConfigBasic{}
+ invHostConfig.WithConfigProfile("invalid_host")
+ invHostExpectedError := []string{"no such host"}
+ authBasicTest(
+ t, "with invalid Command host implicit config file", true, invHostConfig,
+ invHostExpectedError...,
+ )
+
+ //t.Log("Testing Basic Auth with invalid config file path")
+ //invFilePath := &auth_providers.CommandAuthConfigBasic{}
+ //invFilePath.WithConfigFile("invalid-file-path")
+ //invalidPathExpectedError := []string{"no such file or directory", "invalid-file-path"}
+ //authBasicTest(t, "with invalid config file PATH", true, invFilePath, invalidPathExpectedError...)
+
+}
+
+func TestCommandAuthConfigBasic_Build(t *testing.T) {
+ // Skip test if TEST_KEYFACTOR_KC_AUTH is set to 1 or true
+ if os.Getenv("TEST_KEYFACTOR_KC_AUTH") == "1" || os.Getenv("TEST_KEYFACTOR_KC_AUTH") == "true" {
+ t.Skip("Skipping TestBasicAuthAuthenticator_GetHttpClient")
+ return
+ }
+ config := &auth_providers.CommandAuthConfigBasic{
+ Username: os.Getenv(auth_providers.EnvKeyfactorUsername),
+ Password: os.Getenv(auth_providers.EnvKeyfactorPassword),
+ }
+
+ authenticator, err := config.Build()
+ if err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+
+ if authenticator == nil {
+ t.Fatalf("expected a non-nil Authenticator")
+ }
+}
+
+// setBasicEnvVariables sets the basic environment variables
+func setBasicEnvVariables(username, password, domain string) {
+ os.Setenv(auth_providers.EnvKeyfactorUsername, username)
+ os.Setenv(auth_providers.EnvKeyfactorPassword, password)
+ os.Setenv(auth_providers.EnvKeyfactorDomain, domain)
+}
+
+// exportBasicEnvVariables sets the basic environment variables
+func exportBasicEnvVariables() (string, string, string) {
+ username := os.Getenv(auth_providers.EnvKeyfactorUsername)
+ password := os.Getenv(auth_providers.EnvKeyfactorPassword)
+ domain := os.Getenv(auth_providers.EnvKeyfactorDomain)
+ return username, password, domain
+}
+
+// unsetBasicEnvVariables unsets the basic environment variables
+func unsetBasicEnvVariables() {
+ os.Unsetenv(auth_providers.EnvKeyfactorUsername)
+ os.Unsetenv(auth_providers.EnvKeyfactorPassword)
+ os.Unsetenv(auth_providers.EnvKeyfactorDomain)
+}
+
+func authBasicTest(
+ t *testing.T, testName string, allowFail bool, config *auth_providers.CommandAuthConfigBasic,
+ errorContains ...string,
+) {
+ t.Run(
+ fmt.Sprintf("Basic Auth Test %s", testName), func(t *testing.T) {
+
+ err := config.Authenticate()
+ if allowFail {
+ if err == nil {
+ t.Errorf("Basic auth test '%s' should have failed", testName)
+ t.FailNow()
+ return
+ }
+ if len(errorContains) > 0 {
+ for _, ec := range errorContains {
+ if !strings.Contains(err.Error(), ec) {
+ t.Errorf("Basic auth test '%s' failed with unexpected error %v", testName, err)
+ t.FailNow()
+ return
+ }
+ }
+ }
+ t.Logf("Basic auth test '%s' failed as expected with %v", testName, err)
+ return
+ }
+ if err != nil {
+ t.Errorf("Basic auth test '%s' failed with %v", testName, err)
+ t.FailNow()
+ return
+ }
+ },
+ )
+}
diff --git a/auth_providers/auth_core.go b/auth_providers/auth_core.go
new file mode 100644
index 0000000..97d1295
--- /dev/null
+++ b/auth_providers/auth_core.go
@@ -0,0 +1,749 @@
+// Copyright 2024 Keyfactor
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package auth_providers
+
+import (
+ "crypto/tls"
+ "crypto/x509"
+ "encoding/json"
+ "encoding/pem"
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "time"
+)
+
+const (
+ // DefaultCommandPort is the default port for Keyfactor Command API
+ DefaultCommandPort = 443
+
+ // DefaultCommandAPIPath is the default path for Keyfactor Command API
+ DefaultCommandAPIPath = "KeyfactorAPI"
+
+ // DefaultAPIVersion is the default version for Keyfactor Command API
+ DefaultAPIVersion = "1"
+
+ // DefaultAPIClientName is the default client name for Keyfactor Command API
+ DefaultAPIClientName = "APIClient"
+
+ // DefaultProductVersion is the default product version for Keyfactor Command API
+ DefaultProductVersion = "10.5.0.0"
+
+ // DefaultConfigFilePath is the default path for the configuration file
+ DefaultConfigFilePath = ".keyfactor/command_config.json"
+
+ // DefaultConfigProfile is the default profile for the configuration file
+ DefaultConfigProfile = "default"
+
+ // DefaultClientTimeout is the default timeout for the http Client
+ DefaultClientTimeout = 60
+
+ // EnvKeyfactorHostName is the environment variable for the Keyfactor Command hostname
+ EnvKeyfactorHostName = "KEYFACTOR_HOSTNAME"
+
+ // EnvKeyfactorPort is the environment variable for the Keyfactor Command http(s) port
+ EnvKeyfactorPort = "KEYFACTOR_PORT"
+
+ // EnvKeyfactorAPIPath is the environment variable for the Keyfactor Command API path
+ EnvKeyfactorAPIPath = "KEYFACTOR_API_PATH"
+
+ // EnvKeyfactorSkipVerify is the environment variable for skipping TLS verification when communicating with Keyfactor Command
+ EnvKeyfactorSkipVerify = "KEYFACTOR_SKIP_VERIFY"
+
+ // EnvKeyfactorCACert is the environment variable for the CA certificate to be used for TLS verification when communicating with Keyfactor Command API
+ EnvKeyfactorCACert = "KEYFACTOR_CA_CERT"
+
+ // EnvKeyfactorAuthProvider is the environment variable for the authentication provider to be used for Keyfactor Command API
+ EnvKeyfactorAuthProvider = "KEYFACTOR_AUTH_PROVIDER"
+
+ // EnvKeyfactorAuthProfile is the environment variable for the profile of the configuration file
+ EnvKeyfactorAuthProfile = "KEYFACTOR_AUTH_CONFIG_PROFILE"
+
+ // EnvKeyfactorConfigFile is the environment variable for the configuration file to reference for connecting to the Keyfactor Command API
+ EnvKeyfactorConfigFile = "KEYFACTOR_AUTH_CONFIG_FILE"
+
+ // EnvKeyfactorClientTimeout is the environment variable for the timeout for the http Client
+ EnvKeyfactorClientTimeout = "KEYFACTOR_CLIENT_TIMEOUT"
+)
+
+// Authenticator is an interface for authentication to Keyfactor Command API.
+type Authenticator interface {
+ GetHttpClient() (*http.Client, error)
+}
+
+// roundTripperFunc is a helper type to create a custom RoundTripper
+type roundTripperFunc func(req *http.Request) (*http.Response, error)
+
+// RoundTrip executes a single HTTP transaction
+func (f roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
+ return f(req)
+}
+
+// CommandAuthConfig represents the base configuration needed for authentication to Keyfactor Command API.
+type CommandAuthConfig struct {
+ // ConfigType is the type of configuration
+ ConfigType string `json:"config_type"`
+
+ //ConfigProfile is the profile of the configuration
+ ConfigProfile string
+
+ //ConfigFilePath is the path to the configuration file
+ ConfigFilePath string
+
+ // FileConfig
+ FileConfig *Server
+
+ // AuthHeader is the header to be used for authentication to Keyfactor Command API
+ AuthHeader string `json:"auth_header"`
+
+ // CommandHostName is the hostname of the Keyfactor Command API
+ CommandHostName string `json:"host"`
+
+ // CommandPort is the port of the Keyfactor Command API
+ CommandPort int `json:"port"`
+
+ // CommandAPIPath is the path of the Keyfactor Command API, default is "KeyfactorAPI"
+ CommandAPIPath string `json:"api_path"`
+
+ // CommandAPIVersion is the version of the Keyfactor Command API, default is "1"
+ CommandVersion string `json:"command_version"`
+
+ // CommandCACert is the CA certificate to be used for authentication to Keyfactor Command API for use with not widely trusted certificates. This can be a filepath or a string of the certificate in PEM format.
+ CommandCACert string `json:"command_ca_cert"`
+
+ // SkipVerify is a flag to skip verification of the server's certificate chain and host name. Default is false.
+ SkipVerify bool `json:"skip_verify"`
+
+ // HttpClientTimeout is the timeout for the http Client
+ HttpClientTimeout int `json:"client_timeout"`
+
+ // UserAgent is the user agent to be used for authentication to Keyfactor Command API
+ UserAgent string `json:"user_agent,omitempty"`
+
+ // Debug
+ Debug bool `json:"debug,omitempty"`
+
+ // HttpClient is the http Client to be used for authentication to Keyfactor Command API
+ HttpClient *http.Client
+ //DefaultHttpClient *http.Client
+}
+
+// cleanHostName cleans the hostname for authentication to Keyfactor Command API.
+func cleanHostName(hostName string) string {
+ // check if hostname is a URL and if so, extract the hostname
+ if strings.Contains(hostName, "://") {
+ hostName = strings.Split(hostName, "://")[1]
+ //remove any trailing paths
+ hostName = strings.Split(hostName, "/")[0]
+ // remove any trailing slashes
+ hostName = strings.TrimRight(hostName, "/")
+ }
+ return hostName
+}
+
+// WithCommandHostName sets the hostname for authentication to Keyfactor Command API.
+func (c *CommandAuthConfig) WithCommandHostName(hostName string) *CommandAuthConfig {
+ hostName = cleanHostName(hostName)
+ c.CommandHostName = hostName
+ return c
+}
+
+// WithCommandPort sets the port for authentication to Keyfactor Command API.
+func (c *CommandAuthConfig) WithCommandPort(port int) *CommandAuthConfig {
+ c.CommandPort = port
+ return c
+}
+
+// WithCommandAPIPath sets the API path for authentication to Keyfactor Command API.
+func (c *CommandAuthConfig) WithCommandAPIPath(apiPath string) *CommandAuthConfig {
+ c.CommandAPIPath = apiPath
+ return c
+}
+
+// WithCommandCACert sets the CA certificate for authentication to Keyfactor Command API.
+func (c *CommandAuthConfig) WithCommandCACert(caCert string) *CommandAuthConfig {
+ c.CommandCACert = caCert
+ return c
+}
+
+// WithSkipVerify sets the flag to skip verification of the server's certificate chain and host name.
+func (c *CommandAuthConfig) WithSkipVerify(skipVerify bool) *CommandAuthConfig {
+ c.SkipVerify = skipVerify
+ return c
+}
+
+// WithHttpClient sets the http Client for authentication to Keyfactor Command API.
+func (c *CommandAuthConfig) WithHttpClient(client *http.Client) *CommandAuthConfig {
+ c.HttpClient = client
+ return c
+}
+
+// WithConfigFile sets the configuration file for authentication to Keyfactor Command API.
+func (c *CommandAuthConfig) WithConfigFile(configFilePath string) *CommandAuthConfig {
+
+ if c.ConfigProfile == "" {
+ // check if profile is set in environment
+ if profile, ok := os.LookupEnv(EnvKeyfactorAuthProfile); ok {
+ c.ConfigProfile = profile
+ } else {
+ c.ConfigProfile = DefaultConfigProfile
+ }
+ }
+
+ c.ConfigFilePath = configFilePath
+ return c
+}
+
+// WithConfigProfile sets the configuration profile for authentication to Keyfactor Command API.
+func (c *CommandAuthConfig) WithConfigProfile(profile string) *CommandAuthConfig {
+ if profile == "" {
+ // check if profile is set in environment
+ if p, ok := os.LookupEnv(EnvKeyfactorAuthProfile); ok {
+ c.ConfigProfile = p
+ } else {
+ c.ConfigProfile = DefaultConfigProfile
+ }
+ } else {
+ c.ConfigProfile = profile
+ }
+ return c
+}
+
+// WithClientTimeout sets the timeout for the http Client.
+func (c *CommandAuthConfig) WithClientTimeout(timeout int) *CommandAuthConfig {
+ c.HttpClientTimeout = timeout
+ return c
+}
+
+// ValidateAuthConfig validates the authentication configuration for Keyfactor Command API.
+func (c *CommandAuthConfig) ValidateAuthConfig() error {
+ if c.CommandHostName == "" {
+ if hostName, ok := os.LookupEnv(EnvKeyfactorHostName); ok {
+ c.CommandHostName = cleanHostName(hostName)
+ } else {
+ if c.FileConfig != nil && c.FileConfig.Host != "" {
+ c.CommandHostName = cleanHostName(c.FileConfig.Host)
+ } else {
+ return fmt.Errorf("command_host_name or environment variable %s is required", EnvKeyfactorHostName)
+ }
+ }
+ }
+ if c.CommandPort <= 0 {
+ if port, ok := os.LookupEnv(EnvKeyfactorPort); ok {
+ configPort, pErr := strconv.Atoi(port)
+ if pErr == nil {
+ c.CommandPort = configPort
+ }
+ } else {
+ c.CommandPort = DefaultCommandPort
+ }
+ }
+ if c.CommandAPIPath == "" {
+ if apiPath, ok := os.LookupEnv(EnvKeyfactorAPIPath); ok {
+ c.CommandAPIPath = apiPath
+ } else {
+ c.CommandAPIPath = DefaultCommandAPIPath
+ }
+ }
+ if c.HttpClientTimeout <= 0 {
+ if timeout, ok := os.LookupEnv(EnvKeyfactorClientTimeout); ok {
+ configTimeout, tErr := strconv.Atoi(timeout)
+ if tErr == nil {
+ c.HttpClientTimeout = configTimeout
+ }
+ } else {
+ c.HttpClientTimeout = DefaultClientTimeout
+ }
+ }
+
+ if c.CommandCACert == "" {
+ // check if CommandCACert is set in environment
+ if caCert, ok := os.LookupEnv(EnvKeyfactorCACert); ok {
+ c.CommandCACert = caCert
+ }
+ }
+
+ // check for skip verify in environment
+ if skipVerify, ok := os.LookupEnv(EnvKeyfactorSkipVerify); ok {
+ c.SkipVerify = skipVerify == "true" || skipVerify == "1"
+ }
+ return nil
+}
+
+// BuildTransport creates a custom http Transport for authentication to Keyfactor Command API.
+func (c *CommandAuthConfig) BuildTransport() (*http.Transport, error) {
+ output := http.Transport{
+ Proxy: http.ProxyFromEnvironment,
+ TLSClientConfig: &tls.Config{
+ Renegotiation: tls.RenegotiateOnceAsClient,
+ },
+ TLSHandshakeTimeout: 10 * time.Second,
+ }
+
+ if c.SkipVerify {
+ output.TLSClientConfig.InsecureSkipVerify = true
+ }
+
+ if c.CommandCACert != "" {
+ if _, err := os.Stat(c.CommandCACert); err == nil {
+ cert, ioErr := os.ReadFile(c.CommandCACert)
+ if ioErr != nil {
+ return &output, ioErr
+ }
+ // check if output.TLSClientConfig.RootCAs is nil
+ if output.TLSClientConfig.RootCAs == nil {
+ output.TLSClientConfig.RootCAs = x509.NewCertPool()
+ }
+ // Append your custom cert to the pool
+ if ok := output.TLSClientConfig.RootCAs.AppendCertsFromPEM(cert); !ok {
+ return &output, fmt.Errorf("failed to append custom CA cert to pool")
+ }
+ } else {
+ // Append your custom cert to the pool
+ if ok := output.TLSClientConfig.RootCAs.AppendCertsFromPEM([]byte(c.CommandCACert)); !ok {
+ return &output, fmt.Errorf("failed to append custom CA cert to pool")
+ }
+ }
+ }
+
+ return &output, nil
+}
+
+// SetClient sets the http Client for authentication to Keyfactor Command API.
+func (c *CommandAuthConfig) SetClient(client *http.Client) *http.Client {
+ if client != nil {
+ c.HttpClient = client
+ }
+ if c.HttpClient == nil {
+ //// Copy the default transport and apply the custom TLS config
+ //defaultTransport := http.DefaultTransport.(*http.Transport).Clone()
+ ////defaultTransport.TLSClientConfig = tlsConfig
+ //c.HttpClient = &http.Client{Transport: defaultTransport}
+ defaultTimeout := time.Duration(c.HttpClientTimeout) * time.Second
+ c.HttpClient = &http.Client{
+ Transport: &http.Transport{
+ Proxy: http.ProxyFromEnvironment,
+ TLSClientConfig: &tls.Config{
+ Renegotiation: tls.RenegotiateOnceAsClient,
+ },
+ TLSHandshakeTimeout: defaultTimeout,
+ DisableKeepAlives: false,
+ DisableCompression: false,
+ MaxIdleConns: 10,
+ MaxIdleConnsPerHost: 10,
+ MaxConnsPerHost: 10,
+ IdleConnTimeout: defaultTimeout,
+ ResponseHeaderTimeout: defaultTimeout,
+ ExpectContinueTimeout: defaultTimeout,
+ MaxResponseHeaderBytes: 0,
+ WriteBufferSize: 0,
+ ReadBufferSize: 0,
+ ForceAttemptHTTP2: false,
+ },
+ }
+ }
+
+ return c.HttpClient
+}
+
+// updateCACerts updates the CA certs for the http Client.
+func (c *CommandAuthConfig) updateCACerts() error {
+ // check if CommandCACert is set
+ if c.CommandCACert == "" {
+ // check if CommandCACert is set in environment
+ if caCert, ok := os.LookupEnv(EnvKeyfactorCACert); ok {
+ c.CommandCACert = caCert
+ } else {
+ // nothing to do
+ return nil
+ }
+ }
+
+ // ensure Client is set
+ c.SetClient(nil)
+
+ // Load the system certs
+ rootCAs, pErr := x509.SystemCertPool()
+ if pErr != nil {
+ return pErr
+ }
+ if rootCAs == nil {
+ rootCAs = x509.NewCertPool()
+ }
+
+ // check if CommandCACert is a file
+ if _, err := os.Stat(c.CommandCACert); err == nil {
+ cert, ioErr := os.ReadFile(c.CommandCACert)
+ if ioErr != nil {
+ return ioErr
+ }
+ // Append your custom cert to the pool
+ if ok := rootCAs.AppendCertsFromPEM(cert); !ok {
+ return fmt.Errorf("failed to append custom CA cert to pool")
+ }
+ } else {
+ // Append your custom cert to the pool
+ if ok := rootCAs.AppendCertsFromPEM([]byte(c.CommandCACert)); !ok {
+ return fmt.Errorf("failed to append custom CA cert to pool")
+ }
+ }
+
+ //check if c already has a transport and if it does, update the RootCAs else create a new transport
+ if c.HttpClient.Transport != nil {
+ if transport, ok := c.HttpClient.Transport.(*http.Transport); ok {
+ transport.TLSClientConfig.RootCAs = rootCAs
+ } else {
+ c.HttpClient.Transport = &http.Transport{
+ TLSClientConfig: &tls.Config{
+ RootCAs: rootCAs,
+ },
+ }
+ }
+ } else {
+ c.HttpClient.Transport = &http.Transport{
+ TLSClientConfig: &tls.Config{
+ RootCAs: rootCAs,
+ },
+ }
+ }
+
+ // Trust the augmented cert pool in our Client
+ //c.HttpClient.Transport = &http.Transport{
+ // TLSClientConfig: &tls.Config{
+ // RootCAs: rootCAs,
+ // },
+ //}
+
+ return nil
+}
+
+// Authenticate performs the authentication test to Keyfactor Command API and sets Command product version.
+func (c *CommandAuthConfig) Authenticate() error {
+
+ if c.HttpClient == nil {
+ c.SetClient(nil)
+ }
+ //create headers for request
+ headers := map[string]string{
+ "Content-Type": "application/json",
+ "Accept": "application/json",
+ "x-keyfactor-api-version": DefaultAPIVersion,
+ "x-keyfactor-requested-with": DefaultAPIClientName,
+ }
+
+ if c.AuthHeader != "" {
+ headers["Authorization"] = c.AuthHeader
+ }
+
+ endPoint := fmt.Sprintf(
+ "https://%s/%s/Status/Endpoints",
+ c.CommandHostName,
+ //c.CommandPort,
+ c.CommandAPIPath,
+ )
+
+ // create request object
+ req, rErr := http.NewRequest("GET", endPoint, nil)
+ if rErr != nil {
+ return rErr
+ }
+
+ // Set headers from the map
+ for key, value := range headers {
+ req.Header.Set(key, value)
+ }
+
+ c.HttpClient.Timeout = time.Duration(c.HttpClientTimeout) * time.Second
+ cResp, cErr := c.HttpClient.Do(req)
+ if cErr != nil {
+ return cErr
+ } else if cResp == nil {
+ return fmt.Errorf("failed to authenticate, no response received from Keyfactor Command")
+ }
+
+ defer cResp.Body.Close()
+
+ // check if body is empty
+ if cResp.Body == nil {
+ return fmt.Errorf("failed to authenticate, empty response body received from Keyfactor Command")
+ }
+
+ cRespBody, ioErr := io.ReadAll(cResp.Body)
+ if ioErr != nil {
+ return ioErr
+ }
+
+ if cResp.StatusCode != 200 {
+ //convert body to string
+ return fmt.Errorf(
+ "failed to authenticate, received status code %d from Keyfactor Command: %s",
+ cResp.StatusCode,
+ string(cRespBody),
+ )
+ }
+
+ productVersion := cResp.Header.Get("x-keyfactor-product-version")
+ if productVersion != "" {
+ c.CommandVersion = productVersion
+ } else {
+ c.CommandVersion = DefaultProductVersion
+ }
+
+ //decode response to json
+ var response []string
+ if err := json.Unmarshal(cRespBody, &response); err != nil {
+ return err
+ }
+
+ return nil
+
+}
+
+// LoadCACertificates loads the custom CA certificates from a file.
+func LoadCACertificates(certFile string) (*x509.CertPool, error) {
+ // Read the file containing the custom CA certificate
+ certBytes, err := os.ReadFile(certFile)
+ if err != nil {
+ return nil, err
+ }
+
+ // Create a new CertPool and append the custom CA certificate
+ certPool := x509.NewCertPool()
+ if ok := certPool.AppendCertsFromPEM(certBytes); !ok {
+ return nil, err
+ }
+
+ return certPool, nil
+}
+
+// FindCACertificate reads the CA certificate from a file and returns a slice of x509.Certificate.
+func FindCACertificate(caCertificatePath string) ([]*x509.Certificate, error) {
+ if caCertificatePath == "" {
+ return nil, nil
+ }
+
+ buf, err := os.ReadFile(caCertificatePath)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read CA certificate file at path %s: %w", caCertificatePath, err)
+ }
+ // Decode the PEM encoded certificates into a slice of PEM blocks
+ chainBlocks, _, err := DecodePEMBytes(buf)
+ if err != nil {
+ return nil, err
+ }
+ if len(chainBlocks) <= 0 {
+ return nil, fmt.Errorf("didn't find certificate in file at path %s", caCertificatePath)
+ }
+
+ var caChain []*x509.Certificate
+ for _, block := range chainBlocks {
+ // Parse the PEM block into an x509 certificate
+ cert, err := x509.ParseCertificate(block.Bytes)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse CA certificate: %w", err)
+ }
+
+ caChain = append(caChain, cert)
+ }
+
+ return caChain, nil
+}
+
+// DecodePEMBytes decodes the PEM encoded bytes into a slice of PEM blocks.
+func DecodePEMBytes(buf []byte) ([]*pem.Block, []byte, error) {
+ var privKey []byte
+ var certificates []*pem.Block
+ var block *pem.Block
+ for {
+ block, buf = pem.Decode(buf)
+ if block == nil {
+ break
+ } else if strings.Contains(block.Type, "PRIVATE KEY") {
+ privKey = pem.EncodeToMemory(block)
+ } else {
+ certificates = append(certificates, block)
+ }
+ }
+ return certificates, privKey, nil
+}
+
+// LoadConfig loads the configuration file and returns the server configuration.
+func (c *CommandAuthConfig) LoadConfig(profile string, configFilePath string, silentLoad bool) (
+ *Server,
+ error,
+) {
+ if configFilePath == "" {
+ // check if config file is set in environment
+ if config, ok := os.LookupEnv(EnvKeyfactorConfigFile); ok {
+ configFilePath = config
+ } else {
+ homedir, err := os.UserHomeDir()
+ if err != nil {
+ homedir = os.Getenv("HOME")
+ }
+ configFilePath = fmt.Sprintf("%s/%s", homedir, DefaultConfigFilePath)
+ }
+ } else {
+ c.ConfigFilePath = configFilePath
+ }
+ expandedPath, err := expandPath(configFilePath)
+ if err != nil {
+ if !silentLoad {
+ return nil, err
+ }
+ // if silentLoad is true then eat the error and return nil
+ return nil, nil
+ }
+
+ file, err := os.Open(expandedPath)
+ if err != nil {
+ if !silentLoad {
+ return nil, err
+ }
+ // if silentLoad is true then eat the error and return nil
+ return nil, nil
+ }
+ defer file.Close()
+
+ var config Config
+ decoder := json.NewDecoder(file)
+ if jErr := decoder.Decode(&config); jErr != nil {
+ if !silentLoad {
+ return nil, jErr
+ }
+ // if silentLoad is true then eat the error and return nil
+ return nil, nil
+ }
+
+ if profile == "" {
+ if c.ConfigProfile != "" {
+ profile = c.ConfigProfile
+ } else {
+ profile = DefaultConfigProfile
+ }
+ }
+
+ server, ok := config.Servers[profile]
+ if !ok {
+ if !silentLoad {
+ return nil, fmt.Errorf("profile %s not found in config file", profile)
+ }
+ // if silentLoad is true then eat the error and return nil
+ return nil, nil
+ }
+
+ c.FileConfig = &server
+
+ if c.CommandHostName == "" {
+ c.CommandHostName = server.Host
+ }
+ if c.CommandPort <= 0 {
+ c.CommandPort = server.Port
+ }
+ if c.CommandAPIPath == "" {
+ c.CommandAPIPath = server.APIPath
+ }
+ if c.CommandCACert == "" {
+ c.CommandCACert = server.CACertPath
+ }
+ if !c.SkipVerify {
+ c.SkipVerify = server.SkipTLSVerify
+ }
+
+ //if !silentLoad {
+ // c.CommandHostName = server.Host
+ // c.CommandPort = server.Port
+ // c.CommandAPIPath = server.APIPath
+ // c.CommandCACert = server.CACertPath
+ // c.SkipVerify = server.SkipTLSVerify
+ //} else {
+ // if c.CommandHostName == "" {
+ // c.CommandHostName = server.Host
+ // }
+ // if c.CommandPort <= 0 {
+ // c.CommandPort = server.Port
+ // }
+ // if c.CommandAPIPath == "" {
+ // c.CommandAPIPath = server.APIPath
+ // }
+ // if c.CommandCACert == "" {
+ // c.CommandCACert = server.CACertPath
+ // }
+ // if c.SkipVerify {
+ // c.SkipVerify = server.SkipTLSVerify
+ // }
+ //}
+ return &server, nil
+}
+
+// expandPath expands the path to include the user's home directory.
+func expandPath(path string) (string, error) {
+ if path[:2] == "~/" {
+ home, err := os.UserHomeDir()
+ if err != nil {
+ return "", err
+ }
+ return filepath.Join(home, path[2:]), nil
+ }
+ return path, nil
+}
+
+// GetServerConfig returns the server configuration.
+func (c *CommandAuthConfig) GetServerConfig() *Server {
+ server := Server{
+ Host: c.CommandHostName,
+ Port: c.CommandPort,
+ Username: "",
+ Password: "",
+ Domain: "",
+ ClientID: "",
+ ClientSecret: "",
+ OAuthTokenUrl: "",
+ APIPath: c.CommandAPIPath,
+ AuthProvider: AuthProvider{},
+ SkipTLSVerify: c.SkipVerify,
+ CACertPath: c.CommandCACert,
+ AuthType: "",
+ }
+ return &server
+}
+
+// Example usage of CommandAuthConfig
+//
+// This example demonstrates how to use CommandAuthConfig to authenticate to the Keyfactor Command API.
+//
+// func ExampleCommandAuthConfig_Authenticate() {
+// authConfig := &CommandAuthConfig{
+// ConfigFilePath: "/path/to/config.json",
+// ConfigProfile: "default",
+// CommandHostName: "exampleHost",
+// CommandPort: 443,
+// CommandAPIPath: "/api/v1",
+// CommandCACert: "/path/to/ca-cert.pem",
+// SkipVerify: true,
+// HttpClientTimeout: 60,
+// }
+//
+// err := authConfig.Authenticate()
+// if err != nil {
+// fmt.Println("Authentication failed:", err)
+// } else {
+// fmt.Println("Authentication successful")
+// }
+// }
diff --git a/auth_providers/auth_core_test.go b/auth_providers/auth_core_test.go
new file mode 100644
index 0000000..efcf2a9
--- /dev/null
+++ b/auth_providers/auth_core_test.go
@@ -0,0 +1,104 @@
+// Copyright 2024 Keyfactor
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package auth_providers_test
+
+import (
+ "net/http"
+ "testing"
+
+ "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers"
+)
+
+func TestCommandAuthConfig_ValidateAuthConfig(t *testing.T) {
+ config := &auth_providers.CommandAuthConfig{
+ CommandHostName: "test-host",
+ CommandPort: 443,
+ CommandAPIPath: "KeyfactorAPI",
+ }
+
+ err := config.ValidateAuthConfig()
+ if err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+}
+
+func TestCommandAuthConfig_BuildTransport(t *testing.T) {
+ config := &auth_providers.CommandAuthConfig{
+ CommandHostName: "test-host",
+ CommandPort: 443,
+ CommandAPIPath: "KeyfactorAPI",
+ }
+
+ transport, err := config.BuildTransport()
+ if err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+
+ if transport == nil {
+ t.Fatalf("expected a non-nil http.Transport")
+ }
+}
+
+func TestCommandAuthConfig_SetClient(t *testing.T) {
+ config := &auth_providers.CommandAuthConfig{}
+
+ client := &http.Client{}
+ config.SetClient(client)
+
+ if config.HttpClient != client {
+ t.Fatalf("expected HttpClient to be set")
+ }
+}
+
+func TestCommandAuthConfig_Authenticate(t *testing.T) {
+ config := &auth_providers.CommandAuthConfig{
+ CommandHostName: "test-host",
+ CommandPort: 443,
+ CommandAPIPath: "KeyfactorAPI",
+ }
+
+ err := config.Authenticate()
+ if err == nil {
+ t.Fatalf("expected an error, got nil")
+ }
+}
+
+func TestLoadCACertificates(t *testing.T) {
+ _, err := auth_providers.LoadCACertificates("../lib/test_ca_cert.pem")
+ if err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+}
+
+func TestFindCACertificate(t *testing.T) {
+ _, err := auth_providers.FindCACertificate("../lib/test_chain.pem")
+ if err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+}
+
+func TestDecodePEMBytes(t *testing.T) {
+ pemData := []byte(`-----BEGIN CERTIFICATE-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7Q2+1+2+1+2+1+2+1+2+
+-----END CERTIFICATE-----`)
+ blocks, _, err := auth_providers.DecodePEMBytes(pemData)
+ if err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+
+ if len(blocks) == 0 {
+ t.Fatalf("expected non-zero blocks")
+ }
+}
diff --git a/auth_providers/auth_oauth.go b/auth_providers/auth_oauth.go
new file mode 100644
index 0000000..8a8c940
--- /dev/null
+++ b/auth_providers/auth_oauth.go
@@ -0,0 +1,414 @@
+package auth_providers
+
+import (
+ "context"
+ "crypto/x509"
+ "fmt"
+ "net/http"
+ "os"
+ "time"
+
+ "golang.org/x/oauth2"
+ "golang.org/x/oauth2/clientcredentials"
+)
+
+const (
+ // DefaultKeyfactorAuthPort is the default port for Keyfactor authentication
+ DefaultKeyfactorAuthPort = "8444"
+
+ // DefaultTokenPrefix is the default token prefix for Keyfactor authentication headers
+ DefaultTokenPrefix = "Bearer"
+
+ // EnvKeyfactorClientID is the environment variable used to set the Client ID for oauth Client credentials authentication
+ EnvKeyfactorClientID = "KEYFACTOR_AUTH_CLIENT_ID"
+
+ // EnvKeyfactorClientSecret is the environment variable used to set the Client secret for oauth Client credentials authentication
+ EnvKeyfactorClientSecret = "KEYFACTOR_AUTH_CLIENT_SECRET"
+
+ // EnvKeyfactorAuthTokenURL EnvCommandTokenURL is the environment variable used to set the token URL for oauth Client credentials authentication
+ EnvKeyfactorAuthTokenURL = "KEYFACTOR_AUTH_TOKEN_URL"
+
+ // EnvKeyfactorAccessToken is the environment variable used to set the access token for oauth Client credentials authentication
+ EnvKeyfactorAccessToken = "KEYFACTOR_AUTH_ACCESS_TOKEN"
+
+ // EnvKeyfactorAuthAudience is the environment variable used to set the audience for oauth Client credentials
+ //authentication
+ EnvKeyfactorAuthAudience = "KEYFACTOR_AUTH_AUDIENCE"
+
+ // EnvKeyfactorAuthScopes is the environment variable used to set the scopes for oauth Client credentials authentication
+ EnvKeyfactorAuthScopes = "KEYFACTOR_AUTH_SCOPES"
+
+ // EnvAuthCACert is a path to a CA certificate for the OAuth Client credentials authentication
+ EnvAuthCACert = "KEYFACTOR_AUTH_CA_CERT"
+)
+
+// OAuth Authenticator
+var _ Authenticator = &OAuthAuthenticator{}
+
+// OAuthAuthenticator is an Authenticator that uses OAuth2 for authentication.
+type OAuthAuthenticator struct {
+ Client *http.Client
+}
+
+type oauth2Transport struct {
+ base http.RoundTripper
+ src oauth2.TokenSource
+}
+
+// GetHttpClient returns the http client
+func (a *OAuthAuthenticator) GetHttpClient() (*http.Client, error) {
+ return a.Client, nil
+}
+
+// CommandConfigOauth represents the configuration needed for authentication to Keyfactor Command API using OAuth2.
+type CommandConfigOauth struct {
+ // CommandAuthConfig is a reference to the base configuration needed for authentication to Keyfactor Command API
+ CommandAuthConfig
+
+ // ClientID is the Client ID for Keycloak authentication
+ ClientID string `json:"client_id,omitempty"`
+
+ // ClientSecret is the Client secret for Keycloak authentication
+ ClientSecret string `json:"client_secret,omitempty"`
+
+ // Audience is the audience for Keycloak authentication
+ Audience string `json:"audience,omitempty"`
+
+ // Scopes is the scopes for Keycloak authentication
+ Scopes []string `json:"scopes,omitempty"`
+
+ // CACertificatePath is the path to the CA certificate for Keycloak authentication
+ CACertificatePath string `json:"idp_ca_cert,omitempty"`
+
+ // CACertificates is the CA certificates for authentication
+ CACertificates []*x509.Certificate `json:"-"`
+
+ // AccessToken is the access token for Keycloak authentication
+ AccessToken string `json:"access_token,omitempty"`
+
+ // RefreshToken is the refresh token for Keycloak authentication
+ RefreshToken string `json:"refresh_token,omitempty"`
+
+ // Expiry is the expiry time of the access token
+ Expiry time.Time `json:"expiry,omitempty"`
+
+ // TokenURL is the token URL for Keycloak authentication
+ TokenURL string `json:"token_url,omitempty"`
+
+ //// AuthPort
+ //AuthPort string `json:"auth_port,omitempty"`
+
+ //// AuthType is the type of Keycloak auth to use such as client_credentials, password, etc.
+ //AuthType string `json:"auth_type,omitempty"`
+}
+
+// NewOAuthAuthenticatorBuilder creates a new CommandConfigOauth instance.
+func NewOAuthAuthenticatorBuilder() *CommandConfigOauth {
+ return &CommandConfigOauth{}
+}
+
+// WithClientId sets the Client ID for Keycloak authentication.
+func (b *CommandConfigOauth) WithClientId(clientId string) *CommandConfigOauth {
+ b.ClientID = clientId
+ return b
+}
+
+// WithClientSecret sets the Client secret for Keycloak authentication.
+func (b *CommandConfigOauth) WithClientSecret(clientSecret string) *CommandConfigOauth {
+ b.ClientSecret = clientSecret
+ return b
+}
+
+// WithTokenUrl sets the token URL for Keycloak authentication.
+func (b *CommandConfigOauth) WithTokenUrl(tokenUrl string) *CommandConfigOauth {
+ b.TokenURL = tokenUrl
+ return b
+}
+
+// WithScopes sets the scopes for Keycloak authentication.
+func (b *CommandConfigOauth) WithScopes(scopes []string) *CommandConfigOauth {
+ b.Scopes = scopes
+ return b
+}
+
+// WithAudience sets the audience for Keycloak authentication.
+func (b *CommandConfigOauth) WithAudience(audience string) *CommandConfigOauth {
+ b.Audience = audience
+ return b
+}
+
+// WithCaCertificatePath sets the CA certificate path for Keycloak authentication.
+func (b *CommandConfigOauth) WithCaCertificatePath(caCertificatePath string) *CommandConfigOauth {
+ b.CACertificatePath = caCertificatePath
+ return b
+}
+
+// WithCaCertificates sets the CA certificates for Keycloak authentication.
+func (b *CommandConfigOauth) WithCaCertificates(caCertificates []*x509.Certificate) *CommandConfigOauth {
+ b.CACertificates = caCertificates
+ return b
+}
+
+// WithAccessToken sets the access token for Keycloak authentication.
+func (b *CommandConfigOauth) WithAccessToken(accessToken string) *CommandConfigOauth {
+ if accessToken != "" {
+ b.AccessToken = accessToken
+ }
+
+ return b
+}
+
+func (b *CommandConfigOauth) WithHttpClient(httpClient *http.Client) *CommandConfigOauth {
+ b.HttpClient = httpClient
+ return b
+}
+
+// GetHttpClient returns an HTTP client for oAuth authentication.
+func (b *CommandConfigOauth) GetHttpClient() (*http.Client, error) {
+ cErr := b.ValidateAuthConfig()
+ if cErr != nil {
+ return nil, cErr
+ }
+
+ var client http.Client
+ baseTransport, tErr := b.BuildTransport()
+ if tErr != nil {
+ return nil, tErr
+ }
+
+ if b.AccessToken != "" {
+ client.Transport = &oauth2.Transport{
+ Base: baseTransport,
+ Source: oauth2.StaticTokenSource(
+ &oauth2.Token{
+ AccessToken: b.AccessToken,
+ TokenType: DefaultTokenPrefix,
+ },
+ ),
+ }
+ return &client, nil
+ }
+
+ config := &clientcredentials.Config{
+ ClientID: b.ClientID,
+ ClientSecret: b.ClientSecret,
+ TokenURL: b.TokenURL,
+ Scopes: b.Scopes,
+ }
+
+ if len(b.Scopes) == 0 {
+ b.Scopes = []string{"openid", "profile", "email"}
+ }
+
+ if b.Audience != "" {
+ config.EndpointParams = map[string][]string{
+ "Audience": {b.Audience},
+ }
+ }
+
+ ctx := context.WithValue(context.Background(), oauth2.HTTPClient, &http.Client{Transport: baseTransport})
+ tokenSource := config.TokenSource(ctx)
+
+ client = http.Client{
+ Transport: &oauth2Transport{
+ base: baseTransport,
+ src: tokenSource,
+ },
+ }
+
+ return &client, nil
+}
+
+// Build creates an OAuth authenticator.
+func (b *CommandConfigOauth) Build() (Authenticator, error) {
+
+ client, cErr := b.GetHttpClient()
+ if cErr != nil {
+ return nil, cErr
+ }
+
+ return &OAuthAuthenticator{Client: client}, nil
+}
+
+// LoadConfig loads the configuration for Keyfactor Command API using OAuth2.
+func (b *CommandConfigOauth) LoadConfig(profile, path string, silentLoad bool) (*Server, error) {
+ serverConfig, sErr := b.CommandAuthConfig.LoadConfig(profile, path, silentLoad)
+ if sErr != nil {
+ if !silentLoad {
+ return nil, sErr
+ }
+ // if silentLoad is true, return nil and nil
+ return nil, nil
+ }
+
+ if !silentLoad {
+ b.ClientID = serverConfig.ClientID
+ b.ClientSecret = serverConfig.ClientSecret
+ b.TokenURL = serverConfig.OAuthTokenUrl
+ b.CACertificatePath = serverConfig.CACertPath
+
+ } else {
+ if b.ClientID == "" {
+ b.ClientID = serverConfig.ClientID
+ }
+
+ if b.ClientSecret == "" {
+ b.ClientSecret = serverConfig.ClientSecret
+ }
+
+ if b.TokenURL == "" {
+ b.TokenURL = serverConfig.OAuthTokenUrl
+ }
+
+ //if b.AccessToken == "" {
+ // b.AccessToken = serverConfig.AccessToken
+ //}
+
+ //if b.Audience == "" {
+ // b.Audience = serverConfig.Audience
+ //}
+ //
+ //if b.Scopes == nil || len(b.Scopes) == 0 {
+ // b.Scopes = serverConfig.Scopes
+ //}
+
+ if b.CACertificatePath == "" {
+ b.CACertificatePath = serverConfig.CACertPath
+ }
+ }
+
+ return serverConfig, nil
+}
+
+// ValidateAuthConfig validates the configuration for Keyfactor Command API using OAuth2.
+func (b *CommandConfigOauth) ValidateAuthConfig() error {
+
+ silentLoad := true
+ if b.CommandAuthConfig.ConfigProfile != "" {
+ silentLoad = false
+ } else if b.CommandAuthConfig.ConfigFilePath != "" {
+ silentLoad = false
+ }
+
+ serverConfig, cErr := b.CommandAuthConfig.LoadConfig(
+ b.CommandAuthConfig.ConfigProfile,
+ b.CommandAuthConfig.ConfigFilePath,
+ silentLoad,
+ )
+
+ if !silentLoad && cErr != nil {
+ return cErr
+ }
+
+ if b.AccessToken == "" {
+ // check if access token is set in the environment
+ if accessToken, ok := os.LookupEnv(EnvKeyfactorAccessToken); ok {
+ b.AccessToken = accessToken
+ } else {
+ // check if client ID, client secret, and token URL are provided
+ if b.ClientID == "" {
+ if clientId, idOk := os.LookupEnv(EnvKeyfactorClientID); idOk {
+ b.ClientID = clientId
+ } else {
+ if serverConfig != nil && serverConfig.ClientID != "" {
+ b.ClientID = serverConfig.ClientID
+ } else {
+ return fmt.Errorf("client ID or environment variable %s is required", EnvKeyfactorClientID)
+ }
+ }
+ }
+
+ if b.ClientSecret == "" {
+ if clientSecret, sOk := os.LookupEnv(EnvKeyfactorClientSecret); sOk {
+ b.ClientSecret = clientSecret
+ } else {
+ if serverConfig != nil && serverConfig.ClientSecret != "" {
+ b.ClientSecret = serverConfig.ClientSecret
+ } else {
+ return fmt.Errorf(
+ "client secret or environment variable %s is required",
+ EnvKeyfactorClientSecret,
+ )
+ }
+ }
+ }
+
+ if b.TokenURL == "" {
+ if tokenUrl, uOk := os.LookupEnv(EnvKeyfactorAuthTokenURL); uOk {
+ b.TokenURL = tokenUrl
+ } else {
+ if serverConfig != nil && serverConfig.OAuthTokenUrl != "" {
+ b.TokenURL = serverConfig.OAuthTokenUrl
+ } else {
+ return fmt.Errorf(
+ "token URL or environment variable %s is required",
+ EnvKeyfactorAuthTokenURL,
+ )
+ }
+ }
+ }
+ }
+ }
+
+ return b.CommandAuthConfig.ValidateAuthConfig()
+}
+
+// Authenticate authenticates to Keyfactor Command API using OAuth2.
+func (b *CommandConfigOauth) Authenticate() error {
+
+ // validate auth config
+ vErr := b.ValidateAuthConfig()
+ if vErr != nil {
+ return vErr
+ }
+
+ // create oauth Client
+ oauthy, err := b.GetHttpClient()
+
+ if err != nil {
+ return err
+ } else if oauthy == nil {
+ return fmt.Errorf("unable to create http client")
+ }
+
+ b.SetClient(oauthy)
+ //b.DefaultHttpClient = oauthy
+
+ aErr := b.CommandAuthConfig.Authenticate()
+ if aErr != nil {
+ return aErr
+ }
+
+ return nil
+}
+
+// GetServerConfig returns the server configuration for Keyfactor Command API using OAuth2.
+func (b *CommandConfigOauth) GetServerConfig() *Server {
+ server := Server{
+ Host: b.CommandHostName,
+ Port: b.CommandPort,
+ ClientID: b.ClientID,
+ ClientSecret: b.ClientSecret,
+ OAuthTokenUrl: b.TokenURL,
+ APIPath: b.CommandAPIPath,
+ //AuthProvider: AuthProvider{},
+ SkipTLSVerify: b.SkipVerify,
+ CACertPath: b.CommandCACert,
+ AuthType: "oauth",
+ }
+ return &server
+}
+
+// RoundTrip executes a single HTTP transaction, adding the OAuth2 token to the request
+func (t *oauth2Transport) RoundTrip(req *http.Request) (*http.Response, error) {
+ token, err := t.src.Token()
+ if err != nil {
+ return nil, fmt.Errorf("failed to retrieve OAuth token: %w", err)
+ }
+
+ // Clone the request to avoid mutating the original
+ reqCopy := req.Clone(req.Context())
+ token.SetAuthHeader(reqCopy)
+
+ return t.base.RoundTrip(reqCopy)
+}
diff --git a/auth_providers/auth_oauth_test.go b/auth_providers/auth_oauth_test.go
new file mode 100644
index 0000000..debe24e
--- /dev/null
+++ b/auth_providers/auth_oauth_test.go
@@ -0,0 +1,436 @@
+// Copyright 2024 Keyfactor
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package auth_providers_test
+
+import (
+ "fmt"
+ "net/http"
+ "os"
+ "strings"
+ "testing"
+
+ "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers"
+)
+
+func TestOAuthAuthenticator_GetHttpClient(t *testing.T) {
+ // Skip test if TEST_KEYFACTOR_AD_AUTH is set to 1 or true
+ if os.Getenv("TEST_KEYFACTOR_AD_AUTH") == "1" || os.Getenv("TEST_KEYFACTOR_AD_AUTH") == "true" {
+ t.Skip("Skipping TestOAuthAuthenticator_GetHttpClient")
+ return
+ }
+ auth := &auth_providers.OAuthAuthenticator{
+ Client: &http.Client{},
+ }
+
+ client, err := auth.GetHttpClient()
+ if err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+
+ if client == nil {
+ t.Fatalf("expected a non-nil http.Client")
+ }
+}
+
+func TestCommandConfigOauth_ValidateAuthConfig(t *testing.T) {
+ // Skip test if TEST_KEYFACTOR_AD_AUTH is set to 1 or true
+ if os.Getenv("TEST_KEYFACTOR_AD_AUTH") == "1" || os.Getenv("TEST_KEYFACTOR_AD_AUTH") == "true" {
+ t.Skip("Skipping TestOAuthAuthenticator_GetHttpClient")
+ return
+ }
+ config := &auth_providers.CommandConfigOauth{
+ ClientID: os.Getenv(auth_providers.EnvKeyfactorClientID),
+ ClientSecret: os.Getenv(auth_providers.EnvKeyfactorClientSecret),
+ TokenURL: os.Getenv(auth_providers.EnvKeyfactorAuthTokenURL),
+ }
+
+ err := config.ValidateAuthConfig()
+ if err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+}
+
+func TestCommandConfigOauth_GetHttpClient(t *testing.T) {
+ // Skip test if TEST_KEYFACTOR_AD_AUTH is set to 1 or true
+ if os.Getenv("TEST_KEYFACTOR_AD_AUTH") == "1" || os.Getenv("TEST_KEYFACTOR_AD_AUTH") == "true" {
+ t.Skip("Skipping TestOAuthAuthenticator_GetHttpClient")
+ return
+ }
+ config := &auth_providers.CommandConfigOauth{
+ ClientID: os.Getenv(auth_providers.EnvKeyfactorClientID),
+ ClientSecret: os.Getenv(auth_providers.EnvKeyfactorClientSecret),
+ TokenURL: os.Getenv(auth_providers.EnvKeyfactorAuthTokenURL),
+ Scopes: []string{"openid", "profile", "email"},
+ }
+
+ client, err := config.GetHttpClient()
+ if err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+
+ if client == nil {
+ t.Fatalf("expected a non-nil http.Client")
+ }
+}
+
+func TestCommandConfigOauth_Authenticate(t *testing.T) {
+ // Skip test if TEST_KEYFACTOR_AD_AUTH is set to 1 or true
+ if os.Getenv("TEST_KEYFACTOR_AD_AUTH") == "1" || os.Getenv("TEST_KEYFACTOR_AD_AUTH") == "true" {
+ t.Skip("Skipping TestOAuthAuthenticator_GetHttpClient")
+ return
+ }
+ userHome, hErr := os.UserHomeDir()
+ if hErr != nil {
+ userHome = os.Getenv("HOME")
+ }
+
+ configFilePath := fmt.Sprintf("%s/%s", userHome, auth_providers.DefaultConfigFilePath)
+ configFromFile, cErr := auth_providers.ReadConfigFromJSON(configFilePath)
+ if cErr != nil {
+ t.Errorf("unable to load auth config from file %s: %v", configFilePath, cErr)
+ }
+
+ if configFromFile == nil || configFromFile.Servers == nil {
+ t.Errorf("invalid config file %s", configFilePath)
+ t.FailNow()
+ }
+
+ caCertPath := "../lib/certs/int-oidc-lab.eastus2.cloudapp.azure.com.pem"
+
+ //Delete the config file
+ t.Logf("Deleting config file: %s", configFilePath)
+ os.Remove(configFilePath)
+ defer func() {
+ // Write the config file back
+ t.Logf("Writing config file: %s", configFilePath)
+ fErr := auth_providers.WriteConfigToJSON(configFilePath, configFromFile)
+ if fErr != nil {
+ t.Errorf("unable to write auth config to file %s: %v", configFilePath, fErr)
+ }
+ }()
+ //os.Setenv(auth_providers.EnvKeyfactorConfigFile, configFilePath)
+ //os.Setenv(auth_providers.EnvKeyfactorAuthProfile, "oauth")
+ os.Setenv(auth_providers.EnvKeyfactorSkipVerify, "true")
+ os.Setenv(auth_providers.EnvKeyfactorCACert, caCertPath)
+
+ //current working directory
+ cwd, _ := os.Getwd()
+ t.Logf("Current working directory: %s", cwd)
+
+ // Begin test case
+ noParamsTestName := fmt.Sprintf(
+ "w/ complete ENV variables & %s,%s", auth_providers.EnvKeyfactorCACert,
+ auth_providers.EnvKeyfactorSkipVerify,
+ )
+ t.Log(fmt.Sprintf("Testing %s", noParamsTestName))
+ noParamsConfig := &auth_providers.CommandConfigOauth{}
+ authOauthTest(
+ t, noParamsTestName, false, noParamsConfig,
+ )
+ t.Logf("Unsetting environment variable %s", auth_providers.EnvKeyfactorCACert)
+ os.Unsetenv(auth_providers.EnvKeyfactorCACert)
+ t.Logf("Unsetting environment variable %s", auth_providers.EnvKeyfactorSkipVerify)
+ os.Unsetenv(auth_providers.EnvKeyfactorSkipVerify)
+ // end test case
+
+ // Begin test case
+ noParamsTestName = fmt.Sprintf(
+ "w/ complete ENV variables & %s", auth_providers.EnvKeyfactorCACert,
+ )
+ t.Log(fmt.Sprintf("Testing %s", noParamsTestName))
+ t.Logf("Setting environment variable %s", auth_providers.EnvKeyfactorCACert)
+ os.Setenv(auth_providers.EnvKeyfactorCACert, caCertPath)
+ noParamsConfig = &auth_providers.CommandConfigOauth{}
+ authOauthTest(t, noParamsTestName, false, noParamsConfig)
+ t.Logf("Unsetting environment variable %s", auth_providers.EnvKeyfactorCACert)
+ os.Unsetenv(auth_providers.EnvKeyfactorCACert)
+ // end test case
+
+ // Begin test case
+ noParamsTestName = fmt.Sprintf(
+ "w/ complete ENV variables & %s", auth_providers.EnvKeyfactorSkipVerify,
+ )
+ t.Log(fmt.Sprintf("Testing %s", noParamsTestName))
+ t.Logf("Setting environment variable %s", auth_providers.EnvKeyfactorSkipVerify)
+ os.Setenv(auth_providers.EnvKeyfactorSkipVerify, "true")
+ noParamsConfig = &auth_providers.CommandConfigOauth{}
+ authOauthTest(t, noParamsTestName, false, noParamsConfig)
+ t.Logf("Unsetting environment variable %s", auth_providers.EnvKeyfactorSkipVerify)
+ os.Unsetenv(auth_providers.EnvKeyfactorSkipVerify)
+ // end test case
+
+ // Begin test case
+ noParamsConfig = &auth_providers.CommandConfigOauth{}
+ httpsFailEnvExpected := []string{"tls: failed to verify certificate"}
+ authOauthTest(
+ t,
+ fmt.Sprintf("w/o env %s", auth_providers.EnvKeyfactorCACert),
+ true,
+ noParamsConfig,
+ httpsFailEnvExpected...,
+ )
+ // end test case
+
+ t.Log("Testing oAuth with invalid config file path")
+ invFilePath := &auth_providers.CommandConfigOauth{}
+ invFilePath.WithConfigFile("invalid-file-path")
+ invalidPathExpectedError := []string{"no such file or directory", "invalid-file-path"}
+ authOauthTest(t, "with invalid config file PATH", true, invFilePath, invalidPathExpectedError...)
+
+ // Environment variables are not set
+ t.Log("Unsetting environment variables")
+ //keyfactorEnvVars := exportEnvVarsWithPrefix("KEYFACTOR_")
+ clientID, clientSecret, tokenURL := exportOAuthEnvVariables()
+ unsetOAuthEnvVariables()
+ defer func() {
+ t.Log("Resetting environment variables")
+ setOAuthEnvVariables(clientID, clientSecret, tokenURL)
+ }()
+
+ t.Log("Testing oAuth with no Environmental variables")
+ incompleteEnvConfig := &auth_providers.CommandConfigOauth{}
+ incompleteEnvConfigExpectedError := fmt.Sprintf(
+ "client ID or environment variable %s is required",
+ auth_providers.EnvKeyfactorClientID,
+ )
+ authOauthTest(
+ t,
+ "with incomplete Environmental variables",
+ true,
+ incompleteEnvConfig,
+ incompleteEnvConfigExpectedError,
+ )
+
+ t.Log("Testing auth with only clientID")
+ clientIDOnlyConfig := &auth_providers.CommandConfigOauth{
+ ClientID: "test-client-id",
+ }
+ clientIDOnlyConfigExceptedError := fmt.Sprintf(
+ "client secret or environment variable %s is required",
+ auth_providers.EnvKeyfactorClientSecret,
+ )
+ authOauthTest(t, "clientID only", true, clientIDOnlyConfig, clientIDOnlyConfigExceptedError)
+
+ t.Log("Testing auth with w/ full params variables")
+ fullParamsConfig := &auth_providers.CommandConfigOauth{
+ ClientID: clientID,
+ ClientSecret: clientSecret,
+ TokenURL: tokenURL,
+ }
+ fullParamsConfig.WithSkipVerify(true)
+ authOauthTest(t, "w/ full params variables", false, fullParamsConfig)
+
+ t.Log("Testing auth with w/ full params & invalid pass")
+ fullParamsInvalidPassConfig := &auth_providers.CommandConfigOauth{
+ ClientID: clientID,
+ ClientSecret: "invalid-client-secret",
+ TokenURL: tokenURL,
+ }
+ fullParamsInvalidPassConfig.WithSkipVerify(true)
+ invalidCredsExpectedError := []string{
+ "oauth2", "unauthorized_client", "Invalid client or Invalid client credentials",
+ }
+ authOauthTest(t, "w/ full params & invalid pass", true, fullParamsInvalidPassConfig, invalidCredsExpectedError...)
+
+ t.Log("Testing auth with w/ no tokenURL")
+ noTokenURLConfig := &auth_providers.CommandConfigOauth{
+ ClientID: clientID,
+ ClientSecret: clientSecret,
+ }
+ noTokenURLExpectedError := fmt.Sprintf(
+ "token URL or environment variable %s is required",
+ auth_providers.EnvKeyfactorAuthTokenURL,
+ )
+ authOauthTest(t, "w/ no tokenURL", true, noTokenURLConfig, noTokenURLExpectedError)
+
+ // Write the config file back
+ t.Logf("Writing config file: %s", configFilePath)
+ fErr := auth_providers.WriteConfigToJSON(configFilePath, configFromFile)
+ if fErr != nil {
+ t.Errorf("unable to write auth config to file %s: %v", configFilePath, fErr)
+ }
+
+ //unsetOAuthEnvVariables()
+
+ t.Log("Testing oAuth with valid implicit config file profile param, caCert, and skiptls")
+ wCaCertConfigFile := &auth_providers.CommandConfigOauth{}
+ wCaCertConfigFile.
+ WithConfigProfile("oauth").
+ WithCommandCACert(caCertPath).
+ WithSkipVerify(true)
+ authOauthTest(
+ t, "oAuth with valid implicit config file profile param, caCert, and skiptls", false,
+ wCaCertConfigFile,
+ )
+
+ t.Log("Testing oAuth with skiptls param and valid implicit config file")
+ skipTLSConfigFileP := &auth_providers.CommandConfigOauth{}
+ skipTLSConfigFileP.
+ WithConfigProfile("oauth").
+ WithSkipVerify(true)
+ authOauthTest(t, "with skiptls param and valid implicit config file", false, skipTLSConfigFileP)
+
+ t.Log("Testing oAuth with valid implicit config file skiptls config param")
+ skipTLSConfigFileC := &auth_providers.CommandConfigOauth{}
+ skipTLSConfigFileC.
+ WithConfigProfile("oauth-skiptls")
+ authOauthTest(t, "with oAuth with valid implicit config file skiptls config param", false, skipTLSConfigFileC)
+
+ t.Log("Testing oAuth with valid implicit config file skiptls env")
+ t.Logf("Setting environment variable %s", auth_providers.EnvKeyfactorSkipVerify)
+ os.Setenv(auth_providers.EnvKeyfactorSkipVerify, "true")
+ skipTLSConfigFileE := &auth_providers.CommandConfigOauth{}
+ skipTLSConfigFileE.
+ WithConfigProfile("oauth")
+ authOauthTest(t, "oAuth with valid implicit config file skiptls env", false, skipTLSConfigFileE)
+ t.Logf("Unsetting environment variable %s", auth_providers.EnvKeyfactorSkipVerify)
+ os.Unsetenv(auth_providers.EnvKeyfactorSkipVerify)
+
+ t.Log("Testing oAuth with valid implicit config file https fail")
+ httpsFailConfigFile := &auth_providers.CommandConfigOauth{}
+ httpsFailConfigFile.
+ WithConfigProfile("oauth")
+ httpsFailConfigFileExpected := []string{"tls: failed to verify certificate"}
+ authOauthTest(
+ t, "oAuth with valid implicit config file https fail", true, httpsFailConfigFile,
+ httpsFailConfigFileExpected...,
+ )
+
+ t.Log("Testing oAuth with invalid profile implicit config file")
+ invProfile := &auth_providers.CommandConfigOauth{}
+ invProfile.WithConfigProfile("invalid-profile")
+ expectedError := []string{"profile", "invalid-profile", "not found"}
+ authOauthTest(t, "with invalid profile implicit config file", true, invProfile, expectedError...)
+
+ t.Log("Testing oAuth with invalid creds implicit config file")
+ invProfileCreds := &auth_providers.CommandConfigOauth{}
+ invProfileCreds.
+ WithConfigProfile("oauth_invalid_creds").
+ WithSkipVerify(true)
+ authOauthTest(t, "with invalid creds implicit config file", true, invProfileCreds, invalidCredsExpectedError...)
+
+ t.Log("Testing oAuth with invalid Command host implicit config file")
+ invCmdHost := &auth_providers.CommandConfigOauth{}
+ invCmdHost.
+ WithConfigProfile("oauth_invalid_host").
+ WithSkipVerify(true)
+ invHostExpectedError := []string{"no such host"}
+ authOauthTest(t, "with invalid creds implicit config file", true, invCmdHost, invHostExpectedError...)
+}
+
+func TestCommandConfigOauth_Build(t *testing.T) {
+ // Skip test if TEST_KEYFACTOR_AD_AUTH is set to 1 or true
+ if os.Getenv("TEST_KEYFACTOR_AD_AUTH") == "1" || os.Getenv("TEST_KEYFACTOR_AD_AUTH") == "true" {
+ t.Skip("Skipping TestOAuthAuthenticator_GetHttpClient")
+ return
+ }
+ config := &auth_providers.CommandConfigOauth{
+ ClientID: os.Getenv(auth_providers.EnvKeyfactorClientID),
+ ClientSecret: os.Getenv(auth_providers.EnvKeyfactorClientSecret),
+ TokenURL: os.Getenv(auth_providers.EnvKeyfactorAuthTokenURL),
+ Scopes: []string{"openid", "profile", "email"},
+ }
+
+ authenticator, err := config.Build()
+ if err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+
+ if authenticator == nil {
+ t.Fatalf("expected a non-nil Authenticator")
+ }
+}
+
+func authOauthTest(
+ t *testing.T, testName string, allowFail bool, config *auth_providers.CommandConfigOauth,
+ errorContains ...string,
+) {
+ t.Run(
+ fmt.Sprintf("oAuth Auth Test %s", testName), func(t *testing.T) {
+
+ err := config.Authenticate()
+ if allowFail {
+ if err == nil {
+ t.Errorf("oAuth auth test '%s' should have failed", testName)
+ t.FailNow()
+ return
+ }
+ if len(errorContains) > 0 {
+ for _, ec := range errorContains {
+ if !strings.Contains(err.Error(), ec) {
+ t.Errorf("oAuth auth test '%s' failed with unexpected error %v", testName, err)
+ t.FailNow()
+ return
+ }
+ }
+ }
+ t.Logf("oAuth auth test '%s' failed as expected with %v", testName, err)
+ return
+ }
+ if err != nil {
+ t.Errorf("oAuth auth test '%s' failed with %v", testName, err)
+ t.FailNow()
+ return
+ }
+ },
+ )
+}
+
+// setOAuthEnvVariables sets the oAuth environment variables
+func setOAuthEnvVariables(client_id, client_secret, token_url string) {
+ os.Setenv(auth_providers.EnvKeyfactorClientID, client_id)
+ os.Setenv(auth_providers.EnvKeyfactorClientSecret, client_secret)
+ os.Setenv(auth_providers.EnvKeyfactorAuthTokenURL, token_url)
+}
+
+func exportEnvVarsWithPrefix(prefix string) map[string]string {
+ result := make(map[string]string)
+ for _, env := range os.Environ() {
+ // Each environment variable is in the format "KEY=VALUE"
+ pair := strings.SplitN(env, "=", 2)
+ key := pair[0]
+ value := pair[1]
+
+ if strings.HasPrefix(key, prefix) {
+ result[key] = value
+ }
+ }
+ return result
+}
+
+// exportOAuthEnvVariables sets the oAuth environment variables
+func exportOAuthEnvVariables() (string, string, string) {
+ client_id := os.Getenv(auth_providers.EnvKeyfactorClientID)
+ client_secret := os.Getenv(auth_providers.EnvKeyfactorClientSecret)
+ token_url := os.Getenv(auth_providers.EnvKeyfactorAuthTokenURL)
+ return client_id, client_secret, token_url
+}
+
+// unsetOAuthEnvVariables unsets the oAuth environment variables
+func unsetOAuthEnvVariables() {
+ os.Unsetenv(auth_providers.EnvKeyfactorClientID)
+ os.Unsetenv(auth_providers.EnvKeyfactorClientSecret)
+ os.Unsetenv(auth_providers.EnvKeyfactorAuthTokenURL)
+ os.Unsetenv(auth_providers.EnvKeyfactorSkipVerify)
+ os.Unsetenv(auth_providers.EnvKeyfactorConfigFile)
+ os.Unsetenv(auth_providers.EnvKeyfactorAuthProfile)
+ os.Unsetenv(auth_providers.EnvKeyfactorCACert)
+ os.Unsetenv(auth_providers.EnvAuthCACert)
+ //os.Unsetenv(auth_providers.EnvKeyfactorHostName)
+ //os.Unsetenv(auth_providers.EnvKeyfactorUsername)
+ //os.Unsetenv(auth_providers.EnvKeyfactorPassword)
+ //os.Unsetenv(auth_providers.EnvKeyfactorDomain)
+
+}
diff --git a/auth_providers/command_config.go b/auth_providers/command_config.go
new file mode 100644
index 0000000..fee0820
--- /dev/null
+++ b/auth_providers/command_config.go
@@ -0,0 +1,342 @@
+// Copyright 2024 Keyfactor
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package auth_providers
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+
+ "gopkg.in/yaml.v2"
+)
+
+// Server represents the server configuration for authentication.
+type Server struct {
+ Host string `json:"host,omitempty" yaml:"host,omitempty"` // Host is the Command server DNS name or IP address.
+ Port int `json:"port,omitempty" yaml:"port,omitempty"` // Port is the Command server port.
+ //AuthPort int `json:"auth_port,omitempty" yaml:"auth_port,omitempty"` // AuthPort is the authentication port.
+ Username string `json:"username,omitempty" yaml:"username,omitempty"` // Username is the username for authentication.
+ Password string `json:"password,omitempty" yaml:"password,omitempty"` // Password is the password for authentication.
+ Domain string `json:"domain,omitempty" yaml:"domain,omitempty"` // Domain is the domain for authentication.
+ ClientID string `json:"client_id,omitempty" yaml:"client_id,omitempty"` // ClientID is the client ID for OAuth.
+ ClientSecret string `json:"client_secret,omitempty" yaml:"client_secret,omitempty"` // ClientSecret is the client secret for OAuth.
+ OAuthTokenUrl string `json:"token_url,omitempty" yaml:"token_url,omitempty"` // OAuthTokenUrl is full URL for OAuth token request endpoint.
+ APIPath string `json:"api_path,omitempty" yaml:"api_path,omitempty"` // APIPath is the API path.
+ AuthProvider AuthProvider `json:"auth_provider,omitempty" yaml:"auth_provider,omitempty"` // AuthProvider contains the authentication provider details.
+ SkipTLSVerify bool `json:"skip_tls_verify,omitempty" yaml:"skip_tls_verify,omitempty"` // TLSVerify determines whether to verify the TLS certificate.
+ CACertPath string `json:"ca_cert_path,omitempty" yaml:"ca_cert_path,omitempty"` // CACertPath is the path to the CA certificate to trust.
+ AuthType string `json:"auth_type,omitempty" yaml:"auth_type,omitempty"` // AuthType is the type of authentication to use.
+
+}
+
+// AuthProvider represents the authentication provider configuration.
+type AuthProvider struct {
+ Type string `json:"type,omitempty" yaml:"type,omitempty"` // Type is the type of authentication provider.
+ Profile string `json:"profile,omitempty" yaml:"profile,omitempty"` // Profile is the profile of the authentication provider.
+ Parameters map[string]interface{} `json:"parameters,omitempty" yaml:"parameters,omitempty"` // Parameters are additional parameters for the authentication provider.
+}
+
+// Config represents the overall configuration structure.
+type Config struct {
+ Servers map[string]Server `json:"servers,omitempty" yaml:"servers,omitempty"` // Servers is a map of server configurations.
+}
+
+// NewConfig creates a new Config configuration.
+func NewConfig() *Config {
+ return &Config{
+ Servers: make(map[string]Server),
+ }
+}
+
+// ReadConfigFromJSON reads a Config configuration from a JSON file.
+func ReadConfigFromJSON(filePath string) (*Config, error) {
+ file, err := os.Open(filePath)
+ if err != nil {
+ return nil, err
+ }
+ defer file.Close()
+
+ var config Config
+ decoder := json.NewDecoder(file)
+ if err := decoder.Decode(&config); err != nil {
+ return nil, err
+ }
+
+ return &config, nil
+}
+
+// ReadConfigFromYAML reads a Config configuration from a YAML file.
+func ReadConfigFromYAML(filePath string) (*Config, error) {
+ file, err := os.ReadFile(filePath)
+ if err != nil {
+ return nil, err
+ }
+
+ var config Config
+ if err := yaml.Unmarshal(file, &config); err != nil {
+ return nil, err
+ }
+
+ return &config, nil
+}
+
+// ReadServerFromJSON reads a Server configuration from a JSON file.
+func ReadServerFromJSON(filePath string) (*Server, error) {
+ file, err := os.Open(filePath)
+ if err != nil {
+ return nil, err
+ }
+ defer file.Close()
+
+ var server Server
+ decoder := json.NewDecoder(file)
+ if err := decoder.Decode(&server); err != nil {
+ return nil, err
+ }
+
+ return &server, nil
+}
+
+// WriteServerToJSON writes a Server configuration to a JSON file.
+func WriteServerToJSON(filePath string, server *Server) error {
+ file, err := os.Create(filePath)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+
+ encoder := json.NewEncoder(file)
+ encoder.SetIndent("", " ")
+ if err := encoder.Encode(server); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// ReadServerFromYAML reads a Server configuration from a YAML file.
+func ReadServerFromYAML(filePath string) (*Server, error) {
+ file, err := os.ReadFile(filePath)
+ if err != nil {
+ return nil, err
+ }
+
+ var server Server
+ if err := yaml.Unmarshal(file, &server); err != nil {
+ return nil, err
+ }
+
+ return &server, nil
+}
+
+// WriteServerToYAML writes a Server configuration to a YAML file.
+func WriteServerToYAML(filePath string, server *Server) error {
+ data, err := yaml.Marshal(server)
+ if err != nil {
+ return err
+ }
+
+ if err := os.WriteFile(filePath, data, 0644); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// WriteConfigToJSON writes a Config configuration to a JSON file.
+func WriteConfigToJSON(filePath string, config *Config) error {
+ file, err := os.Create(filePath)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+
+ encoder := json.NewEncoder(file)
+ encoder.SetIndent("", " ")
+ if err := encoder.Encode(config); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// WriteConfigToYAML writes a Config configuration to a YAML file.
+func WriteConfigToYAML(filePath string, config *Config) error {
+ data, err := yaml.Marshal(config)
+ if err != nil {
+ return err
+ }
+
+ if err := os.WriteFile(filePath, data, 0644); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// MergeConfigFromFile merges the configuration from a file into the existing Config.
+func MergeConfigFromFile(filePath string, config *Config) error {
+ // Read the file content
+ data, err := os.ReadFile(filePath)
+ if err != nil {
+ return fmt.Errorf("failed to read config file: %w", err)
+ }
+
+ // Determine the file type (JSON or YAML) and unmarshal accordingly
+ var tempConfig Config
+ if json.Valid(data) {
+ if err := json.Unmarshal(data, &tempConfig); err != nil {
+ return fmt.Errorf("failed to unmarshal JSON config: %w", err)
+ }
+ } else {
+ if err := yaml.Unmarshal(data, &tempConfig); err != nil {
+ return fmt.Errorf("failed to unmarshal YAML config: %w", err)
+ }
+ }
+
+ // Merge the temporary config into the existing config
+ for key, server := range tempConfig.Servers {
+ if _, exists := config.Servers[key]; !exists {
+ config.Servers[key] = server
+ }
+ }
+
+ return nil
+}
+
+// GetAuthType returns the type of authentication to use based on the configuration params.
+func (s *Server) GetAuthType() string {
+ if s.ClientID != "" && s.ClientSecret != "" {
+ s.AuthType = "oauth"
+ } else if s.Username != "" && s.Password != "" {
+ s.AuthType = "basic"
+ } else {
+ s.AuthType = ""
+ }
+ return s.AuthType
+}
+
+// GetBasicAuthClientConfig returns the basic auth configuration for the client.
+func (s *Server) GetBasicAuthClientConfig() (*CommandAuthConfigBasic, error) {
+ configType := s.GetAuthType()
+ if configType != "basic" {
+ return nil, fmt.Errorf("invalid auth type: %s", configType)
+ }
+
+ baseConfig := CommandAuthConfig{}
+ baseConfig.
+ WithCommandHostName(s.Host).
+ WithCommandPort(s.Port).
+ WithCommandAPIPath(s.APIPath).
+ WithCommandCACert(s.CACertPath).
+ WithSkipVerify(s.SkipTLSVerify)
+
+ basicConfig := CommandAuthConfigBasic{
+ CommandAuthConfig: baseConfig,
+ }
+ basicConfig.
+ WithUsername(s.Username).
+ WithPassword(s.Password).
+ WithDomain(s.Domain).
+ Build()
+
+ vErr := basicConfig.ValidateAuthConfig()
+ if vErr != nil {
+ return nil, vErr
+ }
+ return &basicConfig, nil
+}
+
+// GetOAuthClientConfig returns the OAuth configuration for the client.
+func (s *Server) GetOAuthClientConfig() (*CommandConfigOauth, error) {
+ configType := s.GetAuthType()
+ if configType != "oauth" {
+ return nil, fmt.Errorf("invalid auth type: %s", configType)
+ }
+ baseConfig := CommandAuthConfig{}
+ baseConfig.
+ WithCommandHostName(s.Host).
+ WithCommandPort(s.Port).
+ WithCommandAPIPath(s.APIPath).
+ WithCommandCACert(s.CACertPath).
+ WithSkipVerify(s.SkipTLSVerify)
+
+ oauthConfig := CommandConfigOauth{
+ CommandAuthConfig: baseConfig,
+ }
+ oauthConfig.
+ WithClientId(s.ClientID).
+ WithClientSecret(s.ClientSecret).
+ WithTokenUrl(s.OAuthTokenUrl).
+ Build()
+
+ vErr := oauthConfig.ValidateAuthConfig()
+ if vErr != nil {
+ return nil, vErr
+ }
+ return &oauthConfig, nil
+}
+
+// Example usage of Config
+//
+// This example demonstrates how to use Config to read and write server configurations.
+//
+// func ExampleConfig_ReadWrite() {
+// config := NewConfig()
+//
+// // Add a server configuration
+// config.Servers["exampleServer"] = Server{
+// Host: "exampleHost",
+// Port: 443,
+// Username: "exampleUser",
+// Password: "examplePassword",
+// Domain: "exampleDomain",
+// ClientID: "exampleClientID",
+// ClientSecret: "exampleClientSecret",
+// OAuthTokenUrl: "https://example.com/oauth/token",
+// APIPath: "/api/v1",
+// SkipTLSVerify: true,
+// CACertPath: "/path/to/ca-cert.pem",
+// AuthType: "oauth",
+// }
+//
+// // Write the configuration to a JSON file
+// err := WriteConfigToJSON("/path/to/config.json", config)
+// if err != nil {
+// fmt.Println("Failed to write config to JSON:", err)
+// }
+//
+// // Read the configuration from a JSON file
+// readConfig, err := ReadConfigFromJSON("/path/to/config.json")
+// if err != nil {
+// fmt.Println("Failed to read config from JSON:", err)
+// } else {
+// fmt.Println("Read config from JSON:", readConfig)
+// }
+//
+// // Write the configuration to a YAML file
+// err = WriteConfigToYAML("/path/to/config.yaml", config)
+// if err != nil {
+// fmt.Println("Failed to write config to YAML:", err)
+// }
+//
+// // Read the configuration from a YAML file
+// readConfig, err = ReadConfigFromYAML("/path/to/config.yaml")
+// if err != nil {
+// fmt.Println("Failed to read config from YAML:", err)
+// } else {
+// fmt.Println("Read config from YAML:", readConfig)
+// }
+// }
diff --git a/auth_providers/command_config_test.go b/auth_providers/command_config_test.go
new file mode 100644
index 0000000..2aeae9f
--- /dev/null
+++ b/auth_providers/command_config_test.go
@@ -0,0 +1,358 @@
+// Copyright 2024 Keyfactor
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package auth_providers_test
+
+import (
+ "encoding/json"
+ "os"
+ "testing"
+
+ "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers"
+ "gopkg.in/yaml.v2"
+)
+
+func TestReadServerFromJSON(t *testing.T) {
+ filePath := "test_config.json"
+ server := &auth_providers.Server{
+ Host: "localhost",
+ Port: 8080,
+ OAuthTokenUrl: "https://auth.localhost:8443/openid/token",
+ Username: "user",
+ Password: "pass",
+ ClientID: "client_id",
+ ClientSecret: "client_secret",
+ Domain: "domain",
+ APIPath: "api",
+ }
+
+ err := auth_providers.WriteServerToJSON(filePath, server)
+ if err != nil {
+ t.Fatalf("failed to write server to JSON: %v", err)
+ }
+ defer os.Remove(filePath)
+
+ readServer, err := auth_providers.ReadServerFromJSON(filePath)
+ if err != nil {
+ t.Fatalf("failed to read server from JSON: %v", err)
+ }
+
+ if !compareServers(readServer, server) {
+ t.Fatalf("expected %v, got %v", server, readServer)
+ }
+}
+
+func TestWriteServerToJSON(t *testing.T) {
+ filePath := "test_server.json"
+ server := &auth_providers.Server{
+ Host: "localhost",
+ Port: 8080,
+ OAuthTokenUrl: "https://auth.localhost:8443/openid/token",
+ Username: "user",
+ Password: "pass",
+ ClientID: "client_id",
+ ClientSecret: "client_secret",
+ Domain: "domain",
+ APIPath: "api",
+ }
+
+ err := auth_providers.WriteServerToJSON(filePath, server)
+ if err != nil {
+ t.Fatalf("failed to write server to JSON: %v", err)
+ }
+ defer os.Remove(filePath)
+
+ file, err := os.ReadFile(filePath)
+ if err != nil {
+ t.Fatalf("failed to read file: %v", err)
+ }
+
+ var readServer auth_providers.Server
+ err = json.Unmarshal(file, &readServer)
+ if err != nil {
+ t.Fatalf("failed to unmarshal JSON: %v", err)
+ }
+
+ if !compareServers(&readServer, server) {
+ t.Fatalf("expected %v, got %v", server, readServer)
+ }
+}
+
+func TestReadServerFromYAML(t *testing.T) {
+ filePath := "test_server.yaml"
+ server := &auth_providers.Server{
+ Host: "localhost",
+ Port: 8080,
+ OAuthTokenUrl: "https://auth.localhost:8443/openid/token",
+ Username: "user",
+ Password: "pass",
+ ClientID: "client_id",
+ ClientSecret: "client_secret",
+ Domain: "domain",
+ APIPath: "api",
+ }
+
+ err := auth_providers.WriteServerToYAML(filePath, server)
+ if err != nil {
+ t.Fatalf("failed to write server to YAML: %v", err)
+ }
+ defer os.Remove(filePath)
+
+ readServer, err := auth_providers.ReadServerFromYAML(filePath)
+ if err != nil {
+ t.Fatalf("failed to read server from YAML: %v", err)
+ }
+
+ if !compareServers(readServer, server) {
+ t.Fatalf("expected %v, got %v", server, readServer)
+ }
+}
+
+func TestWriteServerToYAML(t *testing.T) {
+ filePath := "test_server.yaml"
+ server := &auth_providers.Server{
+ Host: "localhost",
+ Port: 8080,
+ OAuthTokenUrl: "https://auth.localhost:8443/openid/token",
+ Username: "user",
+ Password: "pass",
+ ClientID: "client_id",
+ ClientSecret: "client_secret",
+ Domain: "domain",
+ APIPath: "api",
+ }
+
+ err := auth_providers.WriteServerToYAML(filePath, server)
+ if err != nil {
+ t.Fatalf("failed to write server to YAML: %v", err)
+ }
+ defer os.Remove(filePath)
+
+ file, err := os.ReadFile(filePath)
+ if err != nil {
+ t.Fatalf("failed to read file: %v", err)
+ }
+
+ var readServer auth_providers.Server
+ err = yaml.Unmarshal(file, &readServer)
+ if err != nil {
+ t.Fatalf("failed to unmarshal YAML: %v", err)
+ }
+
+ if !compareServers(&readServer, server) {
+ t.Fatalf("expected %v, got %v", server, readServer)
+ }
+}
+
+func TestMergeConfigFromFile(t *testing.T) {
+ filePath := "test_config.json"
+ config := &auth_providers.Config{
+ Servers: map[string]auth_providers.Server{
+ "server1": {
+ Host: "localhost",
+ Port: 8080,
+ },
+ },
+ }
+
+ err := auth_providers.WriteConfigToJSON(filePath, config)
+ if err != nil {
+ t.Fatalf("failed to write config to JSON: %v", err)
+ }
+ defer os.Remove(filePath)
+
+ newConfig := &auth_providers.Config{
+ Servers: map[string]auth_providers.Server{
+ "server2": {
+ Host: "remotehost",
+ Port: 9090,
+ },
+ },
+ }
+
+ err = auth_providers.MergeConfigFromFile(filePath, newConfig)
+ if err != nil {
+ t.Fatalf("failed to merge config from file: %v", err)
+ }
+
+ if len(newConfig.Servers) != 2 {
+ t.Fatalf("expected 2 servers, got %d", len(newConfig.Servers))
+ }
+
+ if newConfig.Servers["server1"].Host != "localhost" {
+ t.Fatalf("expected server1 host to be localhost, got %s", newConfig.Servers["server1"].Host)
+ }
+
+ if newConfig.Servers["server2"].Host != "remotehost" {
+ t.Fatalf("expected server2 host to be remotehost, got %s", newConfig.Servers["server2"].Host)
+ }
+}
+
+func TestReadFullAuthConfigExample(t *testing.T) {
+ filePath := "../lib/config/full_auth_config_example.json"
+ expectedConfig := auth_providers.Config{
+ Servers: map[string]auth_providers.Server{
+ "default": {
+ Host: "keyfactor.command.kfdelivery.com",
+ OAuthTokenUrl: "idp.keyfactor.command.kfdelivery.com",
+ Username: "keyfactor",
+ Password: "password",
+ ClientID: "client-id",
+ ClientSecret: "client-secret",
+ Domain: "command",
+ APIPath: "KeyfactorAPI",
+ AuthProvider: auth_providers.AuthProvider{
+ Type: "azid",
+ Profile: "azure",
+ Parameters: map[string]interface{}{
+ "secret_name": "command-config-azure",
+ "vault_name": "keyfactor-secrets",
+ },
+ },
+ },
+ "server2": {
+ Host: "keyfactor2.command.kfdelivery.com",
+ OAuthTokenUrl: "idp.keyfactor2.command.kfdelivery.com",
+ Username: "keyfactor2",
+ Password: "password2",
+ ClientID: "client-id2",
+ ClientSecret: "client-secret2",
+ Domain: "command",
+ APIPath: "KeyfactorAPI",
+ AuthProvider: auth_providers.AuthProvider{
+ Type: "azid",
+ Profile: "azure",
+ Parameters: map[string]interface{}{
+ "secret_name": "command-config-azure2",
+ "vault_name": "keyfactor-secrets",
+ },
+ },
+ },
+ },
+ }
+
+ file, err := os.ReadFile(filePath)
+ if err != nil {
+ t.Fatalf("failed to read file: %v", err)
+ }
+
+ var config auth_providers.Config
+ err = json.Unmarshal(file, &config)
+ if err != nil {
+ t.Fatalf("failed to unmarshal JSON: %v", err)
+ }
+
+ if !compareConfigs(&config, &expectedConfig) {
+ t.Fatalf("expected %v, got %v", expectedConfig, config)
+ }
+}
+
+func TestReadOAuthConfigExample(t *testing.T) {
+ filePath := "../lib/config/oauth_config_example.json"
+ expectedConfig := &auth_providers.Config{
+ Servers: map[string]auth_providers.Server{
+ "default": {
+ Host: "keyfactor.command.kfdelivery.com",
+ OAuthTokenUrl: "https://idp.keyfactor.command.kfdelivery.com/oauth2/token",
+ ClientID: "client-id",
+ ClientSecret: "client-secret",
+ APIPath: "KeyfactorAPI",
+ },
+ "server2": {
+ Host: "keyfactor.command.kfdelivery.com",
+ OAuthTokenUrl: "https://idp.keyfactor.command.kfdelivery.com/oauth2/token",
+ ClientID: "client-id",
+ ClientSecret: "client-secret",
+ APIPath: "KeyfactorAPI",
+ },
+ },
+ }
+
+ file, err := os.ReadFile(filePath)
+ if err != nil {
+ t.Fatalf("failed to read file: %v", err)
+ }
+
+ var config auth_providers.Config
+ err = json.Unmarshal(file, &config)
+ if err != nil {
+ t.Fatalf("failed to unmarshal JSON: %v", err)
+ }
+
+ if !compareConfigs(&config, expectedConfig) {
+ t.Fatalf("expected %v, got %v", expectedConfig, config)
+ }
+}
+
+func TestReadBasicAuthConfigExample(t *testing.T) {
+ filePath := "../lib/config/basic_auth_config_example.json"
+ expectedConfig := &auth_providers.Config{
+ Servers: map[string]auth_providers.Server{
+ "default": {
+ Host: "keyfactor.command.kfdelivery.com",
+ Username: "keyfactor",
+ Password: "password",
+ Domain: "command",
+ APIPath: "KeyfactorAPI",
+ },
+ "server2": {
+ Host: "keyfactor2.command.kfdelivery.com",
+ Username: "keyfactor2",
+ Password: "password2",
+ Domain: "command",
+ APIPath: "Keyfactor/API",
+ },
+ },
+ }
+
+ file, err := os.ReadFile(filePath)
+ if err != nil {
+ t.Fatalf("failed to read file: %v", err)
+ }
+
+ var config auth_providers.Config
+ err = json.Unmarshal(file, &config)
+ if err != nil {
+ t.Fatalf("failed to unmarshal JSON: %v", err)
+ }
+
+ if !compareConfigs(&config, expectedConfig) {
+ t.Fatalf("expected %v, got %v", expectedConfig, config)
+ }
+}
+
+func compareConfigs(a, b *auth_providers.Config) bool {
+ if len(a.Servers) != len(b.Servers) {
+ return false
+ }
+ for key, serverA := range a.Servers {
+ serverB, exists := b.Servers[key]
+ if !exists || !compareServers(&serverA, &serverB) {
+ return false
+ }
+ }
+ return true
+}
+
+func compareServers(a, b *auth_providers.Server) bool {
+ return a.Host == b.Host &&
+ a.Port == b.Port &&
+ a.OAuthTokenUrl == b.OAuthTokenUrl &&
+ a.Username == b.Username &&
+ a.Password == b.Password &&
+ a.ClientID == b.ClientID &&
+ a.ClientSecret == b.ClientSecret &&
+ a.Domain == b.Domain &&
+ a.APIPath == b.APIPath
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..e83a7c5
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,22 @@
+// Copyright 2024 Keyfactor
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+module github.com/Keyfactor/keyfactor-auth-client-go
+
+go 1.22
+
+require (
+ golang.org/x/oauth2 v0.23.0
+ gopkg.in/yaml.v2 v2.4.0
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..b65756f
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,10 @@
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
+golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
+golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
diff --git a/integration-manifest.json b/integration-manifest.json
new file mode 100644
index 0000000..a01d975
--- /dev/null
+++ b/integration-manifest.json
@@ -0,0 +1,11 @@
+{
+ "$schema": "https://keyfactor.github.io/integration-manifest-schema.json",
+ "integration_type": "api-client",
+ "name": "Keyfactor Auth Client - Golang",
+ "status": "production",
+ "description": "The Keyfactor Auth Client - Golang is a Go module that handles authentication and authorization for the Keyfactor API.",
+ "support_level": "kf-community",
+ "link_github": false,
+ "update_catalog": false
+}
+
diff --git a/lib/certs/int-oidc-lab.eastus2.cloudapp.azure.com.pem b/lib/certs/int-oidc-lab.eastus2.cloudapp.azure.com.pem
new file mode 100644
index 0000000..6ef8014
--- /dev/null
+++ b/lib/certs/int-oidc-lab.eastus2.cloudapp.azure.com.pem
@@ -0,0 +1,25 @@
+-----BEGIN CERTIFICATE-----
+MIIENTCCAp2gAwIBAgIUXiYBpLlOyvIAuP9iZ8NmIfftHq0wDQYJKoZIhvcNAQEL
+BQAwMDEXMBUGCgmSJomT8ixkAQEMBzI0Mjc4MzcxFTATBgNVBAMMDE1hbmFnZW1l
+bnRDQTAeFw0yNDEwMTYxNjUxMzdaFw0yNTExMTcxNjQyMzVaMDIxMDAuBgNVBAMM
+J2ludC1vaWRjLWxhYi5lYXN0dXMyLmNsb3VkYXBwLmF6dXJlLmNvbTCCASIwDQYJ
+KoZIhvcNAQEBBQADggEPADCCAQoCggEBAO9bxuPyHOytYPN7KIz2IBuuVuKMh7YU
+yuZHJ2CiFfOOOnZlojaKjsp5j8E50h4tc5aPBWDZ0Cma7Ty3pdsssRsa78JwQ8cG
+oOUumrRYySzAET6OJA8Pn00t+lxLxBf4St3buobhQTEMesCsKdCSWbAydiIgkKA8
+E02zPKzXJU6yMmV/JnMPqEQHUBC8yb4NmfXsMplkVYkdhqESL+0xYYJLqireDuR3
+LfDdvZkh0XovgdN9zJpf10KP2os2tOzaa35dXVCBsl7y/IPaDt+zYb0OX88TGOPl
+1AysYO35lS+6CGiENn2ACTZSmvqOcwjOLB5dJd95b1lnUwsJgmMcSesCAwEAAaOB
+xDCBwTAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFOOYYJwB1O/MQlfbORHp1YqM
+Uy0MMEwGA1UdEQRFMEOCJ2ludC1vaWRjLWxhYi5lYXN0dXMyLmNsb3VkYXBwLmF6
+dXJlLmNvbYIMaW50LW9pZGMtbGFihwQUAYhEhwQKCgAEMBMGA1UdJQQMMAoGCCsG
+AQUFBwMBMB0GA1UdDgQWBBTBsBvKKfjz/P14o0c3V/fLm/hXmDAOBgNVHQ8BAf8E
+BAMCBaAwDQYJKoZIhvcNAQELBQADggGBAEizjLNGgjGNML6L4On1EhcDAtGIH6SY
+mkiP/mcMXcxESx/wAZAQO04ERbVX3mnewdmq1+TsnbfxEtetAobaxlRRej6tzFQp
+BBqGqz7uK8O3CpzBZzuBtR2d+GZw6+amBbxGyZvsIW0f2RQqaHp+FgBgJjZc3t+/
+UmaRoS2h26VBXuv40bEgErih/cI80xnJtRKtxS1/hJNvzeDMobgZ0KkZz1j/tHbi
+DzaX8CDCg0fhMH5BuSleG9MSYcW36TL30pv/h92kN8EldQgpWwMW0C3aTZBEpiCw
+NGeiUmbciia//rOi4Rin4uHCe7sllb10MTaLcy251S9vAmmHvSBj33iLlCms2WS7
+02BwsPPGilydfjKqekoC6M1AwxiSDp8ckiXNx4LvW8uyqwHCIOsW3kc7CR0LGvTn
+qLN7Sy5keB9uVs4e69nmU2GG6V0SN1QreuQfroIkgsgYyfz6MvMVhmVVLObELkrY
+RfrOSKEyZnB0+kBfyGykvnQcSaUsqFwXmg==
+-----END CERTIFICATE-----
diff --git a/lib/config/auth_config_schema.json b/lib/config/auth_config_schema.json
new file mode 100644
index 0000000..2af7d48
--- /dev/null
+++ b/lib/config/auth_config_schema.json
@@ -0,0 +1,153 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "title": "Keyfactor Command API Client Configuration",
+ "description": "Configuration file schema for authenticating to the Keyfactor Command API",
+ "properties": {
+ "servers": {
+ "type": "object",
+ "patternProperties": {
+ "^[a-zA-Z0-9_-]+$": {
+ "type": "object",
+ "properties": {
+ "host": {
+ "type": "string",
+ "description": "The hostname of the Keyfactor Command API server"
+ },
+ "auth_port": {
+ "type": "integer",
+ "description": "The port of the Keyfactor Command API server"
+ },
+ "username": {
+ "type": "string",
+ "description": "The username to authenticate with using basic auth"
+ },
+ "password": {
+ "type": "string",
+ "description": "The password to authenticate with using basic auth"
+ },
+ "client_id": {
+ "type": "string",
+ "description": "The client ID to authenticate with using OAuth2"
+ },
+ "token_url": {
+ "type": "string",
+ "description": "The token URL to authenticate with using OAuth2"
+ },
+ "client_secret": {
+ "type": "string",
+ "description": "The client secret to authenticate with using OAuth2"
+ },
+ "domain": {
+ "type": "string",
+ "description": "The Active Directory domain to authenticate with using basic auth"
+ },
+ "api_path": {
+ "type": "string",
+ "description": "The path to the Keyfactor Command API",
+ "default": "KeyfactorAPI"
+ },
+ "auth_provider": {
+ "type": "object",
+ "description": "The auth provider configuration",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "azid",
+ "akv"
+ ]
+ },
+ "profile": {
+ "type": "string",
+ "description": "The profile to use in the auth provider configuration"
+ },
+ "parameters": {
+ "type": "object",
+ "description": "The parameters to use in the auth provider configuration",
+ "properties": {
+ "secret_name": {
+ "type": "string",
+ "description": "The name of the secret to use in the Azure KeyVault auth provider configuration"
+ },
+ "vault_name": {
+ "type": "string",
+ "description": "The name of the vault to use in the Azure KeyVault auth provider configuration"
+ }
+ },
+ "required": []
+ }
+ },
+ "required": [
+ "type",
+ "profile",
+ "parameters"
+ ]
+ }
+ },
+ "oneOf": [
+ {
+ "required": [
+ "username",
+ "password"
+ ],
+ "not": {
+ "required": [
+ "client_id",
+ "client_secret"
+ ]
+ }
+ },
+ {
+ "required": [
+ "client_id",
+ "client_secret",
+ "token_url"
+ ],
+ "not": {
+ "required": [
+ "username",
+ "password"
+ ]
+ }
+ }
+ ],
+ "if": {
+ "required": [
+ "auth_provider"
+ ]
+ },
+ "then": {
+ "required": [
+ "auth_provider"
+ ]
+ },
+ "else": {
+ "if": {
+ "required": [
+ "client_id",
+ "client_secret"
+ ]
+ },
+ "then": {
+ "required": [
+ "token_url",
+ "host"
+ ]
+ },
+ "else": {
+ "required": [
+ "host"
+ ]
+ }
+ }
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "required": [
+ "servers"
+ ],
+ "additionalProperties": false
+}
\ No newline at end of file
diff --git a/lib/config/basic_auth_config_example.json b/lib/config/basic_auth_config_example.json
new file mode 100644
index 0000000..05b5749
--- /dev/null
+++ b/lib/config/basic_auth_config_example.json
@@ -0,0 +1,18 @@
+{
+ "servers": {
+ "default": {
+ "host": "keyfactor.command.kfdelivery.com",
+ "username": "keyfactor",
+ "password": "password",
+ "domain": "command",
+ "api_path": "KeyfactorAPI"
+ },
+ "server2": {
+ "host": "keyfactor2.command.kfdelivery.com",
+ "username": "keyfactor2",
+ "password": "password2",
+ "domain": "command",
+ "api_path": "Keyfactor/API"
+ }
+ }
+}
\ No newline at end of file
diff --git a/lib/config/full_auth_config_example.json b/lib/config/full_auth_config_example.json
new file mode 100644
index 0000000..c2bb73c
--- /dev/null
+++ b/lib/config/full_auth_config_example.json
@@ -0,0 +1,40 @@
+{
+ "servers": {
+ "default": {
+ "host": "keyfactor.command.kfdelivery.com",
+ "token_url": "idp.keyfactor.command.kfdelivery.com",
+ "username": "keyfactor",
+ "password": "password",
+ "client_id": "client-id",
+ "client_secret": "client-secret",
+ "domain": "command",
+ "api_path": "KeyfactorAPI",
+ "auth_provider": {
+ "type": "azid",
+ "profile": "azure",
+ "parameters": {
+ "secret_name": "command-config-azure",
+ "vault_name": "keyfactor-secrets"
+ }
+ }
+ },
+ "server2": {
+ "host": "keyfactor2.command.kfdelivery.com",
+ "token_url": "idp.keyfactor2.command.kfdelivery.com",
+ "username": "keyfactor2",
+ "password": "password2",
+ "client_id": "client-id2",
+ "client_secret": "client-secret2",
+ "domain": "command",
+ "api_path": "KeyfactorAPI",
+ "auth_provider": {
+ "type": "azid",
+ "profile": "azure",
+ "parameters": {
+ "secret_name": "command-config-azure2",
+ "vault_name": "keyfactor-secrets"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/lib/config/oauth_config_example.json b/lib/config/oauth_config_example.json
new file mode 100644
index 0000000..9c6ae86
--- /dev/null
+++ b/lib/config/oauth_config_example.json
@@ -0,0 +1,18 @@
+{
+ "servers": {
+ "default": {
+ "host": "keyfactor.command.kfdelivery.com",
+ "token_url": "https://idp.keyfactor.command.kfdelivery.com/oauth2/token",
+ "client_id": "client-id",
+ "client_secret": "client-secret",
+ "api_path": "KeyfactorAPI"
+ },
+ "server2": {
+ "host": "keyfactor.command.kfdelivery.com",
+ "token_url": "https://idp.keyfactor.command.kfdelivery.com/oauth2/token",
+ "client_id": "client-id",
+ "client_secret": "client-secret",
+ "api_path": "KeyfactorAPI"
+ }
+ }
+}
\ No newline at end of file
diff --git a/lib/main.go b/lib/main.go
new file mode 100644
index 0000000..e41bf2c
--- /dev/null
+++ b/lib/main.go
@@ -0,0 +1,146 @@
+// Copyright 2024 Keyfactor
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/pem"
+ "fmt"
+ "math/big"
+ "os"
+ "time"
+)
+
+func main() {
+ // Generate CA private key
+ caPrivKey, err := rsa.GenerateKey(rand.Reader, 2048)
+ if err != nil {
+ fmt.Printf("Failed to generate CA private key: %v\n", err)
+ return
+ }
+
+ // Create CA certificate template
+ caTemplate := &x509.Certificate{
+ SerialNumber: big.NewInt(1),
+ Subject: pkix.Name{
+ Organization: []string{"My CA Organization"},
+ },
+ NotBefore: time.Now(),
+ NotAfter: time.Now().AddDate(10, 0, 0), // 10 years
+ KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
+ BasicConstraintsValid: true,
+ IsCA: true,
+ }
+
+ // Create CA certificate
+ caCertBytes, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &caPrivKey.PublicKey, caPrivKey)
+ if err != nil {
+ fmt.Printf("Failed to create CA certificate: %v\n", err)
+ return
+ }
+
+ // Encode CA certificate to PEM format
+ caCertPEM := pem.EncodeToMemory(
+ &pem.Block{
+ Type: "CERTIFICATE",
+ Bytes: caCertBytes,
+ },
+ )
+
+ // Encode CA private key to PEM format
+ caPrivKeyPEM := pem.EncodeToMemory(
+ &pem.Block{
+ Type: "RSA PRIVATE KEY",
+ Bytes: x509.MarshalPKCS1PrivateKey(caPrivKey),
+ },
+ )
+
+ // Save CA certificate and private key to files
+ err = os.WriteFile("ca_cert.pem", caCertPEM, 0644)
+ if err != nil {
+ fmt.Printf("Failed to write CA certificate to file: %v\n", err)
+ return
+ }
+ err = os.WriteFile("ca_key.pem", caPrivKeyPEM, 0600)
+ if err != nil {
+ fmt.Printf("Failed to write CA private key to file: %v\n", err)
+ return
+ }
+
+ // Generate leaf private key
+ leafPrivKey, err := rsa.GenerateKey(rand.Reader, 2048)
+ if err != nil {
+ fmt.Printf("Failed to generate leaf private key: %v\n", err)
+ return
+ }
+
+ // Create leaf certificate template
+ leafTemplate := &x509.Certificate{
+ SerialNumber: big.NewInt(2),
+ Subject: pkix.Name{
+ Organization: []string{"My Leaf Organization"},
+ },
+ NotBefore: time.Now(),
+ NotAfter: time.Now().AddDate(1, 0, 0), // 1 year
+ KeyUsage: x509.KeyUsageDigitalSignature,
+ ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
+ }
+
+ // Create leaf certificate signed by the CA
+ leafCertBytes, err := x509.CreateCertificate(
+ rand.Reader,
+ leafTemplate,
+ caTemplate,
+ &leafPrivKey.PublicKey,
+ caPrivKey,
+ )
+ if err != nil {
+ fmt.Printf("Failed to create leaf certificate: %v\n", err)
+ return
+ }
+
+ // Encode leaf certificate to PEM format
+ leafCertPEM := pem.EncodeToMemory(
+ &pem.Block{
+ Type: "CERTIFICATE",
+ Bytes: leafCertBytes,
+ },
+ )
+
+ // Encode leaf private key to PEM format
+ leafPrivKeyPEM := pem.EncodeToMemory(
+ &pem.Block{
+ Type: "RSA PRIVATE KEY",
+ Bytes: x509.MarshalPKCS1PrivateKey(leafPrivKey),
+ },
+ )
+
+ // Save leaf certificate and private key to files
+ err = os.WriteFile("leaf_cert.pem", leafCertPEM, 0644)
+ if err != nil {
+ fmt.Printf("Failed to write leaf certificate to file: %v\n", err)
+ return
+ }
+ err = os.WriteFile("leaf_key.pem", leafPrivKeyPEM, 0600)
+ if err != nil {
+ fmt.Printf("Failed to write leaf private key to file: %v\n", err)
+ return
+ }
+
+ fmt.Println("CA and leaf certificates generated successfully.")
+}
diff --git a/lib/test_ca_cert.pem b/lib/test_ca_cert.pem
new file mode 100644
index 0000000..3dd6c24
--- /dev/null
+++ b/lib/test_ca_cert.pem
@@ -0,0 +1,18 @@
+-----BEGIN CERTIFICATE-----
+MIIC9zCCAd+gAwIBAgIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQKExJNeSBD
+QSBPcmdhbml6YXRpb24wHhcNMjQxMDE2MjIwMTQyWhcNMzQxMDE2MjIwMTQyWjAd
+MRswGQYDVQQKExJNeSBDQSBPcmdhbml6YXRpb24wggEiMA0GCSqGSIb3DQEBAQUA
+A4IBDwAwggEKAoIBAQC3DFiSb9+ykr8kSwlahq3lauN5BhFMsFP4nnYH9M2M0z2c
+cSLwF/ZpGYkzOVbHiwlpuPfmpm0weCnhxfHpX7Yd20g3Jr2M++enYRX8XGprhxw3
+GI1F1UMe/RPJxMhGEMzfIbiWZDXGWJI5v40CItq4Pgvw5jN9NYQlZjQ5YP5wEGI1
+JbYn47pxnL+SctmPIy5647WnNR4EL9Sb5ErNr91hebP/b8LwFhd/f1z+eUo52Xqa
+U/SZYMVrRiPq2nK++kDDoj0SV83q7WscTqRO2qwxyOwklzrUvFn7W1IzijjRciWA
+12GGP1mIaZbPOTIt8dYre9TgVZ1vl9ayrJADGBuNAgMBAAGjQjBAMA4GA1UdDwEB
+/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR7esJSE+f03gVNuqWa
+fIWHu3CPtTANBgkqhkiG9w0BAQsFAAOCAQEAajiD2zpM77agcz6xqO1+Y2RGb147
+bJb8CPBvZECYAel++dyNGy8kJF4vDiuQ4oRExbM6VUbEiYK2aXeapE2IMVpUY6No
+9JRgmQUDut667njWBv3DhjpV5ZhyI4YHZykXzXjI0eIEbdihfdwx237tvJHOzEeg
+4/xRfDuJxBtZOtctRuMBzkuTCQ85lyrOLLOO81mqmnEbF9d+9WmwS4mu9fta7b5H
+dCqTmtVncnxlE5PmstyeJaVhXbx4MA2BMIhJiusBw5WqKFYFbmhuGpeyEJx3JIZU
+tfE5aamMIbwnwk2VBwfUqHAuQVUxlz/tmRyZ7ALvykZJdJVrUxkKttgjWw==
+-----END CERTIFICATE-----
diff --git a/lib/test_chain.pem b/lib/test_chain.pem
new file mode 100644
index 0000000..5546fd8
--- /dev/null
+++ b/lib/test_chain.pem
@@ -0,0 +1,36 @@
+-----BEGIN CERTIFICATE-----
+MIIC6DCCAdCgAwIBAgIBAjANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQKExJNeSBD
+QSBPcmdhbml6YXRpb24wHhcNMjQxMDE2MjIwMTQyWhcNMjUxMDE2MjIwMTQyWjAf
+MR0wGwYDVQQKExRNeSBMZWFmIE9yZ2FuaXphdGlvbjCCASIwDQYJKoZIhvcNAQEB
+BQADggEPADCCAQoCggEBAOuOsSztuF7JDXrqvrM6bKSQ6VqpCEHtR5sJnndlrs/V
+Rxsxltk2qBigGIDqQYByM1pcT3WkA2619BHHxaDfcD2Zvx718dUoF1EGzadiEXOR
+946MZlW194qO7++Y8soB4ru36fSGfsK9wr6gOVSYmHINORV+giUvxgbvIpNfQAZI
+39nWNKkR14BLjmkZO6CXomu2W/ZT1WXJ7FDNyF28ww6h5AOCvox9YIRf3OV0GaNE
+kAdoJsw95t64C407P96a6DnaRjWgGkNYTfKf3yPsavilxguXXpqLFlYU7flONA0T
+/ThTjuiRwOShGHEaRr/4iswuxvPWnHTAGXX2K/2wjqkCAwEAAaMxMC8wDgYDVR0P
+AQH/BAQDAgeAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATANBgkqhkiG
+9w0BAQsFAAOCAQEAIJje0ts+350wREkB3E0ud/KRL7Ra734YhSxlo+i0UKwGR4oM
+hU2xg+0A7B9uJii9L58oMWTZ+JJpA1Fb4tagksUuBI/rxjNWQ7QSvlhx2Zkwsr5x
+l0nlfxMuzfgRBD/eafxIaD/Li/iiPHQNJyU4iNImmw2r8IW+rtbU/sCVG/OjqZEc
+oKsw+qk9veyH/oZb19HSpK28A6voc2YuNsL4ghMbMWicITFle+Sv6yBEHDIxvQB6
+2pvsQ+H6EY25YfAz2AQNaz6U32CA6KdaQQh87wXAnagM4S6s7SmsZVgX/vGcUS5n
+91NEq4QRzFuINWPF38AsyOR9r2B2xuuCrulsQQ==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIC9zCCAd+gAwIBAgIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQKExJNeSBD
+QSBPcmdhbml6YXRpb24wHhcNMjQxMDE2MjIwMTQyWhcNMzQxMDE2MjIwMTQyWjAd
+MRswGQYDVQQKExJNeSBDQSBPcmdhbml6YXRpb24wggEiMA0GCSqGSIb3DQEBAQUA
+A4IBDwAwggEKAoIBAQC3DFiSb9+ykr8kSwlahq3lauN5BhFMsFP4nnYH9M2M0z2c
+cSLwF/ZpGYkzOVbHiwlpuPfmpm0weCnhxfHpX7Yd20g3Jr2M++enYRX8XGprhxw3
+GI1F1UMe/RPJxMhGEMzfIbiWZDXGWJI5v40CItq4Pgvw5jN9NYQlZjQ5YP5wEGI1
+JbYn47pxnL+SctmPIy5647WnNR4EL9Sb5ErNr91hebP/b8LwFhd/f1z+eUo52Xqa
+U/SZYMVrRiPq2nK++kDDoj0SV83q7WscTqRO2qwxyOwklzrUvFn7W1IzijjRciWA
+12GGP1mIaZbPOTIt8dYre9TgVZ1vl9ayrJADGBuNAgMBAAGjQjBAMA4GA1UdDwEB
+/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR7esJSE+f03gVNuqWa
+fIWHu3CPtTANBgkqhkiG9w0BAQsFAAOCAQEAajiD2zpM77agcz6xqO1+Y2RGb147
+bJb8CPBvZECYAel++dyNGy8kJF4vDiuQ4oRExbM6VUbEiYK2aXeapE2IMVpUY6No
+9JRgmQUDut667njWBv3DhjpV5ZhyI4YHZykXzXjI0eIEbdihfdwx237tvJHOzEeg
+4/xRfDuJxBtZOtctRuMBzkuTCQ85lyrOLLOO81mqmnEbF9d+9WmwS4mu9fta7b5H
+dCqTmtVncnxlE5PmstyeJaVhXbx4MA2BMIhJiusBw5WqKFYFbmhuGpeyEJx3JIZU
+tfE5aamMIbwnwk2VBwfUqHAuQVUxlz/tmRyZ7ALvykZJdJVrUxkKttgjWw==
+-----END CERTIFICATE-----
diff --git a/lib/test_leaf_cert.pem b/lib/test_leaf_cert.pem
new file mode 100644
index 0000000..379d028
--- /dev/null
+++ b/lib/test_leaf_cert.pem
@@ -0,0 +1,18 @@
+-----BEGIN CERTIFICATE-----
+MIIC6DCCAdCgAwIBAgIBAjANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQKExJNeSBD
+QSBPcmdhbml6YXRpb24wHhcNMjQxMDE2MjIwMTQyWhcNMjUxMDE2MjIwMTQyWjAf
+MR0wGwYDVQQKExRNeSBMZWFmIE9yZ2FuaXphdGlvbjCCASIwDQYJKoZIhvcNAQEB
+BQADggEPADCCAQoCggEBAOuOsSztuF7JDXrqvrM6bKSQ6VqpCEHtR5sJnndlrs/V
+Rxsxltk2qBigGIDqQYByM1pcT3WkA2619BHHxaDfcD2Zvx718dUoF1EGzadiEXOR
+946MZlW194qO7++Y8soB4ru36fSGfsK9wr6gOVSYmHINORV+giUvxgbvIpNfQAZI
+39nWNKkR14BLjmkZO6CXomu2W/ZT1WXJ7FDNyF28ww6h5AOCvox9YIRf3OV0GaNE
+kAdoJsw95t64C407P96a6DnaRjWgGkNYTfKf3yPsavilxguXXpqLFlYU7flONA0T
+/ThTjuiRwOShGHEaRr/4iswuxvPWnHTAGXX2K/2wjqkCAwEAAaMxMC8wDgYDVR0P
+AQH/BAQDAgeAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATANBgkqhkiG
+9w0BAQsFAAOCAQEAIJje0ts+350wREkB3E0ud/KRL7Ra734YhSxlo+i0UKwGR4oM
+hU2xg+0A7B9uJii9L58oMWTZ+JJpA1Fb4tagksUuBI/rxjNWQ7QSvlhx2Zkwsr5x
+l0nlfxMuzfgRBD/eafxIaD/Li/iiPHQNJyU4iNImmw2r8IW+rtbU/sCVG/OjqZEc
+oKsw+qk9veyH/oZb19HSpK28A6voc2YuNsL4ghMbMWicITFle+Sv6yBEHDIxvQB6
+2pvsQ+H6EY25YfAz2AQNaz6U32CA6KdaQQh87wXAnagM4S6s7SmsZVgX/vGcUS5n
+91NEq4QRzFuINWPF38AsyOR9r2B2xuuCrulsQQ==
+-----END CERTIFICATE-----
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..96f5009
--- /dev/null
+++ b/main.go
@@ -0,0 +1,168 @@
+// Copyright 2024 Keyfactor
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+ "fmt"
+
+ "github.com/Keyfactor/keyfactor-auth-client-go/pkg"
+)
+
+func main() {
+ fmt.Println("Version:", pkg.Version) // print the package version
+ fmt.Println("Build:", pkg.BuildTime) // print the package build
+ fmt.Println("Commit:", pkg.CommitHash) // print the package commit
+ //testClients()
+}
+
+//func testClients() {
+// // URL to test against
+// url := os.Getenv("KEYFACTOR_AUTH_TOKEN_URL")
+// caCertPath := os.Getenv("KEYFACTOR_CA_CERT")
+//
+// // Load the custom root CA certificate
+// caCert, err := os.ReadFile(caCertPath)
+// if err != nil {
+// log.Fatalf("Failed to read root CA certificate: %v", err)
+// }
+//
+// // Create a certificate pool and add the custom root CA
+// caCertPool := x509.NewCertPool()
+// if !caCertPool.AppendCertsFromPEM(caCert) {
+// log.Fatalf("Failed to append root CA certificate to pool")
+// }
+//
+// // OAuth2 client credentials configuration
+// clientId := os.Getenv("KEYFACTOR_AUTH_CLIENT_ID")
+// clientSecret := os.Getenv("KEYFACTOR_AUTH_CLIENT_SECRET")
+// oauthConfig := &clientcredentials.Config{
+// ClientID: clientId,
+// ClientSecret: clientSecret,
+// TokenURL: url,
+// }
+//
+// // Transport with default TLS verification (InsecureSkipVerify = false)
+// transportDefaultTLS := &http.Transport{
+// TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
+// }
+//
+// // Transport with TLS verification skipped (InsecureSkipVerify = true)
+// transportInsecureTLS := &http.Transport{
+// TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+// }
+//
+// // Transport with custom CA verification
+// transportCustomRootCA := &http.Transport{
+// TLSClientConfig: &tls.Config{
+// RootCAs: caCertPool, // Custom root CA pool
+// InsecureSkipVerify: false, // Enforce TLS verification
+// },
+// }
+//
+// // OAuth2 Token Sources
+// tokenSourceDefaultTLS := oauthConfig.TokenSource(context.Background())
+//
+// ctxInsecure := context.WithValue(
+// context.Background(),
+// oauth2.HTTPClient,
+// &http.Client{Transport: transportInsecureTLS},
+// )
+// tokenSourceInsecureTLS := oauthConfig.TokenSource(ctxInsecure)
+//
+// ctxCustomCA := context.WithValue(
+// context.Background(),
+// oauth2.HTTPClient,
+// &http.Client{Transport: transportCustomRootCA},
+// )
+// tokenSourceCustomRootCA := oauthConfig.TokenSource(ctxCustomCA)
+//
+// // OAuth2 clients with different transports
+// oauthClientDefaultTLS := &http.Client{
+// Transport: &oauth2Transport{
+// base: transportDefaultTLS,
+// src: tokenSourceDefaultTLS,
+// },
+// }
+//
+// oauthClientInsecureTLS := &http.Client{
+// Transport: &oauth2Transport{
+// base: transportInsecureTLS,
+// src: tokenSourceInsecureTLS,
+// },
+// }
+//
+// oauthClientCustomRootCA := &http.Client{
+// Transport: &oauth2Transport{
+// base: transportCustomRootCA,
+// src: tokenSourceCustomRootCA,
+// },
+// }
+//
+// // Prepare the GET request
+// req, err := http.NewRequest("GET", url, nil)
+// if err != nil {
+// log.Fatalf("Failed to create request: %v", err)
+// }
+//
+// // Test 1: OAuth2 client with default TLS verification (expected to fail if certificate is invalid)
+// fmt.Println("Testing OAuth2 client with default TLS verification...")
+// resp1, err1 := oauthClientDefaultTLS.Do(req)
+// if err1 != nil {
+// log.Printf("OAuth2 client with default TLS failed as expected: %v\n", err1)
+// } else {
+// fmt.Printf("OAuth2 client with default TLS succeeded: %s\n", resp1.Status)
+// resp1.Body.Close()
+// }
+//
+// // Test 2: OAuth2 client with skipped TLS verification (should succeed)
+// fmt.Println("\nTesting OAuth2 client with skipped TLS verification...")
+// resp2, err2 := oauthClientInsecureTLS.Do(req)
+// if err2 != nil {
+// log.Fatalf("OAuth2 client with skipped TLS failed: %v\n", err2)
+// } else {
+// fmt.Printf("OAuth2 client with skipped TLS succeeded: %s\n", resp2.Status)
+// resp2.Body.Close()
+// }
+//
+// // Test 3: OAuth2 client with custom root CA (should succeed if the CA is valid)
+// fmt.Println("\nTesting OAuth2 client with custom root CA verification...")
+// resp3, err3 := oauthClientCustomRootCA.Do(req)
+// if err3 != nil {
+// log.Fatalf("OAuth2 client with custom root CA failed: %v\n", err3)
+// } else {
+// fmt.Printf("OAuth2 client with custom root CA succeeded: %s\n", resp3.Status)
+// resp3.Body.Close()
+// }
+//}
+//
+//// oauth2Transport is a custom RoundTripper that injects the OAuth2 token into requests
+//type oauth2Transport struct {
+// base http.RoundTripper
+// src oauth2.TokenSource
+//}
+//
+//// RoundTrip executes a single HTTP transaction, adding the OAuth2 token to the request
+//func (t *oauth2Transport) RoundTrip(req *http.Request) (*http.Response, error) {
+// token, err := t.src.Token()
+// if err != nil {
+// return nil, fmt.Errorf("failed to retrieve OAuth token: %w", err)
+// }
+//
+// // Clone the request to avoid mutating the original
+// reqCopy := req.Clone(req.Context())
+// token.SetAuthHeader(reqCopy)
+//
+// return t.base.RoundTrip(reqCopy)
+//}
diff --git a/pkg/version.go b/pkg/version.go
new file mode 100644
index 0000000..1bc3d80
--- /dev/null
+++ b/pkg/version.go
@@ -0,0 +1,21 @@
+// Copyright 2024 Keyfactor
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package pkg
+
+var (
+ Version string
+ BuildTime string
+ CommitHash string
+)
diff --git a/scripts/auth_keycloak.ps1 b/scripts/auth_keycloak.ps1
new file mode 100644
index 0000000..e2f889d
--- /dev/null
+++ b/scripts/auth_keycloak.ps1
@@ -0,0 +1,64 @@
+# Copyright 2024 Keyfactor
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http:#www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+function CheckVars {
+ # Check hostname
+ if (!$env:KEYFACTOR_AUTH_HOSTNAME) {
+ # Check if KEYFACTOR_HOSTNAME is set
+ if (!$env:KEYFACTOR_HOSTNAME) {
+ Write-Host "KEYFACTOR_HOSTNAME is not set"
+ # prompt for hostname until it is set
+ while (!$env:KEYFACTOR_HOSTNAME) {
+ $env:KEYFACTOR_AUTH_HOSTNAME = Read-Host "Enter auth hostname"
+ }
+ } else {
+ Write-Host "Setting auth hostname to $env:KEYFACTOR_HOSTNAME"
+ $env:KEYFACTOR_AUTH_HOSTNAME = $env:KEYFACTOR_HOSTNAME
+ }
+ }
+
+ if (!$env:KEYFACTOR_CLIENT_ID) {
+ Write-Host "KEYFACTOR_CLIENT_ID is not set"
+ # prompt for client_id until it is set
+ while (!$env:KEYFACTOR_CLIENT_ID) {
+ $env:KEYFACTOR_CLIENT_ID = Read-Host "Enter client_id"
+ }
+ }
+
+ if (!$env:KEYFACTOR_CLIENT_SECRET) {
+ Write-Host "KEYFACTOR_CLIENT_SECRET is not set"
+ while (!$env:KEYFACTOR_CLIENT_SECRET) {
+ #prompt for sensitive client_secret until it is set
+ $env:KEYFACTOR_CLIENT_SECRET = Read-Host "Enter client_secret"
+ }
+ }
+}
+
+function AuthClientCredentials {
+ CheckVars
+ $client_id = $env:KEYFACTOR_CLIENT_ID
+ $client_secret = $env:KEYFACTOR_CLIENT_SECRET
+ $grant_type = "client_credentials"
+ $auth_url = "https://$env:KEYFACTOR_AUTH_HOSTNAME:$($env:KEYFACTOR_AUTH_PORT -replace '^$', '8444')/realms/$($env:KEYFACTOR_AUTH_REALM -replace '^$', 'Keyfactor')/protocol/openid-connect/token"
+
+ $response = Invoke-RestMethod -Uri $auth_url -Method POST -Body @{
+ "grant_type" = $grant_type
+ "client_id" = $client_id
+ "client_secret" = $client_secret
+ } -ContentType 'application/x-www-form-urlencoded'
+
+ $env:KEYFACTOR_ACCESS_TOKEN = $response.access_token
+}
+
+AuthClientCredentials
\ No newline at end of file
diff --git a/scripts/auth_keycloak.sh b/scripts/auth_keycloak.sh
new file mode 100755
index 0000000..0d55a0a
--- /dev/null
+++ b/scripts/auth_keycloak.sh
@@ -0,0 +1,68 @@
+#!/usr/bin/env bash
+
+# Copyright 2024 Keyfactor
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http:#www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Input vars
+function checkVars() {
+ # Check hostname
+ if [ -z "$KEYFACTOR_AUTH_HOSTNAME" ]; then
+ # Check if KEYFACTOR_HOSTNAME is set
+ if [ -z "$KEYFACTOR_HOSTNAME" ]; then
+ echo "KEYFACTOR_HOSTNAME is not set"
+ # prompt for hostname until it is set
+ while [ -z "$KEYFACTOR_HOSTNAME" ]; do
+ read -p "Enter auth hostname: " KEYFACTOR_AUTH_HOSTNAME
+ done
+ else
+ echo "Setting auth hostname to $KEYFACTOR_HOSTNAME"
+ KEYFACTOR_AUTH_HOSTNAME="$KEYFACTOR_HOSTNAME"
+ fi
+ fi
+
+ if [ -z "$KEYFACTOR_CLIENT_ID" ]; then
+ echo "KEYFACTOR_CLIENT_ID is not set"
+ # prompt for client_id until it is set
+ while [ -z "$KEYFACTOR_CLIENT_ID" ]; do
+ read -p "Enter client_id: " KEYFACTOR_CLIENT_ID
+ done
+ fi
+
+ if [ -z "$KEYFACTOR_CLIENT_SECRET" ]; then
+ echo "KEYFACTOR_CLIENT_SECRET is not set"
+ while [ -z "$KEYFACTOR_CLIENT_SECRET" ]; do
+ #prompt for sensitive client_secret until it is set
+ read -s -p "Enter client_secret: " KEYFACTOR_CLIENT_SECRET
+ done
+ fi
+}
+
+function authClientCredentials(){
+ checkVars
+ client_id="${KEYFACTOR_CLIENT_ID}"
+ client_secret="${KEYFACTOR_CLIENT_SECRET}"
+ grant_type="client_credentials"
+ auth_url="https://$KEYFACTOR_AUTH_HOSTNAME:${KEYFACTOR_AUTH_PORT:-8444}/realms/${KEYFFACTOR_AUTH_REALM:-Keyfactor}/protocol/openid-connect/token"
+
+ curl -X POST $auth_url \
+ --header 'Content-Type: application/x-www-form-urlencoded' \
+ --data-urlencode "grant_type=$grant_type" \
+ --data-urlencode "client_id=$client_id" \
+ --data-urlencode "client_secret=$client_secret" > keyfactor_auth.json
+
+ export KEYFACTOR_ACCESS_TOKEN=$(cat keyfactor_auth.json | jq -r '.access_token')
+
+}
+
+authClientCredentials
\ No newline at end of file
diff --git a/tag.sh b/tag.sh
new file mode 100755
index 0000000..d42fdd5
--- /dev/null
+++ b/tag.sh
@@ -0,0 +1,5 @@
+RC_VERSION=rc.1
+TAG_VERSION_1=v1.0.0-$RC_VERSION
+git tag -d $TAG_VERSION_1 || true
+git tag $TAG_VERSION_1
+git push origin $TAG_VERSION_1
\ No newline at end of file
From 62f5c72cae33b928be06e4c0a9acc48c2db20412 Mon Sep 17 00:00:00 2001
From: spbsoluble <1661003+spbsoluble@users.noreply.github.com>
Date: Thu, 31 Oct 2024 13:01:27 -0700
Subject: [PATCH 02/10] fix(ci): Add `scan_token` to `starter workflow v3`
---
.github/workflows/keyfactor-starter-workflow.yml | 3 ++-
tag.sh | 2 +-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/keyfactor-starter-workflow.yml b/.github/workflows/keyfactor-starter-workflow.yml
index 01ddd34..64a6352 100644
--- a/.github/workflows/keyfactor-starter-workflow.yml
+++ b/.github/workflows/keyfactor-starter-workflow.yml
@@ -16,4 +16,5 @@ jobs:
token: ${{ secrets.V2BUILDTOKEN}}
APPROVE_README_PUSH: ${{ secrets.APPROVE_README_PUSH}}
gpg_key: ${{ secrets.KF_GPG_PRIVATE_KEY }}
- gpg_pass: ${{ secrets.KF_GPG_PASSPHRASE }}
\ No newline at end of file
+ gpg_pass: ${{ secrets.KF_GPG_PASSPHRASE }}
+ scan_token: ${{ secrets.SAST_TOKEN }}
\ No newline at end of file
diff --git a/tag.sh b/tag.sh
index d42fdd5..115bbfd 100755
--- a/tag.sh
+++ b/tag.sh
@@ -1,4 +1,4 @@
-RC_VERSION=rc.1
+RC_VERSION=rc.2
TAG_VERSION_1=v1.0.0-$RC_VERSION
git tag -d $TAG_VERSION_1 || true
git tag $TAG_VERSION_1
From 6cdf66dfa541ca6f3dafef0e405b095688c2a4a3 Mon Sep 17 00:00:00 2001
From: spbsoluble <1661003+spbsoluble@users.noreply.github.com>
Date: Thu, 31 Oct 2024 13:21:22 -0700
Subject: [PATCH 03/10] chore(docs): Add `CHANGELOG.md`
---
.gitignore | 12 ++++---
.goreleaser.yml | 86 +++++++++++++++++++++++++++++++++++++++++++++++++
CHANGELOG.md | 6 ++++
3 files changed, 100 insertions(+), 4 deletions(-)
create mode 100644 .goreleaser.yml
create mode 100644 CHANGELOG.md
diff --git a/.gitignore b/.gitignore
index 9ab1d97..666404a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -118,6 +118,7 @@ terraform.rc
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
+.idea
*.exe
*.exe~
*.dll
@@ -131,9 +132,12 @@ terraform.rc
*.out
# Dependency directories (remove the comment below to include it)
-# vendor/
+vendor/
-# Go workspace file
-go.work
+.env*
-*.env*
\ No newline at end of file
+*.csv
+/.vs/**/*
+/.vscode/**/*
+.DS_Store
+.auto.tfvars
\ No newline at end of file
diff --git a/.goreleaser.yml b/.goreleaser.yml
new file mode 100644
index 0000000..45ff554
--- /dev/null
+++ b/.goreleaser.yml
@@ -0,0 +1,86 @@
+# Visit https://goreleaser.com for documentation on how to customize this
+# behavior.
+before:
+ hooks:
+ # this is just an example and not a requirement for provider building/publishing
+ - go mod tidy
+builds:
+ - env:
+ # goreleaser does not work with CGO, it could also complicate
+ # usage by users in CI/CD systems like Terraform Cloud where
+ # they are unable to install libraries.
+ - CGO_ENABLED=0
+ mod_timestamp: '{{ .CommitTimestamp }}'
+ flags:
+ - -trimpath
+ ldflags:
+ - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}'
+ goos:
+ - freebsd
+ - windows
+ - linux
+ - darwin
+ goarch:
+ - amd64
+ - '386'
+ - arm
+ - arm64
+ ignore:
+ - goos: darwin
+ goarch: '386'
+ binary: 'keyfactor-auth-client'
+archives:
+ - format: zip
+ name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}'
+checksum:
+ extra_files:
+ - glob: 'integration-manifest.json'
+ name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json'
+ name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS'
+ algorithm: sha256
+signs:
+ - artifacts: checksum
+ args:
+ # if you are using this in a GitHub action or some other automated pipeline, you
+ # need to pass the batch flag to indicate its not interactive.
+ - "--batch"
+ - "--local-user"
+ - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key
+ - "--output"
+ - "${signature}"
+ - "--detach-sign"
+ - "${artifact}"
+release:
+ prerelease: auto
+ extra_files:
+ - glob: 'integration-manifest.json'
+ name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json'
+ # If you want to manually examine the release before its live, uncomment this line:
+ #draft: true
+changelog:
+ sort: asc
+ use: github
+ filters:
+ exclude:
+ - '^test:'
+ - '^chore'
+ - 'merge conflict'
+ - Merge pull request
+ - Merge remote-tracking branch
+ - Merge branch
+ - go mod tidy
+ groups:
+ - title: Dependency updates
+ regexp: "^.*(feat|fix)\\(deps\\)*:+.*$"
+ order: 300
+ - title: 'New Features'
+ regexp: "^.*feat[(\\w)]*:+.*$"
+ order: 100
+ - title: 'Bug fixes'
+ regexp: "^.*fix[(\\w)]*:+.*$"
+ order: 200
+ - title: 'Documentation updates'
+ regexp: "^.*docs[(\\w)]*:+.*$"
+ order: 400
+ - title: Other work
+ order: 9999
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..b9a23ed
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,6 @@
+# v1.0.0
+
+## Features
+- Support for `BasicAuth` client config
+- Support for `OAuth2` client config
+- Support for Keyfactor client config file with `BasicAuth` and `OAuth2` client config(s)
\ No newline at end of file
From 254b2c26b746cc88ba075745c45fc7ff34a06bc5 Mon Sep 17 00:00:00 2001
From: spbsoluble <1661003+spbsoluble@users.noreply.github.com>
Date: Mon, 4 Nov 2024 09:53:52 -0800
Subject: [PATCH 04/10] chore(tests): Remove unnecessary CI commands from
`Validate lab cert` step.
---
.github/config/README.md | 57 +++++++++++++++++-----------------
.github/workflows/go_tests.yml | 4 ---
2 files changed, 28 insertions(+), 33 deletions(-)
diff --git a/.github/config/README.md b/.github/config/README.md
index e1a8977..42fee46 100644
--- a/.github/config/README.md
+++ b/.github/config/README.md
@@ -25,8 +25,8 @@ module "keyfactor_github_test_environment_ad_10_5_0" {
gh_environment_name = "KFC_10_5_0" # Keyfactor Command 10.5.0 environment using Active Directory(/Basic Auth)
gh_repo_name = data.github_repository.repo.name
keyfactor_hostname = var.keyfactor_hostname_10_5_0
- keyfactor_username = var.keyfactor_username_10_5_0
- keyfactor_password = var.keyfactor_password_10_5_0
+ keyfactor_username = var.keyfactor_username_AD
+ keyfactor_password = var.keyfactor_password_AD
}
```
@@ -38,53 +38,52 @@ module "keyfactor_github_test_environment_12_3_0_kc" {
gh_environment_name = "KFC_12_3_0_KC" # Keyfactor Command 12.3.0 environment using Keycloak
gh_repo_name = data.github_repository.repo.name
- keyfactor_hostname = var.keyfactor_hostname_12_3_0_KC
- keyfactor_auth_token_url = var.keyfactor_auth_token_url_12_3_0_KC
- keyfactor_client_id = var.keyfactor_client_id_12_3_0
- keyfactor_client_secret = var.keyfactor_client_secret_12_3_0
+ keyfactor_hostname = var.keyfactor_hostname_12_3_0_OAUTH
+ keyfactor_auth_token_url = var.keyfactor_auth_token_url
+ keyfactor_client_id = var.keyfactor_client_id
+ keyfactor_client_secret = var.keyfactor_client_secret
keyfactor_tls_skip_verify = true
}
```
-
## Requirements
-| Name | Version |
-|---------------------------------------------------------------------------|---------|
-| [terraform](#requirement\_terraform) | >= 1.0 |
-| [github](#requirement\_github) | >=6.2 |
+| Name | Version |
+|------|---------|
+| [terraform](#requirement\_terraform) | >= 1.0 |
+| [github](#requirement\_github) | >=6.2 |
## Providers
-| Name | Version |
-|------------------------------------------------------------|---------|
-| [github](#provider\_github) | 6.3.1 |
+| Name | Version |
+|------|---------|
+| [github](#provider\_github) | 6.3.1 |
## Modules
-| Name | Source | Version |
-|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------|---------|
-| [keyfactor\_github\_test\_environment\_12\_3\_0\_kc](#module\_keyfactor\_github\_test\_environment\_12\_3\_0\_kc) | git::ssh://git@github.com/Keyfactor/terraform-module-keyfactor-github-test-environment-ad.git | main |
-| [keyfactor\_github\_test\_environment\_ad\_10\_5\_0](#module\_keyfactor\_github\_test\_environment\_ad\_10\_5\_0) | git::ssh://git@github.com/Keyfactor/terraform-module-keyfactor-github-test-environment-ad.git | main |
+| Name | Source | Version |
+|------|--------|---------|
+| [keyfactor\_github\_test\_environment\_12\_3\_0\_kc](#module\_keyfactor\_github\_test\_environment\_12\_3\_0\_kc) | git::ssh://git@github.com/Keyfactor/terraform-module-keyfactor-github-test-environment-ad.git | main |
+| [keyfactor\_github\_test\_environment\_ad\_10\_5\_0](#module\_keyfactor\_github\_test\_environment\_ad\_10\_5\_0) | git::ssh://git@github.com/Keyfactor/terraform-module-keyfactor-github-test-environment-ad.git | main |
## Resources
-| Name | Type |
-|---------------------------------------------------------------------------------------------------------------------------|-------------|
+| Name | Type |
+|------|------|
| [github_repository.repo](https://registry.terraform.io/providers/integrations/github/latest/docs/data-sources/repository) | data source |
## Inputs
-| Name | Description | Type | Default | Required |
-|--------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------|----------|---------------------------------------------------------------------------------------------------------|:--------:|
-| [keyfactor\_auth\_token\_url\_12\_3\_0\_KC](#input\_keyfactor\_auth\_token\_url\_12\_3\_0\_KC) | The hostname of the KeyCloak instance to authenticate to for a Keyfactor Command access token | `string` | `"https://int-oidc-lab.eastus2.cloudapp.azure.com:8444/realms/Keyfactor/protocol/openid-connect/token"` | no |
-| [keyfactor\_client\_id\_12\_3\_0](#input\_keyfactor\_client\_id\_12\_3\_0) | The client ID to authenticate with the Keyfactor instance using Keycloak client credentials | `string` | n/a | yes |
-| [keyfactor\_client\_secret\_12\_3\_0](#input\_keyfactor\_client\_secret\_12\_3\_0) | The client secret to authenticate with the Keyfactor instance using Keycloak client credentials | `string` | n/a | yes |
-| [keyfactor\_hostname\_10\_5\_0](#input\_keyfactor\_hostname\_10\_5\_0) | The hostname of the Keyfactor instance | `string` | `"integrations1050-lab.kfdelivery.com"` | no |
-| [keyfactor\_hostname\_12\_3\_0\_KC](#input\_keyfactor\_hostname\_12\_3\_0\_KC) | The hostname of the Keyfactor instance | `string` | `"int-oidc-lab.eastus2.cloudapp.azure.com"` | no |
-| [keyfactor\_password\_10\_5\_0](#input\_keyfactor\_password\_10\_5\_0) | The password to authenticate with the Keyfactor instance | `string` | n/a | yes |
-| [keyfactor\_username\_10\_5\_0](#input\_keyfactor\_username\_10\_5\_0) | The username to authenticate with the Keyfactor instance | `string` | n/a | yes |
+| Name | Description | Type | Default | Required |
+|------|-------------|------|---------|:--------:|
+| [keyfactor\_auth\_token\_url\_12\_3\_0\_KC](#input\_keyfactor\_auth\_token\_url\_12\_3\_0\_KC) | The hostname of the KeyCloak instance to authenticate to for a Keyfactor Command access token | `string` | `"https://int-oidc-lab.eastus2.cloudapp.azure.com:8444/realms/Keyfactor/protocol/openid-connect/token"` | no |
+| [keyfactor\_client\_id\_12\_3\_0](#input\_keyfactor\_client\_id\_12\_3\_0) | The client ID to authenticate with the Keyfactor instance using Keycloak client credentials | `string` | n/a | yes |
+| [keyfactor\_client\_secret\_12\_3\_0](#input\_keyfactor\_client\_secret\_12\_3\_0) | The client secret to authenticate with the Keyfactor instance using Keycloak client credentials | `string` | n/a | yes |
+| [keyfactor\_hostname\_10\_5\_0](#input\_keyfactor\_hostname\_10\_5\_0) | The hostname of the Keyfactor instance | `string` | `"integrations1050-lab.kfdelivery.com"` | no |
+| [keyfactor\_hostname\_12\_3\_0\_KC](#input\_keyfactor\_hostname\_12\_3\_0\_KC) | The hostname of the Keyfactor instance | `string` | `"int-oidc-lab.eastus2.cloudapp.azure.com"` | no |
+| [keyfactor\_password\_10\_5\_0](#input\_keyfactor\_password\_10\_5\_0) | The password to authenticate with the Keyfactor instance | `string` | n/a | yes |
+| [keyfactor\_username\_10\_5\_0](#input\_keyfactor\_username\_10\_5\_0) | The username to authenticate with the Keyfactor instance | `string` | n/a | yes |
## Outputs
diff --git a/.github/workflows/go_tests.yml b/.github/workflows/go_tests.yml
index a40e8ba..57bec35 100644
--- a/.github/workflows/go_tests.yml
+++ b/.github/workflows/go_tests.yml
@@ -26,10 +26,6 @@ jobs:
- name: Validate lab cert is present
run: |
- pwd
- ls -la
- ls -la lib
- ls -la lib/certs
cat lib/certs/int-oidc-lab.eastus2.cloudapp.azure.com.pem
- name: Run tests
From 4e8f09e021b6cb229e4c5c2c9ea7de0e314d99da Mon Sep 17 00:00:00 2001
From: spbsoluble <1661003+spbsoluble@users.noreply.github.com>
Date: Mon, 4 Nov 2024 10:48:03 -0800
Subject: [PATCH 05/10] chore(docs): Update `integration-manifest.json`
---
integration-manifest.json | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/integration-manifest.json b/integration-manifest.json
index a01d975..fb75c92 100644
--- a/integration-manifest.json
+++ b/integration-manifest.json
@@ -6,6 +6,7 @@
"description": "The Keyfactor Auth Client - Golang is a Go module that handles authentication and authorization for the Keyfactor API.",
"support_level": "kf-community",
"link_github": false,
- "update_catalog": false
+ "update_catalog": false,
+ "release_dir": "bin"
}
From 37e9482c39148bba9dc2fdfbbf24fa947067b50f Mon Sep 17 00:00:00 2001
From: spbsoluble <1661003+spbsoluble@users.noreply.github.com>
Date: Mon, 4 Nov 2024 10:48:16 -0800
Subject: [PATCH 06/10] chore(docs): Update `integration-manifest.json`
---
integration-manifest.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/integration-manifest.json b/integration-manifest.json
index fb75c92..f8a6743 100644
--- a/integration-manifest.json
+++ b/integration-manifest.json
@@ -4,7 +4,7 @@
"name": "Keyfactor Auth Client - Golang",
"status": "production",
"description": "The Keyfactor Auth Client - Golang is a Go module that handles authentication and authorization for the Keyfactor API.",
- "support_level": "kf-community",
+ "support_level": "community",
"link_github": false,
"update_catalog": false,
"release_dir": "bin"
From c0806f00e742cd57debe72864ce6f39cb7110336 Mon Sep 17 00:00:00 2001
From: spbsoluble <1661003+spbsoluble@users.noreply.github.com>
Date: Tue, 5 Nov 2024 15:21:27 -0800
Subject: [PATCH 07/10] fix(conf): `MergeConfigFromFile` to return reference to
merged config. feat(conf): Add `Compare` to `Server, Config` types.
---
auth_providers/command_config.go | 40 ++++++++++++++++++++++++++++----
1 file changed, 35 insertions(+), 5 deletions(-)
diff --git a/auth_providers/command_config.go b/auth_providers/command_config.go
index fee0820..fe1d298 100644
--- a/auth_providers/command_config.go
+++ b/auth_providers/command_config.go
@@ -186,23 +186,53 @@ func WriteConfigToYAML(filePath string, config *Config) error {
return nil
}
+func (c *Config) Compare(other *Config) bool {
+ if len(c.Servers) != len(other.Servers) {
+ return false
+ }
+
+ for key, server := range c.Servers {
+ if otherServer, exists := other.Servers[key]; !exists || !server.Compare(&otherServer) {
+ return false
+ }
+ }
+
+ return true
+}
+
+func (s *Server) Compare(other *Server) bool {
+ return s.Host == other.Host &&
+ s.Port == other.Port &&
+ s.Username == other.Username &&
+ s.Password == other.Password &&
+ s.Domain == other.Domain &&
+ s.ClientID == other.ClientID &&
+ s.ClientSecret == other.ClientSecret &&
+ s.OAuthTokenUrl == other.OAuthTokenUrl &&
+ s.APIPath == other.APIPath &&
+ s.SkipTLSVerify == other.SkipTLSVerify &&
+ s.CACertPath == other.CACertPath &&
+ s.AuthType == other.AuthType
+}
+
// MergeConfigFromFile merges the configuration from a file into the existing Config.
-func MergeConfigFromFile(filePath string, config *Config) error {
+func MergeConfigFromFile(filePath string, config *Config) (*Config, error) {
// Read the file content
data, err := os.ReadFile(filePath)
if err != nil {
- return fmt.Errorf("failed to read config file: %w", err)
+
+ return nil, fmt.Errorf("failed to read config file: %w", err)
}
// Determine the file type (JSON or YAML) and unmarshal accordingly
var tempConfig Config
if json.Valid(data) {
if err := json.Unmarshal(data, &tempConfig); err != nil {
- return fmt.Errorf("failed to unmarshal JSON config: %w", err)
+ return nil, fmt.Errorf("failed to unmarshal JSON config: %w", err)
}
} else {
if err := yaml.Unmarshal(data, &tempConfig); err != nil {
- return fmt.Errorf("failed to unmarshal YAML config: %w", err)
+ return nil, fmt.Errorf("failed to unmarshal YAML config: %w", err)
}
}
@@ -213,7 +243,7 @@ func MergeConfigFromFile(filePath string, config *Config) error {
}
}
- return nil
+ return config, nil
}
// GetAuthType returns the type of authentication to use based on the configuration params.
From 0610b0a17c637113df8da3873d342c24e6ca8fb2 Mon Sep 17 00:00:00 2001
From: spbsoluble <1661003+spbsoluble@users.noreply.github.com>
Date: Wed, 6 Nov 2024 09:37:11 -0800
Subject: [PATCH 08/10] fix(tests): Update config test
---
auth_providers/command_config_test.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/auth_providers/command_config_test.go b/auth_providers/command_config_test.go
index 2aeae9f..af6c969 100644
--- a/auth_providers/command_config_test.go
+++ b/auth_providers/command_config_test.go
@@ -181,7 +181,7 @@ func TestMergeConfigFromFile(t *testing.T) {
},
}
- err = auth_providers.MergeConfigFromFile(filePath, newConfig)
+ _, err = auth_providers.MergeConfigFromFile(filePath, newConfig)
if err != nil {
t.Fatalf("failed to merge config from file: %v", err)
}
From 02c17c45ce90307d5800549a06974afb524a654c Mon Sep 17 00:00:00 2001
From: spbsoluble <1661003+spbsoluble@users.noreply.github.com>
Date: Tue, 12 Nov 2024 12:30:27 -0800
Subject: [PATCH 09/10] fix(core): Add timeouts to base transport. chore(deps):
Bump `golang.org/x/oauth2` to `v0.24.0`
---
auth_providers/auth_core.go | 9 ++++++++-
go.mod | 2 +-
go.sum | 6 ++----
3 files changed, 11 insertions(+), 6 deletions(-)
diff --git a/auth_providers/auth_core.go b/auth_providers/auth_core.go
index 97d1295..c2653a4 100644
--- a/auth_providers/auth_core.go
+++ b/auth_providers/auth_core.go
@@ -288,12 +288,19 @@ func (c *CommandAuthConfig) ValidateAuthConfig() error {
// BuildTransport creates a custom http Transport for authentication to Keyfactor Command API.
func (c *CommandAuthConfig) BuildTransport() (*http.Transport, error) {
+ defaultTimeout := time.Duration(c.HttpClientTimeout) * time.Second
output := http.Transport{
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{
Renegotiation: tls.RenegotiateOnceAsClient,
},
- TLSHandshakeTimeout: 10 * time.Second,
+ TLSHandshakeTimeout: defaultTimeout,
+ ResponseHeaderTimeout: defaultTimeout,
+ IdleConnTimeout: defaultTimeout,
+ ExpectContinueTimeout: defaultTimeout,
+ MaxIdleConns: 10,
+ MaxIdleConnsPerHost: 10,
+ MaxConnsPerHost: 10,
}
if c.SkipVerify {
diff --git a/go.mod b/go.mod
index e83a7c5..9c513dd 100644
--- a/go.mod
+++ b/go.mod
@@ -17,6 +17,6 @@ module github.com/Keyfactor/keyfactor-auth-client-go
go 1.22
require (
- golang.org/x/oauth2 v0.23.0
+ golang.org/x/oauth2 v0.24.0
gopkg.in/yaml.v2 v2.4.0
)
diff --git a/go.sum b/go.sum
index b65756f..b855ce3 100644
--- a/go.sum
+++ b/go.sum
@@ -1,9 +1,7 @@
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
-golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
-golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
-golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
+golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
From 0b7b64e8cc50de05d49039728aab5b1bcf0dfbac Mon Sep 17 00:00:00 2001
From: spbsoluble <1661003+spbsoluble@users.noreply.github.com>
Date: Wed, 13 Nov 2024 14:24:07 -0800
Subject: [PATCH 10/10] fix(oauth): Add `audience` to oauth client if provided.
fix(oauth): Support for `audience` and `scopes` via environmental variables.
fix(config): Add `scopes` and `audience` to `Server`. chore(docs): Remove toc
because GitHub creates one for you. chore(docs): Fix `Basic` auth verbiage.
chore(docs): Add `scopes` and `audience` to config file examples.
chore(docs): Document `KEYFACTOR_AUTH_AUDIENCE` in env var table.
---
README.md | 46 +++++++++--------
auth_providers/auth_oauth.go | 87 +++++++++++++++++++++++---------
auth_providers/command_config.go | 2 +
3 files changed, 88 insertions(+), 47 deletions(-)
diff --git a/README.md b/README.md
index c92a028..6eec710 100644
--- a/README.md
+++ b/README.md
@@ -1,22 +1,6 @@
# keyfactor-auth-client-go
-Client library for authenticating to Keyfactor Command
-
-
-
-- [Environment Variables](#environment-variables)
- * [Global](#global)
- * [Basic Auth](#basic-auth)
- * [oAuth Client Credentials](#oauth-client-credentials)
-- [Configuration File](#configuration-file)
- * [Basic Auth](#basic-auth)
- * [oAuth Client Credentials](#oauth-client-credentials)
- * [Auth Providers](#auth-providers)
- + [Azure Id](#azure-id)
- + [Azure KeyVault](#azure-keyvault)
-- [Test Environment Variables](#test-environment-variables)
-
-
+Client library for authenticating to Keyfactor Command.
## Environment Variables
@@ -35,7 +19,7 @@ Client library for authenticating to Keyfactor Command
### Basic Auth
-Currently, only Active Directory `Basic` authentication is supported.
+Currently, only `Basic` authentication auth via `Active Directory` is supported.
| Name | Description | Default |
|--------------------|---------------------------------------------------------------------------------------------|---------|
@@ -50,7 +34,8 @@ Currently, only Active Directory `Basic` authentication is supported.
| KEYFACTOR_AUTH_CLIENT_ID | Keyfactor Auth Client ID | |
| KEYFACTOR_AUTH_CLIENT_SECRET | Keyfactor Auth Client Secret | |
| KEYFACTOR_AUTH_TOKEN_URL | URL to request an access token from Keyfactor Auth | |
-| KEYFACTOR_AUTH_SCOPES | Scopes to request when authenticating to Keyfactor Command API | `openid` |
+| KEYFACTOR_AUTH_SCOPES | Scopes to request when authenticating to Keyfactor Command API. Each scope MUST be separated by `,` | `openid` |
+| KEYFACTOR_AUTH_AUDIENCE | Audience to request when authenticating to Keyfactor Command API | |
| KEYFACTOR_AUTH_ACCESS_TOKEN | Access token to use to authenticate to Keyfactor Command API. This can be supplied directly or generated via client credentials | |
| KEYFACTOR_AUTH_CA_CERT | Either a file path or PEM encoded string to a CA certificate to use when connecting to Keyfactor Auth | |
@@ -64,14 +49,17 @@ These environment variables are used to run go tests. They are not used in the a
| TEST_KEYFACTOR_KC_AUTH | Set to `true` to test Keycloak authentication | false |
## Configuration File
-A JSON or YAML file can be used to store authentication configuration. A configuration file can contain references to
-multiple Keyfactor Command environments and can be referenced by a `profile` name. The `default` profile will be used
-when no profile is specified. Keyfactor tools will look for a config file located at `$HOME/.keyfactor/command_config.json`
+
+A JSON or YAML file can be used to store authentication configuration. A configuration file can contain references to
+multiple Keyfactor Command environments and can be referenced by a `profile` name. The `default` profile will be used
+when no profile is specified. Keyfactor tools will look for a config file located at
+`$HOME/.keyfactor/command_config.json`
by default. The config file should be structured as follows:
### Basic Auth
#### JSON
+
```json
{
"servers": {
@@ -94,6 +82,7 @@ by default. The config file should be structured as follows:
```
#### YAML
+
```yaml
servers:
default:
@@ -113,6 +102,7 @@ servers:
### oAuth Client Credentials
#### JSON
+
```json
{
"servers": {
@@ -121,6 +111,12 @@ servers:
"token_url": "https://idp.keyfactor.command.kfdelivery.com/oauth2/token",
"client_id": "client-id",
"client_secret": "client-secret",
+ "audience": "https://keyfactor.command.kfdelivery.com",
+ "scopes": [
+ "openid",
+ "profile",
+ "email"
+ ],
"api_path": "KeyfactorAPI"
},
"server2": {
@@ -135,6 +131,7 @@ servers:
```
#### YAML
+
```yaml
servers:
default:
@@ -143,6 +140,11 @@ servers:
client_id: client-id
client_secret: client-secret
api_path: KeyfactorAPI
+ audience: https://keyfactor.command.kfdelivery.com
+ scopes:
+ - openid
+ - profile
+ - email
server2:
host: keyfactor.command.kfdelivery.com
token_url: https://idp.keyfactor.command.kfdelivery.com/oauth2/token
diff --git a/auth_providers/auth_oauth.go b/auth_providers/auth_oauth.go
index 8a8c940..6997219 100644
--- a/auth_providers/auth_oauth.go
+++ b/auth_providers/auth_oauth.go
@@ -6,6 +6,7 @@ import (
"fmt"
"net/http"
"os"
+ "strings"
"time"
"golang.org/x/oauth2"
@@ -42,6 +43,11 @@ const (
EnvAuthCACert = "KEYFACTOR_AUTH_CA_CERT"
)
+var (
+ // DefaultScopes is the default scopes for Keyfactor authentication
+ DefaultScopes = []string{"openid"}
+)
+
// OAuth Authenticator
var _ Authenticator = &OAuthAuthenticator{}
@@ -65,40 +71,40 @@ type CommandConfigOauth struct {
// CommandAuthConfig is a reference to the base configuration needed for authentication to Keyfactor Command API
CommandAuthConfig
- // ClientID is the Client ID for Keycloak authentication
+ // ClientID is the Client ID for OAuth authentication
ClientID string `json:"client_id,omitempty"`
- // ClientSecret is the Client secret for Keycloak authentication
+ // ClientSecret is the Client secret for OAuth authentication
ClientSecret string `json:"client_secret,omitempty"`
- // Audience is the audience for Keycloak authentication
+ // Audience is the audience for OAuth authentication
Audience string `json:"audience,omitempty"`
- // Scopes is the scopes for Keycloak authentication
+ // Scopes is the scopes for OAuth authentication
Scopes []string `json:"scopes,omitempty"`
- // CACertificatePath is the path to the CA certificate for Keycloak authentication
+ // CACertificatePath is the path to the CA certificate for OAuth authentication
CACertificatePath string `json:"idp_ca_cert,omitempty"`
// CACertificates is the CA certificates for authentication
CACertificates []*x509.Certificate `json:"-"`
- // AccessToken is the access token for Keycloak authentication
+ // AccessToken is the access token for OAuth authentication
AccessToken string `json:"access_token,omitempty"`
- // RefreshToken is the refresh token for Keycloak authentication
+ // RefreshToken is the refresh token for OAuth authentication
RefreshToken string `json:"refresh_token,omitempty"`
// Expiry is the expiry time of the access token
Expiry time.Time `json:"expiry,omitempty"`
- // TokenURL is the token URL for Keycloak authentication
+ // TokenURL is the token URL for OAuth authentication
TokenURL string `json:"token_url,omitempty"`
//// AuthPort
//AuthPort string `json:"auth_port,omitempty"`
- //// AuthType is the type of Keycloak auth to use such as client_credentials, password, etc.
+ //// AuthType is the type of OAuth auth to use such as client_credentials, password, etc.
//AuthType string `json:"auth_type,omitempty"`
}
@@ -107,49 +113,49 @@ func NewOAuthAuthenticatorBuilder() *CommandConfigOauth {
return &CommandConfigOauth{}
}
-// WithClientId sets the Client ID for Keycloak authentication.
+// WithClientId sets the Client ID for OAuth authentication.
func (b *CommandConfigOauth) WithClientId(clientId string) *CommandConfigOauth {
b.ClientID = clientId
return b
}
-// WithClientSecret sets the Client secret for Keycloak authentication.
+// WithClientSecret sets the Client secret for OAuth authentication.
func (b *CommandConfigOauth) WithClientSecret(clientSecret string) *CommandConfigOauth {
b.ClientSecret = clientSecret
return b
}
-// WithTokenUrl sets the token URL for Keycloak authentication.
+// WithTokenUrl sets the token URL for OAuth authentication.
func (b *CommandConfigOauth) WithTokenUrl(tokenUrl string) *CommandConfigOauth {
b.TokenURL = tokenUrl
return b
}
-// WithScopes sets the scopes for Keycloak authentication.
+// WithScopes sets the scopes for OAuth authentication.
func (b *CommandConfigOauth) WithScopes(scopes []string) *CommandConfigOauth {
b.Scopes = scopes
return b
}
-// WithAudience sets the audience for Keycloak authentication.
+// WithAudience sets the audience for OAuth authentication.
func (b *CommandConfigOauth) WithAudience(audience string) *CommandConfigOauth {
b.Audience = audience
return b
}
-// WithCaCertificatePath sets the CA certificate path for Keycloak authentication.
+// WithCaCertificatePath sets the CA certificate path for OAuth authentication.
func (b *CommandConfigOauth) WithCaCertificatePath(caCertificatePath string) *CommandConfigOauth {
b.CACertificatePath = caCertificatePath
return b
}
-// WithCaCertificates sets the CA certificates for Keycloak authentication.
+// WithCaCertificates sets the CA certificates for OAuth authentication.
func (b *CommandConfigOauth) WithCaCertificates(caCertificates []*x509.Certificate) *CommandConfigOauth {
b.CACertificates = caCertificates
return b
}
-// WithAccessToken sets the access token for Keycloak authentication.
+// WithAccessToken sets the access token for OAuth authentication.
func (b *CommandConfigOauth) WithAccessToken(accessToken string) *CommandConfigOauth {
if accessToken != "" {
b.AccessToken = accessToken
@@ -196,8 +202,14 @@ func (b *CommandConfigOauth) GetHttpClient() (*http.Client, error) {
Scopes: b.Scopes,
}
+ if b.Audience != "" {
+ config.EndpointParams = map[string][]string{
+ "audience": {b.Audience},
+ }
+ }
+
if len(b.Scopes) == 0 {
- b.Scopes = []string{"openid", "profile", "email"}
+ b.Scopes = DefaultScopes
}
if b.Audience != "" {
@@ -264,13 +276,13 @@ func (b *CommandConfigOauth) LoadConfig(profile, path string, silentLoad bool) (
// b.AccessToken = serverConfig.AccessToken
//}
- //if b.Audience == "" {
- // b.Audience = serverConfig.Audience
- //}
- //
- //if b.Scopes == nil || len(b.Scopes) == 0 {
- // b.Scopes = serverConfig.Scopes
- //}
+ if b.Audience == "" {
+ b.Audience = serverConfig.Audience
+ }
+
+ if b.Scopes == nil || len(b.Scopes) == 0 {
+ b.Scopes = serverConfig.Scopes
+ }
if b.CACertificatePath == "" {
b.CACertificatePath = serverConfig.CACertPath
@@ -350,6 +362,29 @@ func (b *CommandConfigOauth) ValidateAuthConfig() error {
}
}
+ if b.Audience == "" {
+ if audience, ok := os.LookupEnv(EnvKeyfactorAuthAudience); ok {
+ b.Audience = audience
+ } else {
+ if serverConfig != nil && serverConfig.Audience != "" {
+ b.Audience = serverConfig.Audience
+ }
+ }
+ }
+
+ if b.Scopes == nil || len(b.Scopes) == 0 {
+ if scopes, ok := os.LookupEnv(EnvKeyfactorAuthScopes); ok {
+ // split the scopes by comma
+ b.Scopes = strings.Split(scopes, ",")
+ } else {
+ if serverConfig != nil && len(serverConfig.Scopes) > 0 {
+ b.Scopes = serverConfig.Scopes
+ } else {
+ b.Scopes = DefaultScopes
+ }
+ }
+ }
+
return b.CommandAuthConfig.ValidateAuthConfig()
}
@@ -391,6 +426,8 @@ func (b *CommandConfigOauth) GetServerConfig() *Server {
ClientSecret: b.ClientSecret,
OAuthTokenUrl: b.TokenURL,
APIPath: b.CommandAPIPath,
+ Scopes: b.Scopes,
+ Audience: b.Audience,
//AuthProvider: AuthProvider{},
SkipTLSVerify: b.SkipVerify,
CACertPath: b.CommandCACert,
diff --git a/auth_providers/command_config.go b/auth_providers/command_config.go
index fe1d298..7141e16 100644
--- a/auth_providers/command_config.go
+++ b/auth_providers/command_config.go
@@ -32,6 +32,8 @@ type Server struct {
Domain string `json:"domain,omitempty" yaml:"domain,omitempty"` // Domain is the domain for authentication.
ClientID string `json:"client_id,omitempty" yaml:"client_id,omitempty"` // ClientID is the client ID for OAuth.
ClientSecret string `json:"client_secret,omitempty" yaml:"client_secret,omitempty"` // ClientSecret is the client secret for OAuth.
+ Scopes []string `json:"scopes,omitempty" yaml:"scopes,omitempty"` // Scopes is the OAuth scopes.
+ Audience string `json:"audience,omitempty" yaml:"audience,omitempty"` // Audience is the OAuth audience.
OAuthTokenUrl string `json:"token_url,omitempty" yaml:"token_url,omitempty"` // OAuthTokenUrl is full URL for OAuth token request endpoint.
APIPath string `json:"api_path,omitempty" yaml:"api_path,omitempty"` // APIPath is the API path.
AuthProvider AuthProvider `json:"auth_provider,omitempty" yaml:"auth_provider,omitempty"` // AuthProvider contains the authentication provider details.