diff --git a/.github/config/.terraform.lock.hcl b/.github/config/.terraform.lock.hcl
new file mode 100644
index 0000000..567b5b4
--- /dev/null
+++ b/.github/config/.terraform.lock.hcl
@@ -0,0 +1,25 @@
+# This file is maintained automatically by "terraform init".
+# Manual edits may be lost in future updates.
+
+provider "registry.terraform.io/integrations/github" {
+ version = "6.3.1"
+ constraints = ">= 6.2.0"
+ hashes = [
+ "h1:fMctJXbbaQU4sBAxAayAVa9wDyIIdSBZX8KzFphKFC0=",
+ "zh:25ae1cb97ec528e6b7e9330489f4a33acc0fa80b909c113a8445656bc524c5b9",
+ "zh:3e1f6300dc10e52a54f13352770ed79f25ff4ba9ac49b776c52a655a3488a20b",
+ "zh:4aaf2877ec22e63358d7c9cd48c7d7947d1a1dc4d03231f0af193d8975d5918a",
+ "zh:4b904a81fac12a2a7606c8d811cb9c4e13581adcaaa19e503a067ac95c515925",
+ "zh:54fe7e0dca04e698631a5b86bdd43ef09a31375e68f8f89970b4315cd5fc6312",
+ "zh:6b14f92cf62784eaf20f43ef58ce966735f30d43deeab077943bd410c0d8b8b2",
+ "zh:86c49a1c11c024b26b6750c446f104922a3fe8464d3706a5fb9a4a05c6ca0b0a",
+ "zh:8939fb6332c4a58c4e90245eb9f0110987ccafff06b45a7ed513f2759a2abe6a",
+ "zh:8b4068a78c1f357325d1151facdb1aff506b9cd79d2bab21a55651255a130e2f",
+ "zh:ae22f5e52f534f19811d7f9480b4eb442f12ff16367b3893abb4e449b029ff6b",
+ "zh:afae9cfd9d49002ddfea552aa4844074b9974bd56ff2c2458f2297fe0df56a5b",
+ "zh:bc7a434408eb16a4fbceec0bd86b108a491408b727071402ad572cdb1afa2eb7",
+ "zh:c8e4728ea2d2c6e3d2c1bc5e7d92ed1121c02bab687702ec2748e3a6a0844150",
+ "zh:f6314b2cff0c0a07a216501cda51b35e6a4c66a2418c7c9966ccfe701e01b6b0",
+ "zh:fbd1fee2c9df3aa19cf8851ce134dea6e45ea01cb85695c1726670c285797e25",
+ ]
+}
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..42fee46
--- /dev/null
+++ b/.github/config/README.md
@@ -0,0 +1,91 @@
+# 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_AD
+ keyfactor_password = var.keyfactor_password_AD
+}
+```
+
+### 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_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 |
+
+## 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..89c8f24
--- /dev/null
+++ b/.github/workflows/go_tests.yml
@@ -0,0 +1,49 @@
+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: |
+ cat lib/certs/int-oidc-lab.eastus2.cloudapp.azure.com.crt
+
+ - 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..64a6352
--- /dev/null
+++ b/.github/workflows/keyfactor-starter-workflow.yml
@@ -0,0 +1,20 @@
+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 }}
+ scan_token: ${{ secrets.SAST_TOKEN }}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6fa5ed6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,146 @@
+### 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
+.idea
+*.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/
+
+.env*
+
+*.csv
+/.vs/**/*
+/.vscode/**/*
+.DS_Store
+.auto.tfvars
+
+.gitconfig
+command_config.json
\ 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
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..067dd61
--- /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-go
+NAME=keyfactor-auth-client-go
+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..fbaa48a 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,154 @@
-# keyfactor-auth-client-go
\ No newline at end of file
+# keyfactor-auth-client-go
+
+Client library for authenticating to Keyfactor Command.
+
+## 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 `Basic Authentication` via `Active Directory` is the *ONLY* supported method of `Basic Authentication`.
+
+| 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. 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 | |
+
+### 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",
+ "audience": "https://keyfactor.command.kfdelivery.com",
+ "scopes": [
+ "openid",
+ "profile",
+ "email"
+ ],
+ "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
+ 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
+ client_id: client-id
+ client_secret: client-secret
+ api_path: KeyfactorAPI
+```
\ 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..c2653a4
--- /dev/null
+++ b/auth_providers/auth_core.go
@@ -0,0 +1,756 @@
+// 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) {
+ defaultTimeout := time.Duration(c.HttpClientTimeout) * time.Second
+ output := http.Transport{
+ Proxy: http.ProxyFromEnvironment,
+ TLSClientConfig: &tls.Config{
+ Renegotiation: tls.RenegotiateOnceAsClient,
+ },
+ TLSHandshakeTimeout: defaultTimeout,
+ ResponseHeaderTimeout: defaultTimeout,
+ IdleConnTimeout: defaultTimeout,
+ ExpectContinueTimeout: defaultTimeout,
+ MaxIdleConns: 10,
+ MaxIdleConnsPerHost: 10,
+ MaxConnsPerHost: 10,
+ }
+
+ 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..6997219
--- /dev/null
+++ b/auth_providers/auth_oauth.go
@@ -0,0 +1,451 @@
+package auth_providers
+
+import (
+ "context"
+ "crypto/x509"
+ "fmt"
+ "net/http"
+ "os"
+ "strings"
+ "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"
+)
+
+var (
+ // DefaultScopes is the default scopes for Keyfactor authentication
+ DefaultScopes = []string{"openid"}
+)
+
+// 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 OAuth authentication
+ ClientID string `json:"client_id,omitempty"`
+
+ // ClientSecret is the Client secret for OAuth authentication
+ ClientSecret string `json:"client_secret,omitempty"`
+
+ // Audience is the audience for OAuth authentication
+ Audience string `json:"audience,omitempty"`
+
+ // Scopes is the scopes for OAuth authentication
+ Scopes []string `json:"scopes,omitempty"`
+
+ // 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 OAuth authentication
+ AccessToken string `json:"access_token,omitempty"`
+
+ // 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 OAuth authentication
+ TokenURL string `json:"token_url,omitempty"`
+
+ //// AuthPort
+ //AuthPort string `json:"auth_port,omitempty"`
+
+ //// AuthType is the type of OAuth 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 OAuth authentication.
+func (b *CommandConfigOauth) WithClientId(clientId string) *CommandConfigOauth {
+ b.ClientID = clientId
+ return b
+}
+
+// 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 OAuth authentication.
+func (b *CommandConfigOauth) WithTokenUrl(tokenUrl string) *CommandConfigOauth {
+ b.TokenURL = tokenUrl
+ return b
+}
+
+// WithScopes sets the scopes for OAuth authentication.
+func (b *CommandConfigOauth) WithScopes(scopes []string) *CommandConfigOauth {
+ b.Scopes = scopes
+ return b
+}
+
+// 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 OAuth authentication.
+func (b *CommandConfigOauth) WithCaCertificatePath(caCertificatePath string) *CommandConfigOauth {
+ b.CACertificatePath = caCertificatePath
+ return b
+}
+
+// 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 OAuth 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 b.Audience != "" {
+ config.EndpointParams = map[string][]string{
+ "audience": {b.Audience},
+ }
+ }
+
+ if len(b.Scopes) == 0 {
+ b.Scopes = DefaultScopes
+ }
+
+ 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,
+ )
+ }
+ }
+ }
+ }
+ }
+
+ 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()
+}
+
+// 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,
+ Scopes: b.Scopes,
+ Audience: b.Audience,
+ //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..0b26f9b
--- /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.crt"
+
+ //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..7141e16
--- /dev/null
+++ b/auth_providers/command_config.go
@@ -0,0 +1,374 @@
+// 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.
+ 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.
+ 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
+}
+
+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) (*Config, error) {
+ // Read the file content
+ data, err := os.ReadFile(filePath)
+ if err != nil {
+
+ 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 nil, fmt.Errorf("failed to unmarshal JSON config: %w", err)
+ }
+ } else {
+ if err := yaml.Unmarshal(data, &tempConfig); err != nil {
+ return nil, 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 config, 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..af6c969
--- /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..9c513dd
--- /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.24.0
+ gopkg.in/yaml.v2 v2.4.0
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..b855ce3
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,8 @@
+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.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=
+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..f8a6743
--- /dev/null
+++ b/integration-manifest.json
@@ -0,0 +1,12 @@
+{
+ "$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": "community",
+ "link_github": false,
+ "update_catalog": false,
+ "release_dir": "bin"
+}
+
diff --git a/lib/certs/KFTrainRoot.crt b/lib/certs/KFTrainRoot.crt
new file mode 100644
index 0000000..69619ee
--- /dev/null
+++ b/lib/certs/KFTrainRoot.crt
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDeTCCAmGgAwIBAgIQSVQNM9+tTo9Dd52qg4MI1DANBgkqhkiG9w0BAQsFADBP
+MRMwEQYKCZImiZPyLGQBGRYDbGFiMRkwFwYKCZImiZPyLGQBGRYJa2V5ZmFjdG9y
+MR0wGwYDVQQDExRrZXlmYWN0b3ItS0ZUUkFJTi1DQTAeFw0xOTA1MTAwMzMyMzJa
+Fw0yNDA1MTAwMzQyMzFaME8xEzARBgoJkiaJk/IsZAEZFgNsYWIxGTAXBgoJkiaJ
+k/IsZAEZFglrZXlmYWN0b3IxHTAbBgNVBAMTFGtleWZhY3Rvci1LRlRSQUlOLUNB
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmqN1+RED9SuRsLnIF4AB
+7uFkaismnxhGXc9LWAVBPc8bt8McchMlHmJqVN1DPR0ZT8tVT8jqIODBULrcWZVo
+6ox15BTrFqzrFUiIuuq16NDW+WYu2rljoMBaOTegkmWs7ZoME+w/MHqFFqPBBvg7
+uDSZW/w+1VKyn7aRA2Bywy6o5UHpladsokVKwNhyMQvfJnJQ2xJio8mhXV1AM15F
+Cp8hQZ8dXj/cAPKQxk31M1thIP7M8yx779QbxIs6PKLNxarmY+D73r8Q3t8scO+G
+VQUwSvbDZiF+kzpl/5YTkeD6gLqfQsQr86YiK5nV5xCb2PL8KwnmMCocVImX2fm3
+vQIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E
+FgQUcBUzPW7ZQuqUMP3RFTCbDU1hTGUwEAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZI
+hvcNAQELBQADggEBAIYye4+Gd8piML1BXzkMNgt6aNOu7hS4h3sYfojtpV40OdJ6
+4/Pt9pC5NecMt8B0ikiZvfu9c+xO20VB3uFDGNWVLqfoaZi+cvMAYH9gMrK8KiNe
+21jekbG1uTuIPZ0oJtEDnn7aJ+rXzVTEe6QHZ/gjVcZoPy1/rdCnzMRdH0NS6xpn
+0HqWpy/IxjnJP0Ux6ZPNzrEmhsUGruVJwF8u5+FTlD9pF55eHqI4COtEqJ8YEMb2
+5s8xCCJVL0al+LbydR0neG4Ic/zA0QEwB7ixFsuytaBUOXv4QVpsu7R4mtWQHdSo
+Jz3I+g117tHDlJfGEoQpsc/gHBwMptPQCobpI30=
+-----END CERTIFICATE-----
diff --git a/lib/certs/int-oidc-lab.eastus2.cloudapp.azure.com.crt b/lib/certs/int-oidc-lab.eastus2.cloudapp.azure.com.crt
new file mode 100644
index 0000000..5930217
--- /dev/null
+++ b/lib/certs/int-oidc-lab.eastus2.cloudapp.azure.com.crt
@@ -0,0 +1,51 @@
+-----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-----
+-----BEGIN CERTIFICATE-----
+MIIEUTCCArmgAwIBAgIUJnfJ3rvwfv/qgrBcu8k5xyVDS+AwDQYJKoZIhvcNAQEL
+BQAwMDEXMBUGCgmSJomT8ixkAQEMBzI0Mjc4MzcxFTATBgNVBAMMDE1hbmFnZW1l
+bnRDQTAeFw0yNDEwMTYxNjUxMzdaFw00OTEwMTcxNjUxMzZaMDAxFzAVBgoJkiaJ
+k/IsZAEBDAcyNDI3ODM3MRUwEwYDVQQDDAxNYW5hZ2VtZW50Q0EwggGiMA0GCSqG
+SIb3DQEBAQUAA4IBjwAwggGKAoIBgQCwQGo4Acvd/baGXKfJPrMoQ+xknaM9hPDB
+dszb2HxS3hW9833utlqShWbzvODxoGyl1PG6x0aKw9N1V8YpC2az66qQgLmtbgDE
+o7GxB2YIw5krJU2gCZ7HifCD0IaRvUaL174RZ71B0/ranFURhS/wYh25dYaNYpBI
+91ZZF9oG+brA71RS8QOH2Nnr+Tz2JPL5IFBHQECM4MwjY2AGkXHtIgpciS8qSzwa
+o1u5xx5eP2AL/lpIhsFdYX6XYat+0pgk08X6N0r0NwbjiA4F8pxqUIwSDTnOnb3k
+zj1A3WBJi3pIk5Fp3mqOnheR9+16A5+ZnFdWMcUaPXxAwUJbnvHiCX1xF+iSiVbI
+ohDJz/E2Jk40EEkt6gSqx7vUy7JkePSg2abLlLN/nfuNyGRJsxJZrvSmKu/Wia7b
+y09I0uIwb/dWdVsu52LTrwJfHBwAzUotKw1Al66wGYxJNx9r3bdbehNOlhGOsS9N
+0Lg7h62buyX7n1JdRruAu3iz+OKnnH8CAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB
+/zAfBgNVHSMEGDAWgBTjmGCcAdTvzEJX2zkR6dWKjFMtDDAdBgNVHQ4EFgQU45hg
+nAHU78xCV9s5EenVioxTLQwwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUA
+A4IBgQBnXdEv0+NkFCLdXWgqecK/aNZcRcPwgplYcKxKJ92Bc7fceX2DcHReD8jS
+IdlyUY2vNwev18rva9dtfhd6NFga8yYBrgnAS3AwHEzVrRL9pWSZ/kJwwlyQDcYJ
+PkXvNUKa+rPYUSPArSoQZDXt5rwtIJNzDdoGninTUvxg8b7xHw61fF21exTyzAGZ
+vak8oBtn7HnLal3+ko8rJ7QXfV6vedU1Ft+/lSJGi+/fxxLjE7MmAPzn6Kg2RB/H
+stOCVYnJD+5z0RGD9C41O8KCq/UYwz7d1zjGm5pmFe8P4qYSiG1lHuCr4VVqhWb7
+9oW+H800REIIiy/coomXtUA+uBxfo0swtXT9zOH1MVP2SNfE8NpRVHZpaW8kMPcY
+V8mHRRVoo+gaSlh+RYZ/c9GnUjrdenPVlxSqc0+qJVXqesL3Xim/KiLxuBOwaueI
+2PzNqluRXfbmhvz52+DAhFF+DN3CE272ZmH0KBUSlxzUCZGxWradBz4N24/GJhkU
+ejYP+u4=
+-----END CERTIFICATE-----
diff --git a/lib/certs/int1230-oauth.eastus2.cloudapp.azure.com.crt b/lib/certs/int1230-oauth.eastus2.cloudapp.azure.com.crt
new file mode 100644
index 0000000..79e1675
--- /dev/null
+++ b/lib/certs/int1230-oauth.eastus2.cloudapp.azure.com.crt
@@ -0,0 +1,51 @@
+-----BEGIN CERTIFICATE-----
+MIIEODCCAqCgAwIBAgIUS8GnNJhEBDF6ZPGlSQ7LYEmuWhcwDQYJKoZIhvcNAQEL
+BQAwMDEXMBUGCgmSJomT8ixkAQEMBzkwMzAyNTQxFTATBgNVBAMMDE1hbmFnZW1l
+bnRDQTAeFw0yNDExMDIwNDQxMDNaFw0yNTEyMDQwNDMxNDZaMDMxMTAvBgNVBAMM
+KGludDEyMzAtb2F1dGguZWFzdHVzMi5jbG91ZGFwcC5henVyZS5jb20wggEiMA0G
+CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC0bq+uuhIyx42i0HaueABtHomSdZxk
+378/ju6jeO1EhO+4qWvedI0s3BiybTQ0Mm6rqbvU/vJxTCzqiQD3smYLyzCUH3Xi
+mawNJK3ta2NeB7LeNJLFoC1Bui7GHL2EqRqrtW3/P62LLXDcleJFX6sXyspvP7NR
+c6XExWlTaLBYMDVA9sCCatDhdd/IA+B2jEYOw+oSRSLt00OLJO1qtC0hUnfhcak/
+ZI2kvyzfTpF3eQE5wqFQZ6NW47vbzxDXPglwAECbLPmTCpCvNu1RBZUUK6BU6+QG
+5ZVg5AsdezB9LW9fr559rKrNqloChQ0iQn5fihlKj7FYSRWbwEgyKYMpAgMBAAGj
+gcYwgcMwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBRR5KPmQmOjEO7DLziN78F6
++1YXSjBOBgNVHREERzBFgihpbnQxMjMwLW9hdXRoLmVhc3R1czIuY2xvdWRhcHAu
+YXp1cmUuY29tgg1pbnQxMjMwLW9hdXRohwSsy3lyhwQKCgAEMBMGA1UdJQQMMAoG
+CCsGAQUFBwMBMB0GA1UdDgQWBBTK1NRSOUvvQ9yrU0ksceThH1YbFjAOBgNVHQ8B
+Af8EBAMCBaAwDQYJKoZIhvcNAQELBQADggGBAK6Ai0AFxtetv423Q3rx3/y320nS
+hCT+uqxmGTP+mCVMu6Wy4ESahLip2CY7hm3l0WuyzJn/MohL4IPzSOaKAzYsCrvy
+gGZCoVUSenJQHgXh7GVaYP8PUGkD7xTagxJLTSWBEXm4HqRzfD6gONQprPJzRbBj
+Z0JpY5qXKerfknZRKWeJdNnTeUj94vcFJ1+GamYns+d4zPa12ekyI09aYDH3B4Gv
+fOavWUdIB5v2p4uveXuHNaYD+UHyWs0xGhPvEaQ2RtG1hh0UHPMDlLbe7hhg3Omx
+JIFZVAEXeFGVDHjsi3Ydy3/8HphaITD0mSyKFiLtOppf+tLd+2+T/D07ZlSiEXe2
+Daq5RnwSuufZHlKw2h2QwezaGS44hKLsyX8Bqs5d+MErGf/Pdi+oF36ZHSAhgxRj
+BZIdWBdZgbVOdbTmE4hR/UrCOX4IY6PJo7az1xh1G8CTbJ/884K/wArJu48zdayr
+IpsJ4J5jZVXbKbi3ds7TRTIJofzrxiOr6oY1Yw==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEUTCCArmgAwIBAgIUWHI07m/uguG9f0DTP2z28AOM4iAwDQYJKoZIhvcNAQEL
+BQAwMDEXMBUGCgmSJomT8ixkAQEMBzkwMzAyNTQxFTATBgNVBAMMDE1hbmFnZW1l
+bnRDQTAeFw0yNDExMDIwNDQxMDNaFw00OTExMDMwNDQxMDJaMDAxFzAVBgoJkiaJ
+k/IsZAEBDAc5MDMwMjU0MRUwEwYDVQQDDAxNYW5hZ2VtZW50Q0EwggGiMA0GCSqG
+SIb3DQEBAQUAA4IBjwAwggGKAoIBgQDOvyiw6xjOLbzGoqi3cFF5AonX3lBfvBJn
+N+yI0c18B99v/Nmsx2rGIMUrdQ4AEWmzk+eyytKJZfyBJO/Q5Lk9h3eX8IB+cee4
+qMozAQzeiP/uCpRnjB6ybkr3Yhde55Z/uUUNcLfJ/tTH/iU9zsq98iPfjlTJWAvi
+Dkv//L1mM3z89K06xR3NwGxh39BryABYQagWw9231QhlhIpLARJ/F+lOxa1U/a9s
+hWYu6V5Lc9wySadJnYvL0qEiDo5e8Fs/lDdkhi3ftxdgwReOBnPXeppLludyTg9A
+Ztd7ouhwtYKYTNvuxAwwCRJ1tuUs6SmHRNA192Pro1r/zo8YPj2/g/e/1aJPsfo2
+vtFd90TqNaGithsQXJTdS4N2dNDPn61e2TMLLIylGY+K/4GwLDka66AAw4KpgdVP
+5W3AOMi43tdWIjfA8+7mBl1PCLaHkKcLM3luay+CvcBfLm9XKwWJUXm8AUUJcjdS
+K5CCTJWiPmTBzi96LaVMrBAESoq1TQ8CAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB
+/zAfBgNVHSMEGDAWgBRR5KPmQmOjEO7DLziN78F6+1YXSjAdBgNVHQ4EFgQUUeSj
+5kJjoxDuwy84je/BevtWF0owDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUA
+A4IBgQC95xM4wb/ZT83cMX7vRavwvtHJvjzc//VLwE+HdC9EcjiAxLXoTMnHzRfK
+WbIjnf04URoc4+QKp2Q9ocke5ZFRVzsKMZSA1neoEFkoLu3nAE52G9novm+jtt+5
+GzfW5eLPpvKCxL1oEjjBczu1iURsGDsi4XaeXDeOXXI0J0XhufhhtbMKz9nHb2pK
+roYQAg4xbCi5JrkyWWOan0HvDoY9iA+Dg2sGxrTOBcE40HwpAokqQlocEYaMPycI
+NEB0lRr80k0pthffMGFHtOt6v0sM+OdeNGfS59XmTZMDqaA1mx6swJaxoXaabalG
+x0JExdAuXTgFzexWabV0RMBB/O/g9UNrLvVWW1rDkj1MY3AaUVsqLVXEhwzr02yX
+VCpq9FdrsxrG8sgMNaYNfeTkUyQz60uyy5KVZDgylQWDLqcTvR/+aNpJSI1M0eWc
+v2+yY17dozM+8Nx3zfgrUkB8n9u3B0A3Waj+S6XKPFzea5RWICBvJanpwUMEi8AP
+2BNUiD8=
+-----END CERTIFICATE-----
diff --git a/lib/certs/int1230c-oauth.eastus2.cloudapp.azure.com.crt b/lib/certs/int1230c-oauth.eastus2.cloudapp.azure.com.crt
new file mode 100644
index 0000000..9d2dc4f
--- /dev/null
+++ b/lib/certs/int1230c-oauth.eastus2.cloudapp.azure.com.crt
@@ -0,0 +1,51 @@
+-----BEGIN CERTIFICATE-----
+MIIEOzCCAqOgAwIBAgIUEf12bS1kO4sGR+w0y4h0SfXFONQwDQYJKoZIhvcNAQEL
+BQAwMDEXMBUGCgmSJomT8ixkAQEMBzg5MDkxOTUxFTATBgNVBAMMDE1hbmFnZW1l
+bnRDQTAeFw0yNDExMDIwNDQ1MDhaFw0yNTEyMDQwNDM1NTFaMDQxMjAwBgNVBAMM
+KWludDEyMzBjLW9hdXRoLmVhc3R1czIuY2xvdWRhcHAuYXp1cmUuY29tMIIBIjAN
+BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsTY1vglrvKK/nFNaqDczIhPESej7
+thjcG9286R03+SK3zIADlTbBUQ6W9TQ5xDRdS+iE9vAdYO9Dm2/gerY9Bj3Q7unK
+lM7qkzUTUWDRq55Jmsh7cr1NbIC04I/H1R02qHAmTdHSuDXMU7ruGcZF7AtYGy4T
+Kckh3Io48Mhto2R0TjHoyiyuwmjiHmQDAox4TL0QosI9TLlrZloloOZzZK4A2Flm
+eST7e6EixIwQgXv83H9R765hIi9sJw6h7dpuF/HiF0kySxPvdMOvesuBL+CVG3WB
+KsOswD/lEJHXgVkmZf2eAOnBhd9F3OWHXAIKZRG197Df+NadxSjZKSXjHwIDAQAB
+o4HIMIHFMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUpp+z+NEGpjmfb1JDiZrb
+tnVO1UowUAYDVR0RBEkwR4IpaW50MTIzMGMtb2F1dGguZWFzdHVzMi5jbG91ZGFw
+cC5henVyZS5jb22CDmludDEyMzBjLW9hdXRohwSsrDvIhwQKCgAEMBMGA1UdJQQM
+MAoGCCsGAQUFBwMBMB0GA1UdDgQWBBTq5FoMzQN+eP2Jp1Zo7d528yYgUDAOBgNV
+HQ8BAf8EBAMCBaAwDQYJKoZIhvcNAQELBQADggGBAFV55yxu6oadzyeodwVILZAK
+Wc5HjZsAnHGNMbqUTrYShxml/qoqJkJbb/KUDSMeptEyFsay0XgzTnnp6ewKUzTs
+BSmVtnVLGBrmdNi/N0Ey47VErUGj0HdfBGgjjUcCVDb9Gi7epzDBTyoS/QaygrVx
+0Ihu92jqRfDihM5tdnFVi8ggP9U/rmdir9CZDJDXEtLlNOECGaJmConF1n5RzRap
+uUJN5RSelKkO/4/33MV3UNJKkShIGn3ol6aczK6JvvW2Tv5/Lwk4Wgc6ADrJNa21
+X4YkNUnOTp0bAikRlHY7M+seuvtVlXXOocF7uaIlQXnTXPlGTGgckuLKoRwe8HvM
+nuJ02RQOUdq5OeadEAOykuXw56nLuCScwzgeEAoTEx1qoc9AW1z0XkCLX/ckbcOb
+QfLG7XU7VrHaxcY7uRLFflZ+1qPeqAgxk87QZGuzi4XZgoez28pXsdJQ21QOJeXJ
+3Vyh6mpY9U9LCIF4WsnkUdldXiimXp5/HVFy/wFrlA==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEUTCCArmgAwIBAgIUQ3V7mqJheNaBTRTLylWD5s5eEBQwDQYJKoZIhvcNAQEL
+BQAwMDEXMBUGCgmSJomT8ixkAQEMBzg5MDkxOTUxFTATBgNVBAMMDE1hbmFnZW1l
+bnRDQTAeFw0yNDExMDIwNDQ1MDhaFw00OTExMDMwNDQ1MDdaMDAxFzAVBgoJkiaJ
+k/IsZAEBDAc4OTA5MTk1MRUwEwYDVQQDDAxNYW5hZ2VtZW50Q0EwggGiMA0GCSqG
+SIb3DQEBAQUAA4IBjwAwggGKAoIBgQCwZeoLXyMvzuuKi23kKZH/+UHWKAepqBkE
+5Om7kk8KjDCQgL9Pmtk3Db6IYd/Fhv1HS5VUYhxmpvAbfob0pE8DF9vKLBAia0ej
+Xo7RTMIUT40CzchSSbdNIpfJRyPOP+sxf1Mi8PU52eFmoJIVIuhRY9EdEWA/wRJb
+J6jdT60TBc/SrJpdcCjuJQchI8iUUZbyEcTxN5pxJrZVrfggXJ3Wdf9wBIBXB1WZ
+2JrbO/wRSRcqYwFZrTCemgpa3uQhlaN34oOGZGJe4dcMswjgFL8lU+0pq9Vwc8z5
+f9lQQbomwmtdcE6W7uNioZVxfT4708pftjoxwitXoIG/yzg2uBAwu+0QHgBy6v26
+C2twpH58CY2fPhtnKxiBYBqwkhPhAL4iGeXsXx5Fp3GZhC4TEdewbDKEwhwRNpyT
+zpIItSelO+1tftQDdJ7J7/SS8WzUeheKNPHx5ZcGTrRC9UN9LhjpxuGClH/ZfbK0
+Z5iFbKOM6Hj1h0F9htBY+saNDeX1ghcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB
+/zAfBgNVHSMEGDAWgBSmn7P40QamOZ9vUkOJmtu2dU7VSjAdBgNVHQ4EFgQUpp+z
++NEGpjmfb1JDiZrbtnVO1UowDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUA
+A4IBgQBAvt1RomGa8JzA9G7GNHn4Mgly1dqxxc7c+QrPwsBVp1qdAoOxPQil1t6N
+k4WvGpu9MnqbAZrCjnB4qtgB0gjQ+331jr6KbG1mMcz55Yg09xnPjw2UcHagfKud
+PsVRW1SnDTJ7J6MJS/Kk+u0lYKZ6fy3b9mA2bDP4gz3vHEXGQMA4wCMoLtsYFbuH
+cD0qjxbjpX0k2qUt80+HYRZhcGBAF/o2EgV8A0JpseoCf/Z6kBi1snSDeOzd/v3M
+Zs12/eh1EVOwB8Qom23LsgYELUlipDfxcsfzPAm60CzSkxr7S3mwhU5Ms4QkWD4o
+EtOiopii5RQnLLcHaHzU98R3zcIb57pSFNNcq3Y3Jw4WhWSNzogDjinPI19iX6P7
+Z3SFE2o5CrhMijQ2F37Wvu0/RPZOcyZmmVjMVJi1njfkI1BfyGS14O53cZQxHmFt
+X/UuhLoTKQFxOVmGTlrbMEIAFvT3lm4EB7EDGCRIFJomYiEda/0EyyMBryg0PjVs
+kU77TYo=
+-----END CERTIFICATE-----
diff --git a/lib/certs/kftrain.keyfactor.lab.crt b/lib/certs/kftrain.keyfactor.lab.crt
new file mode 100644
index 0000000..86149a8
--- /dev/null
+++ b/lib/certs/kftrain.keyfactor.lab.crt
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDETCCAfmgAwIBAgIUZh6TfNnZ/oAeC9+595dN44tpqPUwDQYJKoZIhvcNAQEL
+BQAwIDEeMBwGA1UEAwwVa2Z0cmFpbi5rZXlmYWN0b3IubGFiMB4XDTIzMDgxODE4
+MzY1MFoXDTMzMDgxNTE4MzY1MFowIDEeMBwGA1UEAwwVa2Z0cmFpbi5rZXlmYWN0
+b3IubGFiMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA33yBK9Ynyx6J
+F9KXmhtb0wQa+8qifZBEclFEkIoQRanbJtoNO5hQzJMlZpsXu5Yc1oqUscwXFWRK
+47g8t+5LLTNmxwTNTavsVarTE2yYtHwfX1nwla/WY48uQ2vNE8qQhVQaczDVMjkX
+YQmHx2MJ8GGzvxeAaRzoan3tG++eI4p0qxmka44SCFRkuVzrLEleTNCBPhEqNylL
+Ehg4Xco1rXV/SI9yADzXTm3yJswBhtHvUaAaLdJv6tzNqLKuHFE+cFNktmK8X/3A
+lKStxXtG/PtFGnynlmOo9UEgO/UtCo5YYziEn/Fl29k2JNsbfGtwRtqlX2jpA4Ji
++KMdOS6RxQIDAQABo0MwQTAgBgNVHREEGTAXghVrZnRyYWluLmtleWZhY3Rvci5s
+YWIwHQYDVR0OBBYEFL434O9U4f3lal+DcCfGG7jzpIaZMA0GCSqGSIb3DQEBCwUA
+A4IBAQAPF7OZz1r9wN7S1p/ne81+G8sVVfMJ8UvORgc1umtdcCtWUe7hvrmIacCs
+nH5e8M2SdEdHi5i6NIY/2mSOg3eF208NyQ4kdDg8DHljdy+GbZOjJtIaomTzM49n
+KN59tf+bsv+Z0NNTmbnthHQqbVphXiH4QX9zMbjM56QuCOPyaFavMqd528USz/Wg
+DZMca6BzSv7t7ZZwE1wr/2pMpvwQ8GhOMepFAoMrXdsQKSnD4TMml3ZNHzIrJoo+
+o1D4sVmQa7uYbtj0Msxx84dlo8InbEPmV/0qMVIljVikrsoRaDfd6uslXgsu1RhT
+Y4MgkRouFUKc7m8Gfjxe/WsJNN6c
+-----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 100644
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 100644
index 0000000..115bbfd
--- /dev/null
+++ b/tag.sh
@@ -0,0 +1,5 @@
+RC_VERSION=rc.2
+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