diff --git a/.github/config/.terraform.lock.hcl b/.github/config/.terraform.lock.hcl new file mode 100644 index 0000000..567b5b4 --- /dev/null +++ b/.github/config/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/integrations/github" { + version = "6.3.1" + constraints = ">= 6.2.0" + hashes = [ + "h1:fMctJXbbaQU4sBAxAayAVa9wDyIIdSBZX8KzFphKFC0=", + "zh:25ae1cb97ec528e6b7e9330489f4a33acc0fa80b909c113a8445656bc524c5b9", + "zh:3e1f6300dc10e52a54f13352770ed79f25ff4ba9ac49b776c52a655a3488a20b", + "zh:4aaf2877ec22e63358d7c9cd48c7d7947d1a1dc4d03231f0af193d8975d5918a", + "zh:4b904a81fac12a2a7606c8d811cb9c4e13581adcaaa19e503a067ac95c515925", + "zh:54fe7e0dca04e698631a5b86bdd43ef09a31375e68f8f89970b4315cd5fc6312", + "zh:6b14f92cf62784eaf20f43ef58ce966735f30d43deeab077943bd410c0d8b8b2", + "zh:86c49a1c11c024b26b6750c446f104922a3fe8464d3706a5fb9a4a05c6ca0b0a", + "zh:8939fb6332c4a58c4e90245eb9f0110987ccafff06b45a7ed513f2759a2abe6a", + "zh:8b4068a78c1f357325d1151facdb1aff506b9cd79d2bab21a55651255a130e2f", + "zh:ae22f5e52f534f19811d7f9480b4eb442f12ff16367b3893abb4e449b029ff6b", + "zh:afae9cfd9d49002ddfea552aa4844074b9974bd56ff2c2458f2297fe0df56a5b", + "zh:bc7a434408eb16a4fbceec0bd86b108a491408b727071402ad572cdb1afa2eb7", + "zh:c8e4728ea2d2c6e3d2c1bc5e7d92ed1121c02bab687702ec2748e3a6a0844150", + "zh:f6314b2cff0c0a07a216501cda51b35e6a4c66a2418c7c9966ccfe701e01b6b0", + "zh:fbd1fee2c9df3aa19cf8851ce134dea6e45ea01cb85695c1726670c285797e25", + ] +} diff --git a/.github/config/MODULE.MD b/.github/config/MODULE.MD new file mode 100644 index 0000000..4a0f4ab --- /dev/null +++ b/.github/config/MODULE.MD @@ -0,0 +1,41 @@ +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0 | +| [github](#requirement\_github) | >=6.2 | + +## Providers + +| Name | Version | +|------|---------| +| [github](#provider\_github) | 6.3.1 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [keyfactor\_github\_test\_environment\_12\_3\_0\_kc](#module\_keyfactor\_github\_test\_environment\_12\_3\_0\_kc) | git::ssh://git@github.com/Keyfactor/terraform-module-keyfactor-github-test-environment-ad.git | main | +| [keyfactor\_github\_test\_environment\_ad\_10\_5\_0](#module\_keyfactor\_github\_test\_environment\_ad\_10\_5\_0) | git::ssh://git@github.com/Keyfactor/terraform-module-keyfactor-github-test-environment-ad.git | main | + +## Resources + +| Name | Type | +|------|------| +| [github_repository.repo](https://registry.terraform.io/providers/integrations/github/latest/docs/data-sources/repository) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [keyfactor\_auth\_token\_url\_12\_3\_0\_KC](#input\_keyfactor\_auth\_token\_url\_12\_3\_0\_KC) | The hostname of the KeyCloak instance to authenticate to for a Keyfactor Command access token | `string` | `"https://int-oidc-lab.eastus2.cloudapp.azure.com:8444/realms/Keyfactor/protocol/openid-connect/token"` | no | +| [keyfactor\_client\_id\_12\_3\_0](#input\_keyfactor\_client\_id\_12\_3\_0) | The client ID to authenticate with the Keyfactor instance using Keycloak client credentials | `string` | n/a | yes | +| [keyfactor\_client\_secret\_12\_3\_0](#input\_keyfactor\_client\_secret\_12\_3\_0) | The client secret to authenticate with the Keyfactor instance using Keycloak client credentials | `string` | n/a | yes | +| [keyfactor\_hostname\_10\_5\_0](#input\_keyfactor\_hostname\_10\_5\_0) | The hostname of the Keyfactor instance | `string` | `"integrations1050-lab.kfdelivery.com"` | no | +| [keyfactor\_hostname\_12\_3\_0\_KC](#input\_keyfactor\_hostname\_12\_3\_0\_KC) | The hostname of the Keyfactor instance | `string` | `"int-oidc-lab.eastus2.cloudapp.azure.com"` | no | +| [keyfactor\_password\_10\_5\_0](#input\_keyfactor\_password\_10\_5\_0) | The password to authenticate with the Keyfactor instance | `string` | n/a | yes | +| [keyfactor\_username\_10\_5\_0](#input\_keyfactor\_username\_10\_5\_0) | The username to authenticate with the Keyfactor instance | `string` | n/a | yes | + +## Outputs + +No outputs. diff --git a/.github/config/Makefile b/.github/config/Makefile new file mode 100644 index 0000000..f67d9df --- /dev/null +++ b/.github/config/Makefile @@ -0,0 +1,26 @@ +.DEFAULT_GOAL := help + +##@ Utility +help: ## Display this help + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +deps: ## Install deps for macos + @brew install pre-commit tflint terraform terraform-docs + +docs: ## Run terraform-docs to update module docs. + @terraform-docs markdown . > MODULE.MD + @terraform-docs markdown table --output-file README.md --output-mode inject . + +lint: ## Run tflint + @tflint + +validate: ## Run terraform validate + @terraform init --upgrade + @terraform validate + +precommit/add: ## Install pre-commit hook + @pre-commit install + +precommit/remove: ## Uninstall pre-commit hook + @pre-commit uninstall + diff --git a/.github/config/README.md b/.github/config/README.md new file mode 100644 index 0000000..42fee46 --- /dev/null +++ b/.github/config/README.md @@ -0,0 +1,91 @@ +# GitHub Test Environment Setup + +This code sets up GitHub environments for testing against Keyfactor Command instances that are configured to use +Active Directory or Keycloak for authentication. + +## Requirements + +1. Terraform >= 1.0 +2. GitHub Provider >= 6.2 +3. Keyfactor Command instance(s) configured to use Active Directory or Keycloak for authentication +4. AD or Keycloak credentials for authenticating to the Keyfactor Command instance(s) +5. A GitHub token with access and permissions to the repository where the environments will be created + +## Adding a new environment + +Modify the `environments.tf` file to include the new environment module. The module should be named appropriately. +Example: + +### Active Directory Environment + +```hcl +module "keyfactor_github_test_environment_ad_10_5_0" { + source = "git::ssh://git@github.com/Keyfactor/terraform-module-keyfactor-github-test-environment-ad.git?ref=main" + + gh_environment_name = "KFC_10_5_0" # Keyfactor Command 10.5.0 environment using Active Directory(/Basic Auth) + gh_repo_name = data.github_repository.repo.name + keyfactor_hostname = var.keyfactor_hostname_10_5_0 + keyfactor_username = var.keyfactor_username_AD + keyfactor_password = var.keyfactor_password_AD +} +``` + +### oAuth Client Environment + +```hcl +module "keyfactor_github_test_environment_12_3_0_kc" { + source = "git::ssh://git@github.com/Keyfactor/terraform-module-keyfactor-github-test-environment-kc.git?ref=main" + + gh_environment_name = "KFC_12_3_0_KC" # Keyfactor Command 12.3.0 environment using Keycloak + gh_repo_name = data.github_repository.repo.name + keyfactor_hostname = var.keyfactor_hostname_12_3_0_OAUTH + keyfactor_auth_token_url = var.keyfactor_auth_token_url + keyfactor_client_id = var.keyfactor_client_id + keyfactor_client_secret = var.keyfactor_client_secret + keyfactor_tls_skip_verify = true +} +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0 | +| [github](#requirement\_github) | >=6.2 | + +## Providers + +| Name | Version | +|------|---------| +| [github](#provider\_github) | 6.3.1 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [keyfactor\_github\_test\_environment\_12\_3\_0\_kc](#module\_keyfactor\_github\_test\_environment\_12\_3\_0\_kc) | git::ssh://git@github.com/Keyfactor/terraform-module-keyfactor-github-test-environment-ad.git | main | +| [keyfactor\_github\_test\_environment\_ad\_10\_5\_0](#module\_keyfactor\_github\_test\_environment\_ad\_10\_5\_0) | git::ssh://git@github.com/Keyfactor/terraform-module-keyfactor-github-test-environment-ad.git | main | + +## Resources + +| Name | Type | +|------|------| +| [github_repository.repo](https://registry.terraform.io/providers/integrations/github/latest/docs/data-sources/repository) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [keyfactor\_auth\_token\_url\_12\_3\_0\_KC](#input\_keyfactor\_auth\_token\_url\_12\_3\_0\_KC) | The hostname of the KeyCloak instance to authenticate to for a Keyfactor Command access token | `string` | `"https://int-oidc-lab.eastus2.cloudapp.azure.com:8444/realms/Keyfactor/protocol/openid-connect/token"` | no | +| [keyfactor\_client\_id\_12\_3\_0](#input\_keyfactor\_client\_id\_12\_3\_0) | The client ID to authenticate with the Keyfactor instance using Keycloak client credentials | `string` | n/a | yes | +| [keyfactor\_client\_secret\_12\_3\_0](#input\_keyfactor\_client\_secret\_12\_3\_0) | The client secret to authenticate with the Keyfactor instance using Keycloak client credentials | `string` | n/a | yes | +| [keyfactor\_hostname\_10\_5\_0](#input\_keyfactor\_hostname\_10\_5\_0) | The hostname of the Keyfactor instance | `string` | `"integrations1050-lab.kfdelivery.com"` | no | +| [keyfactor\_hostname\_12\_3\_0\_KC](#input\_keyfactor\_hostname\_12\_3\_0\_KC) | The hostname of the Keyfactor instance | `string` | `"int-oidc-lab.eastus2.cloudapp.azure.com"` | no | +| [keyfactor\_password\_10\_5\_0](#input\_keyfactor\_password\_10\_5\_0) | The password to authenticate with the Keyfactor instance | `string` | n/a | yes | +| [keyfactor\_username\_10\_5\_0](#input\_keyfactor\_username\_10\_5\_0) | The username to authenticate with the Keyfactor instance | `string` | n/a | yes | + +## Outputs + +No outputs. + \ No newline at end of file diff --git a/.github/config/environments.tf b/.github/config/environments.tf new file mode 100644 index 0000000..d26fab7 --- /dev/null +++ b/.github/config/environments.tf @@ -0,0 +1,35 @@ +module "keyfactor_github_test_environment_ad_10_5_0" { + source = "git::ssh://git@github.com/Keyfactor/terraform-module-keyfactor-github-test-environment-ad.git?ref=main" + + gh_environment_name = "KFC_10_5_0" + gh_repo_name = data.github_repository.repo.name + keyfactor_hostname = var.keyfactor_hostname_10_5_0 + keyfactor_username = var.keyfactor_username_10_5_0 + keyfactor_password = var.keyfactor_password_10_5_0 + keyfactor_config_file = base64encode(file("${path.module}/command_config.json")) +} + +# module "keyfactor_github_test_environment_11_5_0_kc" { +# source = "git::ssh://git@github.com/Keyfactor/terraform-module-keyfactor-github-test-environment-kc.git?ref=main" +# +# gh_environment_name = "KFC_11_5_0_KC" +# gh_repo_name = data.github_repository.repo.name +# keyfactor_hostname = var.keyfactor_hostname_11_5_0_KC +# keyfactor_client_id = var.keyfactor_client_id_11_5_0 +# keyfactor_client_secret = var.keyfactor_client_secret_11_5_0 +# keyfactor_auth_hostname = var.keyfactor_auth_hostname_11_5_0_KC +# keyfactor_tls_skip_verify = true +# } + +module "keyfactor_github_test_environment_12_3_0_kc" { + source = "git::ssh://git@github.com/Keyfactor/terraform-module-keyfactor-github-test-environment-ad.git?ref=main" + + gh_environment_name = "KFC_12_3_0_KC" + gh_repo_name = data.github_repository.repo.name + keyfactor_hostname = var.keyfactor_hostname_12_3_0_KC + keyfactor_auth_token_url = var.keyfactor_auth_token_url_12_3_0_KC + keyfactor_client_id = var.keyfactor_client_id_12_3_0 + keyfactor_client_secret = var.keyfactor_client_secret_12_3_0 + keyfactor_tls_skip_verify = true + keyfactor_config_file = base64encode(file("${path.module}/command_config.json")) +} diff --git a/.github/config/providers.tf b/.github/config/providers.tf new file mode 100644 index 0000000..313bfcd --- /dev/null +++ b/.github/config/providers.tf @@ -0,0 +1,20 @@ +terraform { + required_version = ">= 1.0" + required_providers { + github = { + source = "integrations/github" + version = ">=6.2" + } + } + backend "azurerm" { + resource_group_name = "integrations-infra" + storage_account_name = "integrationstfstate" + container_name = "tfstate" + key = "github/repos/keyfactor-auth-client-go.tfstate" + } +} + +provider "github" { + # Configuration options + owner = "Keyfactor" +} \ No newline at end of file diff --git a/.github/config/repo.tf b/.github/config/repo.tf new file mode 100644 index 0000000..51e9c49 --- /dev/null +++ b/.github/config/repo.tf @@ -0,0 +1,3 @@ +data "github_repository" "repo" { + name = "keyfactor-auth-client-go" +} \ No newline at end of file diff --git a/.github/config/variables.tf b/.github/config/variables.tf new file mode 100644 index 0000000..5353f86 --- /dev/null +++ b/.github/config/variables.tf @@ -0,0 +1,38 @@ +variable "keyfactor_hostname_10_5_0" { + description = "The hostname of the Keyfactor instance" + type = string + default = "integrations1050-lab.kfdelivery.com" +} + +variable "keyfactor_username_10_5_0" { + description = "The username to authenticate with the Keyfactor instance" + type = string +} + +variable "keyfactor_password_10_5_0" { + description = "The password to authenticate with the Keyfactor instance" + type = string +} + +variable "keyfactor_client_id_12_3_0" { + description = "The client ID to authenticate with the Keyfactor instance using Keycloak client credentials" + type = string +} + +variable "keyfactor_client_secret_12_3_0" { + description = "The client secret to authenticate with the Keyfactor instance using Keycloak client credentials" + type = string +} + +variable "keyfactor_hostname_12_3_0_KC" { + description = "The hostname of the Keyfactor instance" + type = string + default = "int-oidc-lab.eastus2.cloudapp.azure.com" +} + +variable "keyfactor_auth_token_url_12_3_0_KC" { + description = "The hostname of the KeyCloak instance to authenticate to for a Keyfactor Command access token" + type = string + default = "https://int-oidc-lab.eastus2.cloudapp.azure.com:8444/realms/Keyfactor/protocol/openid-connect/token" +} + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..fa3ed22 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +# See GitHub's documentation for more information on this file: +# https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "daily" \ No newline at end of file diff --git a/.github/workflows/go_tests.yml b/.github/workflows/go_tests.yml new file mode 100644 index 0000000..89c8f24 --- /dev/null +++ b/.github/workflows/go_tests.yml @@ -0,0 +1,49 @@ +name: Go Test Workflow + +on: + push: + workflow_dispatch: + +jobs: + test: + name: Run tests + runs-on: kf-auth-client-runner-set + strategy: + matrix: + environment: [ "KFC_10_5_0", "KFC_12_3_0_KC"] + environment: ${{ matrix.environment }} + steps: + - name: Check out code + uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: 1.22 + + - name: Get Public IP + run: curl -s https://api.ipify.org + + - name: Validate lab cert is present + run: | + cat lib/certs/int-oidc-lab.eastus2.cloudapp.azure.com.crt + + - name: Run tests + run: | + if [ -n "${{ secrets.KEYFACTOR_AUTH_CONFIG_B64 }}" ]; then + mkdir -p ~/.keyfactor + echo "${{ secrets.KEYFACTOR_AUTH_CONFIG_B64 }}" | base64 --decode > ~/.keyfactor/command_config.json + fi + go test -v -cover ./auth_providers/... + env: + KEYFACTOR_PASSWORD: ${{ secrets.KEYFACTOR_PASSWORD }} + KEYFACTOR_USERNAME: ${{ secrets.KEYFACTOR_USERNAME }} + KEYFACTOR_AUTH_CONFIG_B64: ${{ secrets.KEYFACTOR_AUTH_CONFIG_B64 }} + KEYFACTOR_AUTH_CLIENT_ID: ${{ secrets.KEYFACTOR_AUTH_CLIENT_ID }} + KEYFACTOR_AUTH_CLIENT_SECRET: ${{ secrets.KEYFACTOR_AUTH_CLIENT_SECRET }} + KEYFACTOR_AUTH_TOKEN_URL: ${{ vars.KEYFACTOR_AUTH_TOKEN_URL }} + KEYFACTOR_HOSTNAME: ${{ vars.KEYFACTOR_HOSTNAME }} + KEYFACTOR_AUTH_HOSTNAME: ${{ vars.KEYFACTOR_AUTH_HOSTNAME }} + KEYFACTOR_SKIP_VERIFY: ${{ vars.KEYFACTOR_SKIP_VERIFY }} + TEST_KEYFACTOR_AD_AUTH: ${{ vars.TEST_KEYFACTOR_AD_AUTH }} + TEST_KEYFACTOR_KC_AUTH: ${{ vars.TEST_KEYFACTOR_KC_AUTH }} \ No newline at end of file diff --git a/.github/workflows/keyfactor-starter-workflow.yml b/.github/workflows/keyfactor-starter-workflow.yml new file mode 100644 index 0000000..64a6352 --- /dev/null +++ b/.github/workflows/keyfactor-starter-workflow.yml @@ -0,0 +1,20 @@ +name: Keyfactor Bootstrap Workflow + +on: + workflow_dispatch: + pull_request: + types: [ opened, closed, synchronize, edited, reopened ] + push: + create: + branches: + - 'release-*.*' + +jobs: + call-starter-workflow: + uses: keyfactor/actions/.github/workflows/starter.yml@v3 + secrets: + token: ${{ secrets.V2BUILDTOKEN}} + APPROVE_README_PUSH: ${{ secrets.APPROVE_README_PUSH}} + gpg_key: ${{ secrets.KF_GPG_PRIVATE_KEY }} + gpg_pass: ${{ secrets.KF_GPG_PASSPHRASE }} + scan_token: ${{ secrets.SAST_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6fa5ed6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,146 @@ +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Terraform template +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which are likely to contain sensitive data, such as +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject +# to change depending on the environment. +*.tfvars +*.tfvars.json + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Include override files you do wish to add to version control using negated pattern +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc + +### Go template +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +.idea +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +vendor/ + +.env* + +*.csv +/.vs/**/* +/.vscode/**/* +.DS_Store +.auto.tfvars + +.gitconfig +command_config.json \ No newline at end of file diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..45ff554 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,86 @@ +# Visit https://goreleaser.com for documentation on how to customize this +# behavior. +before: + hooks: + # this is just an example and not a requirement for provider building/publishing + - go mod tidy +builds: + - env: + # goreleaser does not work with CGO, it could also complicate + # usage by users in CI/CD systems like Terraform Cloud where + # they are unable to install libraries. + - CGO_ENABLED=0 + mod_timestamp: '{{ .CommitTimestamp }}' + flags: + - -trimpath + ldflags: + - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}' + goos: + - freebsd + - windows + - linux + - darwin + goarch: + - amd64 + - '386' + - arm + - arm64 + ignore: + - goos: darwin + goarch: '386' + binary: 'keyfactor-auth-client' +archives: + - format: zip + name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}' +checksum: + extra_files: + - glob: 'integration-manifest.json' + name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' + name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS' + algorithm: sha256 +signs: + - artifacts: checksum + args: + # if you are using this in a GitHub action or some other automated pipeline, you + # need to pass the batch flag to indicate its not interactive. + - "--batch" + - "--local-user" + - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key + - "--output" + - "${signature}" + - "--detach-sign" + - "${artifact}" +release: + prerelease: auto + extra_files: + - glob: 'integration-manifest.json' + name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' + # If you want to manually examine the release before its live, uncomment this line: + #draft: true +changelog: + sort: asc + use: github + filters: + exclude: + - '^test:' + - '^chore' + - 'merge conflict' + - Merge pull request + - Merge remote-tracking branch + - Merge branch + - go mod tidy + groups: + - title: Dependency updates + regexp: "^.*(feat|fix)\\(deps\\)*:+.*$" + order: 300 + - title: 'New Features' + regexp: "^.*feat[(\\w)]*:+.*$" + order: 100 + - title: 'Bug fixes' + regexp: "^.*fix[(\\w)]*:+.*$" + order: 200 + - title: 'Documentation updates' + regexp: "^.*docs[(\\w)]*:+.*$" + order: 400 + - title: Other work + order: 9999 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b9a23ed --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +# v1.0.0 + +## Features +- Support for `BasicAuth` client config +- Support for `OAuth2` client config +- Support for Keyfactor client config file with `BasicAuth` and `OAuth2` client config(s) \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..067dd61 --- /dev/null +++ b/Makefile @@ -0,0 +1,80 @@ +PROVIDER_DIR := $(PWD) +TEST?=$$(go list ./... | grep -v 'vendor') +HOSTNAME=keyfactor.com +GOFMT_FILES := $$(find $(PROVIDER_DIR) -name '*.go' |grep -v vendor) +NAMESPACE=keyfactor +WEBSITE_REPO=https://github.com/Keyfactor/keyfactor-auth-client-go +NAME=keyfactor-auth-client-go +BINARY=${NAME} +VERSION := $(GITHUB_REF_NAME) +ifeq ($(VERSION),) + VERSION := v1.0.0 +endif +OS_ARCH := $(shell go env GOOS)_$(shell go env GOARCH) +BASEDIR := ${HOME}/go/bin +INSTALLDIR := ${BASEDIR} +MARKDOWN_FILE := README.md +TEMP_TOC_FILE := temp_toc.md + + + +default: build + +build: fmt + go install + +release: + GOOS=darwin GOARCH=amd64 go build -o ./bin/${BINARY}_${VERSION}_darwin_amd64 -ldflags "-X 'keyfactor_auth_client/pkg.Version=${VERSION}' -X 'keyfactor_auth_client/pkg.BuildTime=$$(date)' -X 'keyfactor_auth_client/pkg.CommitHash=$$(git rev-parse HEAD)'" + GOOS=freebsd GOARCH=386 go build -o ./bin/${BINARY}_${VERSION}_freebsd_386 -ldflags "-X 'keyfactor_auth_client/pkg.Version=${VERSION}' -X 'keyfactor_auth_client/pkg.BuildTime=$$(date)' -X 'keyfactor_auth_client/pkg.CommitHash=$$(git rev-parse HEAD)'" + GOOS=freebsd GOARCH=amd64 go build -o ./bin/${BINARY}_${VERSION}_freebsd_amd64 -ldflags "-X 'keyfactor_auth_client/pkg.Version=${VERSION}' -X 'keyfactor_auth_client/pkg.BuildTime=$(date)' -X 'keyfactor_auth_client/pkg.CommitHash=$(git rev-parse HEAD)'" + GOOS=freebsd GOARCH=arm go build -o ./bin/${BINARY}_${VERSION}_freebsd_arm -ldflags "-X 'keyfactor_auth_client/pkg.Version=${VERSION}' -X 'keyfactor_auth_client/pkg.BuildTime=$$(date)' -X 'keyfactor_auth_client/pkg.CommitHash=$$(git rev-parse HEAD)'" + GOOS=linux GOARCH=386 go build -o ./bin/${BINARY}_${VERSION}_linux_386 -ldflags "-X 'keyfactor_auth_client/pkg.Version=${VERSION}' -X 'keyfactor_auth_client/pkg.BuildTime=$$(date)' -X 'keyfactor_auth_client/pkg.CommitHash=$$(git rev-parse HEAD)'" + GOOS=linux GOARCH=amd64 go build -o ./bin/${BINARY}_${VERSION}_linux_amd64 -ldflags "-X 'keyfactor_auth_client/pkg.Version=${VERSION}' -X 'keyfactor_auth_client/pkg.BuildTime=$$(date)' -X 'keyfactor_auth_client/pkg.CommitHash=$$(git rev-parse HEAD)'" + GOOS=linux GOARCH=arm go build -o ./bin/${BINARY}_${VERSION}_linux_arm -ldflags "-X 'keyfactor_auth_client/pkg.Version=${VERSION}' -X 'keyfactor_auth_client/pkg.BuildTime=$$(date)' -X 'keyfactor_auth_client/pkg.CommitHash=$$(git rev-parse HEAD)'" + GOOS=openbsd GOARCH=386 go build -o ./bin/${BINARY}_${VERSION}_openbsd_386 -ldflags "-X 'keyfactor_auth_client/pkg.Version=${VERSION}' -X 'keyfactor_auth_client/pkg.BuildTime=$$(date)' -X 'keyfactor_auth_client/pkg.CommitHash=$$(git rev-parse HEAD)'" + GOOS=openbsd GOARCH=amd64 go build -o ./bin/${BINARY}_${VERSION}_openbsd_amd64 -ldflags "-X 'keyfactor_auth_client/pkg.Version=${VERSION}' -X 'keyfactor_auth_client/pkg.BuildTime=$$(date)' -X 'keyfactor_auth_client/pkg.CommitHash=$$(git rev-parse HEAD)'" + GOOS=solaris GOARCH=amd64 go build -o ./bin/${BINARY}_${VERSION}_solaris_amd64 -ldflags "-X 'keyfactor_auth_client/pkg.Version=${VERSION}' -X 'keyfactor_auth_client/pkg.BuildTime=$$(date)' -X 'keyfactor_auth_client/pkg.CommitHash=$$(git rev-parse HEAD)'" + GOOS=windows GOARCH=386 go build -o ./bin/${BINARY}_${VERSION}_windows_386 -ldflags "-X 'keyfactor_auth_client/pkg.Version=${VERSION}' -X 'keyfactor_auth_client/pkg.BuildTime=$$(date)' -X 'keyfactor_auth_client/pkg.CommitHash=$$(git rev-parse HEAD)'" + GOOS=windows GOARCH=amd64 go build -o ./bin/${BINARY}_${VERSION}_windows_amd64 -ldflags "-X 'keyfactor_auth_client/pkg.Version=${VERSION}' -X 'keyfactor_auth_client/pkg.BuildTime=$$(date)' -X 'keyfactor_auth_client/pkg.CommitHash=$$(git rev-parse HEAD)'" + +install: fmt + go build -o ${BINARY} + rm -rf ${INSTALLDIR}/${BINARY} + mkdir -p ${INSTALLDIR} + chmod oug+x ${BINARY} + cp ${BINARY} ${INSTALLDIR} + mkdir -p ${HOME}/.local/bin || true + mv ${BINARY} ${HOME}/.local/bin/${BINARY} + +vendor: + go mod vendor + +version: + @echo ${VERSION} + +setversion: + sed -i '' -e 's/VERSION = ".*"/VERSION = "$(VERSION)"/' pkg/version/version.go + +test: + go test -i $(TEST) || exit 1 + echo $(TEST) | xargs -t -n4 go test $(TESTARGS) -timeout=30s -parallel=4 + +fmt: + gofmt -w $(GOFMT_FILES) + +prerelease: fmt setversion + git tag -d $(VERSION) || true + git push origin :$(VERSION) || true + git tag $(VERSION) + git push origin $(VERSION) + +check_toc: + @grep -q 'TOC_START' $(MARKDOWN_FILE) && echo "TOC already exists." || (echo "TOC not found. Generating..." && $(MAKE) generate_toc) + +generate_toc: + # check if markdown-toc is installed and if not install it + @command -v markdown-toc >/dev/null 2>&1 || (echo "markdown-toc is not installed. Installing..." && npm install -g markdown-toc) + markdown-toc -i $(MARKDOWN_FILE) --skip 'Table of Contents' + + +.PHONY: build prerelease release install test fmt vendor version setversion \ No newline at end of file diff --git a/README.md b/README.md index 8e27d92..fbaa48a 100644 --- a/README.md +++ b/README.md @@ -1 +1,154 @@ -# keyfactor-auth-client-go \ No newline at end of file +# keyfactor-auth-client-go + +Client library for authenticating to Keyfactor Command. + +## Environment Variables + +### Global + +| Name | Description | Default | +|-------------------------------|-----------------------------------------------------------------------------------------------------------------|----------------------------------------| +| KEYFACTOR_HOSTNAME | Keyfactor Command hostname without protocol and port | | +| KEYFACTOR_PORT | Keyfactor Command port | `443` | +| KEYFACTOR_API_PATH | Keyfactor Command API Path | `KeyfactorAPI` | +| KEYFACTOR_SKIP_VERIFY | Skip TLS verification when connecting to Keyfactor Command | `false` | +| KEYFACTOR_CA_CERT | Either a file path or PEM encoded string to a CA certificate to trust when communicating with Keyfactor Command | | +| KEYFACTOR_CLIENT_TIMEOUT | Timeout for HTTP client requests to Keyfactor Command | `60s` | +| KEYFACTOR_AUTH_CONFIG_FILE | Path to a JSON file containing the authentication configuration | `$HOME/.keyfactor/command_config.json` | +| KEYFACTOR_AUTH_CONFIG_PROFILE | Profile to use from the authentication configuration file | `default` | + +### Basic Auth + +Currently `Basic Authentication` via `Active Directory` is the *ONLY* supported method of `Basic Authentication`. + +| Name | Description | Default | +|--------------------|---------------------------------------------------------------------------------------------|---------| +| KEYFACTOR_USERNAME | Active Directory username to authenticate to Keyfactor Command API | | +| KEYFACTOR_PASSWORD | Password associated with Active Directory username to authenticate to Keyfactor Command API | | +| KEYFACTOR_DOMAIN | Active Directory domain of user. Can be implied from username if it contains `@` or `\\` | | + +### oAuth Client Credentials + +| Name | Description | Default | +|------------------------------|---------------------------------------------------------------------------------------------------------------------------------|----------| +| KEYFACTOR_AUTH_CLIENT_ID | Keyfactor Auth Client ID | | +| KEYFACTOR_AUTH_CLIENT_SECRET | Keyfactor Auth Client Secret | | +| KEYFACTOR_AUTH_TOKEN_URL | URL to request an access token from Keyfactor Auth | | +| KEYFACTOR_AUTH_SCOPES | Scopes to request when authenticating to Keyfactor Command API. Each scope MUST be separated by `,` | `openid` | +| KEYFACTOR_AUTH_AUDIENCE | Audience to request when authenticating to Keyfactor Command API | | +| KEYFACTOR_AUTH_ACCESS_TOKEN | Access token to use to authenticate to Keyfactor Command API. This can be supplied directly or generated via client credentials | | +| KEYFACTOR_AUTH_CA_CERT | Either a file path or PEM encoded string to a CA certificate to use when connecting to Keyfactor Auth | | + +### Test Environment Variables + +These environment variables are used to run go tests. They are not used in the actual client library. + +| Name | Description | Default | +|------------------------|-------------------------------------------------------|---------| +| TEST_KEYFACTOR_AD_AUTH | Set to `true` to test Active Directory authentication | false | +| TEST_KEYFACTOR_KC_AUTH | Set to `true` to test Keycloak authentication | false | + +## Configuration File + +A JSON or YAML file can be used to store authentication configuration. A configuration file can contain references to +multiple Keyfactor Command environments and can be referenced by a `profile` name. The `default` profile will be used +when no profile is specified. Keyfactor tools will look for a config file located at +`$HOME/.keyfactor/command_config.json` +by default. The config file should be structured as follows: + +### Basic Auth + +#### JSON + +```json +{ + "servers": { + "default": { + "host": "keyfactor.command.kfdelivery.com", + "username": "keyfactor", + "password": "password", + "domain": "command", + "api_path": "KeyfactorAPI" + }, + "server2": { + "host": "keyfactor2.command.kfdelivery.com", + "username": "keyfactor2", + "password": "password2", + "domain": "command", + "api_path": "Keyfactor/API" + } + } +} +``` + +#### YAML + +```yaml +servers: + default: + host: keyfactor.command.kfdelivery.com + username: keyfactor + password: password + domain: command + api_path: KeyfactorAPI + server2: + host: keyfactor2.command.kfdelivery.com + username: keyfactor2 + password: password2 + domain: command + api_path: Keyfactor/API +``` + +### oAuth Client Credentials + +#### JSON + +```json +{ + "servers": { + "default": { + "host": "keyfactor.command.kfdelivery.com", + "token_url": "https://idp.keyfactor.command.kfdelivery.com/oauth2/token", + "client_id": "client-id", + "client_secret": "client-secret", + "audience": "https://keyfactor.command.kfdelivery.com", + "scopes": [ + "openid", + "profile", + "email" + ], + "api_path": "KeyfactorAPI" + }, + "server2": { + "host": "keyfactor.command.kfdelivery.com", + "token_url": "https://idp.keyfactor.command.kfdelivery.com/oauth2/token", + "client_id": "client-id", + "client_secret": "client-secret", + "api_path": "KeyfactorAPI" + } + } +} +``` + +#### YAML + +```yaml +servers: + default: + host: keyfactor.command.kfdelivery.com + token_url: https://idp.keyfactor.command.kfdelivery.com/oauth2/token + client_id: client-id + client_secret: client-secret + api_path: KeyfactorAPI + audience: https://keyfactor.command.kfdelivery.com + scopes: + - openid + - profile + - email + server2: + host: keyfactor.command.kfdelivery.com + token_url: https://idp.keyfactor.command.kfdelivery.com/oauth2/token + client_id: client-id + client_secret: client-secret + api_path: KeyfactorAPI +``` \ No newline at end of file diff --git a/auth_providers/auth_basic.go b/auth_providers/auth_basic.go new file mode 100644 index 0000000..eb3f58a --- /dev/null +++ b/auth_providers/auth_basic.go @@ -0,0 +1,288 @@ +// Copyright 2024 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package auth_providers + +import ( + "encoding/base64" + "fmt" + "net/http" + "os" + "strings" +) + +const ( + // EnvKeyfactorUsername is the environment variable for the Keyfactor hostname + EnvKeyfactorUsername = "KEYFACTOR_USERNAME" + + // EnvKeyfactorPassword is the environment variable for the Keyfactor password + EnvKeyfactorPassword = "KEYFACTOR_PASSWORD" + + // EnvKeyfactorDomain is the environment variable for the Keyfactor domain + EnvKeyfactorDomain = "KEYFACTOR_DOMAIN" +) + +// Basic Authenticator +var _ Authenticator = &BasicAuthAuthenticator{} + +// BasicAuthAuthenticator is an Authenticator that uses Basic Auth for authentication. +type BasicAuthAuthenticator struct { + Client *http.Client +} + +// GetHttpClient returns the http client +func (b *BasicAuthAuthenticator) GetHttpClient() (*http.Client, error) { + return b.Client, nil +} + +// CommandAuthConfigBasic represents the base configuration needed for authentication to Keyfactor Command API. +type CommandAuthConfigBasic struct { + // CommandAuthConfig is a reference to the base configuration needed for authentication to Keyfactor Command API + CommandAuthConfig + + // Username is the username to be used for authentication to Keyfactor Command API + Username string `json:"username,omitempty"` + + // Password is the password to be used for authentication to Keyfactor Command API + Password string `json:"password,omitempty"` + + // Domain is the domain of the Active Directory used to authenticate to Keyfactor Command API + Domain string `json:"domain,omitempty"` +} + +// NewBasicAuthAuthenticatorBuilder creates a new instance of CommandAuthConfigBasic +func NewBasicAuthAuthenticatorBuilder() *CommandAuthConfigBasic { + return &CommandAuthConfigBasic{} +} + +// WithUsername sets the username for authentication +func (a *CommandAuthConfigBasic) WithUsername(username string) *CommandAuthConfigBasic { + a.Username = username + return a +} + +// WithPassword sets the password for authentication +func (a *CommandAuthConfigBasic) WithPassword(password string) *CommandAuthConfigBasic { + a.Password = password + return a +} + +// WithDomain sets the domain for authentication +func (a *CommandAuthConfigBasic) WithDomain(domain string) *CommandAuthConfigBasic { + a.Domain = domain + return a +} + +// GetHttpClient returns the http client +func (a *CommandAuthConfigBasic) GetHttpClient() (*http.Client, error) { + //validate the configuration + cErr := a.ValidateAuthConfig() + if cErr != nil { + return nil, cErr + } + + // Encode the username and password in Base64 + var auth string + if a.Domain != "" { + auth = a.Domain + "\\" + a.Username + ":" + a.Password + } else { + auth = a.Username + ":" + a.Password + } + encodedAuth := base64.StdEncoding.EncodeToString([]byte(auth)) + + // Create a custom RoundTripper + transport, tErr := a.CommandAuthConfig.BuildTransport() + if tErr != nil { + return nil, tErr + } + + return &http.Client{ + Transport: roundTripperFunc( + func(req *http.Request) (*http.Response, error) { + // Add the Authorization header to the request + req.Header.Set("Authorization", "Basic "+encodedAuth) + + // Forward the request to the actual transport + return transport.RoundTrip(req) + }, + ), + }, nil +} + +// Build creates a new instance of BasicAuthAuthenticator +func (a *CommandAuthConfigBasic) Build() (Authenticator, error) { + + client, cErr := a.GetHttpClient() + if cErr != nil { + return nil, cErr + } + a.HttpClient = client + + return &BasicAuthAuthenticator{Client: client}, nil +} + +// ValidateAuthConfig validates the basic authentication configuration. +func (a *CommandAuthConfigBasic) ValidateAuthConfig() error { + silentLoad := true + if a.CommandAuthConfig.ConfigProfile != "" { + silentLoad = false + } else if a.CommandAuthConfig.ConfigFilePath != "" { + silentLoad = false + } + serverConfig, cErr := a.CommandAuthConfig.LoadConfig( + a.CommandAuthConfig.ConfigProfile, + a.CommandAuthConfig.ConfigFilePath, + silentLoad, + ) + if !silentLoad && cErr != nil { + return cErr + } + + if a.Username == "" { + if username, ok := os.LookupEnv(EnvKeyfactorUsername); ok { + a.Username = username + } else { + if serverConfig != nil && serverConfig.Username != "" { + a.Username = serverConfig.Username + } else { + return fmt.Errorf("username or environment variable %s is required", EnvKeyfactorUsername) + } + } + } + if a.Password == "" { + if password, ok := os.LookupEnv(EnvKeyfactorPassword); ok { + a.Password = password + } else { + if serverConfig != nil && serverConfig.Password != "" { + a.Password = serverConfig.Password + } else { + return fmt.Errorf("password or environment variable %s is required", EnvKeyfactorPassword) + } + } + } + + domainErr := a.parseUsernameDomain() + if domainErr != nil { + return domainErr + + } + + if a.Domain == "" { + if domain, ok := os.LookupEnv(EnvKeyfactorDomain); ok { + a.Domain = domain + } + } + + return a.CommandAuthConfig.ValidateAuthConfig() +} + +// Authenticate authenticates the request using basic authentication. +func (a *CommandAuthConfigBasic) Authenticate() error { + cErr := a.ValidateAuthConfig() + if cErr != nil { + return cErr + } + + // create oauth Client + authy, err := NewBasicAuthAuthenticatorBuilder(). + WithUsername(a.Username). + WithPassword(a.Password). + WithDomain(a.Domain). + Build() + + if err != nil { + return err + } + + if authy != nil { + bClient, berr := authy.GetHttpClient() + if berr != nil { + return berr + } + a.SetClient(bClient) + } + + return a.CommandAuthConfig.Authenticate() +} + +// parseUsernameDomain parses the username to extract the domain if it's included in the username. +// It supports two formats: "username@domain" and "domain\username". +func (a *CommandAuthConfigBasic) parseUsernameDomain() error { + domainErr := fmt.Errorf("domain or environment variable %s is required", EnvKeyfactorDomain) + if strings.Contains(a.Username, "@") { + dSplit := strings.Split(a.Username, "@") + if len(dSplit) != 2 { + return domainErr + } + a.Username = dSplit[0] // remove domain from username + a.Domain = dSplit[1] + } else if strings.Contains(a.Username, "\\") { + dSplit := strings.Split(a.Username, "\\") + if len(dSplit) != 2 { + return domainErr + } + a.Domain = dSplit[0] + a.Username = dSplit[1] // remove domain from username + } + + return nil +} + +// GetServerConfig returns the server configuration +func (a *CommandAuthConfigBasic) GetServerConfig() *Server { + server := Server{ + Host: a.CommandHostName, + Port: a.CommandPort, + Username: a.Username, + Password: a.Password, + Domain: a.Domain, + ClientID: "", + ClientSecret: "", + OAuthTokenUrl: "", + APIPath: a.CommandAPIPath, + //AuthProvider: AuthProvider{}, + SkipTLSVerify: a.SkipVerify, + CACertPath: a.CommandCACert, + AuthType: "basic", + } + return &server +} + +// Example usage of CommandAuthConfigBasic +// +// This example demonstrates how to use CommandAuthConfigBasic to authenticate a user. +// +// func ExampleCommandAuthConfigBasic_Authenticate() { +// authConfig := &CommandAuthConfigBasic{ +// CommandAuthConfig: CommandAuthConfig{ +// ConfigFilePath: "/path/to/config.json", +// ConfigProfile: "default", +// CommandHostName: "exampleHost", +// CommandPort: 443, +// CommandAPIPath: "/api/v1", +// CommandCACert: "/path/to/ca-cert.pem", +// SkipVerify: true, +// }, +// Username: "exampleUser", +// Password: "examplePassword", +// Domain: "exampleDomain", +// } +// +// err := authConfig.Authenticate() +// if err != nil { +// fmt.Println("Authentication failed:", err) +// } else { +// fmt.Println("Authentication successful") +// } +// } diff --git a/auth_providers/auth_basic_test.go b/auth_providers/auth_basic_test.go new file mode 100644 index 0000000..5c404ae --- /dev/null +++ b/auth_providers/auth_basic_test.go @@ -0,0 +1,308 @@ +// Copyright 2024 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package auth_providers_test + +import ( + "fmt" + "net/http" + "os" + "strings" + "testing" + + "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers" +) + +func TestBasicAuthAuthenticator_GetHttpClient(t *testing.T) { + // Skip test if TEST_KEYFACTOR_KC_AUTH is set to 1 or true + if os.Getenv("TEST_KEYFACTOR_KC_AUTH") == "1" || os.Getenv("TEST_KEYFACTOR_KC_AUTH") == "true" { + t.Skip("Skipping TestBasicAuthAuthenticator_GetHttpClient") + return + } + + auth := &auth_providers.BasicAuthAuthenticator{ + Client: &http.Client{}, + } + + client, err := auth.GetHttpClient() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if client == nil { + t.Fatalf("expected a non-nil http.Client") + } +} + +func TestCommandAuthConfigBasic_ValidateAuthConfig(t *testing.T) { + // Skip test if TEST_KEYFACTOR_KC_AUTH is set to 1 or true + if os.Getenv("TEST_KEYFACTOR_KC_AUTH") == "1" || os.Getenv("TEST_KEYFACTOR_KC_AUTH") == "true" { + t.Skip("Skipping TestBasicAuthAuthenticator_GetHttpClient") + return + } + config := &auth_providers.CommandAuthConfigBasic{ + Username: os.Getenv(auth_providers.EnvKeyfactorUsername), + Password: os.Getenv(auth_providers.EnvKeyfactorPassword), + } + + err := config.ValidateAuthConfig() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } +} + +func TestCommandAuthConfigBasic_GetHttpClient(t *testing.T) { + // Skip test if TEST_KEYFACTOR_KC_AUTH is set to 1 or true + if os.Getenv("TEST_KEYFACTOR_KC_AUTH") == "1" || os.Getenv("TEST_KEYFACTOR_KC_AUTH") == "true" { + t.Skip("Skipping TestBasicAuthAuthenticator_GetHttpClient") + return + } + + config := &auth_providers.CommandAuthConfigBasic{ + Username: os.Getenv(auth_providers.EnvKeyfactorUsername), + Password: os.Getenv(auth_providers.EnvKeyfactorPassword), + } + + client, err := config.GetHttpClient() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if client == nil { + t.Fatalf("expected a non-nil http.Client") + } +} + +func TestCommandAuthConfigBasic_Authenticate(t *testing.T) { + // Skip test if TEST_KEYFACTOR_KC_AUTH is set to 1 or true + if os.Getenv("TEST_KEYFACTOR_KC_AUTH") == "1" || os.Getenv("TEST_KEYFACTOR_KC_AUTH") == "true" { + t.Skip("Skipping TestBasicAuthAuthenticator_GetHttpClient") + return + } + + userHome, hErr := os.UserHomeDir() + if hErr != nil { + userHome = os.Getenv("HOME") + } + + configFilePath := fmt.Sprintf("%s/%s", userHome, auth_providers.DefaultConfigFilePath) + configFromFile, cErr := auth_providers.ReadConfigFromJSON(configFilePath) + if cErr != nil { + t.Errorf("unable to load auth config from file %s: %v", configFilePath, cErr) + } + + if configFromFile == nil || configFromFile.Servers == nil { + t.Errorf("invalid config file %s", configFilePath) + t.FailNow() + } + + // Delete the config file + t.Logf("Deleting config file: %s", configFilePath) + os.Remove(configFilePath) + defer func() { + // Write the config file back + t.Logf("Writing config file: %s", configFilePath) + fErr := auth_providers.WriteConfigToJSON(configFilePath, configFromFile) + if fErr != nil { + t.Errorf("unable to write auth config to file %s: %v", configFilePath, fErr) + } + }() + + t.Log("Testing Basic Auth with Environmental variables") + noParamsConfig := &auth_providers.CommandAuthConfigBasic{} + authBasicTest(t, "with complete Environmental variables", false, noParamsConfig) + + t.Log("Testing Basic Auth with invalid config file path") + invFilePath := &auth_providers.CommandAuthConfigBasic{} + invFilePath.WithConfigFile("invalid-file-path") + invalidPathExpectedError := []string{"no such file or directory", "invalid-file-path"} + authBasicTest(t, "with invalid config file PATH", true, invFilePath, invalidPathExpectedError...) + + // Environment variables are not set + t.Log("Unsetting environment variables") + username, password, domain := exportBasicEnvVariables() + unsetBasicEnvVariables() + defer func() { + t.Log("Resetting environment variables") + setBasicEnvVariables(username, password, domain) + }() + + t.Log("Testing Basic Auth with no Environmental variables") + incompleteEnvConfig := &auth_providers.CommandAuthConfigBasic{} + incompleteEnvConfigExpectedError := "username or environment variable KEYFACTOR_USERNAME is required" + authBasicTest( + t, + "with incomplete Environmental variables", + true, + incompleteEnvConfig, + incompleteEnvConfigExpectedError, + ) + + t.Log("Testing auth with only username") + usernameOnlyConfig := &auth_providers.CommandAuthConfigBasic{ + Username: "test-username", + } + usernameOnlyConfigExceptedError := "password or environment variable KEYFACTOR_PASSWORD is required" + authBasicTest(t, "username only", true, usernameOnlyConfig, usernameOnlyConfigExceptedError) + + t.Log("Testing auth with w/ full params variables") + fullParamsConfig := &auth_providers.CommandAuthConfigBasic{ + Username: username, + Password: password, + Domain: domain, + } + authBasicTest(t, "w/ full params variables", false, fullParamsConfig) + + t.Log("Testing auth with w/ full params variables") + fullParamsinvalidPassConfig := &auth_providers.CommandAuthConfigBasic{ + Username: username, + Password: "invalid-password", + Domain: domain, + } + invalidCredsExpectedError := []string{"401", "Unauthorized", "Access is denied due to invalid credentials"} + authBasicTest(t, "w/ full params & invalid pass", true, fullParamsinvalidPassConfig, invalidCredsExpectedError...) + + t.Log("Testing auth with w/ no domain") + noDomainConfig := &auth_providers.CommandAuthConfigBasic{ + Username: username, + Password: password, + } + authBasicTest(t, "w/ no domain", false, noDomainConfig) + + t.Log("Testing auth with w/ no domain and no domain in username") + usernameNoDomain := strings.Split(username, "@")[0] + t.Logf("Username without domain: %s", usernameNoDomain) + usernameNoDomainConfig := &auth_providers.CommandAuthConfigBasic{ + Username: usernameNoDomain, + Password: password, + } + //TODO: This really SHOULD fail, but it doesn't and the auth header is sent without the domain yet it still authenticates + authBasicTest(t, "w/o domain and no domain in username", false, usernameNoDomainConfig) + + // Write the config file back + t.Logf("Writing config file: %s", configFilePath) + fErr := auth_providers.WriteConfigToJSON(configFilePath, configFromFile) + if fErr != nil { + t.Errorf("unable to write auth config to file %s: %v", configFilePath, fErr) + } + + t.Log("Testing Basic Auth with valid implicit config file") + wConfigFile := &auth_providers.CommandAuthConfigBasic{} + authBasicTest(t, "with valid implicit config file", false, wConfigFile) + + t.Log("Testing Basic Auth with invalid profile implicit config file") + invProfile := &auth_providers.CommandAuthConfigBasic{} + invProfile.WithConfigProfile("invalid-profile") + expectedError := []string{"profile", "invalid-profile", "not found"} + authBasicTest(t, "with invalid profile implicit config file", true, invProfile, expectedError...) + + t.Log("Testing Basic Auth with invalid creds implicit config file") + invProfileCreds := &auth_providers.CommandAuthConfigBasic{} + invProfileCreds.WithConfigProfile("invalid_username") + authBasicTest(t, "with invalid creds implicit config file", true, invProfileCreds, invalidCredsExpectedError...) + + t.Log("Testing Basic Auth with invalid Command host implicit config file") + invHostConfig := &auth_providers.CommandAuthConfigBasic{} + invHostConfig.WithConfigProfile("invalid_host") + invHostExpectedError := []string{"no such host"} + authBasicTest( + t, "with invalid Command host implicit config file", true, invHostConfig, + invHostExpectedError..., + ) + + //t.Log("Testing Basic Auth with invalid config file path") + //invFilePath := &auth_providers.CommandAuthConfigBasic{} + //invFilePath.WithConfigFile("invalid-file-path") + //invalidPathExpectedError := []string{"no such file or directory", "invalid-file-path"} + //authBasicTest(t, "with invalid config file PATH", true, invFilePath, invalidPathExpectedError...) + +} + +func TestCommandAuthConfigBasic_Build(t *testing.T) { + // Skip test if TEST_KEYFACTOR_KC_AUTH is set to 1 or true + if os.Getenv("TEST_KEYFACTOR_KC_AUTH") == "1" || os.Getenv("TEST_KEYFACTOR_KC_AUTH") == "true" { + t.Skip("Skipping TestBasicAuthAuthenticator_GetHttpClient") + return + } + config := &auth_providers.CommandAuthConfigBasic{ + Username: os.Getenv(auth_providers.EnvKeyfactorUsername), + Password: os.Getenv(auth_providers.EnvKeyfactorPassword), + } + + authenticator, err := config.Build() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if authenticator == nil { + t.Fatalf("expected a non-nil Authenticator") + } +} + +// setBasicEnvVariables sets the basic environment variables +func setBasicEnvVariables(username, password, domain string) { + os.Setenv(auth_providers.EnvKeyfactorUsername, username) + os.Setenv(auth_providers.EnvKeyfactorPassword, password) + os.Setenv(auth_providers.EnvKeyfactorDomain, domain) +} + +// exportBasicEnvVariables sets the basic environment variables +func exportBasicEnvVariables() (string, string, string) { + username := os.Getenv(auth_providers.EnvKeyfactorUsername) + password := os.Getenv(auth_providers.EnvKeyfactorPassword) + domain := os.Getenv(auth_providers.EnvKeyfactorDomain) + return username, password, domain +} + +// unsetBasicEnvVariables unsets the basic environment variables +func unsetBasicEnvVariables() { + os.Unsetenv(auth_providers.EnvKeyfactorUsername) + os.Unsetenv(auth_providers.EnvKeyfactorPassword) + os.Unsetenv(auth_providers.EnvKeyfactorDomain) +} + +func authBasicTest( + t *testing.T, testName string, allowFail bool, config *auth_providers.CommandAuthConfigBasic, + errorContains ...string, +) { + t.Run( + fmt.Sprintf("Basic Auth Test %s", testName), func(t *testing.T) { + + err := config.Authenticate() + if allowFail { + if err == nil { + t.Errorf("Basic auth test '%s' should have failed", testName) + t.FailNow() + return + } + if len(errorContains) > 0 { + for _, ec := range errorContains { + if !strings.Contains(err.Error(), ec) { + t.Errorf("Basic auth test '%s' failed with unexpected error %v", testName, err) + t.FailNow() + return + } + } + } + t.Logf("Basic auth test '%s' failed as expected with %v", testName, err) + return + } + if err != nil { + t.Errorf("Basic auth test '%s' failed with %v", testName, err) + t.FailNow() + return + } + }, + ) +} diff --git a/auth_providers/auth_core.go b/auth_providers/auth_core.go new file mode 100644 index 0000000..c2653a4 --- /dev/null +++ b/auth_providers/auth_core.go @@ -0,0 +1,756 @@ +// Copyright 2024 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package auth_providers + +import ( + "crypto/tls" + "crypto/x509" + "encoding/json" + "encoding/pem" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + "time" +) + +const ( + // DefaultCommandPort is the default port for Keyfactor Command API + DefaultCommandPort = 443 + + // DefaultCommandAPIPath is the default path for Keyfactor Command API + DefaultCommandAPIPath = "KeyfactorAPI" + + // DefaultAPIVersion is the default version for Keyfactor Command API + DefaultAPIVersion = "1" + + // DefaultAPIClientName is the default client name for Keyfactor Command API + DefaultAPIClientName = "APIClient" + + // DefaultProductVersion is the default product version for Keyfactor Command API + DefaultProductVersion = "10.5.0.0" + + // DefaultConfigFilePath is the default path for the configuration file + DefaultConfigFilePath = ".keyfactor/command_config.json" + + // DefaultConfigProfile is the default profile for the configuration file + DefaultConfigProfile = "default" + + // DefaultClientTimeout is the default timeout for the http Client + DefaultClientTimeout = 60 + + // EnvKeyfactorHostName is the environment variable for the Keyfactor Command hostname + EnvKeyfactorHostName = "KEYFACTOR_HOSTNAME" + + // EnvKeyfactorPort is the environment variable for the Keyfactor Command http(s) port + EnvKeyfactorPort = "KEYFACTOR_PORT" + + // EnvKeyfactorAPIPath is the environment variable for the Keyfactor Command API path + EnvKeyfactorAPIPath = "KEYFACTOR_API_PATH" + + // EnvKeyfactorSkipVerify is the environment variable for skipping TLS verification when communicating with Keyfactor Command + EnvKeyfactorSkipVerify = "KEYFACTOR_SKIP_VERIFY" + + // EnvKeyfactorCACert is the environment variable for the CA certificate to be used for TLS verification when communicating with Keyfactor Command API + EnvKeyfactorCACert = "KEYFACTOR_CA_CERT" + + // EnvKeyfactorAuthProvider is the environment variable for the authentication provider to be used for Keyfactor Command API + EnvKeyfactorAuthProvider = "KEYFACTOR_AUTH_PROVIDER" + + // EnvKeyfactorAuthProfile is the environment variable for the profile of the configuration file + EnvKeyfactorAuthProfile = "KEYFACTOR_AUTH_CONFIG_PROFILE" + + // EnvKeyfactorConfigFile is the environment variable for the configuration file to reference for connecting to the Keyfactor Command API + EnvKeyfactorConfigFile = "KEYFACTOR_AUTH_CONFIG_FILE" + + // EnvKeyfactorClientTimeout is the environment variable for the timeout for the http Client + EnvKeyfactorClientTimeout = "KEYFACTOR_CLIENT_TIMEOUT" +) + +// Authenticator is an interface for authentication to Keyfactor Command API. +type Authenticator interface { + GetHttpClient() (*http.Client, error) +} + +// roundTripperFunc is a helper type to create a custom RoundTripper +type roundTripperFunc func(req *http.Request) (*http.Response, error) + +// RoundTrip executes a single HTTP transaction +func (f roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return f(req) +} + +// CommandAuthConfig represents the base configuration needed for authentication to Keyfactor Command API. +type CommandAuthConfig struct { + // ConfigType is the type of configuration + ConfigType string `json:"config_type"` + + //ConfigProfile is the profile of the configuration + ConfigProfile string + + //ConfigFilePath is the path to the configuration file + ConfigFilePath string + + // FileConfig + FileConfig *Server + + // AuthHeader is the header to be used for authentication to Keyfactor Command API + AuthHeader string `json:"auth_header"` + + // CommandHostName is the hostname of the Keyfactor Command API + CommandHostName string `json:"host"` + + // CommandPort is the port of the Keyfactor Command API + CommandPort int `json:"port"` + + // CommandAPIPath is the path of the Keyfactor Command API, default is "KeyfactorAPI" + CommandAPIPath string `json:"api_path"` + + // CommandAPIVersion is the version of the Keyfactor Command API, default is "1" + CommandVersion string `json:"command_version"` + + // CommandCACert is the CA certificate to be used for authentication to Keyfactor Command API for use with not widely trusted certificates. This can be a filepath or a string of the certificate in PEM format. + CommandCACert string `json:"command_ca_cert"` + + // SkipVerify is a flag to skip verification of the server's certificate chain and host name. Default is false. + SkipVerify bool `json:"skip_verify"` + + // HttpClientTimeout is the timeout for the http Client + HttpClientTimeout int `json:"client_timeout"` + + // UserAgent is the user agent to be used for authentication to Keyfactor Command API + UserAgent string `json:"user_agent,omitempty"` + + // Debug + Debug bool `json:"debug,omitempty"` + + // HttpClient is the http Client to be used for authentication to Keyfactor Command API + HttpClient *http.Client + //DefaultHttpClient *http.Client +} + +// cleanHostName cleans the hostname for authentication to Keyfactor Command API. +func cleanHostName(hostName string) string { + // check if hostname is a URL and if so, extract the hostname + if strings.Contains(hostName, "://") { + hostName = strings.Split(hostName, "://")[1] + //remove any trailing paths + hostName = strings.Split(hostName, "/")[0] + // remove any trailing slashes + hostName = strings.TrimRight(hostName, "/") + } + return hostName +} + +// WithCommandHostName sets the hostname for authentication to Keyfactor Command API. +func (c *CommandAuthConfig) WithCommandHostName(hostName string) *CommandAuthConfig { + hostName = cleanHostName(hostName) + c.CommandHostName = hostName + return c +} + +// WithCommandPort sets the port for authentication to Keyfactor Command API. +func (c *CommandAuthConfig) WithCommandPort(port int) *CommandAuthConfig { + c.CommandPort = port + return c +} + +// WithCommandAPIPath sets the API path for authentication to Keyfactor Command API. +func (c *CommandAuthConfig) WithCommandAPIPath(apiPath string) *CommandAuthConfig { + c.CommandAPIPath = apiPath + return c +} + +// WithCommandCACert sets the CA certificate for authentication to Keyfactor Command API. +func (c *CommandAuthConfig) WithCommandCACert(caCert string) *CommandAuthConfig { + c.CommandCACert = caCert + return c +} + +// WithSkipVerify sets the flag to skip verification of the server's certificate chain and host name. +func (c *CommandAuthConfig) WithSkipVerify(skipVerify bool) *CommandAuthConfig { + c.SkipVerify = skipVerify + return c +} + +// WithHttpClient sets the http Client for authentication to Keyfactor Command API. +func (c *CommandAuthConfig) WithHttpClient(client *http.Client) *CommandAuthConfig { + c.HttpClient = client + return c +} + +// WithConfigFile sets the configuration file for authentication to Keyfactor Command API. +func (c *CommandAuthConfig) WithConfigFile(configFilePath string) *CommandAuthConfig { + + if c.ConfigProfile == "" { + // check if profile is set in environment + if profile, ok := os.LookupEnv(EnvKeyfactorAuthProfile); ok { + c.ConfigProfile = profile + } else { + c.ConfigProfile = DefaultConfigProfile + } + } + + c.ConfigFilePath = configFilePath + return c +} + +// WithConfigProfile sets the configuration profile for authentication to Keyfactor Command API. +func (c *CommandAuthConfig) WithConfigProfile(profile string) *CommandAuthConfig { + if profile == "" { + // check if profile is set in environment + if p, ok := os.LookupEnv(EnvKeyfactorAuthProfile); ok { + c.ConfigProfile = p + } else { + c.ConfigProfile = DefaultConfigProfile + } + } else { + c.ConfigProfile = profile + } + return c +} + +// WithClientTimeout sets the timeout for the http Client. +func (c *CommandAuthConfig) WithClientTimeout(timeout int) *CommandAuthConfig { + c.HttpClientTimeout = timeout + return c +} + +// ValidateAuthConfig validates the authentication configuration for Keyfactor Command API. +func (c *CommandAuthConfig) ValidateAuthConfig() error { + if c.CommandHostName == "" { + if hostName, ok := os.LookupEnv(EnvKeyfactorHostName); ok { + c.CommandHostName = cleanHostName(hostName) + } else { + if c.FileConfig != nil && c.FileConfig.Host != "" { + c.CommandHostName = cleanHostName(c.FileConfig.Host) + } else { + return fmt.Errorf("command_host_name or environment variable %s is required", EnvKeyfactorHostName) + } + } + } + if c.CommandPort <= 0 { + if port, ok := os.LookupEnv(EnvKeyfactorPort); ok { + configPort, pErr := strconv.Atoi(port) + if pErr == nil { + c.CommandPort = configPort + } + } else { + c.CommandPort = DefaultCommandPort + } + } + if c.CommandAPIPath == "" { + if apiPath, ok := os.LookupEnv(EnvKeyfactorAPIPath); ok { + c.CommandAPIPath = apiPath + } else { + c.CommandAPIPath = DefaultCommandAPIPath + } + } + if c.HttpClientTimeout <= 0 { + if timeout, ok := os.LookupEnv(EnvKeyfactorClientTimeout); ok { + configTimeout, tErr := strconv.Atoi(timeout) + if tErr == nil { + c.HttpClientTimeout = configTimeout + } + } else { + c.HttpClientTimeout = DefaultClientTimeout + } + } + + if c.CommandCACert == "" { + // check if CommandCACert is set in environment + if caCert, ok := os.LookupEnv(EnvKeyfactorCACert); ok { + c.CommandCACert = caCert + } + } + + // check for skip verify in environment + if skipVerify, ok := os.LookupEnv(EnvKeyfactorSkipVerify); ok { + c.SkipVerify = skipVerify == "true" || skipVerify == "1" + } + return nil +} + +// BuildTransport creates a custom http Transport for authentication to Keyfactor Command API. +func (c *CommandAuthConfig) BuildTransport() (*http.Transport, error) { + defaultTimeout := time.Duration(c.HttpClientTimeout) * time.Second + output := http.Transport{ + Proxy: http.ProxyFromEnvironment, + TLSClientConfig: &tls.Config{ + Renegotiation: tls.RenegotiateOnceAsClient, + }, + TLSHandshakeTimeout: defaultTimeout, + ResponseHeaderTimeout: defaultTimeout, + IdleConnTimeout: defaultTimeout, + ExpectContinueTimeout: defaultTimeout, + MaxIdleConns: 10, + MaxIdleConnsPerHost: 10, + MaxConnsPerHost: 10, + } + + if c.SkipVerify { + output.TLSClientConfig.InsecureSkipVerify = true + } + + if c.CommandCACert != "" { + if _, err := os.Stat(c.CommandCACert); err == nil { + cert, ioErr := os.ReadFile(c.CommandCACert) + if ioErr != nil { + return &output, ioErr + } + // check if output.TLSClientConfig.RootCAs is nil + if output.TLSClientConfig.RootCAs == nil { + output.TLSClientConfig.RootCAs = x509.NewCertPool() + } + // Append your custom cert to the pool + if ok := output.TLSClientConfig.RootCAs.AppendCertsFromPEM(cert); !ok { + return &output, fmt.Errorf("failed to append custom CA cert to pool") + } + } else { + // Append your custom cert to the pool + if ok := output.TLSClientConfig.RootCAs.AppendCertsFromPEM([]byte(c.CommandCACert)); !ok { + return &output, fmt.Errorf("failed to append custom CA cert to pool") + } + } + } + + return &output, nil +} + +// SetClient sets the http Client for authentication to Keyfactor Command API. +func (c *CommandAuthConfig) SetClient(client *http.Client) *http.Client { + if client != nil { + c.HttpClient = client + } + if c.HttpClient == nil { + //// Copy the default transport and apply the custom TLS config + //defaultTransport := http.DefaultTransport.(*http.Transport).Clone() + ////defaultTransport.TLSClientConfig = tlsConfig + //c.HttpClient = &http.Client{Transport: defaultTransport} + defaultTimeout := time.Duration(c.HttpClientTimeout) * time.Second + c.HttpClient = &http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + TLSClientConfig: &tls.Config{ + Renegotiation: tls.RenegotiateOnceAsClient, + }, + TLSHandshakeTimeout: defaultTimeout, + DisableKeepAlives: false, + DisableCompression: false, + MaxIdleConns: 10, + MaxIdleConnsPerHost: 10, + MaxConnsPerHost: 10, + IdleConnTimeout: defaultTimeout, + ResponseHeaderTimeout: defaultTimeout, + ExpectContinueTimeout: defaultTimeout, + MaxResponseHeaderBytes: 0, + WriteBufferSize: 0, + ReadBufferSize: 0, + ForceAttemptHTTP2: false, + }, + } + } + + return c.HttpClient +} + +// updateCACerts updates the CA certs for the http Client. +func (c *CommandAuthConfig) updateCACerts() error { + // check if CommandCACert is set + if c.CommandCACert == "" { + // check if CommandCACert is set in environment + if caCert, ok := os.LookupEnv(EnvKeyfactorCACert); ok { + c.CommandCACert = caCert + } else { + // nothing to do + return nil + } + } + + // ensure Client is set + c.SetClient(nil) + + // Load the system certs + rootCAs, pErr := x509.SystemCertPool() + if pErr != nil { + return pErr + } + if rootCAs == nil { + rootCAs = x509.NewCertPool() + } + + // check if CommandCACert is a file + if _, err := os.Stat(c.CommandCACert); err == nil { + cert, ioErr := os.ReadFile(c.CommandCACert) + if ioErr != nil { + return ioErr + } + // Append your custom cert to the pool + if ok := rootCAs.AppendCertsFromPEM(cert); !ok { + return fmt.Errorf("failed to append custom CA cert to pool") + } + } else { + // Append your custom cert to the pool + if ok := rootCAs.AppendCertsFromPEM([]byte(c.CommandCACert)); !ok { + return fmt.Errorf("failed to append custom CA cert to pool") + } + } + + //check if c already has a transport and if it does, update the RootCAs else create a new transport + if c.HttpClient.Transport != nil { + if transport, ok := c.HttpClient.Transport.(*http.Transport); ok { + transport.TLSClientConfig.RootCAs = rootCAs + } else { + c.HttpClient.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: rootCAs, + }, + } + } + } else { + c.HttpClient.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: rootCAs, + }, + } + } + + // Trust the augmented cert pool in our Client + //c.HttpClient.Transport = &http.Transport{ + // TLSClientConfig: &tls.Config{ + // RootCAs: rootCAs, + // }, + //} + + return nil +} + +// Authenticate performs the authentication test to Keyfactor Command API and sets Command product version. +func (c *CommandAuthConfig) Authenticate() error { + + if c.HttpClient == nil { + c.SetClient(nil) + } + //create headers for request + headers := map[string]string{ + "Content-Type": "application/json", + "Accept": "application/json", + "x-keyfactor-api-version": DefaultAPIVersion, + "x-keyfactor-requested-with": DefaultAPIClientName, + } + + if c.AuthHeader != "" { + headers["Authorization"] = c.AuthHeader + } + + endPoint := fmt.Sprintf( + "https://%s/%s/Status/Endpoints", + c.CommandHostName, + //c.CommandPort, + c.CommandAPIPath, + ) + + // create request object + req, rErr := http.NewRequest("GET", endPoint, nil) + if rErr != nil { + return rErr + } + + // Set headers from the map + for key, value := range headers { + req.Header.Set(key, value) + } + + c.HttpClient.Timeout = time.Duration(c.HttpClientTimeout) * time.Second + cResp, cErr := c.HttpClient.Do(req) + if cErr != nil { + return cErr + } else if cResp == nil { + return fmt.Errorf("failed to authenticate, no response received from Keyfactor Command") + } + + defer cResp.Body.Close() + + // check if body is empty + if cResp.Body == nil { + return fmt.Errorf("failed to authenticate, empty response body received from Keyfactor Command") + } + + cRespBody, ioErr := io.ReadAll(cResp.Body) + if ioErr != nil { + return ioErr + } + + if cResp.StatusCode != 200 { + //convert body to string + return fmt.Errorf( + "failed to authenticate, received status code %d from Keyfactor Command: %s", + cResp.StatusCode, + string(cRespBody), + ) + } + + productVersion := cResp.Header.Get("x-keyfactor-product-version") + if productVersion != "" { + c.CommandVersion = productVersion + } else { + c.CommandVersion = DefaultProductVersion + } + + //decode response to json + var response []string + if err := json.Unmarshal(cRespBody, &response); err != nil { + return err + } + + return nil + +} + +// LoadCACertificates loads the custom CA certificates from a file. +func LoadCACertificates(certFile string) (*x509.CertPool, error) { + // Read the file containing the custom CA certificate + certBytes, err := os.ReadFile(certFile) + if err != nil { + return nil, err + } + + // Create a new CertPool and append the custom CA certificate + certPool := x509.NewCertPool() + if ok := certPool.AppendCertsFromPEM(certBytes); !ok { + return nil, err + } + + return certPool, nil +} + +// FindCACertificate reads the CA certificate from a file and returns a slice of x509.Certificate. +func FindCACertificate(caCertificatePath string) ([]*x509.Certificate, error) { + if caCertificatePath == "" { + return nil, nil + } + + buf, err := os.ReadFile(caCertificatePath) + if err != nil { + return nil, fmt.Errorf("failed to read CA certificate file at path %s: %w", caCertificatePath, err) + } + // Decode the PEM encoded certificates into a slice of PEM blocks + chainBlocks, _, err := DecodePEMBytes(buf) + if err != nil { + return nil, err + } + if len(chainBlocks) <= 0 { + return nil, fmt.Errorf("didn't find certificate in file at path %s", caCertificatePath) + } + + var caChain []*x509.Certificate + for _, block := range chainBlocks { + // Parse the PEM block into an x509 certificate + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse CA certificate: %w", err) + } + + caChain = append(caChain, cert) + } + + return caChain, nil +} + +// DecodePEMBytes decodes the PEM encoded bytes into a slice of PEM blocks. +func DecodePEMBytes(buf []byte) ([]*pem.Block, []byte, error) { + var privKey []byte + var certificates []*pem.Block + var block *pem.Block + for { + block, buf = pem.Decode(buf) + if block == nil { + break + } else if strings.Contains(block.Type, "PRIVATE KEY") { + privKey = pem.EncodeToMemory(block) + } else { + certificates = append(certificates, block) + } + } + return certificates, privKey, nil +} + +// LoadConfig loads the configuration file and returns the server configuration. +func (c *CommandAuthConfig) LoadConfig(profile string, configFilePath string, silentLoad bool) ( + *Server, + error, +) { + if configFilePath == "" { + // check if config file is set in environment + if config, ok := os.LookupEnv(EnvKeyfactorConfigFile); ok { + configFilePath = config + } else { + homedir, err := os.UserHomeDir() + if err != nil { + homedir = os.Getenv("HOME") + } + configFilePath = fmt.Sprintf("%s/%s", homedir, DefaultConfigFilePath) + } + } else { + c.ConfigFilePath = configFilePath + } + expandedPath, err := expandPath(configFilePath) + if err != nil { + if !silentLoad { + return nil, err + } + // if silentLoad is true then eat the error and return nil + return nil, nil + } + + file, err := os.Open(expandedPath) + if err != nil { + if !silentLoad { + return nil, err + } + // if silentLoad is true then eat the error and return nil + return nil, nil + } + defer file.Close() + + var config Config + decoder := json.NewDecoder(file) + if jErr := decoder.Decode(&config); jErr != nil { + if !silentLoad { + return nil, jErr + } + // if silentLoad is true then eat the error and return nil + return nil, nil + } + + if profile == "" { + if c.ConfigProfile != "" { + profile = c.ConfigProfile + } else { + profile = DefaultConfigProfile + } + } + + server, ok := config.Servers[profile] + if !ok { + if !silentLoad { + return nil, fmt.Errorf("profile %s not found in config file", profile) + } + // if silentLoad is true then eat the error and return nil + return nil, nil + } + + c.FileConfig = &server + + if c.CommandHostName == "" { + c.CommandHostName = server.Host + } + if c.CommandPort <= 0 { + c.CommandPort = server.Port + } + if c.CommandAPIPath == "" { + c.CommandAPIPath = server.APIPath + } + if c.CommandCACert == "" { + c.CommandCACert = server.CACertPath + } + if !c.SkipVerify { + c.SkipVerify = server.SkipTLSVerify + } + + //if !silentLoad { + // c.CommandHostName = server.Host + // c.CommandPort = server.Port + // c.CommandAPIPath = server.APIPath + // c.CommandCACert = server.CACertPath + // c.SkipVerify = server.SkipTLSVerify + //} else { + // if c.CommandHostName == "" { + // c.CommandHostName = server.Host + // } + // if c.CommandPort <= 0 { + // c.CommandPort = server.Port + // } + // if c.CommandAPIPath == "" { + // c.CommandAPIPath = server.APIPath + // } + // if c.CommandCACert == "" { + // c.CommandCACert = server.CACertPath + // } + // if c.SkipVerify { + // c.SkipVerify = server.SkipTLSVerify + // } + //} + return &server, nil +} + +// expandPath expands the path to include the user's home directory. +func expandPath(path string) (string, error) { + if path[:2] == "~/" { + home, err := os.UserHomeDir() + if err != nil { + return "", err + } + return filepath.Join(home, path[2:]), nil + } + return path, nil +} + +// GetServerConfig returns the server configuration. +func (c *CommandAuthConfig) GetServerConfig() *Server { + server := Server{ + Host: c.CommandHostName, + Port: c.CommandPort, + Username: "", + Password: "", + Domain: "", + ClientID: "", + ClientSecret: "", + OAuthTokenUrl: "", + APIPath: c.CommandAPIPath, + AuthProvider: AuthProvider{}, + SkipTLSVerify: c.SkipVerify, + CACertPath: c.CommandCACert, + AuthType: "", + } + return &server +} + +// Example usage of CommandAuthConfig +// +// This example demonstrates how to use CommandAuthConfig to authenticate to the Keyfactor Command API. +// +// func ExampleCommandAuthConfig_Authenticate() { +// authConfig := &CommandAuthConfig{ +// ConfigFilePath: "/path/to/config.json", +// ConfigProfile: "default", +// CommandHostName: "exampleHost", +// CommandPort: 443, +// CommandAPIPath: "/api/v1", +// CommandCACert: "/path/to/ca-cert.pem", +// SkipVerify: true, +// HttpClientTimeout: 60, +// } +// +// err := authConfig.Authenticate() +// if err != nil { +// fmt.Println("Authentication failed:", err) +// } else { +// fmt.Println("Authentication successful") +// } +// } diff --git a/auth_providers/auth_core_test.go b/auth_providers/auth_core_test.go new file mode 100644 index 0000000..efcf2a9 --- /dev/null +++ b/auth_providers/auth_core_test.go @@ -0,0 +1,104 @@ +// Copyright 2024 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package auth_providers_test + +import ( + "net/http" + "testing" + + "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers" +) + +func TestCommandAuthConfig_ValidateAuthConfig(t *testing.T) { + config := &auth_providers.CommandAuthConfig{ + CommandHostName: "test-host", + CommandPort: 443, + CommandAPIPath: "KeyfactorAPI", + } + + err := config.ValidateAuthConfig() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } +} + +func TestCommandAuthConfig_BuildTransport(t *testing.T) { + config := &auth_providers.CommandAuthConfig{ + CommandHostName: "test-host", + CommandPort: 443, + CommandAPIPath: "KeyfactorAPI", + } + + transport, err := config.BuildTransport() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if transport == nil { + t.Fatalf("expected a non-nil http.Transport") + } +} + +func TestCommandAuthConfig_SetClient(t *testing.T) { + config := &auth_providers.CommandAuthConfig{} + + client := &http.Client{} + config.SetClient(client) + + if config.HttpClient != client { + t.Fatalf("expected HttpClient to be set") + } +} + +func TestCommandAuthConfig_Authenticate(t *testing.T) { + config := &auth_providers.CommandAuthConfig{ + CommandHostName: "test-host", + CommandPort: 443, + CommandAPIPath: "KeyfactorAPI", + } + + err := config.Authenticate() + if err == nil { + t.Fatalf("expected an error, got nil") + } +} + +func TestLoadCACertificates(t *testing.T) { + _, err := auth_providers.LoadCACertificates("../lib/test_ca_cert.pem") + if err != nil { + t.Fatalf("expected no error, got %v", err) + } +} + +func TestFindCACertificate(t *testing.T) { + _, err := auth_providers.FindCACertificate("../lib/test_chain.pem") + if err != nil { + t.Fatalf("expected no error, got %v", err) + } +} + +func TestDecodePEMBytes(t *testing.T) { + pemData := []byte(`-----BEGIN CERTIFICATE----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7Q2+1+2+1+2+1+2+1+2+ +-----END CERTIFICATE-----`) + blocks, _, err := auth_providers.DecodePEMBytes(pemData) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if len(blocks) == 0 { + t.Fatalf("expected non-zero blocks") + } +} diff --git a/auth_providers/auth_oauth.go b/auth_providers/auth_oauth.go new file mode 100644 index 0000000..6997219 --- /dev/null +++ b/auth_providers/auth_oauth.go @@ -0,0 +1,451 @@ +package auth_providers + +import ( + "context" + "crypto/x509" + "fmt" + "net/http" + "os" + "strings" + "time" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/clientcredentials" +) + +const ( + // DefaultKeyfactorAuthPort is the default port for Keyfactor authentication + DefaultKeyfactorAuthPort = "8444" + + // DefaultTokenPrefix is the default token prefix for Keyfactor authentication headers + DefaultTokenPrefix = "Bearer" + + // EnvKeyfactorClientID is the environment variable used to set the Client ID for oauth Client credentials authentication + EnvKeyfactorClientID = "KEYFACTOR_AUTH_CLIENT_ID" + + // EnvKeyfactorClientSecret is the environment variable used to set the Client secret for oauth Client credentials authentication + EnvKeyfactorClientSecret = "KEYFACTOR_AUTH_CLIENT_SECRET" + + // EnvKeyfactorAuthTokenURL EnvCommandTokenURL is the environment variable used to set the token URL for oauth Client credentials authentication + EnvKeyfactorAuthTokenURL = "KEYFACTOR_AUTH_TOKEN_URL" + + // EnvKeyfactorAccessToken is the environment variable used to set the access token for oauth Client credentials authentication + EnvKeyfactorAccessToken = "KEYFACTOR_AUTH_ACCESS_TOKEN" + + // EnvKeyfactorAuthAudience is the environment variable used to set the audience for oauth Client credentials + //authentication + EnvKeyfactorAuthAudience = "KEYFACTOR_AUTH_AUDIENCE" + + // EnvKeyfactorAuthScopes is the environment variable used to set the scopes for oauth Client credentials authentication + EnvKeyfactorAuthScopes = "KEYFACTOR_AUTH_SCOPES" + + // EnvAuthCACert is a path to a CA certificate for the OAuth Client credentials authentication + EnvAuthCACert = "KEYFACTOR_AUTH_CA_CERT" +) + +var ( + // DefaultScopes is the default scopes for Keyfactor authentication + DefaultScopes = []string{"openid"} +) + +// OAuth Authenticator +var _ Authenticator = &OAuthAuthenticator{} + +// OAuthAuthenticator is an Authenticator that uses OAuth2 for authentication. +type OAuthAuthenticator struct { + Client *http.Client +} + +type oauth2Transport struct { + base http.RoundTripper + src oauth2.TokenSource +} + +// GetHttpClient returns the http client +func (a *OAuthAuthenticator) GetHttpClient() (*http.Client, error) { + return a.Client, nil +} + +// CommandConfigOauth represents the configuration needed for authentication to Keyfactor Command API using OAuth2. +type CommandConfigOauth struct { + // CommandAuthConfig is a reference to the base configuration needed for authentication to Keyfactor Command API + CommandAuthConfig + + // ClientID is the Client ID for OAuth authentication + ClientID string `json:"client_id,omitempty"` + + // ClientSecret is the Client secret for OAuth authentication + ClientSecret string `json:"client_secret,omitempty"` + + // Audience is the audience for OAuth authentication + Audience string `json:"audience,omitempty"` + + // Scopes is the scopes for OAuth authentication + Scopes []string `json:"scopes,omitempty"` + + // CACertificatePath is the path to the CA certificate for OAuth authentication + CACertificatePath string `json:"idp_ca_cert,omitempty"` + + // CACertificates is the CA certificates for authentication + CACertificates []*x509.Certificate `json:"-"` + + // AccessToken is the access token for OAuth authentication + AccessToken string `json:"access_token,omitempty"` + + // RefreshToken is the refresh token for OAuth authentication + RefreshToken string `json:"refresh_token,omitempty"` + + // Expiry is the expiry time of the access token + Expiry time.Time `json:"expiry,omitempty"` + + // TokenURL is the token URL for OAuth authentication + TokenURL string `json:"token_url,omitempty"` + + //// AuthPort + //AuthPort string `json:"auth_port,omitempty"` + + //// AuthType is the type of OAuth auth to use such as client_credentials, password, etc. + //AuthType string `json:"auth_type,omitempty"` +} + +// NewOAuthAuthenticatorBuilder creates a new CommandConfigOauth instance. +func NewOAuthAuthenticatorBuilder() *CommandConfigOauth { + return &CommandConfigOauth{} +} + +// WithClientId sets the Client ID for OAuth authentication. +func (b *CommandConfigOauth) WithClientId(clientId string) *CommandConfigOauth { + b.ClientID = clientId + return b +} + +// WithClientSecret sets the Client secret for OAuth authentication. +func (b *CommandConfigOauth) WithClientSecret(clientSecret string) *CommandConfigOauth { + b.ClientSecret = clientSecret + return b +} + +// WithTokenUrl sets the token URL for OAuth authentication. +func (b *CommandConfigOauth) WithTokenUrl(tokenUrl string) *CommandConfigOauth { + b.TokenURL = tokenUrl + return b +} + +// WithScopes sets the scopes for OAuth authentication. +func (b *CommandConfigOauth) WithScopes(scopes []string) *CommandConfigOauth { + b.Scopes = scopes + return b +} + +// WithAudience sets the audience for OAuth authentication. +func (b *CommandConfigOauth) WithAudience(audience string) *CommandConfigOauth { + b.Audience = audience + return b +} + +// WithCaCertificatePath sets the CA certificate path for OAuth authentication. +func (b *CommandConfigOauth) WithCaCertificatePath(caCertificatePath string) *CommandConfigOauth { + b.CACertificatePath = caCertificatePath + return b +} + +// WithCaCertificates sets the CA certificates for OAuth authentication. +func (b *CommandConfigOauth) WithCaCertificates(caCertificates []*x509.Certificate) *CommandConfigOauth { + b.CACertificates = caCertificates + return b +} + +// WithAccessToken sets the access token for OAuth authentication. +func (b *CommandConfigOauth) WithAccessToken(accessToken string) *CommandConfigOauth { + if accessToken != "" { + b.AccessToken = accessToken + } + + return b +} + +func (b *CommandConfigOauth) WithHttpClient(httpClient *http.Client) *CommandConfigOauth { + b.HttpClient = httpClient + return b +} + +// GetHttpClient returns an HTTP client for oAuth authentication. +func (b *CommandConfigOauth) GetHttpClient() (*http.Client, error) { + cErr := b.ValidateAuthConfig() + if cErr != nil { + return nil, cErr + } + + var client http.Client + baseTransport, tErr := b.BuildTransport() + if tErr != nil { + return nil, tErr + } + + if b.AccessToken != "" { + client.Transport = &oauth2.Transport{ + Base: baseTransport, + Source: oauth2.StaticTokenSource( + &oauth2.Token{ + AccessToken: b.AccessToken, + TokenType: DefaultTokenPrefix, + }, + ), + } + return &client, nil + } + + config := &clientcredentials.Config{ + ClientID: b.ClientID, + ClientSecret: b.ClientSecret, + TokenURL: b.TokenURL, + Scopes: b.Scopes, + } + + if b.Audience != "" { + config.EndpointParams = map[string][]string{ + "audience": {b.Audience}, + } + } + + if len(b.Scopes) == 0 { + b.Scopes = DefaultScopes + } + + if b.Audience != "" { + config.EndpointParams = map[string][]string{ + "Audience": {b.Audience}, + } + } + + ctx := context.WithValue(context.Background(), oauth2.HTTPClient, &http.Client{Transport: baseTransport}) + tokenSource := config.TokenSource(ctx) + + client = http.Client{ + Transport: &oauth2Transport{ + base: baseTransport, + src: tokenSource, + }, + } + + return &client, nil +} + +// Build creates an OAuth authenticator. +func (b *CommandConfigOauth) Build() (Authenticator, error) { + + client, cErr := b.GetHttpClient() + if cErr != nil { + return nil, cErr + } + + return &OAuthAuthenticator{Client: client}, nil +} + +// LoadConfig loads the configuration for Keyfactor Command API using OAuth2. +func (b *CommandConfigOauth) LoadConfig(profile, path string, silentLoad bool) (*Server, error) { + serverConfig, sErr := b.CommandAuthConfig.LoadConfig(profile, path, silentLoad) + if sErr != nil { + if !silentLoad { + return nil, sErr + } + // if silentLoad is true, return nil and nil + return nil, nil + } + + if !silentLoad { + b.ClientID = serverConfig.ClientID + b.ClientSecret = serverConfig.ClientSecret + b.TokenURL = serverConfig.OAuthTokenUrl + b.CACertificatePath = serverConfig.CACertPath + + } else { + if b.ClientID == "" { + b.ClientID = serverConfig.ClientID + } + + if b.ClientSecret == "" { + b.ClientSecret = serverConfig.ClientSecret + } + + if b.TokenURL == "" { + b.TokenURL = serverConfig.OAuthTokenUrl + } + + //if b.AccessToken == "" { + // b.AccessToken = serverConfig.AccessToken + //} + + if b.Audience == "" { + b.Audience = serverConfig.Audience + } + + if b.Scopes == nil || len(b.Scopes) == 0 { + b.Scopes = serverConfig.Scopes + } + + if b.CACertificatePath == "" { + b.CACertificatePath = serverConfig.CACertPath + } + } + + return serverConfig, nil +} + +// ValidateAuthConfig validates the configuration for Keyfactor Command API using OAuth2. +func (b *CommandConfigOauth) ValidateAuthConfig() error { + + silentLoad := true + if b.CommandAuthConfig.ConfigProfile != "" { + silentLoad = false + } else if b.CommandAuthConfig.ConfigFilePath != "" { + silentLoad = false + } + + serverConfig, cErr := b.CommandAuthConfig.LoadConfig( + b.CommandAuthConfig.ConfigProfile, + b.CommandAuthConfig.ConfigFilePath, + silentLoad, + ) + + if !silentLoad && cErr != nil { + return cErr + } + + if b.AccessToken == "" { + // check if access token is set in the environment + if accessToken, ok := os.LookupEnv(EnvKeyfactorAccessToken); ok { + b.AccessToken = accessToken + } else { + // check if client ID, client secret, and token URL are provided + if b.ClientID == "" { + if clientId, idOk := os.LookupEnv(EnvKeyfactorClientID); idOk { + b.ClientID = clientId + } else { + if serverConfig != nil && serverConfig.ClientID != "" { + b.ClientID = serverConfig.ClientID + } else { + return fmt.Errorf("client ID or environment variable %s is required", EnvKeyfactorClientID) + } + } + } + + if b.ClientSecret == "" { + if clientSecret, sOk := os.LookupEnv(EnvKeyfactorClientSecret); sOk { + b.ClientSecret = clientSecret + } else { + if serverConfig != nil && serverConfig.ClientSecret != "" { + b.ClientSecret = serverConfig.ClientSecret + } else { + return fmt.Errorf( + "client secret or environment variable %s is required", + EnvKeyfactorClientSecret, + ) + } + } + } + + if b.TokenURL == "" { + if tokenUrl, uOk := os.LookupEnv(EnvKeyfactorAuthTokenURL); uOk { + b.TokenURL = tokenUrl + } else { + if serverConfig != nil && serverConfig.OAuthTokenUrl != "" { + b.TokenURL = serverConfig.OAuthTokenUrl + } else { + return fmt.Errorf( + "token URL or environment variable %s is required", + EnvKeyfactorAuthTokenURL, + ) + } + } + } + } + } + + if b.Audience == "" { + if audience, ok := os.LookupEnv(EnvKeyfactorAuthAudience); ok { + b.Audience = audience + } else { + if serverConfig != nil && serverConfig.Audience != "" { + b.Audience = serverConfig.Audience + } + } + } + + if b.Scopes == nil || len(b.Scopes) == 0 { + if scopes, ok := os.LookupEnv(EnvKeyfactorAuthScopes); ok { + // split the scopes by comma + b.Scopes = strings.Split(scopes, ",") + } else { + if serverConfig != nil && len(serverConfig.Scopes) > 0 { + b.Scopes = serverConfig.Scopes + } else { + b.Scopes = DefaultScopes + } + } + } + + return b.CommandAuthConfig.ValidateAuthConfig() +} + +// Authenticate authenticates to Keyfactor Command API using OAuth2. +func (b *CommandConfigOauth) Authenticate() error { + + // validate auth config + vErr := b.ValidateAuthConfig() + if vErr != nil { + return vErr + } + + // create oauth Client + oauthy, err := b.GetHttpClient() + + if err != nil { + return err + } else if oauthy == nil { + return fmt.Errorf("unable to create http client") + } + + b.SetClient(oauthy) + //b.DefaultHttpClient = oauthy + + aErr := b.CommandAuthConfig.Authenticate() + if aErr != nil { + return aErr + } + + return nil +} + +// GetServerConfig returns the server configuration for Keyfactor Command API using OAuth2. +func (b *CommandConfigOauth) GetServerConfig() *Server { + server := Server{ + Host: b.CommandHostName, + Port: b.CommandPort, + ClientID: b.ClientID, + ClientSecret: b.ClientSecret, + OAuthTokenUrl: b.TokenURL, + APIPath: b.CommandAPIPath, + Scopes: b.Scopes, + Audience: b.Audience, + //AuthProvider: AuthProvider{}, + SkipTLSVerify: b.SkipVerify, + CACertPath: b.CommandCACert, + AuthType: "oauth", + } + return &server +} + +// RoundTrip executes a single HTTP transaction, adding the OAuth2 token to the request +func (t *oauth2Transport) RoundTrip(req *http.Request) (*http.Response, error) { + token, err := t.src.Token() + if err != nil { + return nil, fmt.Errorf("failed to retrieve OAuth token: %w", err) + } + + // Clone the request to avoid mutating the original + reqCopy := req.Clone(req.Context()) + token.SetAuthHeader(reqCopy) + + return t.base.RoundTrip(reqCopy) +} diff --git a/auth_providers/auth_oauth_test.go b/auth_providers/auth_oauth_test.go new file mode 100644 index 0000000..0b26f9b --- /dev/null +++ b/auth_providers/auth_oauth_test.go @@ -0,0 +1,436 @@ +// Copyright 2024 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package auth_providers_test + +import ( + "fmt" + "net/http" + "os" + "strings" + "testing" + + "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers" +) + +func TestOAuthAuthenticator_GetHttpClient(t *testing.T) { + // Skip test if TEST_KEYFACTOR_AD_AUTH is set to 1 or true + if os.Getenv("TEST_KEYFACTOR_AD_AUTH") == "1" || os.Getenv("TEST_KEYFACTOR_AD_AUTH") == "true" { + t.Skip("Skipping TestOAuthAuthenticator_GetHttpClient") + return + } + auth := &auth_providers.OAuthAuthenticator{ + Client: &http.Client{}, + } + + client, err := auth.GetHttpClient() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if client == nil { + t.Fatalf("expected a non-nil http.Client") + } +} + +func TestCommandConfigOauth_ValidateAuthConfig(t *testing.T) { + // Skip test if TEST_KEYFACTOR_AD_AUTH is set to 1 or true + if os.Getenv("TEST_KEYFACTOR_AD_AUTH") == "1" || os.Getenv("TEST_KEYFACTOR_AD_AUTH") == "true" { + t.Skip("Skipping TestOAuthAuthenticator_GetHttpClient") + return + } + config := &auth_providers.CommandConfigOauth{ + ClientID: os.Getenv(auth_providers.EnvKeyfactorClientID), + ClientSecret: os.Getenv(auth_providers.EnvKeyfactorClientSecret), + TokenURL: os.Getenv(auth_providers.EnvKeyfactorAuthTokenURL), + } + + err := config.ValidateAuthConfig() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } +} + +func TestCommandConfigOauth_GetHttpClient(t *testing.T) { + // Skip test if TEST_KEYFACTOR_AD_AUTH is set to 1 or true + if os.Getenv("TEST_KEYFACTOR_AD_AUTH") == "1" || os.Getenv("TEST_KEYFACTOR_AD_AUTH") == "true" { + t.Skip("Skipping TestOAuthAuthenticator_GetHttpClient") + return + } + config := &auth_providers.CommandConfigOauth{ + ClientID: os.Getenv(auth_providers.EnvKeyfactorClientID), + ClientSecret: os.Getenv(auth_providers.EnvKeyfactorClientSecret), + TokenURL: os.Getenv(auth_providers.EnvKeyfactorAuthTokenURL), + Scopes: []string{"openid", "profile", "email"}, + } + + client, err := config.GetHttpClient() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if client == nil { + t.Fatalf("expected a non-nil http.Client") + } +} + +func TestCommandConfigOauth_Authenticate(t *testing.T) { + // Skip test if TEST_KEYFACTOR_AD_AUTH is set to 1 or true + if os.Getenv("TEST_KEYFACTOR_AD_AUTH") == "1" || os.Getenv("TEST_KEYFACTOR_AD_AUTH") == "true" { + t.Skip("Skipping TestOAuthAuthenticator_GetHttpClient") + return + } + userHome, hErr := os.UserHomeDir() + if hErr != nil { + userHome = os.Getenv("HOME") + } + + configFilePath := fmt.Sprintf("%s/%s", userHome, auth_providers.DefaultConfigFilePath) + configFromFile, cErr := auth_providers.ReadConfigFromJSON(configFilePath) + if cErr != nil { + t.Errorf("unable to load auth config from file %s: %v", configFilePath, cErr) + } + + if configFromFile == nil || configFromFile.Servers == nil { + t.Errorf("invalid config file %s", configFilePath) + t.FailNow() + } + + caCertPath := "../lib/certs/int-oidc-lab.eastus2.cloudapp.azure.com.crt" + + //Delete the config file + t.Logf("Deleting config file: %s", configFilePath) + os.Remove(configFilePath) + defer func() { + // Write the config file back + t.Logf("Writing config file: %s", configFilePath) + fErr := auth_providers.WriteConfigToJSON(configFilePath, configFromFile) + if fErr != nil { + t.Errorf("unable to write auth config to file %s: %v", configFilePath, fErr) + } + }() + //os.Setenv(auth_providers.EnvKeyfactorConfigFile, configFilePath) + //os.Setenv(auth_providers.EnvKeyfactorAuthProfile, "oauth") + os.Setenv(auth_providers.EnvKeyfactorSkipVerify, "true") + os.Setenv(auth_providers.EnvKeyfactorCACert, caCertPath) + + //current working directory + cwd, _ := os.Getwd() + t.Logf("Current working directory: %s", cwd) + + // Begin test case + noParamsTestName := fmt.Sprintf( + "w/ complete ENV variables & %s,%s", auth_providers.EnvKeyfactorCACert, + auth_providers.EnvKeyfactorSkipVerify, + ) + t.Log(fmt.Sprintf("Testing %s", noParamsTestName)) + noParamsConfig := &auth_providers.CommandConfigOauth{} + authOauthTest( + t, noParamsTestName, false, noParamsConfig, + ) + t.Logf("Unsetting environment variable %s", auth_providers.EnvKeyfactorCACert) + os.Unsetenv(auth_providers.EnvKeyfactorCACert) + t.Logf("Unsetting environment variable %s", auth_providers.EnvKeyfactorSkipVerify) + os.Unsetenv(auth_providers.EnvKeyfactorSkipVerify) + // end test case + + // Begin test case + noParamsTestName = fmt.Sprintf( + "w/ complete ENV variables & %s", auth_providers.EnvKeyfactorCACert, + ) + t.Log(fmt.Sprintf("Testing %s", noParamsTestName)) + t.Logf("Setting environment variable %s", auth_providers.EnvKeyfactorCACert) + os.Setenv(auth_providers.EnvKeyfactorCACert, caCertPath) + noParamsConfig = &auth_providers.CommandConfigOauth{} + authOauthTest(t, noParamsTestName, false, noParamsConfig) + t.Logf("Unsetting environment variable %s", auth_providers.EnvKeyfactorCACert) + os.Unsetenv(auth_providers.EnvKeyfactorCACert) + // end test case + + // Begin test case + noParamsTestName = fmt.Sprintf( + "w/ complete ENV variables & %s", auth_providers.EnvKeyfactorSkipVerify, + ) + t.Log(fmt.Sprintf("Testing %s", noParamsTestName)) + t.Logf("Setting environment variable %s", auth_providers.EnvKeyfactorSkipVerify) + os.Setenv(auth_providers.EnvKeyfactorSkipVerify, "true") + noParamsConfig = &auth_providers.CommandConfigOauth{} + authOauthTest(t, noParamsTestName, false, noParamsConfig) + t.Logf("Unsetting environment variable %s", auth_providers.EnvKeyfactorSkipVerify) + os.Unsetenv(auth_providers.EnvKeyfactorSkipVerify) + // end test case + + // Begin test case + noParamsConfig = &auth_providers.CommandConfigOauth{} + httpsFailEnvExpected := []string{"tls: failed to verify certificate"} + authOauthTest( + t, + fmt.Sprintf("w/o env %s", auth_providers.EnvKeyfactorCACert), + true, + noParamsConfig, + httpsFailEnvExpected..., + ) + // end test case + + t.Log("Testing oAuth with invalid config file path") + invFilePath := &auth_providers.CommandConfigOauth{} + invFilePath.WithConfigFile("invalid-file-path") + invalidPathExpectedError := []string{"no such file or directory", "invalid-file-path"} + authOauthTest(t, "with invalid config file PATH", true, invFilePath, invalidPathExpectedError...) + + // Environment variables are not set + t.Log("Unsetting environment variables") + //keyfactorEnvVars := exportEnvVarsWithPrefix("KEYFACTOR_") + clientID, clientSecret, tokenURL := exportOAuthEnvVariables() + unsetOAuthEnvVariables() + defer func() { + t.Log("Resetting environment variables") + setOAuthEnvVariables(clientID, clientSecret, tokenURL) + }() + + t.Log("Testing oAuth with no Environmental variables") + incompleteEnvConfig := &auth_providers.CommandConfigOauth{} + incompleteEnvConfigExpectedError := fmt.Sprintf( + "client ID or environment variable %s is required", + auth_providers.EnvKeyfactorClientID, + ) + authOauthTest( + t, + "with incomplete Environmental variables", + true, + incompleteEnvConfig, + incompleteEnvConfigExpectedError, + ) + + t.Log("Testing auth with only clientID") + clientIDOnlyConfig := &auth_providers.CommandConfigOauth{ + ClientID: "test-client-id", + } + clientIDOnlyConfigExceptedError := fmt.Sprintf( + "client secret or environment variable %s is required", + auth_providers.EnvKeyfactorClientSecret, + ) + authOauthTest(t, "clientID only", true, clientIDOnlyConfig, clientIDOnlyConfigExceptedError) + + t.Log("Testing auth with w/ full params variables") + fullParamsConfig := &auth_providers.CommandConfigOauth{ + ClientID: clientID, + ClientSecret: clientSecret, + TokenURL: tokenURL, + } + fullParamsConfig.WithSkipVerify(true) + authOauthTest(t, "w/ full params variables", false, fullParamsConfig) + + t.Log("Testing auth with w/ full params & invalid pass") + fullParamsInvalidPassConfig := &auth_providers.CommandConfigOauth{ + ClientID: clientID, + ClientSecret: "invalid-client-secret", + TokenURL: tokenURL, + } + fullParamsInvalidPassConfig.WithSkipVerify(true) + invalidCredsExpectedError := []string{ + "oauth2", "unauthorized_client", "Invalid client or Invalid client credentials", + } + authOauthTest(t, "w/ full params & invalid pass", true, fullParamsInvalidPassConfig, invalidCredsExpectedError...) + + t.Log("Testing auth with w/ no tokenURL") + noTokenURLConfig := &auth_providers.CommandConfigOauth{ + ClientID: clientID, + ClientSecret: clientSecret, + } + noTokenURLExpectedError := fmt.Sprintf( + "token URL or environment variable %s is required", + auth_providers.EnvKeyfactorAuthTokenURL, + ) + authOauthTest(t, "w/ no tokenURL", true, noTokenURLConfig, noTokenURLExpectedError) + + // Write the config file back + t.Logf("Writing config file: %s", configFilePath) + fErr := auth_providers.WriteConfigToJSON(configFilePath, configFromFile) + if fErr != nil { + t.Errorf("unable to write auth config to file %s: %v", configFilePath, fErr) + } + + //unsetOAuthEnvVariables() + + t.Log("Testing oAuth with valid implicit config file profile param, caCert, and skiptls") + wCaCertConfigFile := &auth_providers.CommandConfigOauth{} + wCaCertConfigFile. + WithConfigProfile("oauth"). + WithCommandCACert(caCertPath). + WithSkipVerify(true) + authOauthTest( + t, "oAuth with valid implicit config file profile param, caCert, and skiptls", false, + wCaCertConfigFile, + ) + + t.Log("Testing oAuth with skiptls param and valid implicit config file") + skipTLSConfigFileP := &auth_providers.CommandConfigOauth{} + skipTLSConfigFileP. + WithConfigProfile("oauth"). + WithSkipVerify(true) + authOauthTest(t, "with skiptls param and valid implicit config file", false, skipTLSConfigFileP) + + t.Log("Testing oAuth with valid implicit config file skiptls config param") + skipTLSConfigFileC := &auth_providers.CommandConfigOauth{} + skipTLSConfigFileC. + WithConfigProfile("oauth-skiptls") + authOauthTest(t, "with oAuth with valid implicit config file skiptls config param", false, skipTLSConfigFileC) + + t.Log("Testing oAuth with valid implicit config file skiptls env") + t.Logf("Setting environment variable %s", auth_providers.EnvKeyfactorSkipVerify) + os.Setenv(auth_providers.EnvKeyfactorSkipVerify, "true") + skipTLSConfigFileE := &auth_providers.CommandConfigOauth{} + skipTLSConfigFileE. + WithConfigProfile("oauth") + authOauthTest(t, "oAuth with valid implicit config file skiptls env", false, skipTLSConfigFileE) + t.Logf("Unsetting environment variable %s", auth_providers.EnvKeyfactorSkipVerify) + os.Unsetenv(auth_providers.EnvKeyfactorSkipVerify) + + t.Log("Testing oAuth with valid implicit config file https fail") + httpsFailConfigFile := &auth_providers.CommandConfigOauth{} + httpsFailConfigFile. + WithConfigProfile("oauth") + httpsFailConfigFileExpected := []string{"tls: failed to verify certificate"} + authOauthTest( + t, "oAuth with valid implicit config file https fail", true, httpsFailConfigFile, + httpsFailConfigFileExpected..., + ) + + t.Log("Testing oAuth with invalid profile implicit config file") + invProfile := &auth_providers.CommandConfigOauth{} + invProfile.WithConfigProfile("invalid-profile") + expectedError := []string{"profile", "invalid-profile", "not found"} + authOauthTest(t, "with invalid profile implicit config file", true, invProfile, expectedError...) + + t.Log("Testing oAuth with invalid creds implicit config file") + invProfileCreds := &auth_providers.CommandConfigOauth{} + invProfileCreds. + WithConfigProfile("oauth_invalid_creds"). + WithSkipVerify(true) + authOauthTest(t, "with invalid creds implicit config file", true, invProfileCreds, invalidCredsExpectedError...) + + t.Log("Testing oAuth with invalid Command host implicit config file") + invCmdHost := &auth_providers.CommandConfigOauth{} + invCmdHost. + WithConfigProfile("oauth_invalid_host"). + WithSkipVerify(true) + invHostExpectedError := []string{"no such host"} + authOauthTest(t, "with invalid creds implicit config file", true, invCmdHost, invHostExpectedError...) +} + +func TestCommandConfigOauth_Build(t *testing.T) { + // Skip test if TEST_KEYFACTOR_AD_AUTH is set to 1 or true + if os.Getenv("TEST_KEYFACTOR_AD_AUTH") == "1" || os.Getenv("TEST_KEYFACTOR_AD_AUTH") == "true" { + t.Skip("Skipping TestOAuthAuthenticator_GetHttpClient") + return + } + config := &auth_providers.CommandConfigOauth{ + ClientID: os.Getenv(auth_providers.EnvKeyfactorClientID), + ClientSecret: os.Getenv(auth_providers.EnvKeyfactorClientSecret), + TokenURL: os.Getenv(auth_providers.EnvKeyfactorAuthTokenURL), + Scopes: []string{"openid", "profile", "email"}, + } + + authenticator, err := config.Build() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if authenticator == nil { + t.Fatalf("expected a non-nil Authenticator") + } +} + +func authOauthTest( + t *testing.T, testName string, allowFail bool, config *auth_providers.CommandConfigOauth, + errorContains ...string, +) { + t.Run( + fmt.Sprintf("oAuth Auth Test %s", testName), func(t *testing.T) { + + err := config.Authenticate() + if allowFail { + if err == nil { + t.Errorf("oAuth auth test '%s' should have failed", testName) + t.FailNow() + return + } + if len(errorContains) > 0 { + for _, ec := range errorContains { + if !strings.Contains(err.Error(), ec) { + t.Errorf("oAuth auth test '%s' failed with unexpected error %v", testName, err) + t.FailNow() + return + } + } + } + t.Logf("oAuth auth test '%s' failed as expected with %v", testName, err) + return + } + if err != nil { + t.Errorf("oAuth auth test '%s' failed with %v", testName, err) + t.FailNow() + return + } + }, + ) +} + +// setOAuthEnvVariables sets the oAuth environment variables +func setOAuthEnvVariables(client_id, client_secret, token_url string) { + os.Setenv(auth_providers.EnvKeyfactorClientID, client_id) + os.Setenv(auth_providers.EnvKeyfactorClientSecret, client_secret) + os.Setenv(auth_providers.EnvKeyfactorAuthTokenURL, token_url) +} + +func exportEnvVarsWithPrefix(prefix string) map[string]string { + result := make(map[string]string) + for _, env := range os.Environ() { + // Each environment variable is in the format "KEY=VALUE" + pair := strings.SplitN(env, "=", 2) + key := pair[0] + value := pair[1] + + if strings.HasPrefix(key, prefix) { + result[key] = value + } + } + return result +} + +// exportOAuthEnvVariables sets the oAuth environment variables +func exportOAuthEnvVariables() (string, string, string) { + client_id := os.Getenv(auth_providers.EnvKeyfactorClientID) + client_secret := os.Getenv(auth_providers.EnvKeyfactorClientSecret) + token_url := os.Getenv(auth_providers.EnvKeyfactorAuthTokenURL) + return client_id, client_secret, token_url +} + +// unsetOAuthEnvVariables unsets the oAuth environment variables +func unsetOAuthEnvVariables() { + os.Unsetenv(auth_providers.EnvKeyfactorClientID) + os.Unsetenv(auth_providers.EnvKeyfactorClientSecret) + os.Unsetenv(auth_providers.EnvKeyfactorAuthTokenURL) + os.Unsetenv(auth_providers.EnvKeyfactorSkipVerify) + os.Unsetenv(auth_providers.EnvKeyfactorConfigFile) + os.Unsetenv(auth_providers.EnvKeyfactorAuthProfile) + os.Unsetenv(auth_providers.EnvKeyfactorCACert) + os.Unsetenv(auth_providers.EnvAuthCACert) + //os.Unsetenv(auth_providers.EnvKeyfactorHostName) + //os.Unsetenv(auth_providers.EnvKeyfactorUsername) + //os.Unsetenv(auth_providers.EnvKeyfactorPassword) + //os.Unsetenv(auth_providers.EnvKeyfactorDomain) + +} diff --git a/auth_providers/command_config.go b/auth_providers/command_config.go new file mode 100644 index 0000000..7141e16 --- /dev/null +++ b/auth_providers/command_config.go @@ -0,0 +1,374 @@ +// Copyright 2024 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package auth_providers + +import ( + "encoding/json" + "fmt" + "os" + + "gopkg.in/yaml.v2" +) + +// Server represents the server configuration for authentication. +type Server struct { + Host string `json:"host,omitempty" yaml:"host,omitempty"` // Host is the Command server DNS name or IP address. + Port int `json:"port,omitempty" yaml:"port,omitempty"` // Port is the Command server port. + //AuthPort int `json:"auth_port,omitempty" yaml:"auth_port,omitempty"` // AuthPort is the authentication port. + Username string `json:"username,omitempty" yaml:"username,omitempty"` // Username is the username for authentication. + Password string `json:"password,omitempty" yaml:"password,omitempty"` // Password is the password for authentication. + Domain string `json:"domain,omitempty" yaml:"domain,omitempty"` // Domain is the domain for authentication. + ClientID string `json:"client_id,omitempty" yaml:"client_id,omitempty"` // ClientID is the client ID for OAuth. + ClientSecret string `json:"client_secret,omitempty" yaml:"client_secret,omitempty"` // ClientSecret is the client secret for OAuth. + Scopes []string `json:"scopes,omitempty" yaml:"scopes,omitempty"` // Scopes is the OAuth scopes. + Audience string `json:"audience,omitempty" yaml:"audience,omitempty"` // Audience is the OAuth audience. + OAuthTokenUrl string `json:"token_url,omitempty" yaml:"token_url,omitempty"` // OAuthTokenUrl is full URL for OAuth token request endpoint. + APIPath string `json:"api_path,omitempty" yaml:"api_path,omitempty"` // APIPath is the API path. + AuthProvider AuthProvider `json:"auth_provider,omitempty" yaml:"auth_provider,omitempty"` // AuthProvider contains the authentication provider details. + SkipTLSVerify bool `json:"skip_tls_verify,omitempty" yaml:"skip_tls_verify,omitempty"` // TLSVerify determines whether to verify the TLS certificate. + CACertPath string `json:"ca_cert_path,omitempty" yaml:"ca_cert_path,omitempty"` // CACertPath is the path to the CA certificate to trust. + AuthType string `json:"auth_type,omitempty" yaml:"auth_type,omitempty"` // AuthType is the type of authentication to use. + +} + +// AuthProvider represents the authentication provider configuration. +type AuthProvider struct { + Type string `json:"type,omitempty" yaml:"type,omitempty"` // Type is the type of authentication provider. + Profile string `json:"profile,omitempty" yaml:"profile,omitempty"` // Profile is the profile of the authentication provider. + Parameters map[string]interface{} `json:"parameters,omitempty" yaml:"parameters,omitempty"` // Parameters are additional parameters for the authentication provider. +} + +// Config represents the overall configuration structure. +type Config struct { + Servers map[string]Server `json:"servers,omitempty" yaml:"servers,omitempty"` // Servers is a map of server configurations. +} + +// NewConfig creates a new Config configuration. +func NewConfig() *Config { + return &Config{ + Servers: make(map[string]Server), + } +} + +// ReadConfigFromJSON reads a Config configuration from a JSON file. +func ReadConfigFromJSON(filePath string) (*Config, error) { + file, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer file.Close() + + var config Config + decoder := json.NewDecoder(file) + if err := decoder.Decode(&config); err != nil { + return nil, err + } + + return &config, nil +} + +// ReadConfigFromYAML reads a Config configuration from a YAML file. +func ReadConfigFromYAML(filePath string) (*Config, error) { + file, err := os.ReadFile(filePath) + if err != nil { + return nil, err + } + + var config Config + if err := yaml.Unmarshal(file, &config); err != nil { + return nil, err + } + + return &config, nil +} + +// ReadServerFromJSON reads a Server configuration from a JSON file. +func ReadServerFromJSON(filePath string) (*Server, error) { + file, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer file.Close() + + var server Server + decoder := json.NewDecoder(file) + if err := decoder.Decode(&server); err != nil { + return nil, err + } + + return &server, nil +} + +// WriteServerToJSON writes a Server configuration to a JSON file. +func WriteServerToJSON(filePath string, server *Server) error { + file, err := os.Create(filePath) + if err != nil { + return err + } + defer file.Close() + + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") + if err := encoder.Encode(server); err != nil { + return err + } + + return nil +} + +// ReadServerFromYAML reads a Server configuration from a YAML file. +func ReadServerFromYAML(filePath string) (*Server, error) { + file, err := os.ReadFile(filePath) + if err != nil { + return nil, err + } + + var server Server + if err := yaml.Unmarshal(file, &server); err != nil { + return nil, err + } + + return &server, nil +} + +// WriteServerToYAML writes a Server configuration to a YAML file. +func WriteServerToYAML(filePath string, server *Server) error { + data, err := yaml.Marshal(server) + if err != nil { + return err + } + + if err := os.WriteFile(filePath, data, 0644); err != nil { + return err + } + + return nil +} + +// WriteConfigToJSON writes a Config configuration to a JSON file. +func WriteConfigToJSON(filePath string, config *Config) error { + file, err := os.Create(filePath) + if err != nil { + return err + } + defer file.Close() + + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") + if err := encoder.Encode(config); err != nil { + return err + } + + return nil +} + +// WriteConfigToYAML writes a Config configuration to a YAML file. +func WriteConfigToYAML(filePath string, config *Config) error { + data, err := yaml.Marshal(config) + if err != nil { + return err + } + + if err := os.WriteFile(filePath, data, 0644); err != nil { + return err + } + + return nil +} + +func (c *Config) Compare(other *Config) bool { + if len(c.Servers) != len(other.Servers) { + return false + } + + for key, server := range c.Servers { + if otherServer, exists := other.Servers[key]; !exists || !server.Compare(&otherServer) { + return false + } + } + + return true +} + +func (s *Server) Compare(other *Server) bool { + return s.Host == other.Host && + s.Port == other.Port && + s.Username == other.Username && + s.Password == other.Password && + s.Domain == other.Domain && + s.ClientID == other.ClientID && + s.ClientSecret == other.ClientSecret && + s.OAuthTokenUrl == other.OAuthTokenUrl && + s.APIPath == other.APIPath && + s.SkipTLSVerify == other.SkipTLSVerify && + s.CACertPath == other.CACertPath && + s.AuthType == other.AuthType +} + +// MergeConfigFromFile merges the configuration from a file into the existing Config. +func MergeConfigFromFile(filePath string, config *Config) (*Config, error) { + // Read the file content + data, err := os.ReadFile(filePath) + if err != nil { + + return nil, fmt.Errorf("failed to read config file: %w", err) + } + + // Determine the file type (JSON or YAML) and unmarshal accordingly + var tempConfig Config + if json.Valid(data) { + if err := json.Unmarshal(data, &tempConfig); err != nil { + return nil, fmt.Errorf("failed to unmarshal JSON config: %w", err) + } + } else { + if err := yaml.Unmarshal(data, &tempConfig); err != nil { + return nil, fmt.Errorf("failed to unmarshal YAML config: %w", err) + } + } + + // Merge the temporary config into the existing config + for key, server := range tempConfig.Servers { + if _, exists := config.Servers[key]; !exists { + config.Servers[key] = server + } + } + + return config, nil +} + +// GetAuthType returns the type of authentication to use based on the configuration params. +func (s *Server) GetAuthType() string { + if s.ClientID != "" && s.ClientSecret != "" { + s.AuthType = "oauth" + } else if s.Username != "" && s.Password != "" { + s.AuthType = "basic" + } else { + s.AuthType = "" + } + return s.AuthType +} + +// GetBasicAuthClientConfig returns the basic auth configuration for the client. +func (s *Server) GetBasicAuthClientConfig() (*CommandAuthConfigBasic, error) { + configType := s.GetAuthType() + if configType != "basic" { + return nil, fmt.Errorf("invalid auth type: %s", configType) + } + + baseConfig := CommandAuthConfig{} + baseConfig. + WithCommandHostName(s.Host). + WithCommandPort(s.Port). + WithCommandAPIPath(s.APIPath). + WithCommandCACert(s.CACertPath). + WithSkipVerify(s.SkipTLSVerify) + + basicConfig := CommandAuthConfigBasic{ + CommandAuthConfig: baseConfig, + } + basicConfig. + WithUsername(s.Username). + WithPassword(s.Password). + WithDomain(s.Domain). + Build() + + vErr := basicConfig.ValidateAuthConfig() + if vErr != nil { + return nil, vErr + } + return &basicConfig, nil +} + +// GetOAuthClientConfig returns the OAuth configuration for the client. +func (s *Server) GetOAuthClientConfig() (*CommandConfigOauth, error) { + configType := s.GetAuthType() + if configType != "oauth" { + return nil, fmt.Errorf("invalid auth type: %s", configType) + } + baseConfig := CommandAuthConfig{} + baseConfig. + WithCommandHostName(s.Host). + WithCommandPort(s.Port). + WithCommandAPIPath(s.APIPath). + WithCommandCACert(s.CACertPath). + WithSkipVerify(s.SkipTLSVerify) + + oauthConfig := CommandConfigOauth{ + CommandAuthConfig: baseConfig, + } + oauthConfig. + WithClientId(s.ClientID). + WithClientSecret(s.ClientSecret). + WithTokenUrl(s.OAuthTokenUrl). + Build() + + vErr := oauthConfig.ValidateAuthConfig() + if vErr != nil { + return nil, vErr + } + return &oauthConfig, nil +} + +// Example usage of Config +// +// This example demonstrates how to use Config to read and write server configurations. +// +// func ExampleConfig_ReadWrite() { +// config := NewConfig() +// +// // Add a server configuration +// config.Servers["exampleServer"] = Server{ +// Host: "exampleHost", +// Port: 443, +// Username: "exampleUser", +// Password: "examplePassword", +// Domain: "exampleDomain", +// ClientID: "exampleClientID", +// ClientSecret: "exampleClientSecret", +// OAuthTokenUrl: "https://example.com/oauth/token", +// APIPath: "/api/v1", +// SkipTLSVerify: true, +// CACertPath: "/path/to/ca-cert.pem", +// AuthType: "oauth", +// } +// +// // Write the configuration to a JSON file +// err := WriteConfigToJSON("/path/to/config.json", config) +// if err != nil { +// fmt.Println("Failed to write config to JSON:", err) +// } +// +// // Read the configuration from a JSON file +// readConfig, err := ReadConfigFromJSON("/path/to/config.json") +// if err != nil { +// fmt.Println("Failed to read config from JSON:", err) +// } else { +// fmt.Println("Read config from JSON:", readConfig) +// } +// +// // Write the configuration to a YAML file +// err = WriteConfigToYAML("/path/to/config.yaml", config) +// if err != nil { +// fmt.Println("Failed to write config to YAML:", err) +// } +// +// // Read the configuration from a YAML file +// readConfig, err = ReadConfigFromYAML("/path/to/config.yaml") +// if err != nil { +// fmt.Println("Failed to read config from YAML:", err) +// } else { +// fmt.Println("Read config from YAML:", readConfig) +// } +// } diff --git a/auth_providers/command_config_test.go b/auth_providers/command_config_test.go new file mode 100644 index 0000000..af6c969 --- /dev/null +++ b/auth_providers/command_config_test.go @@ -0,0 +1,358 @@ +// Copyright 2024 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package auth_providers_test + +import ( + "encoding/json" + "os" + "testing" + + "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers" + "gopkg.in/yaml.v2" +) + +func TestReadServerFromJSON(t *testing.T) { + filePath := "test_config.json" + server := &auth_providers.Server{ + Host: "localhost", + Port: 8080, + OAuthTokenUrl: "https://auth.localhost:8443/openid/token", + Username: "user", + Password: "pass", + ClientID: "client_id", + ClientSecret: "client_secret", + Domain: "domain", + APIPath: "api", + } + + err := auth_providers.WriteServerToJSON(filePath, server) + if err != nil { + t.Fatalf("failed to write server to JSON: %v", err) + } + defer os.Remove(filePath) + + readServer, err := auth_providers.ReadServerFromJSON(filePath) + if err != nil { + t.Fatalf("failed to read server from JSON: %v", err) + } + + if !compareServers(readServer, server) { + t.Fatalf("expected %v, got %v", server, readServer) + } +} + +func TestWriteServerToJSON(t *testing.T) { + filePath := "test_server.json" + server := &auth_providers.Server{ + Host: "localhost", + Port: 8080, + OAuthTokenUrl: "https://auth.localhost:8443/openid/token", + Username: "user", + Password: "pass", + ClientID: "client_id", + ClientSecret: "client_secret", + Domain: "domain", + APIPath: "api", + } + + err := auth_providers.WriteServerToJSON(filePath, server) + if err != nil { + t.Fatalf("failed to write server to JSON: %v", err) + } + defer os.Remove(filePath) + + file, err := os.ReadFile(filePath) + if err != nil { + t.Fatalf("failed to read file: %v", err) + } + + var readServer auth_providers.Server + err = json.Unmarshal(file, &readServer) + if err != nil { + t.Fatalf("failed to unmarshal JSON: %v", err) + } + + if !compareServers(&readServer, server) { + t.Fatalf("expected %v, got %v", server, readServer) + } +} + +func TestReadServerFromYAML(t *testing.T) { + filePath := "test_server.yaml" + server := &auth_providers.Server{ + Host: "localhost", + Port: 8080, + OAuthTokenUrl: "https://auth.localhost:8443/openid/token", + Username: "user", + Password: "pass", + ClientID: "client_id", + ClientSecret: "client_secret", + Domain: "domain", + APIPath: "api", + } + + err := auth_providers.WriteServerToYAML(filePath, server) + if err != nil { + t.Fatalf("failed to write server to YAML: %v", err) + } + defer os.Remove(filePath) + + readServer, err := auth_providers.ReadServerFromYAML(filePath) + if err != nil { + t.Fatalf("failed to read server from YAML: %v", err) + } + + if !compareServers(readServer, server) { + t.Fatalf("expected %v, got %v", server, readServer) + } +} + +func TestWriteServerToYAML(t *testing.T) { + filePath := "test_server.yaml" + server := &auth_providers.Server{ + Host: "localhost", + Port: 8080, + OAuthTokenUrl: "https://auth.localhost:8443/openid/token", + Username: "user", + Password: "pass", + ClientID: "client_id", + ClientSecret: "client_secret", + Domain: "domain", + APIPath: "api", + } + + err := auth_providers.WriteServerToYAML(filePath, server) + if err != nil { + t.Fatalf("failed to write server to YAML: %v", err) + } + defer os.Remove(filePath) + + file, err := os.ReadFile(filePath) + if err != nil { + t.Fatalf("failed to read file: %v", err) + } + + var readServer auth_providers.Server + err = yaml.Unmarshal(file, &readServer) + if err != nil { + t.Fatalf("failed to unmarshal YAML: %v", err) + } + + if !compareServers(&readServer, server) { + t.Fatalf("expected %v, got %v", server, readServer) + } +} + +func TestMergeConfigFromFile(t *testing.T) { + filePath := "test_config.json" + config := &auth_providers.Config{ + Servers: map[string]auth_providers.Server{ + "server1": { + Host: "localhost", + Port: 8080, + }, + }, + } + + err := auth_providers.WriteConfigToJSON(filePath, config) + if err != nil { + t.Fatalf("failed to write config to JSON: %v", err) + } + defer os.Remove(filePath) + + newConfig := &auth_providers.Config{ + Servers: map[string]auth_providers.Server{ + "server2": { + Host: "remotehost", + Port: 9090, + }, + }, + } + + _, err = auth_providers.MergeConfigFromFile(filePath, newConfig) + if err != nil { + t.Fatalf("failed to merge config from file: %v", err) + } + + if len(newConfig.Servers) != 2 { + t.Fatalf("expected 2 servers, got %d", len(newConfig.Servers)) + } + + if newConfig.Servers["server1"].Host != "localhost" { + t.Fatalf("expected server1 host to be localhost, got %s", newConfig.Servers["server1"].Host) + } + + if newConfig.Servers["server2"].Host != "remotehost" { + t.Fatalf("expected server2 host to be remotehost, got %s", newConfig.Servers["server2"].Host) + } +} + +func TestReadFullAuthConfigExample(t *testing.T) { + filePath := "../lib/config/full_auth_config_example.json" + expectedConfig := auth_providers.Config{ + Servers: map[string]auth_providers.Server{ + "default": { + Host: "keyfactor.command.kfdelivery.com", + OAuthTokenUrl: "idp.keyfactor.command.kfdelivery.com", + Username: "keyfactor", + Password: "password", + ClientID: "client-id", + ClientSecret: "client-secret", + Domain: "command", + APIPath: "KeyfactorAPI", + AuthProvider: auth_providers.AuthProvider{ + Type: "azid", + Profile: "azure", + Parameters: map[string]interface{}{ + "secret_name": "command-config-azure", + "vault_name": "keyfactor-secrets", + }, + }, + }, + "server2": { + Host: "keyfactor2.command.kfdelivery.com", + OAuthTokenUrl: "idp.keyfactor2.command.kfdelivery.com", + Username: "keyfactor2", + Password: "password2", + ClientID: "client-id2", + ClientSecret: "client-secret2", + Domain: "command", + APIPath: "KeyfactorAPI", + AuthProvider: auth_providers.AuthProvider{ + Type: "azid", + Profile: "azure", + Parameters: map[string]interface{}{ + "secret_name": "command-config-azure2", + "vault_name": "keyfactor-secrets", + }, + }, + }, + }, + } + + file, err := os.ReadFile(filePath) + if err != nil { + t.Fatalf("failed to read file: %v", err) + } + + var config auth_providers.Config + err = json.Unmarshal(file, &config) + if err != nil { + t.Fatalf("failed to unmarshal JSON: %v", err) + } + + if !compareConfigs(&config, &expectedConfig) { + t.Fatalf("expected %v, got %v", expectedConfig, config) + } +} + +func TestReadOAuthConfigExample(t *testing.T) { + filePath := "../lib/config/oauth_config_example.json" + expectedConfig := &auth_providers.Config{ + Servers: map[string]auth_providers.Server{ + "default": { + Host: "keyfactor.command.kfdelivery.com", + OAuthTokenUrl: "https://idp.keyfactor.command.kfdelivery.com/oauth2/token", + ClientID: "client-id", + ClientSecret: "client-secret", + APIPath: "KeyfactorAPI", + }, + "server2": { + Host: "keyfactor.command.kfdelivery.com", + OAuthTokenUrl: "https://idp.keyfactor.command.kfdelivery.com/oauth2/token", + ClientID: "client-id", + ClientSecret: "client-secret", + APIPath: "KeyfactorAPI", + }, + }, + } + + file, err := os.ReadFile(filePath) + if err != nil { + t.Fatalf("failed to read file: %v", err) + } + + var config auth_providers.Config + err = json.Unmarshal(file, &config) + if err != nil { + t.Fatalf("failed to unmarshal JSON: %v", err) + } + + if !compareConfigs(&config, expectedConfig) { + t.Fatalf("expected %v, got %v", expectedConfig, config) + } +} + +func TestReadBasicAuthConfigExample(t *testing.T) { + filePath := "../lib/config/basic_auth_config_example.json" + expectedConfig := &auth_providers.Config{ + Servers: map[string]auth_providers.Server{ + "default": { + Host: "keyfactor.command.kfdelivery.com", + Username: "keyfactor", + Password: "password", + Domain: "command", + APIPath: "KeyfactorAPI", + }, + "server2": { + Host: "keyfactor2.command.kfdelivery.com", + Username: "keyfactor2", + Password: "password2", + Domain: "command", + APIPath: "Keyfactor/API", + }, + }, + } + + file, err := os.ReadFile(filePath) + if err != nil { + t.Fatalf("failed to read file: %v", err) + } + + var config auth_providers.Config + err = json.Unmarshal(file, &config) + if err != nil { + t.Fatalf("failed to unmarshal JSON: %v", err) + } + + if !compareConfigs(&config, expectedConfig) { + t.Fatalf("expected %v, got %v", expectedConfig, config) + } +} + +func compareConfigs(a, b *auth_providers.Config) bool { + if len(a.Servers) != len(b.Servers) { + return false + } + for key, serverA := range a.Servers { + serverB, exists := b.Servers[key] + if !exists || !compareServers(&serverA, &serverB) { + return false + } + } + return true +} + +func compareServers(a, b *auth_providers.Server) bool { + return a.Host == b.Host && + a.Port == b.Port && + a.OAuthTokenUrl == b.OAuthTokenUrl && + a.Username == b.Username && + a.Password == b.Password && + a.ClientID == b.ClientID && + a.ClientSecret == b.ClientSecret && + a.Domain == b.Domain && + a.APIPath == b.APIPath +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9c513dd --- /dev/null +++ b/go.mod @@ -0,0 +1,22 @@ +// Copyright 2024 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +module github.com/Keyfactor/keyfactor-auth-client-go + +go 1.22 + +require ( + golang.org/x/oauth2 v0.24.0 + gopkg.in/yaml.v2 v2.4.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b855ce3 --- /dev/null +++ b/go.sum @@ -0,0 +1,8 @@ +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/integration-manifest.json b/integration-manifest.json new file mode 100644 index 0000000..f8a6743 --- /dev/null +++ b/integration-manifest.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://keyfactor.github.io/integration-manifest-schema.json", + "integration_type": "api-client", + "name": "Keyfactor Auth Client - Golang", + "status": "production", + "description": "The Keyfactor Auth Client - Golang is a Go module that handles authentication and authorization for the Keyfactor API.", + "support_level": "community", + "link_github": false, + "update_catalog": false, + "release_dir": "bin" +} + diff --git a/lib/certs/KFTrainRoot.crt b/lib/certs/KFTrainRoot.crt new file mode 100644 index 0000000..69619ee --- /dev/null +++ b/lib/certs/KFTrainRoot.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDeTCCAmGgAwIBAgIQSVQNM9+tTo9Dd52qg4MI1DANBgkqhkiG9w0BAQsFADBP +MRMwEQYKCZImiZPyLGQBGRYDbGFiMRkwFwYKCZImiZPyLGQBGRYJa2V5ZmFjdG9y +MR0wGwYDVQQDExRrZXlmYWN0b3ItS0ZUUkFJTi1DQTAeFw0xOTA1MTAwMzMyMzJa +Fw0yNDA1MTAwMzQyMzFaME8xEzARBgoJkiaJk/IsZAEZFgNsYWIxGTAXBgoJkiaJ +k/IsZAEZFglrZXlmYWN0b3IxHTAbBgNVBAMTFGtleWZhY3Rvci1LRlRSQUlOLUNB +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmqN1+RED9SuRsLnIF4AB +7uFkaismnxhGXc9LWAVBPc8bt8McchMlHmJqVN1DPR0ZT8tVT8jqIODBULrcWZVo +6ox15BTrFqzrFUiIuuq16NDW+WYu2rljoMBaOTegkmWs7ZoME+w/MHqFFqPBBvg7 +uDSZW/w+1VKyn7aRA2Bywy6o5UHpladsokVKwNhyMQvfJnJQ2xJio8mhXV1AM15F +Cp8hQZ8dXj/cAPKQxk31M1thIP7M8yx779QbxIs6PKLNxarmY+D73r8Q3t8scO+G +VQUwSvbDZiF+kzpl/5YTkeD6gLqfQsQr86YiK5nV5xCb2PL8KwnmMCocVImX2fm3 +vQIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQUcBUzPW7ZQuqUMP3RFTCbDU1hTGUwEAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZI +hvcNAQELBQADggEBAIYye4+Gd8piML1BXzkMNgt6aNOu7hS4h3sYfojtpV40OdJ6 +4/Pt9pC5NecMt8B0ikiZvfu9c+xO20VB3uFDGNWVLqfoaZi+cvMAYH9gMrK8KiNe +21jekbG1uTuIPZ0oJtEDnn7aJ+rXzVTEe6QHZ/gjVcZoPy1/rdCnzMRdH0NS6xpn +0HqWpy/IxjnJP0Ux6ZPNzrEmhsUGruVJwF8u5+FTlD9pF55eHqI4COtEqJ8YEMb2 +5s8xCCJVL0al+LbydR0neG4Ic/zA0QEwB7ixFsuytaBUOXv4QVpsu7R4mtWQHdSo +Jz3I+g117tHDlJfGEoQpsc/gHBwMptPQCobpI30= +-----END CERTIFICATE----- diff --git a/lib/certs/int-oidc-lab.eastus2.cloudapp.azure.com.crt b/lib/certs/int-oidc-lab.eastus2.cloudapp.azure.com.crt new file mode 100644 index 0000000..5930217 --- /dev/null +++ b/lib/certs/int-oidc-lab.eastus2.cloudapp.azure.com.crt @@ -0,0 +1,51 @@ +-----BEGIN CERTIFICATE----- +MIIENTCCAp2gAwIBAgIUXiYBpLlOyvIAuP9iZ8NmIfftHq0wDQYJKoZIhvcNAQEL +BQAwMDEXMBUGCgmSJomT8ixkAQEMBzI0Mjc4MzcxFTATBgNVBAMMDE1hbmFnZW1l +bnRDQTAeFw0yNDEwMTYxNjUxMzdaFw0yNTExMTcxNjQyMzVaMDIxMDAuBgNVBAMM +J2ludC1vaWRjLWxhYi5lYXN0dXMyLmNsb3VkYXBwLmF6dXJlLmNvbTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAO9bxuPyHOytYPN7KIz2IBuuVuKMh7YU +yuZHJ2CiFfOOOnZlojaKjsp5j8E50h4tc5aPBWDZ0Cma7Ty3pdsssRsa78JwQ8cG +oOUumrRYySzAET6OJA8Pn00t+lxLxBf4St3buobhQTEMesCsKdCSWbAydiIgkKA8 +E02zPKzXJU6yMmV/JnMPqEQHUBC8yb4NmfXsMplkVYkdhqESL+0xYYJLqireDuR3 +LfDdvZkh0XovgdN9zJpf10KP2os2tOzaa35dXVCBsl7y/IPaDt+zYb0OX88TGOPl +1AysYO35lS+6CGiENn2ACTZSmvqOcwjOLB5dJd95b1lnUwsJgmMcSesCAwEAAaOB +xDCBwTAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFOOYYJwB1O/MQlfbORHp1YqM +Uy0MMEwGA1UdEQRFMEOCJ2ludC1vaWRjLWxhYi5lYXN0dXMyLmNsb3VkYXBwLmF6 +dXJlLmNvbYIMaW50LW9pZGMtbGFihwQUAYhEhwQKCgAEMBMGA1UdJQQMMAoGCCsG +AQUFBwMBMB0GA1UdDgQWBBTBsBvKKfjz/P14o0c3V/fLm/hXmDAOBgNVHQ8BAf8E +BAMCBaAwDQYJKoZIhvcNAQELBQADggGBAEizjLNGgjGNML6L4On1EhcDAtGIH6SY +mkiP/mcMXcxESx/wAZAQO04ERbVX3mnewdmq1+TsnbfxEtetAobaxlRRej6tzFQp +BBqGqz7uK8O3CpzBZzuBtR2d+GZw6+amBbxGyZvsIW0f2RQqaHp+FgBgJjZc3t+/ +UmaRoS2h26VBXuv40bEgErih/cI80xnJtRKtxS1/hJNvzeDMobgZ0KkZz1j/tHbi +DzaX8CDCg0fhMH5BuSleG9MSYcW36TL30pv/h92kN8EldQgpWwMW0C3aTZBEpiCw +NGeiUmbciia//rOi4Rin4uHCe7sllb10MTaLcy251S9vAmmHvSBj33iLlCms2WS7 +02BwsPPGilydfjKqekoC6M1AwxiSDp8ckiXNx4LvW8uyqwHCIOsW3kc7CR0LGvTn +qLN7Sy5keB9uVs4e69nmU2GG6V0SN1QreuQfroIkgsgYyfz6MvMVhmVVLObELkrY +RfrOSKEyZnB0+kBfyGykvnQcSaUsqFwXmg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEUTCCArmgAwIBAgIUJnfJ3rvwfv/qgrBcu8k5xyVDS+AwDQYJKoZIhvcNAQEL +BQAwMDEXMBUGCgmSJomT8ixkAQEMBzI0Mjc4MzcxFTATBgNVBAMMDE1hbmFnZW1l +bnRDQTAeFw0yNDEwMTYxNjUxMzdaFw00OTEwMTcxNjUxMzZaMDAxFzAVBgoJkiaJ +k/IsZAEBDAcyNDI3ODM3MRUwEwYDVQQDDAxNYW5hZ2VtZW50Q0EwggGiMA0GCSqG +SIb3DQEBAQUAA4IBjwAwggGKAoIBgQCwQGo4Acvd/baGXKfJPrMoQ+xknaM9hPDB +dszb2HxS3hW9833utlqShWbzvODxoGyl1PG6x0aKw9N1V8YpC2az66qQgLmtbgDE +o7GxB2YIw5krJU2gCZ7HifCD0IaRvUaL174RZ71B0/ranFURhS/wYh25dYaNYpBI +91ZZF9oG+brA71RS8QOH2Nnr+Tz2JPL5IFBHQECM4MwjY2AGkXHtIgpciS8qSzwa +o1u5xx5eP2AL/lpIhsFdYX6XYat+0pgk08X6N0r0NwbjiA4F8pxqUIwSDTnOnb3k +zj1A3WBJi3pIk5Fp3mqOnheR9+16A5+ZnFdWMcUaPXxAwUJbnvHiCX1xF+iSiVbI +ohDJz/E2Jk40EEkt6gSqx7vUy7JkePSg2abLlLN/nfuNyGRJsxJZrvSmKu/Wia7b +y09I0uIwb/dWdVsu52LTrwJfHBwAzUotKw1Al66wGYxJNx9r3bdbehNOlhGOsS9N +0Lg7h62buyX7n1JdRruAu3iz+OKnnH8CAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB +/zAfBgNVHSMEGDAWgBTjmGCcAdTvzEJX2zkR6dWKjFMtDDAdBgNVHQ4EFgQU45hg +nAHU78xCV9s5EenVioxTLQwwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUA +A4IBgQBnXdEv0+NkFCLdXWgqecK/aNZcRcPwgplYcKxKJ92Bc7fceX2DcHReD8jS +IdlyUY2vNwev18rva9dtfhd6NFga8yYBrgnAS3AwHEzVrRL9pWSZ/kJwwlyQDcYJ +PkXvNUKa+rPYUSPArSoQZDXt5rwtIJNzDdoGninTUvxg8b7xHw61fF21exTyzAGZ +vak8oBtn7HnLal3+ko8rJ7QXfV6vedU1Ft+/lSJGi+/fxxLjE7MmAPzn6Kg2RB/H +stOCVYnJD+5z0RGD9C41O8KCq/UYwz7d1zjGm5pmFe8P4qYSiG1lHuCr4VVqhWb7 +9oW+H800REIIiy/coomXtUA+uBxfo0swtXT9zOH1MVP2SNfE8NpRVHZpaW8kMPcY +V8mHRRVoo+gaSlh+RYZ/c9GnUjrdenPVlxSqc0+qJVXqesL3Xim/KiLxuBOwaueI +2PzNqluRXfbmhvz52+DAhFF+DN3CE272ZmH0KBUSlxzUCZGxWradBz4N24/GJhkU +ejYP+u4= +-----END CERTIFICATE----- diff --git a/lib/certs/int1230-oauth.eastus2.cloudapp.azure.com.crt b/lib/certs/int1230-oauth.eastus2.cloudapp.azure.com.crt new file mode 100644 index 0000000..79e1675 --- /dev/null +++ b/lib/certs/int1230-oauth.eastus2.cloudapp.azure.com.crt @@ -0,0 +1,51 @@ +-----BEGIN CERTIFICATE----- +MIIEODCCAqCgAwIBAgIUS8GnNJhEBDF6ZPGlSQ7LYEmuWhcwDQYJKoZIhvcNAQEL +BQAwMDEXMBUGCgmSJomT8ixkAQEMBzkwMzAyNTQxFTATBgNVBAMMDE1hbmFnZW1l +bnRDQTAeFw0yNDExMDIwNDQxMDNaFw0yNTEyMDQwNDMxNDZaMDMxMTAvBgNVBAMM +KGludDEyMzAtb2F1dGguZWFzdHVzMi5jbG91ZGFwcC5henVyZS5jb20wggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC0bq+uuhIyx42i0HaueABtHomSdZxk +378/ju6jeO1EhO+4qWvedI0s3BiybTQ0Mm6rqbvU/vJxTCzqiQD3smYLyzCUH3Xi +mawNJK3ta2NeB7LeNJLFoC1Bui7GHL2EqRqrtW3/P62LLXDcleJFX6sXyspvP7NR +c6XExWlTaLBYMDVA9sCCatDhdd/IA+B2jEYOw+oSRSLt00OLJO1qtC0hUnfhcak/ +ZI2kvyzfTpF3eQE5wqFQZ6NW47vbzxDXPglwAECbLPmTCpCvNu1RBZUUK6BU6+QG +5ZVg5AsdezB9LW9fr559rKrNqloChQ0iQn5fihlKj7FYSRWbwEgyKYMpAgMBAAGj +gcYwgcMwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBRR5KPmQmOjEO7DLziN78F6 ++1YXSjBOBgNVHREERzBFgihpbnQxMjMwLW9hdXRoLmVhc3R1czIuY2xvdWRhcHAu +YXp1cmUuY29tgg1pbnQxMjMwLW9hdXRohwSsy3lyhwQKCgAEMBMGA1UdJQQMMAoG +CCsGAQUFBwMBMB0GA1UdDgQWBBTK1NRSOUvvQ9yrU0ksceThH1YbFjAOBgNVHQ8B +Af8EBAMCBaAwDQYJKoZIhvcNAQELBQADggGBAK6Ai0AFxtetv423Q3rx3/y320nS +hCT+uqxmGTP+mCVMu6Wy4ESahLip2CY7hm3l0WuyzJn/MohL4IPzSOaKAzYsCrvy +gGZCoVUSenJQHgXh7GVaYP8PUGkD7xTagxJLTSWBEXm4HqRzfD6gONQprPJzRbBj +Z0JpY5qXKerfknZRKWeJdNnTeUj94vcFJ1+GamYns+d4zPa12ekyI09aYDH3B4Gv +fOavWUdIB5v2p4uveXuHNaYD+UHyWs0xGhPvEaQ2RtG1hh0UHPMDlLbe7hhg3Omx +JIFZVAEXeFGVDHjsi3Ydy3/8HphaITD0mSyKFiLtOppf+tLd+2+T/D07ZlSiEXe2 +Daq5RnwSuufZHlKw2h2QwezaGS44hKLsyX8Bqs5d+MErGf/Pdi+oF36ZHSAhgxRj +BZIdWBdZgbVOdbTmE4hR/UrCOX4IY6PJo7az1xh1G8CTbJ/884K/wArJu48zdayr +IpsJ4J5jZVXbKbi3ds7TRTIJofzrxiOr6oY1Yw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEUTCCArmgAwIBAgIUWHI07m/uguG9f0DTP2z28AOM4iAwDQYJKoZIhvcNAQEL +BQAwMDEXMBUGCgmSJomT8ixkAQEMBzkwMzAyNTQxFTATBgNVBAMMDE1hbmFnZW1l +bnRDQTAeFw0yNDExMDIwNDQxMDNaFw00OTExMDMwNDQxMDJaMDAxFzAVBgoJkiaJ +k/IsZAEBDAc5MDMwMjU0MRUwEwYDVQQDDAxNYW5hZ2VtZW50Q0EwggGiMA0GCSqG +SIb3DQEBAQUAA4IBjwAwggGKAoIBgQDOvyiw6xjOLbzGoqi3cFF5AonX3lBfvBJn +N+yI0c18B99v/Nmsx2rGIMUrdQ4AEWmzk+eyytKJZfyBJO/Q5Lk9h3eX8IB+cee4 +qMozAQzeiP/uCpRnjB6ybkr3Yhde55Z/uUUNcLfJ/tTH/iU9zsq98iPfjlTJWAvi +Dkv//L1mM3z89K06xR3NwGxh39BryABYQagWw9231QhlhIpLARJ/F+lOxa1U/a9s +hWYu6V5Lc9wySadJnYvL0qEiDo5e8Fs/lDdkhi3ftxdgwReOBnPXeppLludyTg9A +Ztd7ouhwtYKYTNvuxAwwCRJ1tuUs6SmHRNA192Pro1r/zo8YPj2/g/e/1aJPsfo2 +vtFd90TqNaGithsQXJTdS4N2dNDPn61e2TMLLIylGY+K/4GwLDka66AAw4KpgdVP +5W3AOMi43tdWIjfA8+7mBl1PCLaHkKcLM3luay+CvcBfLm9XKwWJUXm8AUUJcjdS +K5CCTJWiPmTBzi96LaVMrBAESoq1TQ8CAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB +/zAfBgNVHSMEGDAWgBRR5KPmQmOjEO7DLziN78F6+1YXSjAdBgNVHQ4EFgQUUeSj +5kJjoxDuwy84je/BevtWF0owDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUA +A4IBgQC95xM4wb/ZT83cMX7vRavwvtHJvjzc//VLwE+HdC9EcjiAxLXoTMnHzRfK +WbIjnf04URoc4+QKp2Q9ocke5ZFRVzsKMZSA1neoEFkoLu3nAE52G9novm+jtt+5 +GzfW5eLPpvKCxL1oEjjBczu1iURsGDsi4XaeXDeOXXI0J0XhufhhtbMKz9nHb2pK +roYQAg4xbCi5JrkyWWOan0HvDoY9iA+Dg2sGxrTOBcE40HwpAokqQlocEYaMPycI +NEB0lRr80k0pthffMGFHtOt6v0sM+OdeNGfS59XmTZMDqaA1mx6swJaxoXaabalG +x0JExdAuXTgFzexWabV0RMBB/O/g9UNrLvVWW1rDkj1MY3AaUVsqLVXEhwzr02yX +VCpq9FdrsxrG8sgMNaYNfeTkUyQz60uyy5KVZDgylQWDLqcTvR/+aNpJSI1M0eWc +v2+yY17dozM+8Nx3zfgrUkB8n9u3B0A3Waj+S6XKPFzea5RWICBvJanpwUMEi8AP +2BNUiD8= +-----END CERTIFICATE----- diff --git a/lib/certs/int1230c-oauth.eastus2.cloudapp.azure.com.crt b/lib/certs/int1230c-oauth.eastus2.cloudapp.azure.com.crt new file mode 100644 index 0000000..9d2dc4f --- /dev/null +++ b/lib/certs/int1230c-oauth.eastus2.cloudapp.azure.com.crt @@ -0,0 +1,51 @@ +-----BEGIN CERTIFICATE----- +MIIEOzCCAqOgAwIBAgIUEf12bS1kO4sGR+w0y4h0SfXFONQwDQYJKoZIhvcNAQEL +BQAwMDEXMBUGCgmSJomT8ixkAQEMBzg5MDkxOTUxFTATBgNVBAMMDE1hbmFnZW1l +bnRDQTAeFw0yNDExMDIwNDQ1MDhaFw0yNTEyMDQwNDM1NTFaMDQxMjAwBgNVBAMM +KWludDEyMzBjLW9hdXRoLmVhc3R1czIuY2xvdWRhcHAuYXp1cmUuY29tMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsTY1vglrvKK/nFNaqDczIhPESej7 +thjcG9286R03+SK3zIADlTbBUQ6W9TQ5xDRdS+iE9vAdYO9Dm2/gerY9Bj3Q7unK +lM7qkzUTUWDRq55Jmsh7cr1NbIC04I/H1R02qHAmTdHSuDXMU7ruGcZF7AtYGy4T +Kckh3Io48Mhto2R0TjHoyiyuwmjiHmQDAox4TL0QosI9TLlrZloloOZzZK4A2Flm +eST7e6EixIwQgXv83H9R765hIi9sJw6h7dpuF/HiF0kySxPvdMOvesuBL+CVG3WB +KsOswD/lEJHXgVkmZf2eAOnBhd9F3OWHXAIKZRG197Df+NadxSjZKSXjHwIDAQAB +o4HIMIHFMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUpp+z+NEGpjmfb1JDiZrb +tnVO1UowUAYDVR0RBEkwR4IpaW50MTIzMGMtb2F1dGguZWFzdHVzMi5jbG91ZGFw +cC5henVyZS5jb22CDmludDEyMzBjLW9hdXRohwSsrDvIhwQKCgAEMBMGA1UdJQQM +MAoGCCsGAQUFBwMBMB0GA1UdDgQWBBTq5FoMzQN+eP2Jp1Zo7d528yYgUDAOBgNV +HQ8BAf8EBAMCBaAwDQYJKoZIhvcNAQELBQADggGBAFV55yxu6oadzyeodwVILZAK +Wc5HjZsAnHGNMbqUTrYShxml/qoqJkJbb/KUDSMeptEyFsay0XgzTnnp6ewKUzTs +BSmVtnVLGBrmdNi/N0Ey47VErUGj0HdfBGgjjUcCVDb9Gi7epzDBTyoS/QaygrVx +0Ihu92jqRfDihM5tdnFVi8ggP9U/rmdir9CZDJDXEtLlNOECGaJmConF1n5RzRap +uUJN5RSelKkO/4/33MV3UNJKkShIGn3ol6aczK6JvvW2Tv5/Lwk4Wgc6ADrJNa21 +X4YkNUnOTp0bAikRlHY7M+seuvtVlXXOocF7uaIlQXnTXPlGTGgckuLKoRwe8HvM +nuJ02RQOUdq5OeadEAOykuXw56nLuCScwzgeEAoTEx1qoc9AW1z0XkCLX/ckbcOb +QfLG7XU7VrHaxcY7uRLFflZ+1qPeqAgxk87QZGuzi4XZgoez28pXsdJQ21QOJeXJ +3Vyh6mpY9U9LCIF4WsnkUdldXiimXp5/HVFy/wFrlA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEUTCCArmgAwIBAgIUQ3V7mqJheNaBTRTLylWD5s5eEBQwDQYJKoZIhvcNAQEL +BQAwMDEXMBUGCgmSJomT8ixkAQEMBzg5MDkxOTUxFTATBgNVBAMMDE1hbmFnZW1l +bnRDQTAeFw0yNDExMDIwNDQ1MDhaFw00OTExMDMwNDQ1MDdaMDAxFzAVBgoJkiaJ +k/IsZAEBDAc4OTA5MTk1MRUwEwYDVQQDDAxNYW5hZ2VtZW50Q0EwggGiMA0GCSqG +SIb3DQEBAQUAA4IBjwAwggGKAoIBgQCwZeoLXyMvzuuKi23kKZH/+UHWKAepqBkE +5Om7kk8KjDCQgL9Pmtk3Db6IYd/Fhv1HS5VUYhxmpvAbfob0pE8DF9vKLBAia0ej +Xo7RTMIUT40CzchSSbdNIpfJRyPOP+sxf1Mi8PU52eFmoJIVIuhRY9EdEWA/wRJb +J6jdT60TBc/SrJpdcCjuJQchI8iUUZbyEcTxN5pxJrZVrfggXJ3Wdf9wBIBXB1WZ +2JrbO/wRSRcqYwFZrTCemgpa3uQhlaN34oOGZGJe4dcMswjgFL8lU+0pq9Vwc8z5 +f9lQQbomwmtdcE6W7uNioZVxfT4708pftjoxwitXoIG/yzg2uBAwu+0QHgBy6v26 +C2twpH58CY2fPhtnKxiBYBqwkhPhAL4iGeXsXx5Fp3GZhC4TEdewbDKEwhwRNpyT +zpIItSelO+1tftQDdJ7J7/SS8WzUeheKNPHx5ZcGTrRC9UN9LhjpxuGClH/ZfbK0 +Z5iFbKOM6Hj1h0F9htBY+saNDeX1ghcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB +/zAfBgNVHSMEGDAWgBSmn7P40QamOZ9vUkOJmtu2dU7VSjAdBgNVHQ4EFgQUpp+z ++NEGpjmfb1JDiZrbtnVO1UowDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUA +A4IBgQBAvt1RomGa8JzA9G7GNHn4Mgly1dqxxc7c+QrPwsBVp1qdAoOxPQil1t6N +k4WvGpu9MnqbAZrCjnB4qtgB0gjQ+331jr6KbG1mMcz55Yg09xnPjw2UcHagfKud +PsVRW1SnDTJ7J6MJS/Kk+u0lYKZ6fy3b9mA2bDP4gz3vHEXGQMA4wCMoLtsYFbuH +cD0qjxbjpX0k2qUt80+HYRZhcGBAF/o2EgV8A0JpseoCf/Z6kBi1snSDeOzd/v3M +Zs12/eh1EVOwB8Qom23LsgYELUlipDfxcsfzPAm60CzSkxr7S3mwhU5Ms4QkWD4o +EtOiopii5RQnLLcHaHzU98R3zcIb57pSFNNcq3Y3Jw4WhWSNzogDjinPI19iX6P7 +Z3SFE2o5CrhMijQ2F37Wvu0/RPZOcyZmmVjMVJi1njfkI1BfyGS14O53cZQxHmFt +X/UuhLoTKQFxOVmGTlrbMEIAFvT3lm4EB7EDGCRIFJomYiEda/0EyyMBryg0PjVs +kU77TYo= +-----END CERTIFICATE----- diff --git a/lib/certs/kftrain.keyfactor.lab.crt b/lib/certs/kftrain.keyfactor.lab.crt new file mode 100644 index 0000000..86149a8 --- /dev/null +++ b/lib/certs/kftrain.keyfactor.lab.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDETCCAfmgAwIBAgIUZh6TfNnZ/oAeC9+595dN44tpqPUwDQYJKoZIhvcNAQEL +BQAwIDEeMBwGA1UEAwwVa2Z0cmFpbi5rZXlmYWN0b3IubGFiMB4XDTIzMDgxODE4 +MzY1MFoXDTMzMDgxNTE4MzY1MFowIDEeMBwGA1UEAwwVa2Z0cmFpbi5rZXlmYWN0 +b3IubGFiMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA33yBK9Ynyx6J +F9KXmhtb0wQa+8qifZBEclFEkIoQRanbJtoNO5hQzJMlZpsXu5Yc1oqUscwXFWRK +47g8t+5LLTNmxwTNTavsVarTE2yYtHwfX1nwla/WY48uQ2vNE8qQhVQaczDVMjkX +YQmHx2MJ8GGzvxeAaRzoan3tG++eI4p0qxmka44SCFRkuVzrLEleTNCBPhEqNylL +Ehg4Xco1rXV/SI9yADzXTm3yJswBhtHvUaAaLdJv6tzNqLKuHFE+cFNktmK8X/3A +lKStxXtG/PtFGnynlmOo9UEgO/UtCo5YYziEn/Fl29k2JNsbfGtwRtqlX2jpA4Ji ++KMdOS6RxQIDAQABo0MwQTAgBgNVHREEGTAXghVrZnRyYWluLmtleWZhY3Rvci5s +YWIwHQYDVR0OBBYEFL434O9U4f3lal+DcCfGG7jzpIaZMA0GCSqGSIb3DQEBCwUA +A4IBAQAPF7OZz1r9wN7S1p/ne81+G8sVVfMJ8UvORgc1umtdcCtWUe7hvrmIacCs +nH5e8M2SdEdHi5i6NIY/2mSOg3eF208NyQ4kdDg8DHljdy+GbZOjJtIaomTzM49n +KN59tf+bsv+Z0NNTmbnthHQqbVphXiH4QX9zMbjM56QuCOPyaFavMqd528USz/Wg +DZMca6BzSv7t7ZZwE1wr/2pMpvwQ8GhOMepFAoMrXdsQKSnD4TMml3ZNHzIrJoo+ +o1D4sVmQa7uYbtj0Msxx84dlo8InbEPmV/0qMVIljVikrsoRaDfd6uslXgsu1RhT +Y4MgkRouFUKc7m8Gfjxe/WsJNN6c +-----END CERTIFICATE----- diff --git a/lib/config/auth_config_schema.json b/lib/config/auth_config_schema.json new file mode 100644 index 0000000..2af7d48 --- /dev/null +++ b/lib/config/auth_config_schema.json @@ -0,0 +1,153 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Keyfactor Command API Client Configuration", + "description": "Configuration file schema for authenticating to the Keyfactor Command API", + "properties": { + "servers": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9_-]+$": { + "type": "object", + "properties": { + "host": { + "type": "string", + "description": "The hostname of the Keyfactor Command API server" + }, + "auth_port": { + "type": "integer", + "description": "The port of the Keyfactor Command API server" + }, + "username": { + "type": "string", + "description": "The username to authenticate with using basic auth" + }, + "password": { + "type": "string", + "description": "The password to authenticate with using basic auth" + }, + "client_id": { + "type": "string", + "description": "The client ID to authenticate with using OAuth2" + }, + "token_url": { + "type": "string", + "description": "The token URL to authenticate with using OAuth2" + }, + "client_secret": { + "type": "string", + "description": "The client secret to authenticate with using OAuth2" + }, + "domain": { + "type": "string", + "description": "The Active Directory domain to authenticate with using basic auth" + }, + "api_path": { + "type": "string", + "description": "The path to the Keyfactor Command API", + "default": "KeyfactorAPI" + }, + "auth_provider": { + "type": "object", + "description": "The auth provider configuration", + "properties": { + "type": { + "type": "string", + "enum": [ + "azid", + "akv" + ] + }, + "profile": { + "type": "string", + "description": "The profile to use in the auth provider configuration" + }, + "parameters": { + "type": "object", + "description": "The parameters to use in the auth provider configuration", + "properties": { + "secret_name": { + "type": "string", + "description": "The name of the secret to use in the Azure KeyVault auth provider configuration" + }, + "vault_name": { + "type": "string", + "description": "The name of the vault to use in the Azure KeyVault auth provider configuration" + } + }, + "required": [] + } + }, + "required": [ + "type", + "profile", + "parameters" + ] + } + }, + "oneOf": [ + { + "required": [ + "username", + "password" + ], + "not": { + "required": [ + "client_id", + "client_secret" + ] + } + }, + { + "required": [ + "client_id", + "client_secret", + "token_url" + ], + "not": { + "required": [ + "username", + "password" + ] + } + } + ], + "if": { + "required": [ + "auth_provider" + ] + }, + "then": { + "required": [ + "auth_provider" + ] + }, + "else": { + "if": { + "required": [ + "client_id", + "client_secret" + ] + }, + "then": { + "required": [ + "token_url", + "host" + ] + }, + "else": { + "required": [ + "host" + ] + } + } + } + }, + "additionalProperties": false + } + }, + "required": [ + "servers" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/lib/config/basic_auth_config_example.json b/lib/config/basic_auth_config_example.json new file mode 100644 index 0000000..05b5749 --- /dev/null +++ b/lib/config/basic_auth_config_example.json @@ -0,0 +1,18 @@ +{ + "servers": { + "default": { + "host": "keyfactor.command.kfdelivery.com", + "username": "keyfactor", + "password": "password", + "domain": "command", + "api_path": "KeyfactorAPI" + }, + "server2": { + "host": "keyfactor2.command.kfdelivery.com", + "username": "keyfactor2", + "password": "password2", + "domain": "command", + "api_path": "Keyfactor/API" + } + } +} \ No newline at end of file diff --git a/lib/config/full_auth_config_example.json b/lib/config/full_auth_config_example.json new file mode 100644 index 0000000..c2bb73c --- /dev/null +++ b/lib/config/full_auth_config_example.json @@ -0,0 +1,40 @@ +{ + "servers": { + "default": { + "host": "keyfactor.command.kfdelivery.com", + "token_url": "idp.keyfactor.command.kfdelivery.com", + "username": "keyfactor", + "password": "password", + "client_id": "client-id", + "client_secret": "client-secret", + "domain": "command", + "api_path": "KeyfactorAPI", + "auth_provider": { + "type": "azid", + "profile": "azure", + "parameters": { + "secret_name": "command-config-azure", + "vault_name": "keyfactor-secrets" + } + } + }, + "server2": { + "host": "keyfactor2.command.kfdelivery.com", + "token_url": "idp.keyfactor2.command.kfdelivery.com", + "username": "keyfactor2", + "password": "password2", + "client_id": "client-id2", + "client_secret": "client-secret2", + "domain": "command", + "api_path": "KeyfactorAPI", + "auth_provider": { + "type": "azid", + "profile": "azure", + "parameters": { + "secret_name": "command-config-azure2", + "vault_name": "keyfactor-secrets" + } + } + } + } +} \ No newline at end of file diff --git a/lib/config/oauth_config_example.json b/lib/config/oauth_config_example.json new file mode 100644 index 0000000..9c6ae86 --- /dev/null +++ b/lib/config/oauth_config_example.json @@ -0,0 +1,18 @@ +{ + "servers": { + "default": { + "host": "keyfactor.command.kfdelivery.com", + "token_url": "https://idp.keyfactor.command.kfdelivery.com/oauth2/token", + "client_id": "client-id", + "client_secret": "client-secret", + "api_path": "KeyfactorAPI" + }, + "server2": { + "host": "keyfactor.command.kfdelivery.com", + "token_url": "https://idp.keyfactor.command.kfdelivery.com/oauth2/token", + "client_id": "client-id", + "client_secret": "client-secret", + "api_path": "KeyfactorAPI" + } + } +} \ No newline at end of file diff --git a/lib/main.go b/lib/main.go new file mode 100644 index 0000000..e41bf2c --- /dev/null +++ b/lib/main.go @@ -0,0 +1,146 @@ +// Copyright 2024 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "math/big" + "os" + "time" +) + +func main() { + // Generate CA private key + caPrivKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + fmt.Printf("Failed to generate CA private key: %v\n", err) + return + } + + // Create CA certificate template + caTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + Organization: []string{"My CA Organization"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), // 10 years + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + IsCA: true, + } + + // Create CA certificate + caCertBytes, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &caPrivKey.PublicKey, caPrivKey) + if err != nil { + fmt.Printf("Failed to create CA certificate: %v\n", err) + return + } + + // Encode CA certificate to PEM format + caCertPEM := pem.EncodeToMemory( + &pem.Block{ + Type: "CERTIFICATE", + Bytes: caCertBytes, + }, + ) + + // Encode CA private key to PEM format + caPrivKeyPEM := pem.EncodeToMemory( + &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(caPrivKey), + }, + ) + + // Save CA certificate and private key to files + err = os.WriteFile("ca_cert.pem", caCertPEM, 0644) + if err != nil { + fmt.Printf("Failed to write CA certificate to file: %v\n", err) + return + } + err = os.WriteFile("ca_key.pem", caPrivKeyPEM, 0600) + if err != nil { + fmt.Printf("Failed to write CA private key to file: %v\n", err) + return + } + + // Generate leaf private key + leafPrivKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + fmt.Printf("Failed to generate leaf private key: %v\n", err) + return + } + + // Create leaf certificate template + leafTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(2), + Subject: pkix.Name{ + Organization: []string{"My Leaf Organization"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(1, 0, 0), // 1 year + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + } + + // Create leaf certificate signed by the CA + leafCertBytes, err := x509.CreateCertificate( + rand.Reader, + leafTemplate, + caTemplate, + &leafPrivKey.PublicKey, + caPrivKey, + ) + if err != nil { + fmt.Printf("Failed to create leaf certificate: %v\n", err) + return + } + + // Encode leaf certificate to PEM format + leafCertPEM := pem.EncodeToMemory( + &pem.Block{ + Type: "CERTIFICATE", + Bytes: leafCertBytes, + }, + ) + + // Encode leaf private key to PEM format + leafPrivKeyPEM := pem.EncodeToMemory( + &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(leafPrivKey), + }, + ) + + // Save leaf certificate and private key to files + err = os.WriteFile("leaf_cert.pem", leafCertPEM, 0644) + if err != nil { + fmt.Printf("Failed to write leaf certificate to file: %v\n", err) + return + } + err = os.WriteFile("leaf_key.pem", leafPrivKeyPEM, 0600) + if err != nil { + fmt.Printf("Failed to write leaf private key to file: %v\n", err) + return + } + + fmt.Println("CA and leaf certificates generated successfully.") +} diff --git a/lib/test_ca_cert.pem b/lib/test_ca_cert.pem new file mode 100644 index 0000000..3dd6c24 --- /dev/null +++ b/lib/test_ca_cert.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC9zCCAd+gAwIBAgIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQKExJNeSBD +QSBPcmdhbml6YXRpb24wHhcNMjQxMDE2MjIwMTQyWhcNMzQxMDE2MjIwMTQyWjAd +MRswGQYDVQQKExJNeSBDQSBPcmdhbml6YXRpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQC3DFiSb9+ykr8kSwlahq3lauN5BhFMsFP4nnYH9M2M0z2c +cSLwF/ZpGYkzOVbHiwlpuPfmpm0weCnhxfHpX7Yd20g3Jr2M++enYRX8XGprhxw3 +GI1F1UMe/RPJxMhGEMzfIbiWZDXGWJI5v40CItq4Pgvw5jN9NYQlZjQ5YP5wEGI1 +JbYn47pxnL+SctmPIy5647WnNR4EL9Sb5ErNr91hebP/b8LwFhd/f1z+eUo52Xqa +U/SZYMVrRiPq2nK++kDDoj0SV83q7WscTqRO2qwxyOwklzrUvFn7W1IzijjRciWA +12GGP1mIaZbPOTIt8dYre9TgVZ1vl9ayrJADGBuNAgMBAAGjQjBAMA4GA1UdDwEB +/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR7esJSE+f03gVNuqWa +fIWHu3CPtTANBgkqhkiG9w0BAQsFAAOCAQEAajiD2zpM77agcz6xqO1+Y2RGb147 +bJb8CPBvZECYAel++dyNGy8kJF4vDiuQ4oRExbM6VUbEiYK2aXeapE2IMVpUY6No +9JRgmQUDut667njWBv3DhjpV5ZhyI4YHZykXzXjI0eIEbdihfdwx237tvJHOzEeg +4/xRfDuJxBtZOtctRuMBzkuTCQ85lyrOLLOO81mqmnEbF9d+9WmwS4mu9fta7b5H +dCqTmtVncnxlE5PmstyeJaVhXbx4MA2BMIhJiusBw5WqKFYFbmhuGpeyEJx3JIZU +tfE5aamMIbwnwk2VBwfUqHAuQVUxlz/tmRyZ7ALvykZJdJVrUxkKttgjWw== +-----END CERTIFICATE----- diff --git a/lib/test_chain.pem b/lib/test_chain.pem new file mode 100644 index 0000000..5546fd8 --- /dev/null +++ b/lib/test_chain.pem @@ -0,0 +1,36 @@ +-----BEGIN CERTIFICATE----- +MIIC6DCCAdCgAwIBAgIBAjANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQKExJNeSBD +QSBPcmdhbml6YXRpb24wHhcNMjQxMDE2MjIwMTQyWhcNMjUxMDE2MjIwMTQyWjAf +MR0wGwYDVQQKExRNeSBMZWFmIE9yZ2FuaXphdGlvbjCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAOuOsSztuF7JDXrqvrM6bKSQ6VqpCEHtR5sJnndlrs/V +Rxsxltk2qBigGIDqQYByM1pcT3WkA2619BHHxaDfcD2Zvx718dUoF1EGzadiEXOR +946MZlW194qO7++Y8soB4ru36fSGfsK9wr6gOVSYmHINORV+giUvxgbvIpNfQAZI +39nWNKkR14BLjmkZO6CXomu2W/ZT1WXJ7FDNyF28ww6h5AOCvox9YIRf3OV0GaNE +kAdoJsw95t64C407P96a6DnaRjWgGkNYTfKf3yPsavilxguXXpqLFlYU7flONA0T +/ThTjuiRwOShGHEaRr/4iswuxvPWnHTAGXX2K/2wjqkCAwEAAaMxMC8wDgYDVR0P +AQH/BAQDAgeAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATANBgkqhkiG +9w0BAQsFAAOCAQEAIJje0ts+350wREkB3E0ud/KRL7Ra734YhSxlo+i0UKwGR4oM +hU2xg+0A7B9uJii9L58oMWTZ+JJpA1Fb4tagksUuBI/rxjNWQ7QSvlhx2Zkwsr5x +l0nlfxMuzfgRBD/eafxIaD/Li/iiPHQNJyU4iNImmw2r8IW+rtbU/sCVG/OjqZEc +oKsw+qk9veyH/oZb19HSpK28A6voc2YuNsL4ghMbMWicITFle+Sv6yBEHDIxvQB6 +2pvsQ+H6EY25YfAz2AQNaz6U32CA6KdaQQh87wXAnagM4S6s7SmsZVgX/vGcUS5n +91NEq4QRzFuINWPF38AsyOR9r2B2xuuCrulsQQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIC9zCCAd+gAwIBAgIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQKExJNeSBD +QSBPcmdhbml6YXRpb24wHhcNMjQxMDE2MjIwMTQyWhcNMzQxMDE2MjIwMTQyWjAd +MRswGQYDVQQKExJNeSBDQSBPcmdhbml6YXRpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQC3DFiSb9+ykr8kSwlahq3lauN5BhFMsFP4nnYH9M2M0z2c +cSLwF/ZpGYkzOVbHiwlpuPfmpm0weCnhxfHpX7Yd20g3Jr2M++enYRX8XGprhxw3 +GI1F1UMe/RPJxMhGEMzfIbiWZDXGWJI5v40CItq4Pgvw5jN9NYQlZjQ5YP5wEGI1 +JbYn47pxnL+SctmPIy5647WnNR4EL9Sb5ErNr91hebP/b8LwFhd/f1z+eUo52Xqa +U/SZYMVrRiPq2nK++kDDoj0SV83q7WscTqRO2qwxyOwklzrUvFn7W1IzijjRciWA +12GGP1mIaZbPOTIt8dYre9TgVZ1vl9ayrJADGBuNAgMBAAGjQjBAMA4GA1UdDwEB +/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR7esJSE+f03gVNuqWa +fIWHu3CPtTANBgkqhkiG9w0BAQsFAAOCAQEAajiD2zpM77agcz6xqO1+Y2RGb147 +bJb8CPBvZECYAel++dyNGy8kJF4vDiuQ4oRExbM6VUbEiYK2aXeapE2IMVpUY6No +9JRgmQUDut667njWBv3DhjpV5ZhyI4YHZykXzXjI0eIEbdihfdwx237tvJHOzEeg +4/xRfDuJxBtZOtctRuMBzkuTCQ85lyrOLLOO81mqmnEbF9d+9WmwS4mu9fta7b5H +dCqTmtVncnxlE5PmstyeJaVhXbx4MA2BMIhJiusBw5WqKFYFbmhuGpeyEJx3JIZU +tfE5aamMIbwnwk2VBwfUqHAuQVUxlz/tmRyZ7ALvykZJdJVrUxkKttgjWw== +-----END CERTIFICATE----- diff --git a/lib/test_leaf_cert.pem b/lib/test_leaf_cert.pem new file mode 100644 index 0000000..379d028 --- /dev/null +++ b/lib/test_leaf_cert.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC6DCCAdCgAwIBAgIBAjANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQKExJNeSBD +QSBPcmdhbml6YXRpb24wHhcNMjQxMDE2MjIwMTQyWhcNMjUxMDE2MjIwMTQyWjAf +MR0wGwYDVQQKExRNeSBMZWFmIE9yZ2FuaXphdGlvbjCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAOuOsSztuF7JDXrqvrM6bKSQ6VqpCEHtR5sJnndlrs/V +Rxsxltk2qBigGIDqQYByM1pcT3WkA2619BHHxaDfcD2Zvx718dUoF1EGzadiEXOR +946MZlW194qO7++Y8soB4ru36fSGfsK9wr6gOVSYmHINORV+giUvxgbvIpNfQAZI +39nWNKkR14BLjmkZO6CXomu2W/ZT1WXJ7FDNyF28ww6h5AOCvox9YIRf3OV0GaNE +kAdoJsw95t64C407P96a6DnaRjWgGkNYTfKf3yPsavilxguXXpqLFlYU7flONA0T +/ThTjuiRwOShGHEaRr/4iswuxvPWnHTAGXX2K/2wjqkCAwEAAaMxMC8wDgYDVR0P +AQH/BAQDAgeAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATANBgkqhkiG +9w0BAQsFAAOCAQEAIJje0ts+350wREkB3E0ud/KRL7Ra734YhSxlo+i0UKwGR4oM +hU2xg+0A7B9uJii9L58oMWTZ+JJpA1Fb4tagksUuBI/rxjNWQ7QSvlhx2Zkwsr5x +l0nlfxMuzfgRBD/eafxIaD/Li/iiPHQNJyU4iNImmw2r8IW+rtbU/sCVG/OjqZEc +oKsw+qk9veyH/oZb19HSpK28A6voc2YuNsL4ghMbMWicITFle+Sv6yBEHDIxvQB6 +2pvsQ+H6EY25YfAz2AQNaz6U32CA6KdaQQh87wXAnagM4S6s7SmsZVgX/vGcUS5n +91NEq4QRzFuINWPF38AsyOR9r2B2xuuCrulsQQ== +-----END CERTIFICATE----- diff --git a/main.go b/main.go new file mode 100644 index 0000000..96f5009 --- /dev/null +++ b/main.go @@ -0,0 +1,168 @@ +// Copyright 2024 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + + "github.com/Keyfactor/keyfactor-auth-client-go/pkg" +) + +func main() { + fmt.Println("Version:", pkg.Version) // print the package version + fmt.Println("Build:", pkg.BuildTime) // print the package build + fmt.Println("Commit:", pkg.CommitHash) // print the package commit + //testClients() +} + +//func testClients() { +// // URL to test against +// url := os.Getenv("KEYFACTOR_AUTH_TOKEN_URL") +// caCertPath := os.Getenv("KEYFACTOR_CA_CERT") +// +// // Load the custom root CA certificate +// caCert, err := os.ReadFile(caCertPath) +// if err != nil { +// log.Fatalf("Failed to read root CA certificate: %v", err) +// } +// +// // Create a certificate pool and add the custom root CA +// caCertPool := x509.NewCertPool() +// if !caCertPool.AppendCertsFromPEM(caCert) { +// log.Fatalf("Failed to append root CA certificate to pool") +// } +// +// // OAuth2 client credentials configuration +// clientId := os.Getenv("KEYFACTOR_AUTH_CLIENT_ID") +// clientSecret := os.Getenv("KEYFACTOR_AUTH_CLIENT_SECRET") +// oauthConfig := &clientcredentials.Config{ +// ClientID: clientId, +// ClientSecret: clientSecret, +// TokenURL: url, +// } +// +// // Transport with default TLS verification (InsecureSkipVerify = false) +// transportDefaultTLS := &http.Transport{ +// TLSClientConfig: &tls.Config{InsecureSkipVerify: false}, +// } +// +// // Transport with TLS verification skipped (InsecureSkipVerify = true) +// transportInsecureTLS := &http.Transport{ +// TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, +// } +// +// // Transport with custom CA verification +// transportCustomRootCA := &http.Transport{ +// TLSClientConfig: &tls.Config{ +// RootCAs: caCertPool, // Custom root CA pool +// InsecureSkipVerify: false, // Enforce TLS verification +// }, +// } +// +// // OAuth2 Token Sources +// tokenSourceDefaultTLS := oauthConfig.TokenSource(context.Background()) +// +// ctxInsecure := context.WithValue( +// context.Background(), +// oauth2.HTTPClient, +// &http.Client{Transport: transportInsecureTLS}, +// ) +// tokenSourceInsecureTLS := oauthConfig.TokenSource(ctxInsecure) +// +// ctxCustomCA := context.WithValue( +// context.Background(), +// oauth2.HTTPClient, +// &http.Client{Transport: transportCustomRootCA}, +// ) +// tokenSourceCustomRootCA := oauthConfig.TokenSource(ctxCustomCA) +// +// // OAuth2 clients with different transports +// oauthClientDefaultTLS := &http.Client{ +// Transport: &oauth2Transport{ +// base: transportDefaultTLS, +// src: tokenSourceDefaultTLS, +// }, +// } +// +// oauthClientInsecureTLS := &http.Client{ +// Transport: &oauth2Transport{ +// base: transportInsecureTLS, +// src: tokenSourceInsecureTLS, +// }, +// } +// +// oauthClientCustomRootCA := &http.Client{ +// Transport: &oauth2Transport{ +// base: transportCustomRootCA, +// src: tokenSourceCustomRootCA, +// }, +// } +// +// // Prepare the GET request +// req, err := http.NewRequest("GET", url, nil) +// if err != nil { +// log.Fatalf("Failed to create request: %v", err) +// } +// +// // Test 1: OAuth2 client with default TLS verification (expected to fail if certificate is invalid) +// fmt.Println("Testing OAuth2 client with default TLS verification...") +// resp1, err1 := oauthClientDefaultTLS.Do(req) +// if err1 != nil { +// log.Printf("OAuth2 client with default TLS failed as expected: %v\n", err1) +// } else { +// fmt.Printf("OAuth2 client with default TLS succeeded: %s\n", resp1.Status) +// resp1.Body.Close() +// } +// +// // Test 2: OAuth2 client with skipped TLS verification (should succeed) +// fmt.Println("\nTesting OAuth2 client with skipped TLS verification...") +// resp2, err2 := oauthClientInsecureTLS.Do(req) +// if err2 != nil { +// log.Fatalf("OAuth2 client with skipped TLS failed: %v\n", err2) +// } else { +// fmt.Printf("OAuth2 client with skipped TLS succeeded: %s\n", resp2.Status) +// resp2.Body.Close() +// } +// +// // Test 3: OAuth2 client with custom root CA (should succeed if the CA is valid) +// fmt.Println("\nTesting OAuth2 client with custom root CA verification...") +// resp3, err3 := oauthClientCustomRootCA.Do(req) +// if err3 != nil { +// log.Fatalf("OAuth2 client with custom root CA failed: %v\n", err3) +// } else { +// fmt.Printf("OAuth2 client with custom root CA succeeded: %s\n", resp3.Status) +// resp3.Body.Close() +// } +//} +// +//// oauth2Transport is a custom RoundTripper that injects the OAuth2 token into requests +//type oauth2Transport struct { +// base http.RoundTripper +// src oauth2.TokenSource +//} +// +//// RoundTrip executes a single HTTP transaction, adding the OAuth2 token to the request +//func (t *oauth2Transport) RoundTrip(req *http.Request) (*http.Response, error) { +// token, err := t.src.Token() +// if err != nil { +// return nil, fmt.Errorf("failed to retrieve OAuth token: %w", err) +// } +// +// // Clone the request to avoid mutating the original +// reqCopy := req.Clone(req.Context()) +// token.SetAuthHeader(reqCopy) +// +// return t.base.RoundTrip(reqCopy) +//} diff --git a/pkg/version.go b/pkg/version.go new file mode 100644 index 0000000..1bc3d80 --- /dev/null +++ b/pkg/version.go @@ -0,0 +1,21 @@ +// Copyright 2024 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pkg + +var ( + Version string + BuildTime string + CommitHash string +) diff --git a/scripts/auth_keycloak.ps1 b/scripts/auth_keycloak.ps1 new file mode 100644 index 0000000..e2f889d --- /dev/null +++ b/scripts/auth_keycloak.ps1 @@ -0,0 +1,64 @@ +# Copyright 2024 Keyfactor +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http:#www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +function CheckVars { + # Check hostname + if (!$env:KEYFACTOR_AUTH_HOSTNAME) { + # Check if KEYFACTOR_HOSTNAME is set + if (!$env:KEYFACTOR_HOSTNAME) { + Write-Host "KEYFACTOR_HOSTNAME is not set" + # prompt for hostname until it is set + while (!$env:KEYFACTOR_HOSTNAME) { + $env:KEYFACTOR_AUTH_HOSTNAME = Read-Host "Enter auth hostname" + } + } else { + Write-Host "Setting auth hostname to $env:KEYFACTOR_HOSTNAME" + $env:KEYFACTOR_AUTH_HOSTNAME = $env:KEYFACTOR_HOSTNAME + } + } + + if (!$env:KEYFACTOR_CLIENT_ID) { + Write-Host "KEYFACTOR_CLIENT_ID is not set" + # prompt for client_id until it is set + while (!$env:KEYFACTOR_CLIENT_ID) { + $env:KEYFACTOR_CLIENT_ID = Read-Host "Enter client_id" + } + } + + if (!$env:KEYFACTOR_CLIENT_SECRET) { + Write-Host "KEYFACTOR_CLIENT_SECRET is not set" + while (!$env:KEYFACTOR_CLIENT_SECRET) { + #prompt for sensitive client_secret until it is set + $env:KEYFACTOR_CLIENT_SECRET = Read-Host "Enter client_secret" + } + } +} + +function AuthClientCredentials { + CheckVars + $client_id = $env:KEYFACTOR_CLIENT_ID + $client_secret = $env:KEYFACTOR_CLIENT_SECRET + $grant_type = "client_credentials" + $auth_url = "https://$env:KEYFACTOR_AUTH_HOSTNAME:$($env:KEYFACTOR_AUTH_PORT -replace '^$', '8444')/realms/$($env:KEYFACTOR_AUTH_REALM -replace '^$', 'Keyfactor')/protocol/openid-connect/token" + + $response = Invoke-RestMethod -Uri $auth_url -Method POST -Body @{ + "grant_type" = $grant_type + "client_id" = $client_id + "client_secret" = $client_secret + } -ContentType 'application/x-www-form-urlencoded' + + $env:KEYFACTOR_ACCESS_TOKEN = $response.access_token +} + +AuthClientCredentials \ No newline at end of file diff --git a/scripts/auth_keycloak.sh b/scripts/auth_keycloak.sh new file mode 100644 index 0000000..0d55a0a --- /dev/null +++ b/scripts/auth_keycloak.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash + +# Copyright 2024 Keyfactor +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http:#www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Input vars +function checkVars() { + # Check hostname + if [ -z "$KEYFACTOR_AUTH_HOSTNAME" ]; then + # Check if KEYFACTOR_HOSTNAME is set + if [ -z "$KEYFACTOR_HOSTNAME" ]; then + echo "KEYFACTOR_HOSTNAME is not set" + # prompt for hostname until it is set + while [ -z "$KEYFACTOR_HOSTNAME" ]; do + read -p "Enter auth hostname: " KEYFACTOR_AUTH_HOSTNAME + done + else + echo "Setting auth hostname to $KEYFACTOR_HOSTNAME" + KEYFACTOR_AUTH_HOSTNAME="$KEYFACTOR_HOSTNAME" + fi + fi + + if [ -z "$KEYFACTOR_CLIENT_ID" ]; then + echo "KEYFACTOR_CLIENT_ID is not set" + # prompt for client_id until it is set + while [ -z "$KEYFACTOR_CLIENT_ID" ]; do + read -p "Enter client_id: " KEYFACTOR_CLIENT_ID + done + fi + + if [ -z "$KEYFACTOR_CLIENT_SECRET" ]; then + echo "KEYFACTOR_CLIENT_SECRET is not set" + while [ -z "$KEYFACTOR_CLIENT_SECRET" ]; do + #prompt for sensitive client_secret until it is set + read -s -p "Enter client_secret: " KEYFACTOR_CLIENT_SECRET + done + fi +} + +function authClientCredentials(){ + checkVars + client_id="${KEYFACTOR_CLIENT_ID}" + client_secret="${KEYFACTOR_CLIENT_SECRET}" + grant_type="client_credentials" + auth_url="https://$KEYFACTOR_AUTH_HOSTNAME:${KEYFACTOR_AUTH_PORT:-8444}/realms/${KEYFFACTOR_AUTH_REALM:-Keyfactor}/protocol/openid-connect/token" + + curl -X POST $auth_url \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode "grant_type=$grant_type" \ + --data-urlencode "client_id=$client_id" \ + --data-urlencode "client_secret=$client_secret" > keyfactor_auth.json + + export KEYFACTOR_ACCESS_TOKEN=$(cat keyfactor_auth.json | jq -r '.access_token') + +} + +authClientCredentials \ No newline at end of file diff --git a/tag.sh b/tag.sh new file mode 100644 index 0000000..115bbfd --- /dev/null +++ b/tag.sh @@ -0,0 +1,5 @@ +RC_VERSION=rc.2 +TAG_VERSION_1=v1.0.0-$RC_VERSION +git tag -d $TAG_VERSION_1 || true +git tag $TAG_VERSION_1 +git push origin $TAG_VERSION_1 \ No newline at end of file