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