From 9a7c3ed447ac3539bdffd8e56f73050b2ee60249 Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Fri, 3 May 2024 08:59:27 -0700 Subject: [PATCH 01/57] feat: Initial commit --- .gitignore | 139 ++++++++++++++++ README.md | 37 ++++- auth_providers/active_directory/ad_auth.go | 76 +++++++++ auth_providers/active_directory/models.go | 16 ++ auth_providers/auth_core.go | 31 ++++ .../keycloak/keycloak_auth_certificate.go | 1 + .../keycloak_auth_client_credentials.go | 157 ++++++++++++++++++ auth_providers/keycloak/models.go | 56 +++++++ auth_providers/models.go | 28 ++++ go.mod | 3 + main.go | 21 +++ scripts/auth_keycloak.sh | 54 ++++++ 12 files changed, 618 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 auth_providers/active_directory/ad_auth.go create mode 100644 auth_providers/active_directory/models.go create mode 100644 auth_providers/auth_core.go create mode 100644 auth_providers/keycloak/keycloak_auth_certificate.go create mode 100644 auth_providers/keycloak/keycloak_auth_client_credentials.go create mode 100644 auth_providers/keycloak/models.go create mode 100644 auth_providers/models.go create mode 100644 go.mod create mode 100644 main.go create mode 100755 scripts/auth_keycloak.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9ab1d97 --- /dev/null +++ b/.gitignore @@ -0,0 +1,139 @@ +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Terraform template +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which are likely to contain sensitive data, such as +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject +# to change depending on the environment. +*.tfvars +*.tfvars.json + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Include override files you do wish to add to version control using negated pattern +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc + +### Go template +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +*.env* \ No newline at end of file diff --git a/README.md b/README.md index 8e27d92..e8bee6a 100644 --- a/README.md +++ b/README.md @@ -1 +1,36 @@ -# 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 | + +### Active Directory + +| 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 `\\` | | + +### Keycloak + +| Name | Description | Default | +|--------------------------|---------------------------------------------------------------------------------------------------------------------------------|-------------| +| KEYFACTOR_AUTH_HOST_NAME | Hostname of Keycloak instance to authenticate to Keyfactor Command | | +| KEYFACTOR_AUTH_REALM | Keyfactor Auth Realm | `Keyfactor` | +| KEYFACTOR_ACCESS_TOKEN | Access token to use to authenticate to Keyfactor Command API. This can be supplied directly or generated via client credentials | | +| KEYFACTOR_SCOPES | Scopes to request when authenticating to Keyfactor Command API | `openid` | + +#### Client Credentials + +| Name | Description | Default | +|------------------------------|------------------------------|---------| +| KEYFACTOR_AUTH_CLIENT_ID | Keyfactor Auth Client ID | | +| KEYFACTOR_AUTH_CLIENT_SECRET | Keyfactor Auth Client Secret | | diff --git a/auth_providers/active_directory/ad_auth.go b/auth_providers/active_directory/ad_auth.go new file mode 100644 index 0000000..5c661ab --- /dev/null +++ b/auth_providers/active_directory/ad_auth.go @@ -0,0 +1,76 @@ +package active_directory + +import ( + "encoding/base64" + "fmt" + "os" + "strings" +) + +func (c *CommandAuthConfigActiveDirectory) Authenticate() error { + cErr := c.ValidateAuthConfig() + if cErr != nil { + return cErr + } + + c.AuthHeader = fmt.Sprintf("Basic %s", c.getBasicAuthHeader()) + return nil +} + +func (c *CommandAuthConfigActiveDirectory) getBasicAuthHeader() string { + authStr := fmt.Sprintf("%s@%s:%s", c.Domain, c.Username, c.Password) + return base64.StdEncoding.EncodeToString([]byte(authStr)) +} + +func (c *CommandAuthConfigActiveDirectory) ValidateAuthConfig() error { + cErr := c.CommandAuthConfigBasic.ValidateAuthConfig() + if cErr != nil { + return cErr + } + + if c.Username == "" { + if username, ok := os.LookupEnv(EnvKeyfactorUsername); ok { + c.Username = username + } else { + return fmt.Errorf("username or environment variable %s is required", EnvKeyfactorUsername) + } + } + + if c.Password == "" { + if password, ok := os.LookupEnv(EnvKeyfactorPassword); ok { + c.Password = password + } else { + return fmt.Errorf("password or environment variable KEYFACTOR_PASSWORD is required") + } + } + + if c.Domain == "" { + if domain, ok := os.LookupEnv("KEYFACTOR_DOMAIN"); ok { + c.Domain = domain + } else { + //check if domain is in username with @ or \\ + if strings.Contains(c.Username, "@") { + domain := strings.Split(c.Username, "@") + if len(domain) != 2 { + return fmt.Errorf("domain or environment variable %s is required", EnvKeyfactorDomain) + } + c.Username = domain[0] // remove domain from username + c.Domain = domain[1] + } else if strings.Contains(c.Username, "\\") { + domain := strings.Split(c.Username, "\\") + if len(domain) != 2 { + return fmt.Errorf("domain or environment variable %s is required", EnvKeyfactorDomain) + } + c.Domain = domain[0] + c.Username = domain[1] // remove domain from username + } else { + return fmt.Errorf("domain or environment variable %s is required", EnvKeyfactorDomain) + } + } + } + return nil +} + +func (c *CommandAuthConfigActiveDirectory) GetAuthHeader() string { + return c.AuthHeader +} diff --git a/auth_providers/active_directory/models.go b/auth_providers/active_directory/models.go new file mode 100644 index 0000000..cd157ba --- /dev/null +++ b/auth_providers/active_directory/models.go @@ -0,0 +1,16 @@ +package active_directory + +import ( + "keyfactor_auth/auth_providers" +) + +const ( + EnvKeyfactorDomain = "KEYFACTOR_DOMAIN" + EnvKeyfactorUsername = "KEYFACTOR_USERNAME" + EnvKeyfactorPassword = "KEYFACTOR_PASSWORD" +) + +type CommandAuthConfigActiveDirectory struct { + auth_providers.CommandAuthConfigBasic + Domain string `json:"domain"` +} diff --git a/auth_providers/auth_core.go b/auth_providers/auth_core.go new file mode 100644 index 0000000..eeab654 --- /dev/null +++ b/auth_providers/auth_core.go @@ -0,0 +1,31 @@ +package auth_providers + +import ( + "fmt" + "os" +) + +func (c *CommandAuthConfig) ValidateAuthConfig() error { + if c.CommandHostName == "" { + if hostName, ok := os.LookupEnv(EnvKeyfactorHostName); ok { + c.CommandHostName = hostName + } else { + return fmt.Errorf("command_host_name or environment variable %s is required", EnvKeyfactorHostName) + } + } + if c.CommandPort == "" { + if port, ok := os.LookupEnv(EnvKeyfactorPort); ok { + c.CommandPort = port + } else { + c.CommandPort = DefaultCommandPort + } + } + if c.CommandAPIPath == "" { + if apiPath, ok := os.LookupEnv(EnvKeyfactorAPIPath); ok { + c.CommandAPIPath = apiPath + } else { + c.CommandAPIPath = DefaultCommandAPIPath + } + } + return nil +} diff --git a/auth_providers/keycloak/keycloak_auth_certificate.go b/auth_providers/keycloak/keycloak_auth_certificate.go new file mode 100644 index 0000000..cf172a0 --- /dev/null +++ b/auth_providers/keycloak/keycloak_auth_certificate.go @@ -0,0 +1 @@ +package keycloak diff --git a/auth_providers/keycloak/keycloak_auth_client_credentials.go b/auth_providers/keycloak/keycloak_auth_client_credentials.go new file mode 100644 index 0000000..b564907 --- /dev/null +++ b/auth_providers/keycloak/keycloak_auth_client_credentials.go @@ -0,0 +1,157 @@ +package keycloak + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "os" + "strings" + "time" +) + +const ( + DefaultKeyfactorAuthPort = "8444" + DefaultKeyfactorRealm = "Keyfactor" + EnvKeyfactorClientID = "KEYFACTOR_CLIENT_ID" + EnvKeyfactorClientSecret = "KEYFACTOR_CLIENT_SECRET" + EnvKeyfactorAuthRealm = "KEYFACTOR_AUTH_REALM" + EnvKeyfactorAuthTokenURL = "KEYFACTOR_AUTH_TOKEN_URL" +) + +func (c *CommandAuthKeyCloakClientCredentials) Authenticate() error { + c.HttpClient = &http.Client{} + c.AuthType = "client_credentials" + cErr := c.ValidateAuthConfig() + if cErr != nil { + return cErr + } + + token, tErr := c.GetToken() + if tErr != nil { + return tErr + } + if token == "" { + return fmt.Errorf("failed to get Bearer token using client credentials") + } + + c.AuthHeader = fmt.Sprintf("Bearer %s", token) + + return nil +} + +func (c *CommandAuthKeyCloakClientCredentials) ValidateAuthConfig() error { + cErr := c.CommandAuthConfigKeyCloak.ValidateAuthConfig() + if cErr != nil { + return cErr + } + + if c.ClientID == "" { + if clientID, ok := os.LookupEnv(EnvKeyfactorClientID); ok { + c.ClientID = clientID + } else { + return fmt.Errorf("client_id or environment variable %s is required", EnvKeyfactorClientID) + } + } + if c.ClientSecret == "" { + if clientSecret, ok := os.LookupEnv(EnvKeyfactorClientSecret); ok { + c.ClientSecret = clientSecret + } else { + return fmt.Errorf("client_secret or environment variable %s is required", EnvKeyfactorClientSecret) + } + } + if c.Realm == "" { + if realm, ok := os.LookupEnv(EnvKeyfactorAuthRealm); ok { + c.Realm = realm + } else { + c.Realm = DefaultKeyfactorRealm + } + } + + if c.TokenURL == "" { + if tokenURL, ok := os.LookupEnv(EnvKeyfactorAuthTokenURL); ok { + c.TokenURL = tokenURL + } else { + c.TokenURL = fmt.Sprintf( + "https://%s:%s/realms/%s/protocol/openid-connect/token", + c.AuthHostName, + c.AuthPort, + c.Realm, + ) + } + } + return nil +} + +func (c *CommandAuthKeyCloakClientCredentials) GetToken() (string, error) { + if c.AccessToken != "" && time.Now().Before(c.Expiry) { + return c.AccessToken, nil + } + + // Use refresh token if available and not expired + if c.RefreshToken != "" && time.Now().After(c.Expiry) { + return c.refreshAccessToken() + } + + // Otherwise, get a new access token using client credentials + return c.requestNewToken() +} + +func (c *CommandAuthKeyCloakClientCredentials) requestNewToken() (string, error) { + formData := url.Values{} + formData.Set("grant_type", "client_credentials") + formData.Set("client_id", c.ClientID) + formData.Set("client_secret", c.ClientSecret) + + return c.doTokenRequest(formData.Encode()) +} + +func (c *CommandAuthKeyCloakClientCredentials) refreshAccessToken() (string, error) { + formData := url.Values{} + formData.Set("grant_type", "refresh_token") + formData.Set("client_id", c.ClientID) + formData.Set("client_secret", c.ClientSecret) + formData.Set("refresh_token", c.RefreshToken) + return c.doTokenRequest(formData.Encode()) +} + +func (c *CommandAuthKeyCloakClientCredentials) doTokenRequest(data string) (string, error) { + requestBody := strings.NewReader(data) + req, reqErr := http.NewRequest("POST", c.TokenURL, requestBody) + if reqErr != nil { + return "", reqErr + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, tkRespErr := c.HttpClient.Do(req) + if tkRespErr != nil { + return "", tkRespErr + } + defer resp.Body.Close() + + // check response status code + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + body, ioErr := io.ReadAll(resp.Body) + if ioErr != nil { + return "", ioErr + } + + var tokenResponse struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + ExpiresIn int `json:"expires_in"` + } + if err := json.Unmarshal(body, &tokenResponse); err != nil { + return "", err + } + + c.AccessToken = tokenResponse.AccessToken + c.RefreshToken = tokenResponse.RefreshToken + c.Expiry = time.Now().Add(time.Duration(tokenResponse.ExpiresIn-30) * time.Second) // Subtract 30 seconds to account for delay + + return c.AccessToken, nil +} diff --git a/auth_providers/keycloak/models.go b/auth_providers/keycloak/models.go new file mode 100644 index 0000000..c5aa986 --- /dev/null +++ b/auth_providers/keycloak/models.go @@ -0,0 +1,56 @@ +package keycloak + +import ( + "os" + "time" + + "keyfactor_auth/auth_providers" +) + +const ( + EnvKeyfactorAuthHostname = "KEYFACTOR_AUTH_HOST_NAME" + EnvKeyfactorAuthPort = "KEYFACTOR_AUTH_PORT" + EnvKeyfactorAccessToken = "KEYFACTOR_ACCESS_TOKEN" +) + +type CommandAuthConfigKeyCloak struct { + auth_providers.CommandAuthConfig + AuthHostName string `json:"auth_host_name"` + AuthPort string `json:"auth_port"` + AuthType string `json:"auth_type"` // The type of Keycloak auth to use such as client_credentials, password, etc. +} + +func (c *CommandAuthConfigKeyCloak) ValidateAuthConfig() error { + pErr := c.CommandAuthConfig.ValidateAuthConfig() + if pErr != nil { + return pErr + } + + if c.AuthHostName == "" { + if authHostName, ok := os.LookupEnv(EnvKeyfactorAuthHostname); ok { + c.AuthHostName = authHostName + } else { + c.AuthHostName = c.CommandHostName + } + } + if c.AuthPort == "" { + if port, ok := os.LookupEnv(EnvKeyfactorAuthPort); ok { + c.AuthPort = port + } else { + c.AuthPort = DefaultKeyfactorAuthPort + } + } + return nil + +} + +type CommandAuthKeyCloakClientCredentials struct { + CommandAuthConfigKeyCloak + ClientID string `json:"client_id"` + ClientSecret string `json:"client_secret"` + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + Expiry time.Time `json:"expiry"` + Realm string `json:"realm"` + TokenURL string `json:"token_url"` +} diff --git a/auth_providers/models.go b/auth_providers/models.go new file mode 100644 index 0000000..7659d8e --- /dev/null +++ b/auth_providers/models.go @@ -0,0 +1,28 @@ +package auth_providers + +import ( + "net/http" +) + +type CommandAuthConfig struct { + ConfigType string `json:"config_type"` + AuthHeader string `json:"auth_header"` + CommandHostName string `json:"command_host_name"` + CommandPort string `json:"command_port"` + CommandAPIPath string `json:"command_api_path"` + HttpClient *http.Client +} + +const ( + DefaultCommandPort = "443" + DefaultCommandAPIPath = "KeyfactorAPI" + EnvKeyfactorHostName = "KEYFACTOR_HOSTNAME" + EnvKeyfactorPort = "KEYFACTOR_PORT" + EnvKeyfactorAPIPath = "KEYFACTOR_API_PATH" +) + +type CommandAuthConfigBasic struct { + CommandAuthConfig + Username string `json:"username"` + Password string `json:"password"` +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..bbe8ccd --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module keyfactor_auth + +go 1.22 diff --git a/main.go b/main.go new file mode 100644 index 0000000..e36cb7b --- /dev/null +++ b/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "log" + + "keyfactor_auth/auth_providers/keycloak" +) + +func main() { + //try client auth + keyfactorAuthConfig := keycloak.CommandAuthKeyCloakClientCredentials{} + aErr := keyfactorAuthConfig.Authenticate() + if aErr != nil { + log.Fatalf("[ERROR] %s\n", aErr) + } + log.Println("[INFO] Successfully authenticated with Keyfactor") + log.Println("[INFO] Token: ", keyfactorAuthConfig.AccessToken) + log.Println("[INFO] Refresh Token: ", keyfactorAuthConfig.RefreshToken) + log.Println("[INFO] Token Expiry: ", keyfactorAuthConfig.Expiry) + +} diff --git a/scripts/auth_keycloak.sh b/scripts/auth_keycloak.sh new file mode 100755 index 0000000..3f2554b --- /dev/null +++ b/scripts/auth_keycloak.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash + +# 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/realms/$KEYFFACTOR_AUTH_REALM/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 From 32d7f1b5d981f524c8336d8e70a27e8b079d9a72 Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Fri, 3 May 2024 09:26:21 -0700 Subject: [PATCH 02/57] feat(script): Adding powershell script for auth against Keycloak. --- scripts/auth_keycloak.ps1 | 50 +++++++++++++++++++++++++++++++++++++++ scripts/auth_keycloak.sh | 2 +- 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 scripts/auth_keycloak.ps1 diff --git a/scripts/auth_keycloak.ps1 b/scripts/auth_keycloak.ps1 new file mode 100644 index 0000000..3e97c9c --- /dev/null +++ b/scripts/auth_keycloak.ps1 @@ -0,0 +1,50 @@ +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 index 3f2554b..b01f7a4 100755 --- a/scripts/auth_keycloak.sh +++ b/scripts/auth_keycloak.sh @@ -39,7 +39,7 @@ function authClientCredentials(){ client_id="${KEYFACTOR_CLIENT_ID}" client_secret="${KEYFACTOR_CLIENT_SECRET}" grant_type="client_credentials" - auth_url="https://$KEYFACTOR_AUTH_HOSTNAME:$KEYFACTOR_AUTH_PORT/realms/$KEYFFACTOR_AUTH_REALM/protocol/openid-connect/token" + 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' \ From d2a5c807e16dba0dec8e54d685ab0b4234cd1290 Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Fri, 3 May 2024 10:23:58 -0700 Subject: [PATCH 03/57] chore(docs): Add license headers feat(pkg): Add Makefile for build ops and pkg versioning. --- Makefile | 80 +++++++++++++++++++ README.md | 10 +++ auth_providers/active_directory/ad_auth.go | 14 ++++ auth_providers/active_directory/models.go | 16 +++- auth_providers/auth_core.go | 14 ++++ .../keycloak/keycloak_auth_certificate.go | 14 ++++ .../keycloak_auth_client_credentials.go | 14 ++++ .../keycloak_auth_client_credentials_test.go | 75 +++++++++++++++++ auth_providers/keycloak/models.go | 16 +++- auth_providers/models.go | 14 ++++ go.mod | 16 +++- main.go | 32 +++++--- pkg/version.go | 7 ++ scripts/auth_keycloak.ps1 | 14 ++++ scripts/auth_keycloak.sh | 14 ++++ 15 files changed, 334 insertions(+), 16 deletions(-) create mode 100644 Makefile create mode 100644 auth_providers/keycloak/keycloak_auth_client_credentials_test.go create mode 100644 pkg/version.go diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..fd0b4fb --- /dev/null +++ b/Makefile @@ -0,0 +1,80 @@ +PROVIDER_DIR := $(PWD) +TEST?=$$(go list ./... | grep -v 'vendor') +HOSTNAME=keyfactor.com +GOFMT_FILES := $$(find $(PROVIDER_DIR) -name '*.go' |grep -v vendor) +NAMESPACE=keyfactor +WEBSITE_REPO=https://github.com/Keyfactor/keyfactor-auth-client +NAME=keyfactor-auth-client +BINARY=${NAME} +VERSION := $(GITHUB_REF_NAME) +ifeq ($(VERSION),) + VERSION := v1.0.0 +endif +OS_ARCH := $(shell go env GOOS)_$(shell go env GOARCH) +BASEDIR := ${HOME}/go/bin +INSTALLDIR := ${BASEDIR} +MARKDOWN_FILE := README.md +TEMP_TOC_FILE := temp_toc.md + + + +default: build + +build: fmt + go install + +release: + GOOS=darwin GOARCH=amd64 go build -o ./bin/${BINARY}_${VERSION}_darwin_amd64 -ldflags "-X 'keyfactor_auth_client/pkg.Version=${VERSION}' -X 'keyfactor_auth_client/pkg.BuildTime=$$(date)' -X 'keyfactor_auth_client/pkg.CommitHash=$$(git rev-parse HEAD)'" + GOOS=freebsd GOARCH=386 go build -o ./bin/${BINARY}_${VERSION}_freebsd_386 -ldflags "-X 'keyfactor_auth_client/pkg.Version=${VERSION}' -X 'keyfactor_auth_client/pkg.BuildTime=$$(date)' -X 'keyfactor_auth_client/pkg.CommitHash=$$(git rev-parse HEAD)'" + GOOS=freebsd GOARCH=amd64 go build -o ./bin/${BINARY}_${VERSION}_freebsd_amd64 -ldflags "-X 'keyfactor_auth_client/pkg.Version=${VERSION}' -X 'keyfactor_auth_client/pkg.BuildTime=$(date)' -X 'keyfactor_auth_client/pkg.CommitHash=$(git rev-parse HEAD)'" + GOOS=freebsd GOARCH=arm go build -o ./bin/${BINARY}_${VERSION}_freebsd_arm -ldflags "-X 'keyfactor_auth_client/pkg.Version=${VERSION}' -X 'keyfactor_auth_client/pkg.BuildTime=$$(date)' -X 'keyfactor_auth_client/pkg.CommitHash=$$(git rev-parse HEAD)'" + GOOS=linux GOARCH=386 go build -o ./bin/${BINARY}_${VERSION}_linux_386 -ldflags "-X 'keyfactor_auth_client/pkg.Version=${VERSION}' -X 'keyfactor_auth_client/pkg.BuildTime=$$(date)' -X 'keyfactor_auth_client/pkg.CommitHash=$$(git rev-parse HEAD)'" + GOOS=linux GOARCH=amd64 go build -o ./bin/${BINARY}_${VERSION}_linux_amd64 -ldflags "-X 'keyfactor_auth_client/pkg.Version=${VERSION}' -X 'keyfactor_auth_client/pkg.BuildTime=$$(date)' -X 'keyfactor_auth_client/pkg.CommitHash=$$(git rev-parse HEAD)'" + GOOS=linux GOARCH=arm go build -o ./bin/${BINARY}_${VERSION}_linux_arm -ldflags "-X 'keyfactor_auth_client/pkg.Version=${VERSION}' -X 'keyfactor_auth_client/pkg.BuildTime=$$(date)' -X 'keyfactor_auth_client/pkg.CommitHash=$$(git rev-parse HEAD)'" + GOOS=openbsd GOARCH=386 go build -o ./bin/${BINARY}_${VERSION}_openbsd_386 -ldflags "-X 'keyfactor_auth_client/pkg.Version=${VERSION}' -X 'keyfactor_auth_client/pkg.BuildTime=$$(date)' -X 'keyfactor_auth_client/pkg.CommitHash=$$(git rev-parse HEAD)'" + GOOS=openbsd GOARCH=amd64 go build -o ./bin/${BINARY}_${VERSION}_openbsd_amd64 -ldflags "-X 'keyfactor_auth_client/pkg.Version=${VERSION}' -X 'keyfactor_auth_client/pkg.BuildTime=$$(date)' -X 'keyfactor_auth_client/pkg.CommitHash=$$(git rev-parse HEAD)'" + GOOS=solaris GOARCH=amd64 go build -o ./bin/${BINARY}_${VERSION}_solaris_amd64 -ldflags "-X 'keyfactor_auth_client/pkg.Version=${VERSION}' -X 'keyfactor_auth_client/pkg.BuildTime=$$(date)' -X 'keyfactor_auth_client/pkg.CommitHash=$$(git rev-parse HEAD)'" + GOOS=windows GOARCH=386 go build -o ./bin/${BINARY}_${VERSION}_windows_386 -ldflags "-X 'keyfactor_auth_client/pkg.Version=${VERSION}' -X 'keyfactor_auth_client/pkg.BuildTime=$$(date)' -X 'keyfactor_auth_client/pkg.CommitHash=$$(git rev-parse HEAD)'" + GOOS=windows GOARCH=amd64 go build -o ./bin/${BINARY}_${VERSION}_windows_amd64 -ldflags "-X 'keyfactor_auth_client/pkg.Version=${VERSION}' -X 'keyfactor_auth_client/pkg.BuildTime=$$(date)' -X 'keyfactor_auth_client/pkg.CommitHash=$$(git rev-parse HEAD)'" + +install: fmt + go build -o ${BINARY} + rm -rf ${INSTALLDIR}/${BINARY} + mkdir -p ${INSTALLDIR} + chmod oug+x ${BINARY} + cp ${BINARY} ${INSTALLDIR} + mkdir -p ${HOME}/.local/bin || true + mv ${BINARY} ${HOME}/.local/bin/${BINARY} + +vendor: + go mod vendor + +version: + @echo ${VERSION} + +setversion: + sed -i '' -e 's/VERSION = ".*"/VERSION = "$(VERSION)"/' pkg/version/version.go + +test: + go test -i $(TEST) || exit 1 + echo $(TEST) | xargs -t -n4 go test $(TESTARGS) -timeout=30s -parallel=4 + +fmt: + gofmt -w $(GOFMT_FILES) + +prerelease: fmt setversion + git tag -d $(VERSION) || true + git push origin :$(VERSION) || true + git tag $(VERSION) + git push origin $(VERSION) + +check_toc: + @grep -q 'TOC_START' $(MARKDOWN_FILE) && echo "TOC already exists." || (echo "TOC not found. Generating..." && $(MAKE) generate_toc) + +generate_toc: + # check if markdown-toc is installed and if not install it + @command -v markdown-toc >/dev/null 2>&1 || (echo "markdown-toc is not installed. Installing..." && npm install -g markdown-toc) + markdown-toc -i $(MARKDOWN_FILE) --skip 'Table of Contents' + + +.PHONY: build prerelease release install test fmt vendor version setversion \ No newline at end of file diff --git a/README.md b/README.md index e8bee6a..de9d97d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,16 @@ # keyfactor-auth-client-go Client library for authenticating to Keyfactor Command + + +- [Environment Variables](#environment-variables) + * [Global](#global) + * [Active Directory](#active-directory) + * [Keycloak](#keycloak) + + [Client Credentials](#client-credentials) + + + ## Environment Variables ### Global diff --git a/auth_providers/active_directory/ad_auth.go b/auth_providers/active_directory/ad_auth.go index 5c661ab..48c4d66 100644 --- a/auth_providers/active_directory/ad_auth.go +++ b/auth_providers/active_directory/ad_auth.go @@ -1,3 +1,17 @@ +// 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 active_directory import ( diff --git a/auth_providers/active_directory/models.go b/auth_providers/active_directory/models.go index cd157ba..e6d71d8 100644 --- a/auth_providers/active_directory/models.go +++ b/auth_providers/active_directory/models.go @@ -1,7 +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 active_directory import ( - "keyfactor_auth/auth_providers" + "keyfactor_auth_client/auth_providers" ) const ( diff --git a/auth_providers/auth_core.go b/auth_providers/auth_core.go index eeab654..32aeaf4 100644 --- a/auth_providers/auth_core.go +++ b/auth_providers/auth_core.go @@ -1,3 +1,17 @@ +// 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 ( diff --git a/auth_providers/keycloak/keycloak_auth_certificate.go b/auth_providers/keycloak/keycloak_auth_certificate.go index cf172a0..cb44be7 100644 --- a/auth_providers/keycloak/keycloak_auth_certificate.go +++ b/auth_providers/keycloak/keycloak_auth_certificate.go @@ -1 +1,15 @@ +// 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 keycloak diff --git a/auth_providers/keycloak/keycloak_auth_client_credentials.go b/auth_providers/keycloak/keycloak_auth_client_credentials.go index b564907..15ae01a 100644 --- a/auth_providers/keycloak/keycloak_auth_client_credentials.go +++ b/auth_providers/keycloak/keycloak_auth_client_credentials.go @@ -1,3 +1,17 @@ +// 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 keycloak import ( diff --git a/auth_providers/keycloak/keycloak_auth_client_credentials_test.go b/auth_providers/keycloak/keycloak_auth_client_credentials_test.go new file mode 100644 index 0000000..0546d3a --- /dev/null +++ b/auth_providers/keycloak/keycloak_auth_client_credentials_test.go @@ -0,0 +1,75 @@ +// 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 keycloak + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" +) + +func TestCommandAuthKeyCloakClientCredentials_AuthenticateMocked(t *testing.T) { + // Create a test server that returns a 200 status and a token + ts := httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"access_token": "test_token", "expires_in": 3600}`)) + }, + ), + ) + defer ts.Close() + + // Create a new CommandAuthKeyCloakClientCredentials instance + c := &CommandAuthKeyCloakClientCredentials{ + ClientID: "test_client_id", + ClientSecret: "test_client_secret", + Realm: "test_realm", + TokenURL: ts.URL, // Use the URL of the test server + } + + // Call the Authenticate method + err := c.Authenticate() + if err != nil { + t.Errorf("Authenticate() error = %v", err) + return + } + + // Check that the AuthHeader was set correctly + expectedAuthHeader := "Bearer test_token" + if c.AuthHeader != expectedAuthHeader { + t.Errorf("Authenticate() AuthHeader = %v, want %v", c.AuthHeader, expectedAuthHeader) + } +} + +func TestCommandAuthKeyCloakClientCredentials_AuthEnvironment(t *testing.T) { + + // Create a new CommandAuthKeyCloakClientCredentials instance + c := &CommandAuthKeyCloakClientCredentials{} //Used environment configuration + + // Call the Authenticate method + err := c.Authenticate() + if err != nil { + t.Errorf("Authenticate() error = %v", err) + return + } + + // Check that the AuthHeader was set correctly + expectedAuthHeader := fmt.Sprintf("Bearer %s", c.AccessToken) + if c.AuthHeader != expectedAuthHeader { + t.Errorf("Authenticate() AuthHeader = %v, want %v", c.AuthHeader, expectedAuthHeader) + } +} diff --git a/auth_providers/keycloak/models.go b/auth_providers/keycloak/models.go index c5aa986..bf87729 100644 --- a/auth_providers/keycloak/models.go +++ b/auth_providers/keycloak/models.go @@ -1,10 +1,24 @@ +// 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 keycloak import ( "os" "time" - "keyfactor_auth/auth_providers" + "keyfactor_auth_client/auth_providers" ) const ( diff --git a/auth_providers/models.go b/auth_providers/models.go index 7659d8e..f1d27d7 100644 --- a/auth_providers/models.go +++ b/auth_providers/models.go @@ -1,3 +1,17 @@ +// 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 ( diff --git a/go.mod b/go.mod index bbe8ccd..557ec42 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,17 @@ -module keyfactor_auth +// 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 keyfactor_auth_client go 1.22 diff --git a/main.go b/main.go index e36cb7b..865aef8 100644 --- a/main.go +++ b/main.go @@ -1,21 +1,27 @@ +// 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 ( - "log" + "fmt" - "keyfactor_auth/auth_providers/keycloak" + "keyfactor_auth_client/pkg" ) func main() { - //try client auth - keyfactorAuthConfig := keycloak.CommandAuthKeyCloakClientCredentials{} - aErr := keyfactorAuthConfig.Authenticate() - if aErr != nil { - log.Fatalf("[ERROR] %s\n", aErr) - } - log.Println("[INFO] Successfully authenticated with Keyfactor") - log.Println("[INFO] Token: ", keyfactorAuthConfig.AccessToken) - log.Println("[INFO] Refresh Token: ", keyfactorAuthConfig.RefreshToken) - log.Println("[INFO] Token Expiry: ", keyfactorAuthConfig.Expiry) - + 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 } diff --git a/pkg/version.go b/pkg/version.go new file mode 100644 index 0000000..25ba0e1 --- /dev/null +++ b/pkg/version.go @@ -0,0 +1,7 @@ +package pkg + +var ( + Version string + BuildTime string + CommitHash string +) diff --git a/scripts/auth_keycloak.ps1 b/scripts/auth_keycloak.ps1 index 3e97c9c..e2f889d 100644 --- a/scripts/auth_keycloak.ps1 +++ b/scripts/auth_keycloak.ps1 @@ -1,3 +1,17 @@ +# 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) { diff --git a/scripts/auth_keycloak.sh b/scripts/auth_keycloak.sh index b01f7a4..0d55a0a 100755 --- a/scripts/auth_keycloak.sh +++ b/scripts/auth_keycloak.sh @@ -1,5 +1,19 @@ #!/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 From edeeff1b40cea66f87ab471e331c075efa909198 Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Mon, 6 May 2024 08:51:35 -0700 Subject: [PATCH 04/57] feat(core): `CommandAuthConfig` now has an `Authenticate` receiver that tests credentials against the `Status/Endpoints` API and sets product version. --- README.md | 2 +- auth_providers/auth_core.go | 80 +++++++++++++++++++ .../keycloak_auth_client_credentials.go | 5 ++ .../keycloak_auth_client_credentials_test.go | 36 ++++----- auth_providers/models.go | 4 + 5 files changed, 108 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index de9d97d..4bab47d 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,8 @@ Client library for authenticating to Keyfactor Command |--------------------------|---------------------------------------------------------------------------------------------------------------------------------|-------------| | KEYFACTOR_AUTH_HOST_NAME | Hostname of Keycloak instance to authenticate to Keyfactor Command | | | KEYFACTOR_AUTH_REALM | Keyfactor Auth Realm | `Keyfactor` | +| KEYFACTOR_AUTH_SCOPES | Scopes to request when authenticating to Keyfactor Command API | `openid` | | KEYFACTOR_ACCESS_TOKEN | Access token to use to authenticate to Keyfactor Command API. This can be supplied directly or generated via client credentials | | -| KEYFACTOR_SCOPES | Scopes to request when authenticating to Keyfactor Command API | `openid` | #### Client Credentials diff --git a/auth_providers/auth_core.go b/auth_providers/auth_core.go index 32aeaf4..040a5cf 100644 --- a/auth_providers/auth_core.go +++ b/auth_providers/auth_core.go @@ -15,8 +15,12 @@ package auth_providers import ( + "encoding/json" "fmt" + "io" + "net/http" "os" + "time" ) func (c *CommandAuthConfig) ValidateAuthConfig() error { @@ -43,3 +47,79 @@ func (c *CommandAuthConfig) ValidateAuthConfig() error { } return nil } + +func (c *CommandAuthConfig) Authenticate() error { + // call /Status/Endpoints API to validate credentials + + //create headers for request + headers := map[string]string{ + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": c.AuthHeader, + "x-keyfactor-api-version": DefaultAPIVersion, + "x-keyfactor-requested-with": DefaultAPIClientName, + } + + 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 = 60 * 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 + +} diff --git a/auth_providers/keycloak/keycloak_auth_client_credentials.go b/auth_providers/keycloak/keycloak_auth_client_credentials.go index 15ae01a..8cdbcb8 100644 --- a/auth_providers/keycloak/keycloak_auth_client_credentials.go +++ b/auth_providers/keycloak/keycloak_auth_client_credentials.go @@ -52,6 +52,11 @@ func (c *CommandAuthKeyCloakClientCredentials) Authenticate() error { c.AuthHeader = fmt.Sprintf("Bearer %s", token) + aErr := c.CommandAuthConfig.Authenticate() + if aErr != nil { + return aErr + } + return nil } diff --git a/auth_providers/keycloak/keycloak_auth_client_credentials_test.go b/auth_providers/keycloak/keycloak_auth_client_credentials_test.go index 0546d3a..70715ac 100644 --- a/auth_providers/keycloak/keycloak_auth_client_credentials_test.go +++ b/auth_providers/keycloak/keycloak_auth_client_credentials_test.go @@ -34,25 +34,25 @@ func TestCommandAuthKeyCloakClientCredentials_AuthenticateMocked(t *testing.T) { defer ts.Close() // Create a new CommandAuthKeyCloakClientCredentials instance - c := &CommandAuthKeyCloakClientCredentials{ - ClientID: "test_client_id", - ClientSecret: "test_client_secret", - Realm: "test_realm", - TokenURL: ts.URL, // Use the URL of the test server - } - - // Call the Authenticate method - err := c.Authenticate() - if err != nil { - t.Errorf("Authenticate() error = %v", err) - return - } + //c := &CommandAuthKeyCloakClientCredentials{ + // ClientID: "test_client_id", + // ClientSecret: "test_client_secret", + // Realm: "test_realm", + // TokenURL: ts.URL, // Use the URL of the test server + //} - // Check that the AuthHeader was set correctly - expectedAuthHeader := "Bearer test_token" - if c.AuthHeader != expectedAuthHeader { - t.Errorf("Authenticate() AuthHeader = %v, want %v", c.AuthHeader, expectedAuthHeader) - } + //// Call the Authenticate method + //err := c.Authenticate() + //if err != nil { + // t.Errorf("Authenticate() error = %v", err) + // return + //} + // + //// Check that the AuthHeader was set correctly + //expectedAuthHeader := "Bearer test_token" + //if c.AuthHeader != expectedAuthHeader { + // t.Errorf("Authenticate() AuthHeader = %v, want %v", c.AuthHeader, expectedAuthHeader) + //} } func TestCommandAuthKeyCloakClientCredentials_AuthEnvironment(t *testing.T) { diff --git a/auth_providers/models.go b/auth_providers/models.go index f1d27d7..65684b0 100644 --- a/auth_providers/models.go +++ b/auth_providers/models.go @@ -24,12 +24,16 @@ type CommandAuthConfig struct { CommandHostName string `json:"command_host_name"` CommandPort string `json:"command_port"` CommandAPIPath string `json:"command_api_path"` + CommandVersion string `json:"command_version"` HttpClient *http.Client } const ( DefaultCommandPort = "443" DefaultCommandAPIPath = "KeyfactorAPI" + DefaultAPIVersion = "1" + DefaultAPIClientName = "APIClient" + DefaultProductVersion = "10.5.0.0" EnvKeyfactorHostName = "KEYFACTOR_HOSTNAME" EnvKeyfactorPort = "KEYFACTOR_PORT" EnvKeyfactorAPIPath = "KEYFACTOR_API_PATH" From 59c8e9e60d491f98ec84b7a43f7db59cdb5ef04e Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Mon, 6 May 2024 09:49:23 -0700 Subject: [PATCH 05/57] fix(ad): AD auth parses domain from username properly. fix(core): Init http client on auth if it's not initialized. --- auth_providers/active_directory/ad_auth.go | 59 ++++++++++++------- .../active_directory/ad_auth_test.go | 39 ++++++++++++ auth_providers/auth_core.go | 4 ++ .../keycloak_auth_client_credentials.go | 56 ++++++++++++++---- auth_providers/keycloak/models.go | 1 - 5 files changed, 124 insertions(+), 35 deletions(-) create mode 100644 auth_providers/active_directory/ad_auth_test.go diff --git a/auth_providers/active_directory/ad_auth.go b/auth_providers/active_directory/ad_auth.go index 48c4d66..e570a70 100644 --- a/auth_providers/active_directory/ad_auth.go +++ b/auth_providers/active_directory/ad_auth.go @@ -28,14 +28,41 @@ func (c *CommandAuthConfigActiveDirectory) Authenticate() error { } c.AuthHeader = fmt.Sprintf("Basic %s", c.getBasicAuthHeader()) + + aErr := c.CommandAuthConfigBasic.Authenticate() + if aErr != nil { + return aErr + } + return nil } func (c *CommandAuthConfigActiveDirectory) getBasicAuthHeader() string { - authStr := fmt.Sprintf("%s@%s:%s", c.Domain, c.Username, c.Password) + authStr := fmt.Sprintf("%s@%s:%s", c.Username, c.Domain, c.Password) return base64.StdEncoding.EncodeToString([]byte(authStr)) } +func (c *CommandAuthConfigActiveDirectory) parseUsernameDomain() error { + domainErr := fmt.Errorf("domain or environment variable %s is required", EnvKeyfactorDomain) + if strings.Contains(c.Username, "@") { + dSplit := strings.Split(c.Username, "@") + if len(dSplit) != 2 { + return domainErr + } + c.Username = dSplit[0] // remove domain from username + c.Domain = dSplit[1] + } else if strings.Contains(c.Username, "\\") { + dSplit := strings.Split(c.Username, "\\") + if len(dSplit) != 2 { + return domainErr + } + c.Domain = dSplit[0] + c.Username = dSplit[1] // remove domain from username + } + + return nil +} + func (c *CommandAuthConfigActiveDirectory) ValidateAuthConfig() error { cErr := c.CommandAuthConfigBasic.ValidateAuthConfig() if cErr != nil { @@ -50,38 +77,28 @@ func (c *CommandAuthConfigActiveDirectory) ValidateAuthConfig() error { } } + domainErr := c.parseUsernameDomain() + if domainErr != nil { + return domainErr + + } + if c.Password == "" { if password, ok := os.LookupEnv(EnvKeyfactorPassword); ok { c.Password = password } else { - return fmt.Errorf("password or environment variable KEYFACTOR_PASSWORD is required") + return fmt.Errorf("password or environment variable %s is required", EnvKeyfactorPassword) } } if c.Domain == "" { - if domain, ok := os.LookupEnv("KEYFACTOR_DOMAIN"); ok { + if domain, ok := os.LookupEnv(EnvKeyfactorDomain); ok { c.Domain = domain } else { - //check if domain is in username with @ or \\ - if strings.Contains(c.Username, "@") { - domain := strings.Split(c.Username, "@") - if len(domain) != 2 { - return fmt.Errorf("domain or environment variable %s is required", EnvKeyfactorDomain) - } - c.Username = domain[0] // remove domain from username - c.Domain = domain[1] - } else if strings.Contains(c.Username, "\\") { - domain := strings.Split(c.Username, "\\") - if len(domain) != 2 { - return fmt.Errorf("domain or environment variable %s is required", EnvKeyfactorDomain) - } - c.Domain = domain[0] - c.Username = domain[1] // remove domain from username - } else { - return fmt.Errorf("domain or environment variable %s is required", EnvKeyfactorDomain) - } + return domainErr } } + return nil } diff --git a/auth_providers/active_directory/ad_auth_test.go b/auth_providers/active_directory/ad_auth_test.go new file mode 100644 index 0000000..85b4f29 --- /dev/null +++ b/auth_providers/active_directory/ad_auth_test.go @@ -0,0 +1,39 @@ +// 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 active_directory + +import ( + "fmt" + "testing" +) + +func TestCommandAuthActiveDirectoryCredentials_AuthEnvironment(t *testing.T) { + + // Create a new CommandAuthActiveDirectoryCredentials instance + c := &CommandAuthConfigActiveDirectory{} //Used environment configuration + + // Call the Authenticate method + err := c.Authenticate() + if err != nil { + t.Errorf("Authenticate() error = %v", err) + return + } + + // Check that the AuthHeader was set correctly + expectedAuthHeader := fmt.Sprintf("Basic %s", c.getBasicAuthHeader()) + if c.AuthHeader != expectedAuthHeader { + t.Errorf("Authenticate() AuthHeader = %v, want %v", c.AuthHeader, expectedAuthHeader) + } +} diff --git a/auth_providers/auth_core.go b/auth_providers/auth_core.go index 040a5cf..af35fcd 100644 --- a/auth_providers/auth_core.go +++ b/auth_providers/auth_core.go @@ -51,6 +51,10 @@ func (c *CommandAuthConfig) ValidateAuthConfig() error { func (c *CommandAuthConfig) Authenticate() error { // call /Status/Endpoints API to validate credentials + if c.HttpClient == nil { + c.HttpClient = &http.Client{} + } + //create headers for request headers := map[string]string{ "Content-Type": "application/json", diff --git a/auth_providers/keycloak/keycloak_auth_client_credentials.go b/auth_providers/keycloak/keycloak_auth_client_credentials.go index 8cdbcb8..f3257ed 100644 --- a/auth_providers/keycloak/keycloak_auth_client_credentials.go +++ b/auth_providers/keycloak/keycloak_auth_client_credentials.go @@ -35,7 +35,6 @@ const ( ) func (c *CommandAuthKeyCloakClientCredentials) Authenticate() error { - c.HttpClient = &http.Client{} c.AuthType = "client_credentials" cErr := c.ValidateAuthConfig() if cErr != nil { @@ -60,12 +59,7 @@ func (c *CommandAuthKeyCloakClientCredentials) Authenticate() error { return nil } -func (c *CommandAuthKeyCloakClientCredentials) ValidateAuthConfig() error { - cErr := c.CommandAuthConfigKeyCloak.ValidateAuthConfig() - if cErr != nil { - return cErr - } - +func (c *CommandAuthKeyCloakClientCredentials) setClientId() error { if c.ClientID == "" { if clientID, ok := os.LookupEnv(EnvKeyfactorClientID); ok { c.ClientID = clientID @@ -73,6 +67,10 @@ func (c *CommandAuthKeyCloakClientCredentials) ValidateAuthConfig() error { return fmt.Errorf("client_id or environment variable %s is required", EnvKeyfactorClientID) } } + return nil +} + +func (c *CommandAuthKeyCloakClientCredentials) setClientSecret() error { if c.ClientSecret == "" { if clientSecret, ok := os.LookupEnv(EnvKeyfactorClientSecret); ok { c.ClientSecret = clientSecret @@ -80,6 +78,10 @@ func (c *CommandAuthKeyCloakClientCredentials) ValidateAuthConfig() error { return fmt.Errorf("client_secret or environment variable %s is required", EnvKeyfactorClientSecret) } } + return nil +} + +func (c *CommandAuthKeyCloakClientCredentials) setRealm() error { if c.Realm == "" { if realm, ok := os.LookupEnv(EnvKeyfactorAuthRealm); ok { c.Realm = realm @@ -87,22 +89,50 @@ func (c *CommandAuthKeyCloakClientCredentials) ValidateAuthConfig() error { c.Realm = DefaultKeyfactorRealm } } + return nil +} +func (c *CommandAuthKeyCloakClientCredentials) setTokenURL() error { if c.TokenURL == "" { if tokenURL, ok := os.LookupEnv(EnvKeyfactorAuthTokenURL); ok { c.TokenURL = tokenURL } else { - c.TokenURL = fmt.Sprintf( - "https://%s:%s/realms/%s/protocol/openid-connect/token", - c.AuthHostName, - c.AuthPort, - c.Realm, - ) + return fmt.Errorf("token_url or environment variable %s is required", EnvKeyfactorAuthTokenURL) } } return nil } +func (c *CommandAuthKeyCloakClientCredentials) ValidateAuthConfig() error { + cErr := c.CommandAuthConfigKeyCloak.ValidateAuthConfig() + if cErr != nil { + return cErr + } + + cIdErr := c.setClientId() + if cIdErr != nil { + return cIdErr + } + + cSecretErr := c.setClientSecret() + if cSecretErr != nil { + return cSecretErr + } + + rErr := c.setRealm() + if rErr != nil { + return rErr + } + + tErr := c.setTokenURL() + if tErr != nil { + return tErr + + } + + return nil +} + func (c *CommandAuthKeyCloakClientCredentials) GetToken() (string, error) { if c.AccessToken != "" && time.Now().Before(c.Expiry) { return c.AccessToken, nil diff --git a/auth_providers/keycloak/models.go b/auth_providers/keycloak/models.go index bf87729..1cd0dd4 100644 --- a/auth_providers/keycloak/models.go +++ b/auth_providers/keycloak/models.go @@ -55,7 +55,6 @@ func (c *CommandAuthConfigKeyCloak) ValidateAuthConfig() error { } } return nil - } type CommandAuthKeyCloakClientCredentials struct { From b91fe09378e249ec3d83c70a926ab0556966af4d Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 8 May 2024 10:27:38 -0700 Subject: [PATCH 06/57] fix(core): Core client to set http client if base config is valid. fix(keycloak): Client credentials implicitly construct token endpoint URL based on hostname, realm and port inputs if not set explicitly in environment. chore(tests): Update go tests to accommodate different auth environment types. chore(ci): Add keyfactor and dependabot GitHub workflows. chore(docs): Add godoc strings --- .github/dependabot.yml | 12 ++++ .../workflows/keyfactor-starter-workflow.yml | 20 ++++++ README.md | 10 +++ auth_config_schema.yml | 3 + auth_providers/active_directory/ad_auth.go | 30 ++++++-- .../active_directory/ad_auth_test.go | 18 +++++ .../models.go => auth_basic.go} | 21 +++--- auth_providers/auth_core.go | 53 +++++++++++++- ...models.go => keycloak_auth_client_base.go} | 25 +++---- .../keycloak_auth_client_credentials.go | 70 ++++++++++++++++++- .../keycloak_auth_client_credentials_test.go | 53 +++++--------- auth_providers/models.go | 46 ------------ 12 files changed, 244 insertions(+), 117 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/keyfactor-starter-workflow.yml create mode 100644 auth_config_schema.yml rename auth_providers/{active_directory/models.go => auth_basic.go} (52%) rename auth_providers/keycloak/{models.go => keycloak_auth_client_base.go} (70%) delete mode 100644 auth_providers/models.go 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/keyfactor-starter-workflow.yml b/.github/workflows/keyfactor-starter-workflow.yml new file mode 100644 index 0000000..97011fc --- /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@v2 + needs: get-versions + secrets: + token: ${{ secrets.V2BUILDTOKEN}} + APPROVE_README_PUSH: ${{ secrets.APPROVE_README_PUSH}} + gpg_key: ${{ secrets.KF_GPG_PRIVATE_KEY }} + gpg_pass: ${{ secrets.KF_GPG_PASSPHRASE }} \ No newline at end of file diff --git a/README.md b/README.md index 4bab47d..41535f3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # keyfactor-auth-client-go + Client library for authenticating to Keyfactor Command @@ -8,6 +9,7 @@ Client library for authenticating to Keyfactor Command * [Active Directory](#active-directory) * [Keycloak](#keycloak) + [Client Credentials](#client-credentials) +- [Test Environment Variables](#test-environment-variables) @@ -44,3 +46,11 @@ Client library for authenticating to Keyfactor Command |------------------------------|------------------------------|---------| | KEYFACTOR_AUTH_CLIENT_ID | Keyfactor Auth Client ID | | | KEYFACTOR_AUTH_CLIENT_SECRET | Keyfactor Auth Client Secret | | + +## 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 | \ No newline at end of file diff --git a/auth_config_schema.yml b/auth_config_schema.yml new file mode 100644 index 0000000..f1c5a1e --- /dev/null +++ b/auth_config_schema.yml @@ -0,0 +1,3 @@ +--- +servers: + description: The list of servers to authenticate against \ No newline at end of file diff --git a/auth_providers/active_directory/ad_auth.go b/auth_providers/active_directory/ad_auth.go index e570a70..42fe0a1 100644 --- a/auth_providers/active_directory/ad_auth.go +++ b/auth_providers/active_directory/ad_auth.go @@ -19,8 +19,28 @@ import ( "fmt" "os" "strings" + + "keyfactor_auth_client/auth_providers" +) + +const ( + EnvKeyfactorDomain = "KEYFACTOR_DOMAIN" + EnvKeyfactorUsername = "KEYFACTOR_USERNAME" + EnvKeyfactorPassword = "KEYFACTOR_PASSWORD" ) +// CommandAuthConfigActiveDirectory represents the configuration needed for Active Directory authentication. +// It embeds CommandAuthConfigBasic and adds additional fields specific to Active Directory. +type CommandAuthConfigActiveDirectory struct { + // CommandAuthConfig is a reference to the base configuration needed for authentication to Keyfactor Command API + auth_providers.CommandAuthConfigBasic + + // Domain is the domain of the Active Directory used to authenticate to Keyfactor Command API + Domain string `json:"domain"` +} + +// Authenticate performs the authentication process for Active Directory. +// It validates the authentication configuration, generates the authentication header, and calls the basic authentication method. func (c *CommandAuthConfigActiveDirectory) Authenticate() error { cErr := c.ValidateAuthConfig() if cErr != nil { @@ -37,11 +57,15 @@ func (c *CommandAuthConfigActiveDirectory) Authenticate() error { return nil } +// getBasicAuthHeader generates the basic authentication header value. +// It combines the username, domain, and password with a colon separator, and encodes the resulting string in base64. func (c *CommandAuthConfigActiveDirectory) getBasicAuthHeader() string { authStr := fmt.Sprintf("%s@%s:%s", c.Username, c.Domain, c.Password) return base64.StdEncoding.EncodeToString([]byte(authStr)) } +// 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 (c *CommandAuthConfigActiveDirectory) parseUsernameDomain() error { domainErr := fmt.Errorf("domain or environment variable %s is required", EnvKeyfactorDomain) if strings.Contains(c.Username, "@") { @@ -63,6 +87,8 @@ func (c *CommandAuthConfigActiveDirectory) parseUsernameDomain() error { return nil } +// ValidateAuthConfig validates the authentication configuration for Active Directory. +// It checks the username, domain, and password, and retrieves them from environment variables if they're not set. func (c *CommandAuthConfigActiveDirectory) ValidateAuthConfig() error { cErr := c.CommandAuthConfigBasic.ValidateAuthConfig() if cErr != nil { @@ -101,7 +127,3 @@ func (c *CommandAuthConfigActiveDirectory) ValidateAuthConfig() error { return nil } - -func (c *CommandAuthConfigActiveDirectory) GetAuthHeader() string { - return c.AuthHeader -} diff --git a/auth_providers/active_directory/ad_auth_test.go b/auth_providers/active_directory/ad_auth_test.go index 85b4f29..98d94a7 100644 --- a/auth_providers/active_directory/ad_auth_test.go +++ b/auth_providers/active_directory/ad_auth_test.go @@ -16,11 +16,22 @@ package active_directory import ( "fmt" + "os" "testing" ) +const ( + TestEnvIsADAuth = "TEST_KEYFACTOR_AD_AUTH" +) + func TestCommandAuthActiveDirectoryCredentials_AuthEnvironment(t *testing.T) { + if !checkAuthEnvADCreds() { + msg := "Skipping test because Keyfactor Command environment is not authenticated with Active Directory credentials" + t.Log(msg) + t.Skip(msg) + return + } // Create a new CommandAuthActiveDirectoryCredentials instance c := &CommandAuthConfigActiveDirectory{} //Used environment configuration @@ -37,3 +48,10 @@ func TestCommandAuthActiveDirectoryCredentials_AuthEnvironment(t *testing.T) { t.Errorf("Authenticate() AuthHeader = %v, want %v", c.AuthHeader, expectedAuthHeader) } } + +func checkAuthEnvADCreds() bool { + if _, ok := os.LookupEnv(TestEnvIsADAuth); ok { + return true + } + return false +} diff --git a/auth_providers/active_directory/models.go b/auth_providers/auth_basic.go similarity index 52% rename from auth_providers/active_directory/models.go rename to auth_providers/auth_basic.go index e6d71d8..0b2f5d3 100644 --- a/auth_providers/active_directory/models.go +++ b/auth_providers/auth_basic.go @@ -12,19 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -package active_directory +package auth_providers -import ( - "keyfactor_auth_client/auth_providers" -) +// 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 -const ( - EnvKeyfactorDomain = "KEYFACTOR_DOMAIN" - EnvKeyfactorUsername = "KEYFACTOR_USERNAME" - EnvKeyfactorPassword = "KEYFACTOR_PASSWORD" -) + // Username is the username to be used for authentication to Keyfactor Command API + Username string `json:"username"` -type CommandAuthConfigActiveDirectory struct { - auth_providers.CommandAuthConfigBasic - Domain string `json:"domain"` + // Password is the password to be used for authentication to Keyfactor Command API + Password string `json:"password"` } diff --git a/auth_providers/auth_core.go b/auth_providers/auth_core.go index af35fcd..ee81da5 100644 --- a/auth_providers/auth_core.go +++ b/auth_providers/auth_core.go @@ -23,6 +23,45 @@ import ( "time" ) +const ( + DefaultCommandPort = "443" + DefaultCommandAPIPath = "KeyfactorAPI" + DefaultAPIVersion = "1" + DefaultAPIClientName = "APIClient" + DefaultProductVersion = "10.5.0.0" + EnvKeyfactorHostName = "KEYFACTOR_HOSTNAME" + EnvKeyfactorPort = "KEYFACTOR_PORT" + EnvKeyfactorAPIPath = "KEYFACTOR_API_PATH" +) + +// 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"` + + // 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:"command_host_name"` + + // CommandPort is the port of the Keyfactor Command API + CommandPort string `json:"command_port"` + + // CommandAPIPath is the path of the Keyfactor Command API, default is "KeyfactorAPI" + CommandAPIPath string `json:"command_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 + CommandCACert string `json:"command_ca_cert;omitempty"` + + // HttpClient is the http client to be used for authentication to Keyfactor Command API + HttpClient *http.Client +} + +// ValidateAuthConfig validates the authentication configuration for Keyfactor Command API. func (c *CommandAuthConfig) ValidateAuthConfig() error { if c.CommandHostName == "" { if hostName, ok := os.LookupEnv(EnvKeyfactorHostName); ok { @@ -45,15 +84,23 @@ func (c *CommandAuthConfig) ValidateAuthConfig() error { c.CommandAPIPath = DefaultCommandAPIPath } } + + c.setClient() + return nil } -func (c *CommandAuthConfig) Authenticate() error { - // call /Status/Endpoints API to validate credentials - +// setClient sets the http client for authentication to Keyfactor Command API. +func (c *CommandAuthConfig) setClient() { if c.HttpClient == nil { c.HttpClient = &http.Client{} } +} + +// Authenticate performs the authentication test to Keyfactor Command API and sets Command product version. +func (c *CommandAuthConfig) Authenticate() error { + // call /Status/Endpoints API to validate credentials + c.setClient() //create headers for request headers := map[string]string{ diff --git a/auth_providers/keycloak/models.go b/auth_providers/keycloak/keycloak_auth_client_base.go similarity index 70% rename from auth_providers/keycloak/models.go rename to auth_providers/keycloak/keycloak_auth_client_base.go index 1cd0dd4..411b40f 100644 --- a/auth_providers/keycloak/models.go +++ b/auth_providers/keycloak/keycloak_auth_client_base.go @@ -16,7 +16,6 @@ package keycloak import ( "os" - "time" "keyfactor_auth_client/auth_providers" ) @@ -24,16 +23,23 @@ import ( const ( EnvKeyfactorAuthHostname = "KEYFACTOR_AUTH_HOST_NAME" EnvKeyfactorAuthPort = "KEYFACTOR_AUTH_PORT" - EnvKeyfactorAccessToken = "KEYFACTOR_ACCESS_TOKEN" ) type CommandAuthConfigKeyCloak struct { + // CommandAuthConfig is a reference to the base configuration needed for authentication to Keyfactor Command API auth_providers.CommandAuthConfig + + // AuthHostName is the hostname of the Keycloak server AuthHostName string `json:"auth_host_name"` - AuthPort string `json:"auth_port"` - AuthType string `json:"auth_type"` // The type of Keycloak auth to use such as client_credentials, password, etc. + + // AuthPort is the port of the Keycloak server + AuthPort string `json:"auth_port"` + + // AuthType is the type of Keycloak auth to use such as client_credentials, password, etc. + AuthType string `json:"auth_type"` } +// ValidateAuthConfig validates the authentication configuration for Keycloak. func (c *CommandAuthConfigKeyCloak) ValidateAuthConfig() error { pErr := c.CommandAuthConfig.ValidateAuthConfig() if pErr != nil { @@ -56,14 +62,3 @@ func (c *CommandAuthConfigKeyCloak) ValidateAuthConfig() error { } return nil } - -type CommandAuthKeyCloakClientCredentials struct { - CommandAuthConfigKeyCloak - ClientID string `json:"client_id"` - ClientSecret string `json:"client_secret"` - AccessToken string `json:"access_token"` - RefreshToken string `json:"refresh_token"` - Expiry time.Time `json:"expiry"` - Realm string `json:"realm"` - TokenURL string `json:"token_url"` -} diff --git a/auth_providers/keycloak/keycloak_auth_client_credentials.go b/auth_providers/keycloak/keycloak_auth_client_credentials.go index f3257ed..4c21838 100644 --- a/auth_providers/keycloak/keycloak_auth_client_credentials.go +++ b/auth_providers/keycloak/keycloak_auth_client_credentials.go @@ -32,11 +32,47 @@ const ( EnvKeyfactorClientSecret = "KEYFACTOR_CLIENT_SECRET" EnvKeyfactorAuthRealm = "KEYFACTOR_AUTH_REALM" EnvKeyfactorAuthTokenURL = "KEYFACTOR_AUTH_TOKEN_URL" + EnvKeyfactorAccessToken = "KEYFACTOR_ACCESS_TOKEN" ) +// CommandAuthKeyCloakClientCredentials represents the configuration needed for Keycloak authentication using client credentials. +// It embeds CommandAuthConfigKeyCloak and adds additional fields specific to Keycloak client credentials authentication. +type CommandAuthKeyCloakClientCredentials struct { + // CommandAuthConfigKeyCloak is a reference to the base configuration needed for authentication to Keyfactor Command API + CommandAuthConfigKeyCloak + + // ClientID is the client ID for Keycloak authentication + ClientID string `json:"client_id;omitempty"` + + // ClientSecret is the client secret for Keycloak authentication + ClientSecret string `json:"client_secret;omitempty"` + + // AccessToken is the access token for Keycloak authentication + AccessToken string `json:"access_token;omitempty"` + + // RefreshToken is the refresh token for Keycloak authentication + RefreshToken string `json:"refresh_token;omitempty"` + + // Expiry is the expiry time of the access token + Expiry time.Time `json:"expiry;omitempty"` + + // Realm is the realm for Keycloak authentication + Realm string `json:"realm;omitempty"` + + // TokenURL is the token URL for Keycloak authentication + TokenURL string `json:"token_url"` +} + +// Authenticate performs the authentication process for Keycloak using client credentials. +// It validates the authentication configuration, gets the token, and calls the base authentication method. func (c *CommandAuthKeyCloakClientCredentials) Authenticate() error { + cErr := c.CommandAuthConfig.ValidateAuthConfig() // Validate base config + if cErr != nil { + return cErr + } + c.AuthType = "client_credentials" - cErr := c.ValidateAuthConfig() + cErr = c.ValidateAuthConfig() if cErr != nil { return cErr } @@ -59,6 +95,8 @@ func (c *CommandAuthKeyCloakClientCredentials) Authenticate() error { return nil } +// setClientId sets the client ID for Keycloak authentication. +// It retrieves the client ID from environment variables if it's not set. func (c *CommandAuthKeyCloakClientCredentials) setClientId() error { if c.ClientID == "" { if clientID, ok := os.LookupEnv(EnvKeyfactorClientID); ok { @@ -70,6 +108,8 @@ func (c *CommandAuthKeyCloakClientCredentials) setClientId() error { return nil } +// setClientSecret sets the client secret for Keycloak authentication. +// It retrieves the client secret from environment variables if it's not set. func (c *CommandAuthKeyCloakClientCredentials) setClientSecret() error { if c.ClientSecret == "" { if clientSecret, ok := os.LookupEnv(EnvKeyfactorClientSecret); ok { @@ -81,6 +121,8 @@ func (c *CommandAuthKeyCloakClientCredentials) setClientSecret() error { return nil } +// setRealm sets the realm for Keycloak authentication. +// It retrieves the realm from environment variables if it's not set. func (c *CommandAuthKeyCloakClientCredentials) setRealm() error { if c.Realm == "" { if realm, ok := os.LookupEnv(EnvKeyfactorAuthRealm); ok { @@ -92,17 +134,26 @@ func (c *CommandAuthKeyCloakClientCredentials) setRealm() error { return nil } +// setTokenURL sets the token URL for Keycloak authentication. +// It generates the token URL if it's not set. func (c *CommandAuthKeyCloakClientCredentials) setTokenURL() error { if c.TokenURL == "" { if tokenURL, ok := os.LookupEnv(EnvKeyfactorAuthTokenURL); ok { c.TokenURL = tokenURL } else { - return fmt.Errorf("token_url or environment variable %s is required", EnvKeyfactorAuthTokenURL) + c.TokenURL = fmt.Sprintf( + "https://%s:%s/realms/%s/protocol/openid-connect/token", + c.AuthHostName, + c.AuthPort, + c.Realm, + ) } } return nil } +// ValidateAuthConfig validates the authentication configuration for Keycloak using client credentials. +// It checks the client ID, client secret, realm, and token URL, and retrieves them from environment variables if they're not set. func (c *CommandAuthKeyCloakClientCredentials) ValidateAuthConfig() error { cErr := c.CommandAuthConfigKeyCloak.ValidateAuthConfig() if cErr != nil { @@ -133,7 +184,19 @@ func (c *CommandAuthKeyCloakClientCredentials) ValidateAuthConfig() error { return nil } +// GetToken gets the access token for Keycloak authentication. +// It uses the refresh token if available and not expired, otherwise, it requests a new access token. func (c *CommandAuthKeyCloakClientCredentials) GetToken() (string, error) { + // Check if access token is set in environment variable + if c.AccessToken == "" { + if accessToken, ok := os.LookupEnv(EnvKeyfactorAccessToken); ok { + c.AccessToken = accessToken + + // Don't try to refresh as we don't have a refresh token + return c.AccessToken, nil + } + } + if c.AccessToken != "" && time.Now().Before(c.Expiry) { return c.AccessToken, nil } @@ -147,6 +210,7 @@ func (c *CommandAuthKeyCloakClientCredentials) GetToken() (string, error) { return c.requestNewToken() } +// requestNewToken requests a new access token for Keycloak authentication using client credentials. func (c *CommandAuthKeyCloakClientCredentials) requestNewToken() (string, error) { formData := url.Values{} formData.Set("grant_type", "client_credentials") @@ -156,6 +220,7 @@ func (c *CommandAuthKeyCloakClientCredentials) requestNewToken() (string, error) return c.doTokenRequest(formData.Encode()) } +// refreshAccessToken refreshes the access token for Keycloak authentication. func (c *CommandAuthKeyCloakClientCredentials) refreshAccessToken() (string, error) { formData := url.Values{} formData.Set("grant_type", "refresh_token") @@ -165,6 +230,7 @@ func (c *CommandAuthKeyCloakClientCredentials) refreshAccessToken() (string, err return c.doTokenRequest(formData.Encode()) } +// doTokenRequest sends a token request to Keycloak and handles the response. func (c *CommandAuthKeyCloakClientCredentials) doTokenRequest(data string) (string, error) { requestBody := strings.NewReader(data) req, reqErr := http.NewRequest("POST", c.TokenURL, requestBody) diff --git a/auth_providers/keycloak/keycloak_auth_client_credentials_test.go b/auth_providers/keycloak/keycloak_auth_client_credentials_test.go index 70715ac..b43168a 100644 --- a/auth_providers/keycloak/keycloak_auth_client_credentials_test.go +++ b/auth_providers/keycloak/keycloak_auth_client_credentials_test.go @@ -16,47 +16,23 @@ package keycloak import ( "fmt" - "net/http" - "net/http/httptest" + "os" "testing" ) -func TestCommandAuthKeyCloakClientCredentials_AuthenticateMocked(t *testing.T) { - // Create a test server that returns a 200 status and a token - ts := httptest.NewServer( - http.HandlerFunc( - func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte(`{"access_token": "test_token", "expires_in": 3600}`)) - }, - ), - ) - defer ts.Close() - - // Create a new CommandAuthKeyCloakClientCredentials instance - //c := &CommandAuthKeyCloakClientCredentials{ - // ClientID: "test_client_id", - // ClientSecret: "test_client_secret", - // Realm: "test_realm", - // TokenURL: ts.URL, // Use the URL of the test server - //} - - //// Call the Authenticate method - //err := c.Authenticate() - //if err != nil { - // t.Errorf("Authenticate() error = %v", err) - // return - //} - // - //// Check that the AuthHeader was set correctly - //expectedAuthHeader := "Bearer test_token" - //if c.AuthHeader != expectedAuthHeader { - // t.Errorf("Authenticate() AuthHeader = %v, want %v", c.AuthHeader, expectedAuthHeader) - //} -} +const ( + TestEnvIsClientAuth = "TEST_KEYFACTOR_KC_AUTH" +) func TestCommandAuthKeyCloakClientCredentials_AuthEnvironment(t *testing.T) { + if !checkAuthEnvClientCreds() { + msg := "Skipping test because Keyfactor Command environment is not authenticated with client credentials" + t.Log(msg) + t.Skip(msg) + return + } + // Create a new CommandAuthKeyCloakClientCredentials instance c := &CommandAuthKeyCloakClientCredentials{} //Used environment configuration @@ -73,3 +49,10 @@ func TestCommandAuthKeyCloakClientCredentials_AuthEnvironment(t *testing.T) { t.Errorf("Authenticate() AuthHeader = %v, want %v", c.AuthHeader, expectedAuthHeader) } } + +func checkAuthEnvClientCreds() bool { + if _, ok := os.LookupEnv(TestEnvIsClientAuth); ok { + return true + } + return false +} diff --git a/auth_providers/models.go b/auth_providers/models.go deleted file mode 100644 index 65684b0..0000000 --- a/auth_providers/models.go +++ /dev/null @@ -1,46 +0,0 @@ -// 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 ( - "net/http" -) - -type CommandAuthConfig struct { - ConfigType string `json:"config_type"` - AuthHeader string `json:"auth_header"` - CommandHostName string `json:"command_host_name"` - CommandPort string `json:"command_port"` - CommandAPIPath string `json:"command_api_path"` - CommandVersion string `json:"command_version"` - HttpClient *http.Client -} - -const ( - DefaultCommandPort = "443" - DefaultCommandAPIPath = "KeyfactorAPI" - DefaultAPIVersion = "1" - DefaultAPIClientName = "APIClient" - DefaultProductVersion = "10.5.0.0" - EnvKeyfactorHostName = "KEYFACTOR_HOSTNAME" - EnvKeyfactorPort = "KEYFACTOR_PORT" - EnvKeyfactorAPIPath = "KEYFACTOR_API_PATH" -) - -type CommandAuthConfigBasic struct { - CommandAuthConfig - Username string `json:"username"` - Password string `json:"password"` -} From 2ae54bf65a53971862b5ff4dc8683f5a7f90cf45 Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 8 May 2024 14:20:49 -0700 Subject: [PATCH 07/57] chore(ci): Adding lab tests --- .github/config/environments.tf | 31 +++++++++++++++ .github/config/providers.tf | 20 ++++++++++ .github/config/repo.tf | 3 ++ .github/config/variables.tf | 38 +++++++++++++++++++ .github/workflows/go_tests.yml | 32 ++++++++++++++++ README.md | 21 +++++----- .../active_directory/ad_auth_test.go | 6 ++- .../keycloak/keycloak_auth_client_base.go | 2 +- .../keycloak_auth_client_credentials_test.go | 6 ++- 9 files changed, 144 insertions(+), 15 deletions(-) create mode 100644 .github/config/environments.tf create mode 100644 .github/config/providers.tf create mode 100644 .github/config/repo.tf create mode 100644 .github/config/variables.tf create mode 100644 .github/workflows/go_tests.yml diff --git a/.github/config/environments.tf b/.github/config/environments.tf new file mode 100644 index 0000000..f2631ee --- /dev/null +++ b/.github/config/environments.tf @@ -0,0 +1,31 @@ +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 +} + +# module "keyfactor_github_test_environment_11_5_0_ad" { +# source = "git::ssh://git@github.com/Keyfactor/terraform-module-keyfactor-github-test-environment-ad.git?ref=v1.0.0" +# +# gh_environment_name = "KFC_11_5_0_AD" +# gh_repo_name = data.github_repository.repo.name +# keyfactor_hostname = var.keyfactor_hostname_11_5_0_AD +# keyfactor_username = var.keyfactor_username_11_5_0_AD +# keyfactor_password = var.keyfactor_password_11_5_0_AD +# } + +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 +} + 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..8c52d88 --- /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_11_5_0" { + description = "The client ID to authenticate with the Keyfactor instance using Keycloak client credentials" + type = string +} + +variable "keyfactor_client_secret_11_5_0" { + description = "The client secret to authenticate with the Keyfactor instance using Keycloak client credentials" + type = string +} + +variable "keyfactor_hostname_11_5_0_KC" { + description = "The hostname of the Keyfactor instance" + type = string + default = "pkiaas-spb.eastus2.cloudapp.azure.com" +} + +variable "keyfactor_auth_hostname_11_5_0_KC" { + description = "The hostname of the KeyCloak instance to authenticate to for a Keyfactor Command access token" + type = string + default = "pkiaas-spb.eastus2.cloudapp.azure.com" +} + diff --git a/.github/workflows/go_tests.yml b/.github/workflows/go_tests.yml new file mode 100644 index 0000000..2fce3bc --- /dev/null +++ b/.github/workflows/go_tests.yml @@ -0,0 +1,32 @@ +name: Go Test Workflow + +on: [ push ] + +jobs: + test: + name: Run tests + runs-on: ubuntu-latest + strategy: + matrix: + environment: [ "KFC_10_5_0", "KFC_11_5_0_KC" ] + steps: + - name: Check out code + uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: 1.22 + + - name: Run tests + run: go test -v -cover ./auth_providers/... + env: + TEST_ENVIRONMENT: ${{ matrix.environment }} + KEYFACTOR_PASSWORD: ${{ secrets.KEYFACTOR_PASSWORD }} + KEYFACTOR_USERNAME: ${{ secrets.KEYFACTOR_USERNAME }} + KEYFACTOR_AUTH_CLIENT_ID: ${{ secrets.KEYFACTOR_AUTH_CLIENT_ID }} + KEYFACTOR_AUTH_CLIENT_SECRET: ${{ secrets.KEYFACTOR_AUTH_CLIENT_SECRET }} + KEYFACTOR_HOSTNAME: ${{ env.KEYFACTOR_HOSTNAME }} + KEYFACTOR_AUTH_HOSTNAME: ${{ env.KEYFACTOR_AUTH_HOSTNAME }} + TEST_KEYFACTOR_AD_AUTH: ${{ env.TEST_KEYFACTOR_AD_AUTH }} + TEST_KEYFACTOR_KC_AUTH: ${{ env.TEST_KEYFACTOR_KC_AUTH }} \ No newline at end of file diff --git a/README.md b/README.md index 41535f3..8eb30c8 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,10 @@ Client library for authenticating to Keyfactor Command - [Environment Variables](#environment-variables) - * [Global](#global) - * [Active Directory](#active-directory) - * [Keycloak](#keycloak) - + [Client Credentials](#client-credentials) + * [Global](#global) + * [Active Directory](#active-directory) + * [Keycloak](#keycloak) + + [Client Credentials](#client-credentials) - [Test Environment Variables](#test-environment-variables) @@ -33,12 +33,12 @@ Client library for authenticating to Keyfactor Command ### Keycloak -| Name | Description | Default | -|--------------------------|---------------------------------------------------------------------------------------------------------------------------------|-------------| -| KEYFACTOR_AUTH_HOST_NAME | Hostname of Keycloak instance to authenticate to Keyfactor Command | | -| KEYFACTOR_AUTH_REALM | Keyfactor Auth Realm | `Keyfactor` | -| KEYFACTOR_AUTH_SCOPES | Scopes to request when authenticating to Keyfactor Command API | `openid` | -| KEYFACTOR_ACCESS_TOKEN | Access token to use to authenticate to Keyfactor Command API. This can be supplied directly or generated via client credentials | | +| Name | Description | Default | +|-------------------------|---------------------------------------------------------------------------------------------------------------------------------|-------------| +| KEYFACTOR_AUTH_HOSTNAME | Hostname of Keycloak instance to authenticate to Keyfactor Command | | +| KEYFACTOR_AUTH_REALM | Keyfactor Auth Realm | `Keyfactor` | +| KEYFACTOR_AUTH_SCOPES | Scopes to request when authenticating to Keyfactor Command API | `openid` | +| KEYFACTOR_ACCESS_TOKEN | Access token to use to authenticate to Keyfactor Command API. This can be supplied directly or generated via client credentials | | #### Client Credentials @@ -48,6 +48,7 @@ Client library for authenticating to Keyfactor Command | KEYFACTOR_AUTH_CLIENT_SECRET | Keyfactor Auth Client Secret | | ## Test Environment Variables + These environment variables are used to run go tests. They are not used in the actual client library. | Name | Description | Default | diff --git a/auth_providers/active_directory/ad_auth_test.go b/auth_providers/active_directory/ad_auth_test.go index 98d94a7..50d06b5 100644 --- a/auth_providers/active_directory/ad_auth_test.go +++ b/auth_providers/active_directory/ad_auth_test.go @@ -50,8 +50,10 @@ func TestCommandAuthActiveDirectoryCredentials_AuthEnvironment(t *testing.T) { } func checkAuthEnvADCreds() bool { - if _, ok := os.LookupEnv(TestEnvIsADAuth); ok { - return true + if isAdAuth, ok := os.LookupEnv(TestEnvIsADAuth); ok { + if isAdAuth == "true" || isAdAuth == "1" { + return true + } } return false } diff --git a/auth_providers/keycloak/keycloak_auth_client_base.go b/auth_providers/keycloak/keycloak_auth_client_base.go index 411b40f..55fd429 100644 --- a/auth_providers/keycloak/keycloak_auth_client_base.go +++ b/auth_providers/keycloak/keycloak_auth_client_base.go @@ -21,7 +21,7 @@ import ( ) const ( - EnvKeyfactorAuthHostname = "KEYFACTOR_AUTH_HOST_NAME" + EnvKeyfactorAuthHostname = "KEYFACTOR_AUTH_HOSTNAME" EnvKeyfactorAuthPort = "KEYFACTOR_AUTH_PORT" ) diff --git a/auth_providers/keycloak/keycloak_auth_client_credentials_test.go b/auth_providers/keycloak/keycloak_auth_client_credentials_test.go index b43168a..ab10428 100644 --- a/auth_providers/keycloak/keycloak_auth_client_credentials_test.go +++ b/auth_providers/keycloak/keycloak_auth_client_credentials_test.go @@ -51,8 +51,10 @@ func TestCommandAuthKeyCloakClientCredentials_AuthEnvironment(t *testing.T) { } func checkAuthEnvClientCreds() bool { - if _, ok := os.LookupEnv(TestEnvIsClientAuth); ok { - return true + if isKCAuth, ok := os.LookupEnv(TestEnvIsClientAuth); ok { + if isKCAuth == "true" || isKCAuth == "1" { + return true + } } return false } From 53a23c9302064826cb15ab880b77e260d669126e Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 8 May 2024 14:25:31 -0700 Subject: [PATCH 08/57] chore(ci): Allow manual tests --- .github/workflows/go_tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/go_tests.yml b/.github/workflows/go_tests.yml index 2fce3bc..64dfd7e 100644 --- a/.github/workflows/go_tests.yml +++ b/.github/workflows/go_tests.yml @@ -1,6 +1,6 @@ name: Go Test Workflow -on: [ push ] +on: [ push, workflow_dispatch ] jobs: test: From 356c7fb87d0a72c5025f59b7b103357eaca4d6a2 Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 8 May 2024 14:56:12 -0700 Subject: [PATCH 09/57] chore(ci): Allow manual tests --- .github/workflows/go_tests.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/go_tests.yml b/.github/workflows/go_tests.yml index 64dfd7e..ade9d6d 100644 --- a/.github/workflows/go_tests.yml +++ b/.github/workflows/go_tests.yml @@ -6,9 +6,7 @@ jobs: test: name: Run tests runs-on: ubuntu-latest - strategy: - matrix: - environment: [ "KFC_10_5_0", "KFC_11_5_0_KC" ] + environment: "KFC_10_5_0" steps: - name: Check out code uses: actions/checkout@v3 @@ -21,7 +19,6 @@ jobs: - name: Run tests run: go test -v -cover ./auth_providers/... env: - TEST_ENVIRONMENT: ${{ matrix.environment }} KEYFACTOR_PASSWORD: ${{ secrets.KEYFACTOR_PASSWORD }} KEYFACTOR_USERNAME: ${{ secrets.KEYFACTOR_USERNAME }} KEYFACTOR_AUTH_CLIENT_ID: ${{ secrets.KEYFACTOR_AUTH_CLIENT_ID }} From 2e016b4875ae0578305675fdd00a85c31409b761 Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 8 May 2024 15:04:35 -0700 Subject: [PATCH 10/57] chore(ci): Allow manual tests --- .github/workflows/go_tests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/go_tests.yml b/.github/workflows/go_tests.yml index ade9d6d..9d8f7ae 100644 --- a/.github/workflows/go_tests.yml +++ b/.github/workflows/go_tests.yml @@ -23,7 +23,7 @@ jobs: KEYFACTOR_USERNAME: ${{ secrets.KEYFACTOR_USERNAME }} KEYFACTOR_AUTH_CLIENT_ID: ${{ secrets.KEYFACTOR_AUTH_CLIENT_ID }} KEYFACTOR_AUTH_CLIENT_SECRET: ${{ secrets.KEYFACTOR_AUTH_CLIENT_SECRET }} - KEYFACTOR_HOSTNAME: ${{ env.KEYFACTOR_HOSTNAME }} - KEYFACTOR_AUTH_HOSTNAME: ${{ env.KEYFACTOR_AUTH_HOSTNAME }} - TEST_KEYFACTOR_AD_AUTH: ${{ env.TEST_KEYFACTOR_AD_AUTH }} - TEST_KEYFACTOR_KC_AUTH: ${{ env.TEST_KEYFACTOR_KC_AUTH }} \ No newline at end of file + KEYFACTOR_HOSTNAME: ${{ secrets.KEYFACTOR_HOSTNAME }} + KEYFACTOR_AUTH_HOSTNAME: ${{ secrets.KEYFACTOR_AUTH_HOSTNAME }} + TEST_KEYFACTOR_AD_AUTH: ${{ secrets.TEST_KEYFACTOR_AD_AUTH }} + TEST_KEYFACTOR_KC_AUTH: ${{ secrets.TEST_KEYFACTOR_KC_AUTH }} \ No newline at end of file From 569c0efb466a287a75cfe4fa40ca173de23a0ac5 Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 8 May 2024 15:07:00 -0700 Subject: [PATCH 11/57] chore(ci): --- .github/workflows/go_tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/go_tests.yml b/.github/workflows/go_tests.yml index 9d8f7ae..8926fc3 100644 --- a/.github/workflows/go_tests.yml +++ b/.github/workflows/go_tests.yml @@ -1,6 +1,6 @@ name: Go Test Workflow -on: [ push, workflow_dispatch ] +on: [ push ] jobs: test: From 44748835644fe7003eb3417e773eec6750ef637e Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 8 May 2024 15:08:49 -0700 Subject: [PATCH 12/57] chore(ci): vars --- .github/workflows/go_tests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/go_tests.yml b/.github/workflows/go_tests.yml index 8926fc3..e4a6a06 100644 --- a/.github/workflows/go_tests.yml +++ b/.github/workflows/go_tests.yml @@ -23,7 +23,7 @@ jobs: KEYFACTOR_USERNAME: ${{ secrets.KEYFACTOR_USERNAME }} KEYFACTOR_AUTH_CLIENT_ID: ${{ secrets.KEYFACTOR_AUTH_CLIENT_ID }} KEYFACTOR_AUTH_CLIENT_SECRET: ${{ secrets.KEYFACTOR_AUTH_CLIENT_SECRET }} - KEYFACTOR_HOSTNAME: ${{ secrets.KEYFACTOR_HOSTNAME }} - KEYFACTOR_AUTH_HOSTNAME: ${{ secrets.KEYFACTOR_AUTH_HOSTNAME }} - TEST_KEYFACTOR_AD_AUTH: ${{ secrets.TEST_KEYFACTOR_AD_AUTH }} - TEST_KEYFACTOR_KC_AUTH: ${{ secrets.TEST_KEYFACTOR_KC_AUTH }} \ No newline at end of file + KEYFACTOR_HOSTNAME: ${{ vars.KEYFACTOR_HOSTNAME }} + KEYFACTOR_AUTH_HOSTNAME: ${{ vars.KEYFACTOR_AUTH_HOSTNAME }} + TEST_KEYFACTOR_AD_AUTH: ${{ vars.TEST_KEYFACTOR_AD_AUTH }} + TEST_KEYFACTOR_KC_AUTH: ${{ vars.TEST_KEYFACTOR_KC_AUTH }} \ No newline at end of file From d8389bd9b697da16fe5bd4c1b00d23876c137470 Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 8 May 2024 15:11:54 -0700 Subject: [PATCH 13/57] chore(ci): multi env --- .github/workflows/go_tests.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/go_tests.yml b/.github/workflows/go_tests.yml index e4a6a06..a3d10d7 100644 --- a/.github/workflows/go_tests.yml +++ b/.github/workflows/go_tests.yml @@ -6,7 +6,10 @@ jobs: test: name: Run tests runs-on: ubuntu-latest - environment: "KFC_10_5_0" + strategy: + matrix: + environment: [ "KFC_10_5_0", "KFC_11_5_0_KC"] + environment: ${{ matrix.environment }} steps: - name: Check out code uses: actions/checkout@v3 From fa93edb6c6cc218ef616053d3be69ff8765a51df Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 8 May 2024 15:13:51 -0700 Subject: [PATCH 14/57] fix: update env var names for client creds --- auth_providers/keycloak/keycloak_auth_client_credentials.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/auth_providers/keycloak/keycloak_auth_client_credentials.go b/auth_providers/keycloak/keycloak_auth_client_credentials.go index 4c21838..a93601e 100644 --- a/auth_providers/keycloak/keycloak_auth_client_credentials.go +++ b/auth_providers/keycloak/keycloak_auth_client_credentials.go @@ -28,8 +28,8 @@ import ( const ( DefaultKeyfactorAuthPort = "8444" DefaultKeyfactorRealm = "Keyfactor" - EnvKeyfactorClientID = "KEYFACTOR_CLIENT_ID" - EnvKeyfactorClientSecret = "KEYFACTOR_CLIENT_SECRET" + EnvKeyfactorClientID = "KEYFACTOR_AUTH_CLIENT_ID" + EnvKeyfactorClientSecret = "KEYFACTOR_AUTH_CLIENT_SECRET" EnvKeyfactorAuthRealm = "KEYFACTOR_AUTH_REALM" EnvKeyfactorAuthTokenURL = "KEYFACTOR_AUTH_TOKEN_URL" EnvKeyfactorAccessToken = "KEYFACTOR_ACCESS_TOKEN" From 61bc39107246d0a255971130e07d4fb819a9a4da Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Mon, 13 May 2024 08:58:07 -0700 Subject: [PATCH 15/57] feat(core): Allow for passing CA certs to config. feat(core): Allow to skip TLS verification --- .github/config/environments.tf | 10 --- auth_providers/auth_core.go | 82 ++++++++++++++++--- .../keycloak/keycloak_auth_client_base.go | 62 ++++++++++++++ 3 files changed, 134 insertions(+), 20 deletions(-) diff --git a/.github/config/environments.tf b/.github/config/environments.tf index f2631ee..75ffdfb 100644 --- a/.github/config/environments.tf +++ b/.github/config/environments.tf @@ -8,16 +8,6 @@ module "keyfactor_github_test_environment_ad_10_5_0" { keyfactor_password = var.keyfactor_password_10_5_0 } -# module "keyfactor_github_test_environment_11_5_0_ad" { -# source = "git::ssh://git@github.com/Keyfactor/terraform-module-keyfactor-github-test-environment-ad.git?ref=v1.0.0" -# -# gh_environment_name = "KFC_11_5_0_AD" -# gh_repo_name = data.github_repository.repo.name -# keyfactor_hostname = var.keyfactor_hostname_11_5_0_AD -# keyfactor_username = var.keyfactor_username_11_5_0_AD -# keyfactor_password = var.keyfactor_password_11_5_0_AD -# } - 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" diff --git a/auth_providers/auth_core.go b/auth_providers/auth_core.go index ee81da5..a801fc3 100644 --- a/auth_providers/auth_core.go +++ b/auth_providers/auth_core.go @@ -15,6 +15,8 @@ package auth_providers import ( + "crypto/tls" + "crypto/x509" "encoding/json" "fmt" "io" @@ -24,14 +26,15 @@ import ( ) const ( - DefaultCommandPort = "443" - DefaultCommandAPIPath = "KeyfactorAPI" - DefaultAPIVersion = "1" - DefaultAPIClientName = "APIClient" - DefaultProductVersion = "10.5.0.0" - EnvKeyfactorHostName = "KEYFACTOR_HOSTNAME" - EnvKeyfactorPort = "KEYFACTOR_PORT" - EnvKeyfactorAPIPath = "KEYFACTOR_API_PATH" + DefaultCommandPort = "443" + DefaultCommandAPIPath = "KeyfactorAPI" + DefaultAPIVersion = "1" + DefaultAPIClientName = "APIClient" + DefaultProductVersion = "10.5.0.0" + EnvKeyfactorHostName = "KEYFACTOR_HOSTNAME" + EnvKeyfactorPort = "KEYFACTOR_PORT" + EnvKeyfactorAPIPath = "KEYFACTOR_API_PATH" + EnvKeyfactorSkipVerify = "KEYFACTOR_SKIP_VERIFY" ) // CommandAuthConfig represents the base configuration needed for authentication to Keyfactor Command API. @@ -54,8 +57,11 @@ type CommandAuthConfig struct { // 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 - CommandCACert string `json:"command_ca_cert;omitempty"` + // 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"` // HttpClient is the http client to be used for authentication to Keyfactor Command API HttpClient *http.Client @@ -87,6 +93,18 @@ func (c *CommandAuthConfig) ValidateAuthConfig() error { c.setClient() + if c.SkipVerify { + c.HttpClient.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + return nil + } + + caErr := c.updateCACerts() + if caErr != nil { + return caErr + } + return nil } @@ -97,6 +115,50 @@ func (c *CommandAuthConfig) setClient() { } } +// updateCACerts updates the CA certs for the http client. +func (c *CommandAuthConfig) updateCACerts() error { + // check if CommandCACert is set + if c.CommandCACert == "" { + return nil + } + + c.setClient() + // 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") + } + } + + // 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 { // call /Status/Endpoints API to validate credentials diff --git a/auth_providers/keycloak/keycloak_auth_client_base.go b/auth_providers/keycloak/keycloak_auth_client_base.go index 55fd429..e3f374f 100644 --- a/auth_providers/keycloak/keycloak_auth_client_base.go +++ b/auth_providers/keycloak/keycloak_auth_client_base.go @@ -15,6 +15,10 @@ package keycloak import ( + "crypto/tls" + "crypto/x509" + "fmt" + "net/http" "os" "keyfactor_auth_client/auth_providers" @@ -37,6 +41,9 @@ type CommandAuthConfigKeyCloak struct { // AuthType is the type of Keycloak auth to use such as client_credentials, password, etc. AuthType string `json:"auth_type"` + + // Auth CA Cert is the CA certificate to be used for authentication to Keycloak for use with not widely trusted certificates. This can be a filepath or a string of the certificate in PEM format. + AuthCACert string `json:"auth_ca_cert"` } // ValidateAuthConfig validates the authentication configuration for Keycloak. @@ -60,5 +67,60 @@ func (c *CommandAuthConfigKeyCloak) ValidateAuthConfig() error { c.AuthPort = DefaultKeyfactorAuthPort } } + + caErr := c.updateCACerts() + if caErr != nil { + return caErr + } + return nil +} + +func (c *CommandAuthConfigKeyCloak) updateCACerts() error { + // check if CommandCACert is set + if c.AuthCACert == "" { + return 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.AuthCACert); err == nil { + cert, ioErr := os.ReadFile(c.AuthCACert) + 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.AuthCACert)); !ok { + return fmt.Errorf("failed to append custom CA cert to pool") + } + } + + // check if client already has a tls config + if c.HttpClient.Transport == nil { + // Trust the augmented cert pool in our client + c.HttpClient.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: rootCAs, + }, + } + } else { + // Trust the augmented cert pool in our client + c.HttpClient.Transport.(*http.Transport).TLSClientConfig = &tls.Config{ + RootCAs: rootCAs, + } + } + return nil } From 77ac0bd3263c77b158685834119af3afed217a89 Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Mon, 13 May 2024 09:42:13 -0700 Subject: [PATCH 16/57] fix(core): Check for `KEYFACTOR_SKIP_VERIFY` in environment --- .github/config/environments.tf | 13 +++++++------ .github/workflows/go_tests.yml | 1 + auth_providers/auth_core.go | 5 +++++ 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.github/config/environments.tf b/.github/config/environments.tf index 75ffdfb..7719571 100644 --- a/.github/config/environments.tf +++ b/.github/config/environments.tf @@ -11,11 +11,12 @@ module "keyfactor_github_test_environment_ad_10_5_0" { 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 + 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 } diff --git a/.github/workflows/go_tests.yml b/.github/workflows/go_tests.yml index a3d10d7..cc67909 100644 --- a/.github/workflows/go_tests.yml +++ b/.github/workflows/go_tests.yml @@ -28,5 +28,6 @@ jobs: KEYFACTOR_AUTH_CLIENT_SECRET: ${{ secrets.KEYFACTOR_AUTH_CLIENT_SECRET }} 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/auth_providers/auth_core.go b/auth_providers/auth_core.go index a801fc3..d7a3290 100644 --- a/auth_providers/auth_core.go +++ b/auth_providers/auth_core.go @@ -93,6 +93,11 @@ func (c *CommandAuthConfig) ValidateAuthConfig() error { c.setClient() + // check for skip verify in environment + if skipVerify, ok := os.LookupEnv(EnvKeyfactorSkipVerify); ok { + c.SkipVerify = skipVerify == "true" || skipVerify == "1" + } + if c.SkipVerify { c.HttpClient.Transport = &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, From 40a8e219052bb9d09c891832ff9adb9af7dde7c9 Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Mon, 13 May 2024 09:52:01 -0700 Subject: [PATCH 17/57] chore(docs): Update docs with CA cert env variables --- README.md | 13 ++++++++----- auth_providers/auth_core.go | 1 + 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8eb30c8..32c4445 100644 --- a/README.md +++ b/README.md @@ -17,11 +17,13 @@ Client library for authenticating to Keyfactor Command ### 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 | +| 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 | | ### Active Directory @@ -39,6 +41,7 @@ Client library for authenticating to Keyfactor Command | KEYFACTOR_AUTH_REALM | Keyfactor Auth Realm | `Keyfactor` | | KEYFACTOR_AUTH_SCOPES | Scopes to request when authenticating to Keyfactor Command API | `openid` | | KEYFACTOR_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 | | #### Client Credentials diff --git a/auth_providers/auth_core.go b/auth_providers/auth_core.go index d7a3290..38d570e 100644 --- a/auth_providers/auth_core.go +++ b/auth_providers/auth_core.go @@ -35,6 +35,7 @@ const ( EnvKeyfactorPort = "KEYFACTOR_PORT" EnvKeyfactorAPIPath = "KEYFACTOR_API_PATH" EnvKeyfactorSkipVerify = "KEYFACTOR_SKIP_VERIFY" + EnvKeyfactorCACert = "KEYFACTOR_CA_CERT" ) // CommandAuthConfig represents the base configuration needed for authentication to Keyfactor Command API. From 85f2ef794c59ffe374f9216c18159c9a9dc7a768 Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Mon, 13 May 2024 09:58:42 -0700 Subject: [PATCH 18/57] feat(kc): Add support for custom auth CA cert. --- auth_providers/auth_core.go | 9 ++++++++- auth_providers/keycloak/keycloak_auth_client_base.go | 8 +++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/auth_providers/auth_core.go b/auth_providers/auth_core.go index 38d570e..cf22407 100644 --- a/auth_providers/auth_core.go +++ b/auth_providers/auth_core.go @@ -125,10 +125,17 @@ func (c *CommandAuthConfig) setClient() { func (c *CommandAuthConfig) updateCACerts() error { // check if CommandCACert is set if c.CommandCACert == "" { - return nil + // check if CommandCACert is set in environment + if caCert, ok := os.LookupEnv(EnvKeyfactorCACert); ok { + c.CommandCACert = caCert + } else { + return nil + } } + // ensure client is set c.setClient() + // Load the system certs rootCAs, pErr := x509.SystemCertPool() if pErr != nil { diff --git a/auth_providers/keycloak/keycloak_auth_client_base.go b/auth_providers/keycloak/keycloak_auth_client_base.go index e3f374f..dc545bf 100644 --- a/auth_providers/keycloak/keycloak_auth_client_base.go +++ b/auth_providers/keycloak/keycloak_auth_client_base.go @@ -27,6 +27,7 @@ import ( const ( EnvKeyfactorAuthHostname = "KEYFACTOR_AUTH_HOSTNAME" EnvKeyfactorAuthPort = "KEYFACTOR_AUTH_PORT" + EnvAuthCACert = "KEYFACTOR_AUTH_CA_CERT" ) type CommandAuthConfigKeyCloak struct { @@ -78,7 +79,12 @@ func (c *CommandAuthConfigKeyCloak) ValidateAuthConfig() error { func (c *CommandAuthConfigKeyCloak) updateCACerts() error { // check if CommandCACert is set if c.AuthCACert == "" { - return nil + // check environment for auth CA cert + if authCACert, ok := os.LookupEnv(EnvAuthCACert); ok { + c.AuthCACert = authCACert + } else { + return nil + } } // Load the system certs From 9f47aa57bdcb64be4f16306d8dc5f4e97f6ad455 Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Mon, 2 Sep 2024 15:11:51 -0700 Subject: [PATCH 19/57] feat(core): Add base oauth client that uses `golang.org/x/oauth2` --- auth_providers/auth_core.go | 15 +- auth_providers/auth_oauth.go | 186 ++++++++++++++++++ .../keycloak_auth_client_credentials.go | 37 +++- go.mod | 2 + 4 files changed, 226 insertions(+), 14 deletions(-) create mode 100644 auth_providers/auth_oauth.go diff --git a/auth_providers/auth_core.go b/auth_providers/auth_core.go index cf22407..a960a69 100644 --- a/auth_providers/auth_core.go +++ b/auth_providers/auth_core.go @@ -91,8 +91,7 @@ func (c *CommandAuthConfig) ValidateAuthConfig() error { c.CommandAPIPath = DefaultCommandAPIPath } } - - c.setClient() + c.SetClient(nil) // check for skip verify in environment if skipVerify, ok := os.LookupEnv(EnvKeyfactorSkipVerify); ok { @@ -114,11 +113,15 @@ func (c *CommandAuthConfig) ValidateAuthConfig() error { return nil } -// setClient sets the http client for authentication to Keyfactor Command API. -func (c *CommandAuthConfig) setClient() { +// 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 { c.HttpClient = &http.Client{} } + return c.HttpClient } // updateCACerts updates the CA certs for the http client. @@ -134,7 +137,7 @@ func (c *CommandAuthConfig) updateCACerts() error { } // ensure client is set - c.setClient() + c.SetClient(nil) // Load the system certs rootCAs, pErr := x509.SystemCertPool() @@ -175,7 +178,7 @@ func (c *CommandAuthConfig) updateCACerts() error { // Authenticate performs the authentication test to Keyfactor Command API and sets Command product version. func (c *CommandAuthConfig) Authenticate() error { // call /Status/Endpoints API to validate credentials - c.setClient() + c.SetClient(nil) //create headers for request headers := map[string]string{ diff --git a/auth_providers/auth_oauth.go b/auth_providers/auth_oauth.go new file mode 100644 index 0000000..0af145b --- /dev/null +++ b/auth_providers/auth_oauth.go @@ -0,0 +1,186 @@ +package auth_providers + +import ( + "context" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "fmt" + "net/http" + "os" + "strings" + "time" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/clientcredentials" +) + +type Authenticator interface { + GetHttpClient() (*http.Client, error) +} + +// OAuth Authenticator + +var _ Authenticator = &OAuthAuthenticator{} + +// OAuthAuthenticator is an Authenticator that uses OAuth2 for authentication. +type OAuthAuthenticator struct { + client *http.Client +} + +type OAuthAuthenticatorBuilder struct { + clientId string + clientSecret string + tokenUrl string + audience string + scopes []string + caCertificatePath string + caCertificates []*x509.Certificate +} + +func NewOAuthAuthenticatorBuilder() *OAuthAuthenticatorBuilder { + return &OAuthAuthenticatorBuilder{} +} + +func (b *OAuthAuthenticatorBuilder) WithClientId(clientId string) *OAuthAuthenticatorBuilder { + b.clientId = clientId + return b +} + +func (b *OAuthAuthenticatorBuilder) WithClientSecret(clientSecret string) *OAuthAuthenticatorBuilder { + b.clientSecret = clientSecret + return b +} + +func (b *OAuthAuthenticatorBuilder) WithTokenUrl(tokenUrl string) *OAuthAuthenticatorBuilder { + b.tokenUrl = tokenUrl + return b +} + +func (b *OAuthAuthenticatorBuilder) WithScopes(scopes []string) *OAuthAuthenticatorBuilder { + b.scopes = scopes + return b +} + +func (b *OAuthAuthenticatorBuilder) WithAudience(audience string) *OAuthAuthenticatorBuilder { + b.audience = audience + return b +} + +func (b *OAuthAuthenticatorBuilder) WithCaCertificatePath(caCertificatePath string) *OAuthAuthenticatorBuilder { + b.caCertificatePath = caCertificatePath + return b +} + +func (b *OAuthAuthenticatorBuilder) WithCaCertificates(caCertificates []*x509.Certificate) *OAuthAuthenticatorBuilder { + b.caCertificates = caCertificates + return b +} + +func (b *OAuthAuthenticatorBuilder) Build() (Authenticator, error) { + 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, + }, + } + } + + tokenSource := config.TokenSource(context.Background()) + oauthTransport := &oauth2.Transport{ + Base: http.DefaultTransport, + Source: tokenSource, + } + + if b.caCertificates == nil { + var err error + b.caCertificates, err = findCaCertificate(b.caCertificatePath) + if err != nil { + return nil, fmt.Errorf("failed to find CA certificates: %w", err) + } + } + + if len(b.caCertificates) > 0 { + tlsConfig := &tls.Config{ + Renegotiation: tls.RenegotiateOnceAsClient, + } + + tlsConfig.RootCAs = x509.NewCertPool() + for _, caCert := range b.caCertificates { + tlsConfig.RootCAs.AddCert(caCert) + } + + customTransport := http.DefaultTransport.(*http.Transport).Clone() + customTransport.TLSClientConfig = tlsConfig + customTransport.TLSHandshakeTimeout = 10 * time.Second + + // Wrap the custom transport with the oauth2.Transport + oauthTransport.Base = customTransport + } + + client := &http.Client{ + Transport: oauthTransport, + } + + return &OAuthAuthenticator{client: client}, nil +} + +func (a *OAuthAuthenticator) GetHttpClient() (*http.Client, error) { + return a.client, nil +} + +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 +} + +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 +} diff --git a/auth_providers/keycloak/keycloak_auth_client_credentials.go b/auth_providers/keycloak/keycloak_auth_client_credentials.go index a93601e..54d7078 100644 --- a/auth_providers/keycloak/keycloak_auth_client_credentials.go +++ b/auth_providers/keycloak/keycloak_auth_client_credentials.go @@ -23,16 +23,18 @@ import ( "os" "strings" "time" + + "keyfactor_auth_client/auth_providers" ) const ( - DefaultKeyfactorAuthPort = "8444" - DefaultKeyfactorRealm = "Keyfactor" - EnvKeyfactorClientID = "KEYFACTOR_AUTH_CLIENT_ID" - EnvKeyfactorClientSecret = "KEYFACTOR_AUTH_CLIENT_SECRET" - EnvKeyfactorAuthRealm = "KEYFACTOR_AUTH_REALM" - EnvKeyfactorAuthTokenURL = "KEYFACTOR_AUTH_TOKEN_URL" - EnvKeyfactorAccessToken = "KEYFACTOR_ACCESS_TOKEN" + DefaultKeyfactorAuthPort = "8444" + DefaultKeyfactorAuthRealm = "Keyfactor" + EnvKeyfactorClientID = "KEYFACTOR_AUTH_CLIENT_ID" + EnvKeyfactorClientSecret = "KEYFACTOR_AUTH_CLIENT_SECRET" + EnvKeyfactorAuthRealm = "KEYFACTOR_AUTH_REALM" + EnvKeyfactorAuthTokenURL = "KEYFACTOR_AUTH_TOKEN_URL" + EnvKeyfactorAccessToken = "KEYFACTOR_ACCESS_TOKEN" ) // CommandAuthKeyCloakClientCredentials represents the configuration needed for Keycloak authentication using client credentials. @@ -87,6 +89,25 @@ func (c *CommandAuthKeyCloakClientCredentials) Authenticate() error { c.AuthHeader = fmt.Sprintf("Bearer %s", token) + // create oauth client + oauthy, err := auth_providers.NewOAuthAuthenticatorBuilder(). + WithClientId(c.ClientID). + WithClientSecret(c.ClientSecret). + WithTokenUrl(c.TokenURL). + Build() + + if err != nil { + return err + } + + if oauthy != nil { + oClient, oerr := oauthy.GetHttpClient() + if oerr != nil { + return oerr + } + c.SetClient(oClient) + } + aErr := c.CommandAuthConfig.Authenticate() if aErr != nil { return aErr @@ -128,7 +149,7 @@ func (c *CommandAuthKeyCloakClientCredentials) setRealm() error { if realm, ok := os.LookupEnv(EnvKeyfactorAuthRealm); ok { c.Realm = realm } else { - c.Realm = DefaultKeyfactorRealm + c.Realm = DefaultKeyfactorAuthRealm } } return nil diff --git a/go.mod b/go.mod index 557ec42..0cec800 100644 --- a/go.mod +++ b/go.mod @@ -15,3 +15,5 @@ module keyfactor_auth_client go 1.22 + +require golang.org/x/oauth2 v0.22.0 From 0b51af07c35cd0917661cd26c06adbd98672981d Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 3 Sep 2024 07:07:43 -0700 Subject: [PATCH 20/57] fix(module): Add go.sum and name module correctly. --- go.mod | 2 +- go.sum | 4 ++++ tag.sh | 5 +++++ 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 go.sum create mode 100755 tag.sh diff --git a/go.mod b/go.mod index 0cec800..e7809d2 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -module keyfactor_auth_client +module keyfactor-auth-client-go go 1.22 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..280e973 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= +golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= diff --git a/tag.sh b/tag.sh new file mode 100755 index 0000000..936ef91 --- /dev/null +++ b/tag.sh @@ -0,0 +1,5 @@ +RC_VERSION=rc.1 +TAG_VERSION_1=v0.0.1-$RC_VERSION +git tag -d $TAG_VERSION_1 || true +git tag $TAG_VERSION_1 +git push origin $TAG_VERSION_1 \ No newline at end of file From 8073944f0ee6bb32b67cf86f1e13a14165716fea Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 3 Sep 2024 07:12:11 -0700 Subject: [PATCH 21/57] fix(module): name module correctly. --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index e7809d2..c1abd39 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -module keyfactor-auth-client-go +module github.com/Keyfactor/keyfactor-auth-client-go go 1.22 From cd02c0786111e4f67a494a5147d6ee99b0aa7314 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 3 Sep 2024 07:21:53 -0700 Subject: [PATCH 22/57] fix(module): ref name module correctly. --- auth_providers/active_directory/ad_auth.go | 2 +- auth_providers/keycloak/keycloak_auth_client_base.go | 2 +- auth_providers/keycloak/keycloak_auth_client_credentials.go | 2 +- main.go | 2 +- tag.sh | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/auth_providers/active_directory/ad_auth.go b/auth_providers/active_directory/ad_auth.go index 42fe0a1..1c6a559 100644 --- a/auth_providers/active_directory/ad_auth.go +++ b/auth_providers/active_directory/ad_auth.go @@ -20,7 +20,7 @@ import ( "os" "strings" - "keyfactor_auth_client/auth_providers" + "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers" ) const ( diff --git a/auth_providers/keycloak/keycloak_auth_client_base.go b/auth_providers/keycloak/keycloak_auth_client_base.go index dc545bf..66aa6a5 100644 --- a/auth_providers/keycloak/keycloak_auth_client_base.go +++ b/auth_providers/keycloak/keycloak_auth_client_base.go @@ -21,7 +21,7 @@ import ( "net/http" "os" - "keyfactor_auth_client/auth_providers" + "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers" ) const ( diff --git a/auth_providers/keycloak/keycloak_auth_client_credentials.go b/auth_providers/keycloak/keycloak_auth_client_credentials.go index 54d7078..6462871 100644 --- a/auth_providers/keycloak/keycloak_auth_client_credentials.go +++ b/auth_providers/keycloak/keycloak_auth_client_credentials.go @@ -24,7 +24,7 @@ import ( "strings" "time" - "keyfactor_auth_client/auth_providers" + "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers" ) const ( diff --git a/main.go b/main.go index 865aef8..f1615e2 100644 --- a/main.go +++ b/main.go @@ -17,7 +17,7 @@ package main import ( "fmt" - "keyfactor_auth_client/pkg" + "github.com/Keyfactor/keyfactor-auth-client-go/pkg" // Correct ) func main() { diff --git a/tag.sh b/tag.sh index 936ef91..4f82a33 100755 --- a/tag.sh +++ b/tag.sh @@ -1,4 +1,4 @@ -RC_VERSION=rc.1 +RC_VERSION=rc.2 TAG_VERSION_1=v0.0.1-$RC_VERSION git tag -d $TAG_VERSION_1 || true git tag $TAG_VERSION_1 From 0573ca65f37f783ebcb7692da81f6501d3ddcd6f Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 3 Sep 2024 08:10:28 -0700 Subject: [PATCH 23/57] feat(core): Add `KEYFACTOR_AUTH_PROVIDER` environmental variable. --- auth_providers/auth_core.go | 21 +++++++++++---------- tag.sh | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/auth_providers/auth_core.go b/auth_providers/auth_core.go index a960a69..c7c9f04 100644 --- a/auth_providers/auth_core.go +++ b/auth_providers/auth_core.go @@ -26,16 +26,17 @@ import ( ) const ( - DefaultCommandPort = "443" - DefaultCommandAPIPath = "KeyfactorAPI" - DefaultAPIVersion = "1" - DefaultAPIClientName = "APIClient" - DefaultProductVersion = "10.5.0.0" - EnvKeyfactorHostName = "KEYFACTOR_HOSTNAME" - EnvKeyfactorPort = "KEYFACTOR_PORT" - EnvKeyfactorAPIPath = "KEYFACTOR_API_PATH" - EnvKeyfactorSkipVerify = "KEYFACTOR_SKIP_VERIFY" - EnvKeyfactorCACert = "KEYFACTOR_CA_CERT" + DefaultCommandPort = "443" + DefaultCommandAPIPath = "KeyfactorAPI" + DefaultAPIVersion = "1" + DefaultAPIClientName = "APIClient" + DefaultProductVersion = "10.5.0.0" + EnvKeyfactorHostName = "KEYFACTOR_HOSTNAME" + EnvKeyfactorPort = "KEYFACTOR_PORT" + EnvKeyfactorAPIPath = "KEYFACTOR_API_PATH" + EnvKeyfactorSkipVerify = "KEYFACTOR_SKIP_VERIFY" + EnvKeyfactorCACert = "KEYFACTOR_CA_CERT" + EnvKeyfactorAuthProvider = "KEYFACTOR_AUTH_PROVIDER" ) // CommandAuthConfig represents the base configuration needed for authentication to Keyfactor Command API. diff --git a/tag.sh b/tag.sh index 4f82a33..f8d6436 100755 --- a/tag.sh +++ b/tag.sh @@ -1,4 +1,4 @@ -RC_VERSION=rc.2 +RC_VERSION=rc.4 TAG_VERSION_1=v0.0.1-$RC_VERSION git tag -d $TAG_VERSION_1 || true git tag $TAG_VERSION_1 From e1916133d295bb8a25a7d1e394d40e74b0c47a85 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 3 Sep 2024 10:15:51 -0700 Subject: [PATCH 24/57] feat(core): Use custom HTTP transport for basic auth client. --- auth_providers/auth_basic.go | 110 +++++++++++ auth_providers/auth_core.go | 91 ++++++++- auth_providers/auth_oauth.go | 183 ++++++++++-------- .../keycloak/keycloak_auth_client_base.go | 2 +- .../keycloak_auth_client_credentials.go | 47 ++--- .../keycloak_auth_client_credentials_test.go | 9 +- tag.sh | 2 +- 7 files changed, 327 insertions(+), 117 deletions(-) diff --git a/auth_providers/auth_basic.go b/auth_providers/auth_basic.go index 0b2f5d3..8a09fa3 100644 --- a/auth_providers/auth_basic.go +++ b/auth_providers/auth_basic.go @@ -14,6 +14,53 @@ package auth_providers +import ( + "encoding/base64" + "fmt" + "net/http" +) + +var _ Authenticator = &BasicAuthAuthenticator{} + +type BasicAuthAuthenticator struct { + client *http.Client +} + +func BasicAuthTransport(username, password string) *http.Client { + // Encode the username and password in Base64 + auth := username + ":" + password + encodedAuth := base64.StdEncoding.EncodeToString([]byte(auth)) + + // Create a custom RoundTripper + transport := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + // You can customize other transport settings here + } + + 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) + }, + ), + } +} + +// roundTripperFunc is a helper type to create a custom RoundTripper +type roundTripperFunc func(req *http.Request) (*http.Response, error) + +func (f roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return f(req) +} + +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 @@ -25,3 +72,66 @@ type CommandAuthConfigBasic struct { // Password is the password to be used for authentication to Keyfactor Command API Password string `json:"password"` } + +func NewBasicAuthAuthenticatorBuilder() *CommandAuthConfigBasic { + return &CommandAuthConfigBasic{} +} + +func (a *CommandAuthConfigBasic) WithUsername(username string) *CommandAuthConfigBasic { + a.Username = username + return a +} + +func (a *CommandAuthConfigBasic) WithPassword(password string) *CommandAuthConfigBasic { + a.Password = password + return a +} + +func (a *CommandAuthConfigBasic) Build() (Authenticator, error) { + + client := BasicAuthTransport(a.Username, a.Password) + a.HttpClient = client + + return &BasicAuthAuthenticator{client: client}, nil +} + +func (a *CommandAuthConfigBasic) ValidateAuthConfig() error { + if a.Username == "" { + return fmt.Errorf("username is required") + } + if a.Password == "" { + return fmt.Errorf("password is required") + } + + return a.CommandAuthConfig.ValidateAuthConfig() +} + +func (a *CommandAuthConfigBasic) Authenticate() error { + cErr := a.ValidateAuthConfig() + if cErr != nil { + return cErr + } + //basicAuth := fmt.Sprintf("%s:%s", c.Username, c.Password) + //basicAuth = base64.StdEncoding.EncodeToString([]byte(basicAuth)) + //c.AuthHeader = fmt.Sprintf("Basic %s", basicAuth) + + // create oauth client + authy, err := NewBasicAuthAuthenticatorBuilder(). + WithUsername(a.Username). + WithPassword(a.Password). + 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() +} diff --git a/auth_providers/auth_core.go b/auth_providers/auth_core.go index c7c9f04..4394a5d 100644 --- a/auth_providers/auth_core.go +++ b/auth_providers/auth_core.go @@ -18,10 +18,12 @@ import ( "crypto/tls" "crypto/x509" "encoding/json" + "encoding/pem" "fmt" "io" "net/http" "os" + "strings" "time" ) @@ -39,6 +41,11 @@ const ( EnvKeyfactorAuthProvider = "KEYFACTOR_AUTH_PROVIDER" ) +// Authenticator is an interface for authentication to Keyfactor Command API. +type Authenticator interface { + GetHttpClient() (*http.Client, error) +} + // CommandAuthConfig represents the base configuration needed for authentication to Keyfactor Command API. type CommandAuthConfig struct { // ConfigType is the type of configuration @@ -69,6 +76,36 @@ type CommandAuthConfig struct { HttpClient *http.Client } +func (c *CommandAuthConfig) WithCommandHostName(hostName string) *CommandAuthConfig { + c.CommandHostName = hostName + return c +} + +func (c *CommandAuthConfig) WithCommandPort(port string) *CommandAuthConfig { + c.CommandPort = port + return c +} + +func (c *CommandAuthConfig) WithCommandAPIPath(apiPath string) *CommandAuthConfig { + c.CommandAPIPath = apiPath + return c +} + +func (c *CommandAuthConfig) WithCommandCACert(caCert string) *CommandAuthConfig { + c.CommandCACert = caCert + return c +} + +func (c *CommandAuthConfig) WithSkipVerify(skipVerify bool) *CommandAuthConfig { + c.SkipVerify = skipVerify + return c +} + +func (c *CommandAuthConfig) WithHttpClient(client *http.Client) *CommandAuthConfig { + c.HttpClient = client + return c +} + // ValidateAuthConfig validates the authentication configuration for Keyfactor Command API. func (c *CommandAuthConfig) ValidateAuthConfig() error { if c.CommandHostName == "" { @@ -185,11 +222,14 @@ func (c *CommandAuthConfig) Authenticate() error { headers := map[string]string{ "Content-Type": "application/json", "Accept": "application/json", - "Authorization": c.AuthHeader, "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, @@ -253,3 +293,52 @@ func (c *CommandAuthConfig) Authenticate() error { return nil } + +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 +} + +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 +} diff --git a/auth_providers/auth_oauth.go b/auth_providers/auth_oauth.go index 0af145b..051ad22 100644 --- a/auth_providers/auth_oauth.go +++ b/auth_providers/auth_oauth.go @@ -4,23 +4,35 @@ import ( "context" "crypto/tls" "crypto/x509" - "encoding/pem" "fmt" "net/http" - "os" - "strings" "time" "golang.org/x/oauth2" "golang.org/x/oauth2/clientcredentials" ) -type Authenticator interface { - GetHttpClient() (*http.Client, error) -} +const ( + // DefaultKeyfactorAuthPort is the default port for Keyfactor authentication + DefaultKeyfactorAuthPort = "8444" -// OAuth Authenticator + // 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_ACCESS_TOKEN" +) + +// OAuth Authenticator var _ Authenticator = &OAuthAuthenticator{} // OAuthAuthenticator is an Authenticator that uses OAuth2 for authentication. @@ -28,67 +40,72 @@ type OAuthAuthenticator struct { client *http.Client } -type OAuthAuthenticatorBuilder struct { - clientId string - clientSecret string - tokenUrl string - audience string - scopes []string - caCertificatePath string - caCertificates []*x509.Certificate +func (a *OAuthAuthenticator) GetHttpClient() (*http.Client, error) { + return a.client, nil +} + +type CommandConfigOauth struct { + CommandAuthConfig + ClientID string `json:"client_id,omitempty"` + ClientSecret string `json:"client_secret,omitempty"` + TokenURL string `json:"token_url,omitempty"` + Audience string `json:"audience,omitempty"` + Scopes []string `json:"scopes,omitempty"` + CACertificatePath string `json:"idp_ca_cert_path,omitempty"` + CACertificates []*x509.Certificate `json:"-"` } -func NewOAuthAuthenticatorBuilder() *OAuthAuthenticatorBuilder { - return &OAuthAuthenticatorBuilder{} +func NewOAuthAuthenticatorBuilder() *CommandConfigOauth { + return &CommandConfigOauth{} } -func (b *OAuthAuthenticatorBuilder) WithClientId(clientId string) *OAuthAuthenticatorBuilder { - b.clientId = clientId +func (b *CommandConfigOauth) WithClientId(clientId string) *CommandConfigOauth { + b.ClientID = clientId return b } -func (b *OAuthAuthenticatorBuilder) WithClientSecret(clientSecret string) *OAuthAuthenticatorBuilder { - b.clientSecret = clientSecret +func (b *CommandConfigOauth) WithClientSecret(clientSecret string) *CommandConfigOauth { + b.ClientSecret = clientSecret return b } -func (b *OAuthAuthenticatorBuilder) WithTokenUrl(tokenUrl string) *OAuthAuthenticatorBuilder { - b.tokenUrl = tokenUrl +func (b *CommandConfigOauth) WithTokenUrl(tokenUrl string) *CommandConfigOauth { + b.TokenURL = tokenUrl return b } -func (b *OAuthAuthenticatorBuilder) WithScopes(scopes []string) *OAuthAuthenticatorBuilder { - b.scopes = scopes +func (b *CommandConfigOauth) WithScopes(scopes []string) *CommandConfigOauth { + b.Scopes = scopes return b } -func (b *OAuthAuthenticatorBuilder) WithAudience(audience string) *OAuthAuthenticatorBuilder { - b.audience = audience +func (b *CommandConfigOauth) WithAudience(audience string) *CommandConfigOauth { + b.Audience = audience return b } -func (b *OAuthAuthenticatorBuilder) WithCaCertificatePath(caCertificatePath string) *OAuthAuthenticatorBuilder { - b.caCertificatePath = caCertificatePath +func (b *CommandConfigOauth) WithCaCertificatePath(caCertificatePath string) *CommandConfigOauth { + b.CACertificatePath = caCertificatePath return b } -func (b *OAuthAuthenticatorBuilder) WithCaCertificates(caCertificates []*x509.Certificate) *OAuthAuthenticatorBuilder { - b.caCertificates = caCertificates +func (b *CommandConfigOauth) WithCaCertificates(caCertificates []*x509.Certificate) *CommandConfigOauth { + b.CACertificates = caCertificates return b } -func (b *OAuthAuthenticatorBuilder) Build() (Authenticator, error) { +func (b *CommandConfigOauth) Build() (Authenticator, error) { config := &clientcredentials.Config{ - ClientID: b.clientId, - ClientSecret: b.clientSecret, - TokenURL: b.tokenUrl, - Scopes: b.scopes, + ClientID: b.ClientID, + ClientSecret: b.ClientSecret, + TokenURL: b.TokenURL, + Scopes: b.Scopes, } - if b.audience != "" { + if b.Audience != "" { config.EndpointParams = map[string][]string{ - "audience": { - b.audience, + "Audience": { + b.Audience, }, } } @@ -99,21 +116,21 @@ func (b *OAuthAuthenticatorBuilder) Build() (Authenticator, error) { Source: tokenSource, } - if b.caCertificates == nil { + if b.CACertificates == nil { var err error - b.caCertificates, err = findCaCertificate(b.caCertificatePath) + b.CACertificates, err = FindCACertificate(b.CACertificatePath) if err != nil { return nil, fmt.Errorf("failed to find CA certificates: %w", err) } } - if len(b.caCertificates) > 0 { + if len(b.CACertificates) > 0 { tlsConfig := &tls.Config{ Renegotiation: tls.RenegotiateOnceAsClient, } tlsConfig.RootCAs = x509.NewCertPool() - for _, caCert := range b.caCertificates { + for _, caCert := range b.CACertificates { tlsConfig.RootCAs.AddCert(caCert) } @@ -132,55 +149,57 @@ func (b *OAuthAuthenticatorBuilder) Build() (Authenticator, error) { return &OAuthAuthenticator{client: client}, nil } -func (a *OAuthAuthenticator) GetHttpClient() (*http.Client, error) { - return a.client, nil -} - -func findCaCertificate(caCertificatePath string) ([]*x509.Certificate, error) { - if caCertificatePath == "" { - return nil, nil +func (b *CommandConfigOauth) ValidateAuthConfig() error { + if b.ClientID == "" { + return fmt.Errorf("client ID is required") } - buf, err := os.ReadFile(caCertificatePath) - if err != nil { - return nil, fmt.Errorf("failed to read CA certificate file at path %s: %w", caCertificatePath, err) + if b.ClientSecret == "" { + return fmt.Errorf("client secret is required") } - // Decode the PEM encoded certificates into a slice of PEM blocks - chainBlocks, _, err := decodePEMBytes(buf) - if err != nil { - return nil, err + + if b.TokenURL == "" { + return fmt.Errorf("token URL is required") } - if len(chainBlocks) <= 0 { - return nil, fmt.Errorf("didn't find certificate in file at path %s", caCertificatePath) + + if len(b.Scopes) == 0 { + return fmt.Errorf("at least one scope is required") } - 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) - } + return b.CommandAuthConfig.ValidateAuthConfig() +} + +func (b *CommandConfigOauth) Authenticate() error { - caChain = append(caChain, cert) + // validate auth config + vErr := b.ValidateAuthConfig() + if vErr != nil { + return vErr } - return caChain, nil -} + // create oauth client + oauthy, err := NewOAuthAuthenticatorBuilder(). + WithClientId(b.ClientID). + WithClientSecret(b.ClientSecret). + WithTokenUrl(b.TokenURL). + Build() + + if err != nil { + return err + } -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) + if oauthy != nil { + oClient, oerr := oauthy.GetHttpClient() + if oerr != nil { + return oerr } + b.SetClient(oClient) } - return certificates, privKey, nil + + aErr := b.CommandAuthConfig.Authenticate() + if aErr != nil { + return aErr + } + + return nil } diff --git a/auth_providers/keycloak/keycloak_auth_client_base.go b/auth_providers/keycloak/keycloak_auth_client_base.go index 66aa6a5..9d5cd99 100644 --- a/auth_providers/keycloak/keycloak_auth_client_base.go +++ b/auth_providers/keycloak/keycloak_auth_client_base.go @@ -65,7 +65,7 @@ func (c *CommandAuthConfigKeyCloak) ValidateAuthConfig() error { if port, ok := os.LookupEnv(EnvKeyfactorAuthPort); ok { c.AuthPort = port } else { - c.AuthPort = DefaultKeyfactorAuthPort + c.AuthPort = auth_providers.DefaultKeyfactorAuthPort } } diff --git a/auth_providers/keycloak/keycloak_auth_client_credentials.go b/auth_providers/keycloak/keycloak_auth_client_credentials.go index 6462871..b5d42ed 100644 --- a/auth_providers/keycloak/keycloak_auth_client_credentials.go +++ b/auth_providers/keycloak/keycloak_auth_client_credentials.go @@ -28,13 +28,8 @@ import ( ) const ( - DefaultKeyfactorAuthPort = "8444" DefaultKeyfactorAuthRealm = "Keyfactor" - EnvKeyfactorClientID = "KEYFACTOR_AUTH_CLIENT_ID" - EnvKeyfactorClientSecret = "KEYFACTOR_AUTH_CLIENT_SECRET" EnvKeyfactorAuthRealm = "KEYFACTOR_AUTH_REALM" - EnvKeyfactorAuthTokenURL = "KEYFACTOR_AUTH_TOKEN_URL" - EnvKeyfactorAccessToken = "KEYFACTOR_ACCESS_TOKEN" ) // CommandAuthKeyCloakClientCredentials represents the configuration needed for Keycloak authentication using client credentials. @@ -68,26 +63,21 @@ type CommandAuthKeyCloakClientCredentials struct { // Authenticate performs the authentication process for Keycloak using client credentials. // It validates the authentication configuration, gets the token, and calls the base authentication method. func (c *CommandAuthKeyCloakClientCredentials) Authenticate() error { - cErr := c.CommandAuthConfig.ValidateAuthConfig() // Validate base config - if cErr != nil { - return cErr - } - c.AuthType = "client_credentials" - cErr = c.ValidateAuthConfig() + cErr := c.ValidateAuthConfig() if cErr != nil { return cErr } - token, tErr := c.GetToken() - if tErr != nil { - return tErr - } - if token == "" { - return fmt.Errorf("failed to get Bearer token using client credentials") - } - - c.AuthHeader = fmt.Sprintf("Bearer %s", token) + //token, tErr := c.GetToken() + //if tErr != nil { + // return tErr + //} + //if token == "" { + // return fmt.Errorf("failed to get Bearer token using client credentials") + //} + // + //c.AuthHeader = fmt.Sprintf("Bearer %s", token) // create oauth client oauthy, err := auth_providers.NewOAuthAuthenticatorBuilder(). @@ -120,10 +110,10 @@ func (c *CommandAuthKeyCloakClientCredentials) Authenticate() error { // It retrieves the client ID from environment variables if it's not set. func (c *CommandAuthKeyCloakClientCredentials) setClientId() error { if c.ClientID == "" { - if clientID, ok := os.LookupEnv(EnvKeyfactorClientID); ok { + if clientID, ok := os.LookupEnv(auth_providers.EnvKeyfactorClientID); ok { c.ClientID = clientID } else { - return fmt.Errorf("client_id or environment variable %s is required", EnvKeyfactorClientID) + return fmt.Errorf("client_id or environment variable %s is required", auth_providers.EnvKeyfactorClientID) } } return nil @@ -133,10 +123,13 @@ func (c *CommandAuthKeyCloakClientCredentials) setClientId() error { // It retrieves the client secret from environment variables if it's not set. func (c *CommandAuthKeyCloakClientCredentials) setClientSecret() error { if c.ClientSecret == "" { - if clientSecret, ok := os.LookupEnv(EnvKeyfactorClientSecret); ok { + if clientSecret, ok := os.LookupEnv(auth_providers.EnvKeyfactorClientSecret); ok { c.ClientSecret = clientSecret } else { - return fmt.Errorf("client_secret or environment variable %s is required", EnvKeyfactorClientSecret) + return fmt.Errorf( + "client_secret or environment variable %s is required", + auth_providers.EnvKeyfactorClientSecret, + ) } } return nil @@ -159,7 +152,7 @@ func (c *CommandAuthKeyCloakClientCredentials) setRealm() error { // It generates the token URL if it's not set. func (c *CommandAuthKeyCloakClientCredentials) setTokenURL() error { if c.TokenURL == "" { - if tokenURL, ok := os.LookupEnv(EnvKeyfactorAuthTokenURL); ok { + if tokenURL, ok := os.LookupEnv(auth_providers.EnvKeyfactorAuthTokenURL); ok { c.TokenURL = tokenURL } else { c.TokenURL = fmt.Sprintf( @@ -202,7 +195,7 @@ func (c *CommandAuthKeyCloakClientCredentials) ValidateAuthConfig() error { } - return nil + return c.CommandAuthConfig.ValidateAuthConfig() } // GetToken gets the access token for Keycloak authentication. @@ -210,7 +203,7 @@ func (c *CommandAuthKeyCloakClientCredentials) ValidateAuthConfig() error { func (c *CommandAuthKeyCloakClientCredentials) GetToken() (string, error) { // Check if access token is set in environment variable if c.AccessToken == "" { - if accessToken, ok := os.LookupEnv(EnvKeyfactorAccessToken); ok { + if accessToken, ok := os.LookupEnv(auth_providers.EnvKeyfactorAccessToken); ok { c.AccessToken = accessToken // Don't try to refresh as we don't have a refresh token diff --git a/auth_providers/keycloak/keycloak_auth_client_credentials_test.go b/auth_providers/keycloak/keycloak_auth_client_credentials_test.go index ab10428..64ed06f 100644 --- a/auth_providers/keycloak/keycloak_auth_client_credentials_test.go +++ b/auth_providers/keycloak/keycloak_auth_client_credentials_test.go @@ -15,7 +15,6 @@ package keycloak import ( - "fmt" "os" "testing" ) @@ -44,10 +43,10 @@ func TestCommandAuthKeyCloakClientCredentials_AuthEnvironment(t *testing.T) { } // Check that the AuthHeader was set correctly - expectedAuthHeader := fmt.Sprintf("Bearer %s", c.AccessToken) - if c.AuthHeader != expectedAuthHeader { - t.Errorf("Authenticate() AuthHeader = %v, want %v", c.AuthHeader, expectedAuthHeader) - } + //expectedAuthHeader := fmt.Sprintf("Bearer %s", c.AccessToken) + //if c.AuthHeader != expectedAuthHeader { + // t.Errorf("Authenticate() AuthHeader = %v, want %v", c.AuthHeader, expectedAuthHeader) + //} else if c.HttpClient.Tra } func checkAuthEnvClientCreds() bool { diff --git a/tag.sh b/tag.sh index f8d6436..c1e0379 100755 --- a/tag.sh +++ b/tag.sh @@ -1,4 +1,4 @@ -RC_VERSION=rc.4 +RC_VERSION=rc.5 TAG_VERSION_1=v0.0.1-$RC_VERSION git tag -d $TAG_VERSION_1 || true git tag $TAG_VERSION_1 From 7bc0781f2a6b3eb0fbc3bac6e609e0f970c51088 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 3 Sep 2024 10:22:52 -0700 Subject: [PATCH 25/57] fix(oauth): Add environment vars: `KEYFACTOR_AUTH_AUDIENCE, KEYFACTOR_AUTH_SCOPES` --- auth_providers/auth_oauth.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/auth_providers/auth_oauth.go b/auth_providers/auth_oauth.go index 051ad22..a4cc016 100644 --- a/auth_providers/auth_oauth.go +++ b/auth_providers/auth_oauth.go @@ -30,6 +30,13 @@ const ( // EnvKeyfactorAccessToken is the environment variable used to set the access token for oauth client credentials authentication EnvKeyfactorAccessToken = "KEYFACTOR_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" ) // OAuth Authenticator From 4729d45bf7976e6a306be6f4139c1712dcee970c Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 3 Sep 2024 10:28:48 -0700 Subject: [PATCH 26/57] fix(oauth): Scope `KEYFACTOR_AUTH_HOSTNAME, KEYFACTOR_AUTH_PORT, KEYFACTOR_AUTH_CA_CERT` to `auth_oauth.go` --- auth_providers/auth_oauth.go | 9 +++++++++ auth_providers/keycloak/keycloak_auth_client_base.go | 6 ------ tag.sh | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/auth_providers/auth_oauth.go b/auth_providers/auth_oauth.go index a4cc016..df51e35 100644 --- a/auth_providers/auth_oauth.go +++ b/auth_providers/auth_oauth.go @@ -37,6 +37,15 @@ const ( // EnvKeyfactorAuthScopes is the environment variable used to set the scopes for oauth client credentials authentication EnvKeyfactorAuthScopes = "KEYFACTOR_AUTH_SCOPES" + + // EnvKeyfactorAuthHostname is the environment variable used to set the hostname for oauth client credentials authentication + EnvKeyfactorAuthHostname = "KEYFACTOR_AUTH_HOSTNAME" + + // EnvKeyfactorAuthPort is the environment variable used to set the port for oauth client credentials authentication + EnvKeyfactorAuthPort = "KEYFACTOR_AUTH_PORT" + + // EnvAuthCACert is a path to a CA certificate for the OAuth client credentials authentication + EnvAuthCACert = "KEYFACTOR_AUTH_CA_CERT" ) // OAuth Authenticator diff --git a/auth_providers/keycloak/keycloak_auth_client_base.go b/auth_providers/keycloak/keycloak_auth_client_base.go index 9d5cd99..48d94b8 100644 --- a/auth_providers/keycloak/keycloak_auth_client_base.go +++ b/auth_providers/keycloak/keycloak_auth_client_base.go @@ -24,12 +24,6 @@ import ( "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers" ) -const ( - EnvKeyfactorAuthHostname = "KEYFACTOR_AUTH_HOSTNAME" - EnvKeyfactorAuthPort = "KEYFACTOR_AUTH_PORT" - EnvAuthCACert = "KEYFACTOR_AUTH_CA_CERT" -) - type CommandAuthConfigKeyCloak struct { // CommandAuthConfig is a reference to the base configuration needed for authentication to Keyfactor Command API auth_providers.CommandAuthConfig diff --git a/tag.sh b/tag.sh index c1e0379..1f84e80 100755 --- a/tag.sh +++ b/tag.sh @@ -1,4 +1,4 @@ -RC_VERSION=rc.5 +RC_VERSION=rc.6 TAG_VERSION_1=v0.0.1-$RC_VERSION git tag -d $TAG_VERSION_1 || true git tag $TAG_VERSION_1 From 559cefef4da9f76e28683e6778280707217e3d68 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 16 Oct 2024 16:42:55 -0700 Subject: [PATCH 27/57] feat(core): Simplify oauth and basic auth feat(core): Add ability to load from config file on disk and/or environmental variables feat(tests): Add tests for auth methods --- .github/config/variables.tf | 4 +- .../workflows/keyfactor-starter-workflow.yml | 3 +- auth_config/auth_config_example.json | 42 +++ auth_config/auth_config_schema.json | 110 +++++++ auth_config/command_config.go | 173 +++++++++++ auth_providers/active_directory/ad_auth.go | 129 -------- .../active_directory/ad_auth_test.go | 59 ---- auth_providers/auth_basic.go | 121 +++++--- auth_providers/auth_basic_test.go | 77 +++++ auth_providers/auth_core.go | 243 +++++++++++++-- auth_providers/auth_core_test.go | 90 ++++++ auth_providers/auth_oauth.go | 140 +++++---- auth_providers/auth_oauth_test.go | 87 ++++++ .../keycloak/keycloak_auth_certificate.go | 15 - .../keycloak/keycloak_auth_client_base.go | 126 -------- .../keycloak_auth_client_credentials.go | 286 ------------------ .../keycloak_auth_client_credentials_test.go | 59 ---- go.mod | 5 +- go.sum | 4 + lib/main.go | 132 ++++++++ lib/test_ca_cert.pem | 18 ++ lib/test_chain.pem | 36 +++ lib/test_leaf_cert.pem | 18 ++ tag.sh | 2 +- 24 files changed, 1181 insertions(+), 798 deletions(-) create mode 100644 auth_config/auth_config_example.json create mode 100644 auth_config/auth_config_schema.json create mode 100644 auth_config/command_config.go delete mode 100644 auth_providers/active_directory/ad_auth.go delete mode 100644 auth_providers/active_directory/ad_auth_test.go create mode 100644 auth_providers/auth_basic_test.go create mode 100644 auth_providers/auth_core_test.go create mode 100644 auth_providers/auth_oauth_test.go delete mode 100644 auth_providers/keycloak/keycloak_auth_certificate.go delete mode 100644 auth_providers/keycloak/keycloak_auth_client_base.go delete mode 100644 auth_providers/keycloak/keycloak_auth_client_credentials.go delete mode 100644 auth_providers/keycloak/keycloak_auth_client_credentials_test.go create mode 100644 lib/main.go create mode 100644 lib/test_ca_cert.pem create mode 100644 lib/test_chain.pem create mode 100644 lib/test_leaf_cert.pem diff --git a/.github/config/variables.tf b/.github/config/variables.tf index 8c52d88..c61463b 100644 --- a/.github/config/variables.tf +++ b/.github/config/variables.tf @@ -27,12 +27,12 @@ variable "keyfactor_client_secret_11_5_0" { variable "keyfactor_hostname_11_5_0_KC" { description = "The hostname of the Keyfactor instance" type = string - default = "pkiaas-spb.eastus2.cloudapp.azure.com" + default = "int-oidc-lab.eastus2.cloudapp.azure.com" } variable "keyfactor_auth_hostname_11_5_0_KC" { description = "The hostname of the KeyCloak instance to authenticate to for a Keyfactor Command access token" type = string - default = "pkiaas-spb.eastus2.cloudapp.azure.com" + default = "int-oidc-lab.eastus2.cloudapp.azure.com" } diff --git a/.github/workflows/keyfactor-starter-workflow.yml b/.github/workflows/keyfactor-starter-workflow.yml index 97011fc..01ddd34 100644 --- a/.github/workflows/keyfactor-starter-workflow.yml +++ b/.github/workflows/keyfactor-starter-workflow.yml @@ -11,8 +11,7 @@ on: jobs: call-starter-workflow: - uses: keyfactor/actions/.github/workflows/starter.yml@v2 - needs: get-versions + uses: keyfactor/actions/.github/workflows/starter.yml@v3 secrets: token: ${{ secrets.V2BUILDTOKEN}} APPROVE_README_PUSH: ${{ secrets.APPROVE_README_PUSH}} diff --git a/auth_config/auth_config_example.json b/auth_config/auth_config_example.json new file mode 100644 index 0000000..d2f4fac --- /dev/null +++ b/auth_config/auth_config_example.json @@ -0,0 +1,42 @@ +{ + "servers": { + "default_profile": { + "host": "keyfactor.command.kfdelivery.com", + "auth_hostname": "idp.keyfactor.command.kfdelivery.com", + "auth_port": 8444, + "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" + } + } + }, + "alternative_profile": { + "host": "keyfactor2.command.kfdelivery.com", + "auth_hostname": "idp.keyfactor2.command.kfdelivery.com", + "auth_port": 8444, + "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/auth_config/auth_config_schema.json b/auth_config/auth_config_schema.json new file mode 100644 index 0000000..7b9bfc9 --- /dev/null +++ b/auth_config/auth_config_schema.json @@ -0,0 +1,110 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "servers": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9_-]+$": { + "type": "object", + "properties": { + "host": { + "type": "string" + }, + "auth_hostname": { + "type": "string" + }, + "auth_port": { + "type": "integer" + }, + "username": { + "type": "string" + }, + "password": { + "type": "string" + }, + "client_id": { + "type": "string" + }, + "client_secret": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "api_path": { + "type": "string" + }, + "auth_provider": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "profile": { + "type": "string" + }, + "parameters": { + "type": "object", + "properties": { + "secret_name": { + "type": "string" + }, + "vault_name": { + "type": "string" + } + }, + "required": [ + "secret_name", + "vault_name" + ] + } + }, + "required": [ + "type", + "profile", + "parameters" + ] + } + }, + "oneOf": [ + { + "required": ["username", "password"], + "not": { + "required": ["client_id", "client_secret"] + } + }, + { + "required": ["client_id", "client_secret"], + "not": { + "required": ["username", "password"] + } + } + ], + "if": { + "required": ["auth_provider"] + }, + "then": { + "required": ["auth_provider"] + }, + "else": { + "if": { + "required": ["client_id", "client_secret"] + }, + "then": { + "required": ["auth_hostname", "host"] + }, + "else": { + "required": ["host"] + } + } + } + }, + "additionalProperties": false + } + }, + "required": [ + "servers" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/auth_config/command_config.go b/auth_config/command_config.go new file mode 100644 index 0000000..4fb5bd4 --- /dev/null +++ b/auth_config/command_config.go @@ -0,0 +1,173 @@ +package authconfig + +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. + AuthHostname string `json:"auth_hostname,omitempty" yaml:"auth_hostname,omitempty"` // AuthHostname is the authentication hostname. + 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. + 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. + Domain string `json:"domain,omitempty" yaml:"domain,omitempty"` // Domain is the domain for authentication. + 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. +} + +// 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 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. +} + +// UnmarshalJSON unmarshals the JSON data into the Config struct. +//func (c *Config) UnmarshalJSON(data []byte) error { +// return json.Unmarshal(data, c) +//} + +// UnmarshalYAML unmarshals the YAML data into the Config struct. +//func (c *Config) UnmarshalYAML(data []byte) error { +// return yaml.Unmarshal(data, c) +//} + +// ReadServerFromJSON reads a Server configuration from a JSON file. +func ReadServerFromJSON(filePath string) (*Server, error) { + file, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer file.Close() + + var server Server + decoder := json.NewDecoder(file) + if err := decoder.Decode(&server); err != nil { + return nil, err + } + + return &server, nil +} + +// WriteServerToJSON writes a Server configuration to a JSON file. +func WriteServerToJSON(filePath string, server *Server) error { + file, err := os.Create(filePath) + if err != nil { + return err + } + defer file.Close() + + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") + if err := encoder.Encode(server); err != nil { + return err + } + + return nil +} + +// ReadServerFromYAML reads a Server configuration from a YAML file. +func ReadServerFromYAML(filePath string) (*Server, error) { + file, err := os.ReadFile(filePath) + if err != nil { + return nil, err + } + + var server Server + if err := yaml.Unmarshal(file, &server); err != nil { + return nil, err + } + + return &server, nil +} + +// WriteServerToYAML writes a Server configuration to a YAML file. +func WriteServerToYAML(filePath string, server *Server) error { + data, err := yaml.Marshal(server) + if err != nil { + return err + } + + if err := os.WriteFile(filePath, data, 0644); err != nil { + return err + } + + return nil +} + +// WriteConfigToJSON writes a Config configuration to a JSON file. +func WriteConfigToJSON(filePath string, config *Config) error { + file, err := os.Create(filePath) + if err != nil { + return err + } + defer file.Close() + + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") + if err := encoder.Encode(config); err != nil { + return err + } + + return nil +} + +// WriteConfigToYAML writes a Config configuration to a YAML file. +func WriteConfigToYAML(filePath string, config *Config) error { + data, err := yaml.Marshal(config) + if err != nil { + return err + } + + if err := os.WriteFile(filePath, data, 0644); err != nil { + return err + } + + return nil +} + +// MergeConfigFromFile merges the configuration from a file into the existing Config. +func MergeConfigFromFile(filePath string, config *Config) error { + // Read the file content + data, err := os.ReadFile(filePath) + if err != nil { + return fmt.Errorf("failed to read config file: %w", err) + } + + // Determine the file type (JSON or YAML) and unmarshal accordingly + var tempConfig Config + if json.Valid(data) { + if err := json.Unmarshal(data, &tempConfig); err != nil { + return fmt.Errorf("failed to unmarshal JSON config: %w", err) + } + } else { + if err := yaml.Unmarshal(data, &tempConfig); err != nil { + return fmt.Errorf("failed to unmarshal YAML config: %w", err) + } + } + + // Merge the temporary config into the existing config + for key, server := range tempConfig.Servers { + if _, exists := config.Servers[key]; !exists { + config.Servers[key] = server + } + } + + return nil +} diff --git a/auth_providers/active_directory/ad_auth.go b/auth_providers/active_directory/ad_auth.go deleted file mode 100644 index 1c6a559..0000000 --- a/auth_providers/active_directory/ad_auth.go +++ /dev/null @@ -1,129 +0,0 @@ -// 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 active_directory - -import ( - "encoding/base64" - "fmt" - "os" - "strings" - - "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers" -) - -const ( - EnvKeyfactorDomain = "KEYFACTOR_DOMAIN" - EnvKeyfactorUsername = "KEYFACTOR_USERNAME" - EnvKeyfactorPassword = "KEYFACTOR_PASSWORD" -) - -// CommandAuthConfigActiveDirectory represents the configuration needed for Active Directory authentication. -// It embeds CommandAuthConfigBasic and adds additional fields specific to Active Directory. -type CommandAuthConfigActiveDirectory struct { - // CommandAuthConfig is a reference to the base configuration needed for authentication to Keyfactor Command API - auth_providers.CommandAuthConfigBasic - - // Domain is the domain of the Active Directory used to authenticate to Keyfactor Command API - Domain string `json:"domain"` -} - -// Authenticate performs the authentication process for Active Directory. -// It validates the authentication configuration, generates the authentication header, and calls the basic authentication method. -func (c *CommandAuthConfigActiveDirectory) Authenticate() error { - cErr := c.ValidateAuthConfig() - if cErr != nil { - return cErr - } - - c.AuthHeader = fmt.Sprintf("Basic %s", c.getBasicAuthHeader()) - - aErr := c.CommandAuthConfigBasic.Authenticate() - if aErr != nil { - return aErr - } - - return nil -} - -// getBasicAuthHeader generates the basic authentication header value. -// It combines the username, domain, and password with a colon separator, and encodes the resulting string in base64. -func (c *CommandAuthConfigActiveDirectory) getBasicAuthHeader() string { - authStr := fmt.Sprintf("%s@%s:%s", c.Username, c.Domain, c.Password) - return base64.StdEncoding.EncodeToString([]byte(authStr)) -} - -// 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 (c *CommandAuthConfigActiveDirectory) parseUsernameDomain() error { - domainErr := fmt.Errorf("domain or environment variable %s is required", EnvKeyfactorDomain) - if strings.Contains(c.Username, "@") { - dSplit := strings.Split(c.Username, "@") - if len(dSplit) != 2 { - return domainErr - } - c.Username = dSplit[0] // remove domain from username - c.Domain = dSplit[1] - } else if strings.Contains(c.Username, "\\") { - dSplit := strings.Split(c.Username, "\\") - if len(dSplit) != 2 { - return domainErr - } - c.Domain = dSplit[0] - c.Username = dSplit[1] // remove domain from username - } - - return nil -} - -// ValidateAuthConfig validates the authentication configuration for Active Directory. -// It checks the username, domain, and password, and retrieves them from environment variables if they're not set. -func (c *CommandAuthConfigActiveDirectory) ValidateAuthConfig() error { - cErr := c.CommandAuthConfigBasic.ValidateAuthConfig() - if cErr != nil { - return cErr - } - - if c.Username == "" { - if username, ok := os.LookupEnv(EnvKeyfactorUsername); ok { - c.Username = username - } else { - return fmt.Errorf("username or environment variable %s is required", EnvKeyfactorUsername) - } - } - - domainErr := c.parseUsernameDomain() - if domainErr != nil { - return domainErr - - } - - if c.Password == "" { - if password, ok := os.LookupEnv(EnvKeyfactorPassword); ok { - c.Password = password - } else { - return fmt.Errorf("password or environment variable %s is required", EnvKeyfactorPassword) - } - } - - if c.Domain == "" { - if domain, ok := os.LookupEnv(EnvKeyfactorDomain); ok { - c.Domain = domain - } else { - return domainErr - } - } - - return nil -} diff --git a/auth_providers/active_directory/ad_auth_test.go b/auth_providers/active_directory/ad_auth_test.go deleted file mode 100644 index 50d06b5..0000000 --- a/auth_providers/active_directory/ad_auth_test.go +++ /dev/null @@ -1,59 +0,0 @@ -// 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 active_directory - -import ( - "fmt" - "os" - "testing" -) - -const ( - TestEnvIsADAuth = "TEST_KEYFACTOR_AD_AUTH" -) - -func TestCommandAuthActiveDirectoryCredentials_AuthEnvironment(t *testing.T) { - - if !checkAuthEnvADCreds() { - msg := "Skipping test because Keyfactor Command environment is not authenticated with Active Directory credentials" - t.Log(msg) - t.Skip(msg) - return - } - // Create a new CommandAuthActiveDirectoryCredentials instance - c := &CommandAuthConfigActiveDirectory{} //Used environment configuration - - // Call the Authenticate method - err := c.Authenticate() - if err != nil { - t.Errorf("Authenticate() error = %v", err) - return - } - - // Check that the AuthHeader was set correctly - expectedAuthHeader := fmt.Sprintf("Basic %s", c.getBasicAuthHeader()) - if c.AuthHeader != expectedAuthHeader { - t.Errorf("Authenticate() AuthHeader = %v, want %v", c.AuthHeader, expectedAuthHeader) - } -} - -func checkAuthEnvADCreds() bool { - if isAdAuth, ok := os.LookupEnv(TestEnvIsADAuth); ok { - if isAdAuth == "true" || isAdAuth == "1" { - return true - } - } - return false -} diff --git a/auth_providers/auth_basic.go b/auth_providers/auth_basic.go index 8a09fa3..e308535 100644 --- a/auth_providers/auth_basic.go +++ b/auth_providers/auth_basic.go @@ -18,47 +18,25 @@ import ( "encoding/base64" "fmt" "net/http" + "os" ) +const ( + EnvKeyfactorUsername = "KEYFACTOR_USERNAME" + EnvKeyfactorPassword = "KEYFACTOR_PASSWORD" +) + +// Basic Authenticator var _ Authenticator = &BasicAuthAuthenticator{} +// BasicAuthAuthenticator is an Authenticator that uses Basic Auth for authentication. type BasicAuthAuthenticator struct { - client *http.Client -} - -func BasicAuthTransport(username, password string) *http.Client { - // Encode the username and password in Base64 - auth := username + ":" + password - encodedAuth := base64.StdEncoding.EncodeToString([]byte(auth)) - - // Create a custom RoundTripper - transport := &http.Transport{ - Proxy: http.ProxyFromEnvironment, - // You can customize other transport settings here - } - - 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) - }, - ), - } -} - -// roundTripperFunc is a helper type to create a custom RoundTripper -type roundTripperFunc func(req *http.Request) (*http.Response, error) - -func (f roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) { - return f(req) + Client *http.Client } +// GetHttpClient returns the http client func (b *BasicAuthAuthenticator) GetHttpClient() (*http.Client, error) { - return b.client, nil + return b.Client, nil } // CommandAuthConfigBasic represents the base configuration needed for authentication to Keyfactor Command API. @@ -67,55 +45,112 @@ type CommandAuthConfigBasic struct { CommandAuthConfig // Username is the username to be used for authentication to Keyfactor Command API - Username string `json:"username"` + Username string `json:"username,omitempty"` // Password is the password to be used for authentication to Keyfactor Command API - Password string `json:"password"` + Password string `json:"password,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 } +// 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 + 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 := BasicAuthTransport(a.Username, a.Password) + client, cErr := a.GetHttpClient() + if cErr != nil { + return nil, cErr + } a.HttpClient = client - return &BasicAuthAuthenticator{client: client}, nil + return &BasicAuthAuthenticator{Client: client}, nil } +// ValidateAuthConfig validates the configuration func (a *CommandAuthConfigBasic) ValidateAuthConfig() error { + serverConfig, _ := a.CommandAuthConfig.LoadConfig( + a.CommandAuthConfig.ConfigProfile, + a.CommandAuthConfig.ConfigFilePath, + ) if a.Username == "" { - return fmt.Errorf("username is required") + 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 == "" { - return fmt.Errorf("password is required") + 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) + } + } } return a.CommandAuthConfig.ValidateAuthConfig() } +// Authenticate authenticates the user func (a *CommandAuthConfigBasic) Authenticate() error { cErr := a.ValidateAuthConfig() if cErr != nil { return cErr } - //basicAuth := fmt.Sprintf("%s:%s", c.Username, c.Password) - //basicAuth = base64.StdEncoding.EncodeToString([]byte(basicAuth)) - //c.AuthHeader = fmt.Sprintf("Basic %s", basicAuth) - // create oauth client + // create oauth Client authy, err := NewBasicAuthAuthenticatorBuilder(). WithUsername(a.Username). WithPassword(a.Password). diff --git a/auth_providers/auth_basic_test.go b/auth_providers/auth_basic_test.go new file mode 100644 index 0000000..50dcff9 --- /dev/null +++ b/auth_providers/auth_basic_test.go @@ -0,0 +1,77 @@ +package auth_providers_test + +import ( + "net/http" + "os" + "testing" + + "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers" +) + +func TestBasicAuthAuthenticator_GetHttpClient(t *testing.T) { + 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) { + 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) { + 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) { + config := &auth_providers.CommandAuthConfigBasic{} + + err := config.Authenticate() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } +} + +func TestCommandAuthConfigBasic_Build(t *testing.T) { + 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") + } +} diff --git a/auth_providers/auth_core.go b/auth_providers/auth_core.go index 4394a5d..ad49bae 100644 --- a/auth_providers/auth_core.go +++ b/auth_providers/auth_core.go @@ -23,22 +23,30 @@ import ( "io" "net/http" "os" + "path/filepath" + "strconv" "strings" "time" + + authconfig "github.com/Keyfactor/keyfactor-auth-client-go/auth_config" ) const ( - DefaultCommandPort = "443" - DefaultCommandAPIPath = "KeyfactorAPI" - DefaultAPIVersion = "1" - DefaultAPIClientName = "APIClient" - DefaultProductVersion = "10.5.0.0" - EnvKeyfactorHostName = "KEYFACTOR_HOSTNAME" - EnvKeyfactorPort = "KEYFACTOR_PORT" - EnvKeyfactorAPIPath = "KEYFACTOR_API_PATH" - EnvKeyfactorSkipVerify = "KEYFACTOR_SKIP_VERIFY" - EnvKeyfactorCACert = "KEYFACTOR_CA_CERT" - EnvKeyfactorAuthProvider = "KEYFACTOR_AUTH_PROVIDER" + DefaultCommandPort = 443 + DefaultCommandAPIPath = "KeyfactorAPI" + DefaultAPIVersion = "1" + DefaultAPIClientName = "APIClient" + DefaultProductVersion = "10.5.0.0" + DefaultConfigFilePath = "~/.keyfactor/command_config.json" + DefaultClientTimeout = 60 + + EnvKeyfactorHostName = "KEYFACTOR_HOSTNAME" + EnvKeyfactorPort = "KEYFACTOR_PORT" + EnvKeyfactorAPIPath = "KEYFACTOR_API_PATH" + EnvKeyfactorSkipVerify = "KEYFACTOR_SKIP_VERIFY" + EnvKeyfactorCACert = "KEYFACTOR_CA_CERT" + EnvKeyfactorAuthProvider = "KEYFACTOR_AUTH_PROVIDER" + EnvKeyfactorClientTimeout = "KEYFACTOR_CLIENT_TIMEOUT" ) // Authenticator is an interface for authentication to Keyfactor Command API. @@ -46,11 +54,28 @@ 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 *authconfig.Server + // AuthHeader is the header to be used for authentication to Keyfactor Command API AuthHeader string `json:"auth_header"` @@ -58,7 +83,7 @@ type CommandAuthConfig struct { CommandHostName string `json:"command_host_name"` // CommandPort is the port of the Keyfactor Command API - CommandPort string `json:"command_port"` + CommandPort int `json:"command_port"` // CommandAPIPath is the path of the Keyfactor Command API, default is "KeyfactorAPI" CommandAPIPath string `json:"command_api_path"` @@ -72,52 +97,91 @@ type CommandAuthConfig struct { // SkipVerify is a flag to skip verification of the server's certificate chain and host name. Default is false. SkipVerify bool `json:"skip_verify"` - // HttpClient is the http client to be used for authentication to Keyfactor Command API + // HttpClientTimeout is the timeout for the http Client + HttpClientTimeout int `json:"client_timeout"` + + // HttpClient is the http Client to be used for authentication to Keyfactor Command API HttpClient *http.Client } +// WithCommandHostName sets the hostname for authentication to Keyfactor Command API. func (c *CommandAuthConfig) WithCommandHostName(hostName string) *CommandAuthConfig { c.CommandHostName = hostName return c } -func (c *CommandAuthConfig) WithCommandPort(port string) *CommandAuthConfig { +// 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 == "" { + c.ConfigProfile = "default" + } + + c.ConfigFilePath = configFilePath + return c +} + +// WithConfigProfile sets the configuration profile for authentication to Keyfactor Command API. +func (c *CommandAuthConfig) WithConfigProfile(profile string) *CommandAuthConfig { + 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 = hostName } else { - return fmt.Errorf("command_host_name or environment variable %s is required", EnvKeyfactorHostName) + if c.FileConfig != nil { + c.CommandHostName = c.FileConfig.Host + } else { + return fmt.Errorf("command_host_name or environment variable %s is required", EnvKeyfactorHostName) + } } } - if c.CommandPort == "" { + if c.CommandPort <= 0 { if port, ok := os.LookupEnv(EnvKeyfactorPort); ok { - c.CommandPort = port + configPort, pErr := strconv.Atoi(port) + if pErr == nil { + c.CommandPort = configPort + } } else { c.CommandPort = DefaultCommandPort } @@ -129,6 +193,16 @@ func (c *CommandAuthConfig) ValidateAuthConfig() error { 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 + } + } c.SetClient(nil) // check for skip verify in environment @@ -151,7 +225,52 @@ func (c *CommandAuthConfig) ValidateAuthConfig() error { return nil } -// SetClient sets the http client for authentication to Keyfactor Command API. +// BuildTransport creates a custom http Transport for authentication to Keyfactor Command API. +func (c *CommandAuthConfig) BuildTransport() (*http.Transport, error) { + output := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + TLSClientConfig: &tls.Config{ + Renegotiation: tls.RenegotiateOnceAsClient, + }, + TLSHandshakeTimeout: 10 * time.Second, + } + if c.SkipVerify { + output.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + } + + // Load the system certs + if c.CommandCACert != "" { + rootCAs, pErr := x509.SystemCertPool() + if pErr != nil { + return nil, 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 nil, ioErr + } + // Append your custom cert to the pool + if ok := rootCAs.AppendCertsFromPEM(cert); !ok { + return nil, 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 nil, fmt.Errorf("failed to append custom CA cert to pool") + } + } + + output.TLSClientConfig.RootCAs = rootCAs + } + 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 @@ -162,7 +281,7 @@ func (c *CommandAuthConfig) SetClient(client *http.Client) *http.Client { return c.HttpClient } -// updateCACerts updates the CA certs for the http client. +// updateCACerts updates the CA certs for the http Client. func (c *CommandAuthConfig) updateCACerts() error { // check if CommandCACert is set if c.CommandCACert == "" { @@ -174,7 +293,7 @@ func (c *CommandAuthConfig) updateCACerts() error { } } - // ensure client is set + // ensure Client is set c.SetClient(nil) // Load the system certs @@ -203,7 +322,7 @@ func (c *CommandAuthConfig) updateCACerts() error { } } - // Trust the augmented cert pool in our client + // Trust the augmented cert pool in our Client c.HttpClient.Transport = &http.Transport{ TLSClientConfig: &tls.Config{ RootCAs: rootCAs, @@ -247,7 +366,7 @@ func (c *CommandAuthConfig) Authenticate() error { req.Header.Set(key, value) } - c.HttpClient.Timeout = 60 * time.Second + c.HttpClient.Timeout = time.Duration(c.HttpClientTimeout) * time.Second cResp, cErr := c.HttpClient.Do(req) if cErr != nil { @@ -294,6 +413,24 @@ func (c *CommandAuthConfig) Authenticate() error { } +// 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 @@ -326,6 +463,7 @@ func FindCACertificate(caCertificatePath string) ([]*x509.Certificate, error) { 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 @@ -342,3 +480,64 @@ func DecodePEMBytes(buf []byte) ([]*pem.Block, []byte, error) { } return certificates, privKey, nil } + +// LoadConfig loads the configuration file and returns the server configuration. +func (c *CommandAuthConfig) LoadConfig(profile string, configFilePath string) (*authconfig.Server, error) { + if configFilePath == "" { + if c.ConfigFilePath != "" { + configFilePath = c.ConfigFilePath + } else { + configFilePath = DefaultConfigFilePath + } + } + expandedPath, err := expandPath(configFilePath) + if err != nil { + return nil, err + } + + file, err := os.Open(expandedPath) + if err != nil { + return nil, err + } + defer file.Close() + + var config authconfig.Config + decoder := json.NewDecoder(file) + if jErr := decoder.Decode(&config); jErr != nil { + return nil, jErr + } + + if profile == "" { + if c.ConfigProfile != "" { + profile = c.ConfigProfile + } else { + profile = "default" + } + } + + server, ok := config.Servers[profile] + if !ok { + return nil, fmt.Errorf("profile %s not found in config file", profile) + } + + c.FileConfig = &server + c.CommandHostName = server.Host + c.CommandPort = server.Port + c.CommandAPIPath = server.APIPath + c.CommandCACert = server.CACertPath // TODO: Implement CACert in config file + 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 +} diff --git a/auth_providers/auth_core_test.go b/auth_providers/auth_core_test.go new file mode 100644 index 0000000..5a3e938 --- /dev/null +++ b/auth_providers/auth_core_test.go @@ -0,0 +1,90 @@ +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 index df51e35..ace3776 100644 --- a/auth_providers/auth_oauth.go +++ b/auth_providers/auth_oauth.go @@ -2,7 +2,6 @@ package auth_providers import ( "context" - "crypto/tls" "crypto/x509" "fmt" "net/http" @@ -19,32 +18,32 @@ const ( // 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 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 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 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 is the environment variable used to set the access token for oauth Client credentials authentication EnvKeyfactorAccessToken = "KEYFACTOR_ACCESS_TOKEN" - // EnvKeyfactorAuthAudience is the environment variable used to set the audience for oauth client credentials + // 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 is the environment variable used to set the scopes for oauth Client credentials authentication EnvKeyfactorAuthScopes = "KEYFACTOR_AUTH_SCOPES" - // EnvKeyfactorAuthHostname is the environment variable used to set the hostname for oauth client credentials authentication + // EnvKeyfactorAuthHostname is the environment variable used to set the hostname for oauth Client credentials authentication EnvKeyfactorAuthHostname = "KEYFACTOR_AUTH_HOSTNAME" - // EnvKeyfactorAuthPort is the environment variable used to set the port for oauth client credentials authentication + // EnvKeyfactorAuthPort is the environment variable used to set the port for oauth Client credentials authentication EnvKeyfactorAuthPort = "KEYFACTOR_AUTH_PORT" - // EnvAuthCACert is a path to a CA certificate for the OAuth client credentials authentication + // EnvAuthCACert is a path to a CA certificate for the OAuth Client credentials authentication EnvAuthCACert = "KEYFACTOR_AUTH_CA_CERT" ) @@ -53,64 +52,107 @@ var _ Authenticator = &OAuthAuthenticator{} // OAuthAuthenticator is an Authenticator that uses OAuth2 for authentication. type OAuthAuthenticator struct { - client *http.Client + Client *http.Client } func (a *OAuthAuthenticator) GetHttpClient() (*http.Client, error) { - return a.client, nil + return a.Client, nil } type CommandConfigOauth struct { CommandAuthConfig - ClientID string `json:"client_id,omitempty"` - ClientSecret string `json:"client_secret,omitempty"` - TokenURL string `json:"token_url,omitempty"` - Audience string `json:"audience,omitempty"` - Scopes []string `json:"scopes,omitempty"` - CACertificatePath string `json:"idp_ca_cert_path,omitempty"` - CACertificates []*x509.Certificate `json:"-"` + + // ClientID is the Client ID for Keycloak authentication + ClientID string `json:"client_id,omitempty"` + + // ClientSecret is the Client secret for Keycloak authentication + ClientSecret string `json:"client_secret,omitempty"` + + // Audience is the audience for Keycloak authentication + Audience string `json:"audience,omitempty"` + + // Scopes is the scopes for Keycloak authentication + Scopes []string `json:"scopes,omitempty"` + + // CACertificatePath is the path to the CA certificate for Keycloak authentication + CACertificatePath string `json:"idp_ca_cert,omitempty"` + + // CACertificates is the CA certificates for authentication + CACertificates []*x509.Certificate `json:"-"` + + // AccessToken is the access token for Keycloak authentication + AccessToken string `json:"access_token;omitempty"` + + // RefreshToken is the refresh token for Keycloak authentication + RefreshToken string `json:"refresh_token;omitempty"` + + // Expiry is the expiry time of the access token + Expiry time.Time `json:"expiry;omitempty"` + + // TokenURL is the token URL for Keycloak authentication + TokenURL string `json:"token_url"` + + // AuthPort + AuthPort string `json:"auth_port,omitempty"` + + // AuthType is the type of Keycloak auth to use such as client_credentials, password, etc. + AuthType string `json:"auth_type,omitempty"` } +// NewOAuthAuthenticatorBuilder creates a new CommandConfigOauth instance. func NewOAuthAuthenticatorBuilder() *CommandConfigOauth { return &CommandConfigOauth{} } +// WithClientId sets the Client ID for Keycloak authentication. func (b *CommandConfigOauth) WithClientId(clientId string) *CommandConfigOauth { b.ClientID = clientId return b } +// WithClientSecret sets the Client secret for Keycloak authentication. func (b *CommandConfigOauth) WithClientSecret(clientSecret string) *CommandConfigOauth { b.ClientSecret = clientSecret return b } +// WithTokenUrl sets the token URL for Keycloak authentication. func (b *CommandConfigOauth) WithTokenUrl(tokenUrl string) *CommandConfigOauth { b.TokenURL = tokenUrl return b } +// WithScopes sets the scopes for Keycloak authentication. func (b *CommandConfigOauth) WithScopes(scopes []string) *CommandConfigOauth { b.Scopes = scopes return b } +// WithAudience sets the audience for Keycloak authentication. func (b *CommandConfigOauth) WithAudience(audience string) *CommandConfigOauth { b.Audience = audience return b } +// WithCACertificatePath sets the CA certificate path for Keycloak authentication. func (b *CommandConfigOauth) WithCaCertificatePath(caCertificatePath string) *CommandConfigOauth { b.CACertificatePath = caCertificatePath return b } +// WithCACertificates sets the CA certificates for Keycloak authentication. func (b *CommandConfigOauth) WithCaCertificates(caCertificates []*x509.Certificate) *CommandConfigOauth { b.CACertificates = caCertificates return b } -func (b *CommandConfigOauth) Build() (Authenticator, error) { +func (b *CommandConfigOauth) GetHttpClient() (*http.Client, error) { + //validate the configuration + cErr := b.ValidateAuthConfig() + if cErr != nil { + return nil, cErr + } + config := &clientcredentials.Config{ ClientID: b.ClientID, ClientSecret: b.ClientSecret, @@ -118,6 +160,10 @@ func (b *CommandConfigOauth) Build() (Authenticator, error) { Scopes: b.Scopes, } + if b.Scopes == nil || len(b.Scopes) == 0 { + b.Scopes = []string{"openid", "profile", "email"} + } + if b.Audience != "" { config.EndpointParams = map[string][]string{ "Audience": { @@ -126,61 +172,49 @@ func (b *CommandConfigOauth) Build() (Authenticator, error) { } } + transport, tErr := b.BuildTransport() + if tErr != nil { + return nil, tErr + } + tokenSource := config.TokenSource(context.Background()) oauthTransport := &oauth2.Transport{ - Base: http.DefaultTransport, + Base: transport, Source: tokenSource, } - if b.CACertificates == nil { - var err error - b.CACertificates, err = FindCACertificate(b.CACertificatePath) - if err != nil { - return nil, fmt.Errorf("failed to find CA certificates: %w", err) - } - } - - if len(b.CACertificates) > 0 { - tlsConfig := &tls.Config{ - Renegotiation: tls.RenegotiateOnceAsClient, - } - - tlsConfig.RootCAs = x509.NewCertPool() - for _, caCert := range b.CACertificates { - tlsConfig.RootCAs.AddCert(caCert) - } + return &http.Client{ + Transport: oauthTransport, + }, nil - customTransport := http.DefaultTransport.(*http.Transport).Clone() - customTransport.TLSClientConfig = tlsConfig - customTransport.TLSHandshakeTimeout = 10 * time.Second +} - // Wrap the custom transport with the oauth2.Transport - oauthTransport.Base = customTransport - } +func (b *CommandConfigOauth) Build() (Authenticator, error) { - client := &http.Client{ - Transport: oauthTransport, + client, cErr := b.GetHttpClient() + if cErr != nil { + return nil, cErr } - return &OAuthAuthenticator{client: client}, nil + return &OAuthAuthenticator{Client: client}, nil } func (b *CommandConfigOauth) ValidateAuthConfig() error { if b.ClientID == "" { - return fmt.Errorf("client ID is required") + return fmt.Errorf("Client ID is required") } if b.ClientSecret == "" { - return fmt.Errorf("client secret is required") + return fmt.Errorf("Client secret is required") } if b.TokenURL == "" { return fmt.Errorf("token URL is required") } - if len(b.Scopes) == 0 { - return fmt.Errorf("at least one scope is required") - } + //if len(b.Scopes) == 0 { + // return fmt.Errorf("at least one scope is required") + //} return b.CommandAuthConfig.ValidateAuthConfig() } @@ -193,7 +227,7 @@ func (b *CommandConfigOauth) Authenticate() error { return vErr } - // create oauth client + // create oauth Client oauthy, err := NewOAuthAuthenticatorBuilder(). WithClientId(b.ClientID). WithClientSecret(b.ClientSecret). diff --git a/auth_providers/auth_oauth_test.go b/auth_providers/auth_oauth_test.go new file mode 100644 index 0000000..5b17147 --- /dev/null +++ b/auth_providers/auth_oauth_test.go @@ -0,0 +1,87 @@ +package auth_providers_test + +import ( + "net/http" + "os" + "testing" + + "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers" +) + +func TestOAuthAuthenticator_GetHttpClient(t *testing.T) { + 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) { + 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) { + 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) { + 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"}, + } + + err := config.Authenticate() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } +} + +func TestCommandConfigOauth_Build(t *testing.T) { + 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") + } +} diff --git a/auth_providers/keycloak/keycloak_auth_certificate.go b/auth_providers/keycloak/keycloak_auth_certificate.go deleted file mode 100644 index cb44be7..0000000 --- a/auth_providers/keycloak/keycloak_auth_certificate.go +++ /dev/null @@ -1,15 +0,0 @@ -// 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 keycloak diff --git a/auth_providers/keycloak/keycloak_auth_client_base.go b/auth_providers/keycloak/keycloak_auth_client_base.go deleted file mode 100644 index 48d94b8..0000000 --- a/auth_providers/keycloak/keycloak_auth_client_base.go +++ /dev/null @@ -1,126 +0,0 @@ -// 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 keycloak - -import ( - "crypto/tls" - "crypto/x509" - "fmt" - "net/http" - "os" - - "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers" -) - -type CommandAuthConfigKeyCloak struct { - // CommandAuthConfig is a reference to the base configuration needed for authentication to Keyfactor Command API - auth_providers.CommandAuthConfig - - // AuthHostName is the hostname of the Keycloak server - AuthHostName string `json:"auth_host_name"` - - // AuthPort is the port of the Keycloak server - AuthPort string `json:"auth_port"` - - // AuthType is the type of Keycloak auth to use such as client_credentials, password, etc. - AuthType string `json:"auth_type"` - - // Auth CA Cert is the CA certificate to be used for authentication to Keycloak for use with not widely trusted certificates. This can be a filepath or a string of the certificate in PEM format. - AuthCACert string `json:"auth_ca_cert"` -} - -// ValidateAuthConfig validates the authentication configuration for Keycloak. -func (c *CommandAuthConfigKeyCloak) ValidateAuthConfig() error { - pErr := c.CommandAuthConfig.ValidateAuthConfig() - if pErr != nil { - return pErr - } - - if c.AuthHostName == "" { - if authHostName, ok := os.LookupEnv(EnvKeyfactorAuthHostname); ok { - c.AuthHostName = authHostName - } else { - c.AuthHostName = c.CommandHostName - } - } - if c.AuthPort == "" { - if port, ok := os.LookupEnv(EnvKeyfactorAuthPort); ok { - c.AuthPort = port - } else { - c.AuthPort = auth_providers.DefaultKeyfactorAuthPort - } - } - - caErr := c.updateCACerts() - if caErr != nil { - return caErr - } - return nil -} - -func (c *CommandAuthConfigKeyCloak) updateCACerts() error { - // check if CommandCACert is set - if c.AuthCACert == "" { - // check environment for auth CA cert - if authCACert, ok := os.LookupEnv(EnvAuthCACert); ok { - c.AuthCACert = authCACert - } else { - return 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.AuthCACert); err == nil { - cert, ioErr := os.ReadFile(c.AuthCACert) - 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.AuthCACert)); !ok { - return fmt.Errorf("failed to append custom CA cert to pool") - } - } - - // check if client already has a tls config - if c.HttpClient.Transport == nil { - // Trust the augmented cert pool in our client - c.HttpClient.Transport = &http.Transport{ - TLSClientConfig: &tls.Config{ - RootCAs: rootCAs, - }, - } - } else { - // Trust the augmented cert pool in our client - c.HttpClient.Transport.(*http.Transport).TLSClientConfig = &tls.Config{ - RootCAs: rootCAs, - } - } - - return nil -} diff --git a/auth_providers/keycloak/keycloak_auth_client_credentials.go b/auth_providers/keycloak/keycloak_auth_client_credentials.go deleted file mode 100644 index b5d42ed..0000000 --- a/auth_providers/keycloak/keycloak_auth_client_credentials.go +++ /dev/null @@ -1,286 +0,0 @@ -// 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 keycloak - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "os" - "strings" - "time" - - "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers" -) - -const ( - DefaultKeyfactorAuthRealm = "Keyfactor" - EnvKeyfactorAuthRealm = "KEYFACTOR_AUTH_REALM" -) - -// CommandAuthKeyCloakClientCredentials represents the configuration needed for Keycloak authentication using client credentials. -// It embeds CommandAuthConfigKeyCloak and adds additional fields specific to Keycloak client credentials authentication. -type CommandAuthKeyCloakClientCredentials struct { - // CommandAuthConfigKeyCloak is a reference to the base configuration needed for authentication to Keyfactor Command API - CommandAuthConfigKeyCloak - - // ClientID is the client ID for Keycloak authentication - ClientID string `json:"client_id;omitempty"` - - // ClientSecret is the client secret for Keycloak authentication - ClientSecret string `json:"client_secret;omitempty"` - - // AccessToken is the access token for Keycloak authentication - AccessToken string `json:"access_token;omitempty"` - - // RefreshToken is the refresh token for Keycloak authentication - RefreshToken string `json:"refresh_token;omitempty"` - - // Expiry is the expiry time of the access token - Expiry time.Time `json:"expiry;omitempty"` - - // Realm is the realm for Keycloak authentication - Realm string `json:"realm;omitempty"` - - // TokenURL is the token URL for Keycloak authentication - TokenURL string `json:"token_url"` -} - -// Authenticate performs the authentication process for Keycloak using client credentials. -// It validates the authentication configuration, gets the token, and calls the base authentication method. -func (c *CommandAuthKeyCloakClientCredentials) Authenticate() error { - c.AuthType = "client_credentials" - cErr := c.ValidateAuthConfig() - if cErr != nil { - return cErr - } - - //token, tErr := c.GetToken() - //if tErr != nil { - // return tErr - //} - //if token == "" { - // return fmt.Errorf("failed to get Bearer token using client credentials") - //} - // - //c.AuthHeader = fmt.Sprintf("Bearer %s", token) - - // create oauth client - oauthy, err := auth_providers.NewOAuthAuthenticatorBuilder(). - WithClientId(c.ClientID). - WithClientSecret(c.ClientSecret). - WithTokenUrl(c.TokenURL). - Build() - - if err != nil { - return err - } - - if oauthy != nil { - oClient, oerr := oauthy.GetHttpClient() - if oerr != nil { - return oerr - } - c.SetClient(oClient) - } - - aErr := c.CommandAuthConfig.Authenticate() - if aErr != nil { - return aErr - } - - return nil -} - -// setClientId sets the client ID for Keycloak authentication. -// It retrieves the client ID from environment variables if it's not set. -func (c *CommandAuthKeyCloakClientCredentials) setClientId() error { - if c.ClientID == "" { - if clientID, ok := os.LookupEnv(auth_providers.EnvKeyfactorClientID); ok { - c.ClientID = clientID - } else { - return fmt.Errorf("client_id or environment variable %s is required", auth_providers.EnvKeyfactorClientID) - } - } - return nil -} - -// setClientSecret sets the client secret for Keycloak authentication. -// It retrieves the client secret from environment variables if it's not set. -func (c *CommandAuthKeyCloakClientCredentials) setClientSecret() error { - if c.ClientSecret == "" { - if clientSecret, ok := os.LookupEnv(auth_providers.EnvKeyfactorClientSecret); ok { - c.ClientSecret = clientSecret - } else { - return fmt.Errorf( - "client_secret or environment variable %s is required", - auth_providers.EnvKeyfactorClientSecret, - ) - } - } - return nil -} - -// setRealm sets the realm for Keycloak authentication. -// It retrieves the realm from environment variables if it's not set. -func (c *CommandAuthKeyCloakClientCredentials) setRealm() error { - if c.Realm == "" { - if realm, ok := os.LookupEnv(EnvKeyfactorAuthRealm); ok { - c.Realm = realm - } else { - c.Realm = DefaultKeyfactorAuthRealm - } - } - return nil -} - -// setTokenURL sets the token URL for Keycloak authentication. -// It generates the token URL if it's not set. -func (c *CommandAuthKeyCloakClientCredentials) setTokenURL() error { - if c.TokenURL == "" { - if tokenURL, ok := os.LookupEnv(auth_providers.EnvKeyfactorAuthTokenURL); ok { - c.TokenURL = tokenURL - } else { - c.TokenURL = fmt.Sprintf( - "https://%s:%s/realms/%s/protocol/openid-connect/token", - c.AuthHostName, - c.AuthPort, - c.Realm, - ) - } - } - return nil -} - -// ValidateAuthConfig validates the authentication configuration for Keycloak using client credentials. -// It checks the client ID, client secret, realm, and token URL, and retrieves them from environment variables if they're not set. -func (c *CommandAuthKeyCloakClientCredentials) ValidateAuthConfig() error { - cErr := c.CommandAuthConfigKeyCloak.ValidateAuthConfig() - if cErr != nil { - return cErr - } - - cIdErr := c.setClientId() - if cIdErr != nil { - return cIdErr - } - - cSecretErr := c.setClientSecret() - if cSecretErr != nil { - return cSecretErr - } - - rErr := c.setRealm() - if rErr != nil { - return rErr - } - - tErr := c.setTokenURL() - if tErr != nil { - return tErr - - } - - return c.CommandAuthConfig.ValidateAuthConfig() -} - -// GetToken gets the access token for Keycloak authentication. -// It uses the refresh token if available and not expired, otherwise, it requests a new access token. -func (c *CommandAuthKeyCloakClientCredentials) GetToken() (string, error) { - // Check if access token is set in environment variable - if c.AccessToken == "" { - if accessToken, ok := os.LookupEnv(auth_providers.EnvKeyfactorAccessToken); ok { - c.AccessToken = accessToken - - // Don't try to refresh as we don't have a refresh token - return c.AccessToken, nil - } - } - - if c.AccessToken != "" && time.Now().Before(c.Expiry) { - return c.AccessToken, nil - } - - // Use refresh token if available and not expired - if c.RefreshToken != "" && time.Now().After(c.Expiry) { - return c.refreshAccessToken() - } - - // Otherwise, get a new access token using client credentials - return c.requestNewToken() -} - -// requestNewToken requests a new access token for Keycloak authentication using client credentials. -func (c *CommandAuthKeyCloakClientCredentials) requestNewToken() (string, error) { - formData := url.Values{} - formData.Set("grant_type", "client_credentials") - formData.Set("client_id", c.ClientID) - formData.Set("client_secret", c.ClientSecret) - - return c.doTokenRequest(formData.Encode()) -} - -// refreshAccessToken refreshes the access token for Keycloak authentication. -func (c *CommandAuthKeyCloakClientCredentials) refreshAccessToken() (string, error) { - formData := url.Values{} - formData.Set("grant_type", "refresh_token") - formData.Set("client_id", c.ClientID) - formData.Set("client_secret", c.ClientSecret) - formData.Set("refresh_token", c.RefreshToken) - return c.doTokenRequest(formData.Encode()) -} - -// doTokenRequest sends a token request to Keycloak and handles the response. -func (c *CommandAuthKeyCloakClientCredentials) doTokenRequest(data string) (string, error) { - requestBody := strings.NewReader(data) - req, reqErr := http.NewRequest("POST", c.TokenURL, requestBody) - if reqErr != nil { - return "", reqErr - } - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - - resp, tkRespErr := c.HttpClient.Do(req) - if tkRespErr != nil { - return "", tkRespErr - } - defer resp.Body.Close() - - // check response status code - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode) - } - - body, ioErr := io.ReadAll(resp.Body) - if ioErr != nil { - return "", ioErr - } - - var tokenResponse struct { - AccessToken string `json:"access_token"` - RefreshToken string `json:"refresh_token"` - ExpiresIn int `json:"expires_in"` - } - if err := json.Unmarshal(body, &tokenResponse); err != nil { - return "", err - } - - c.AccessToken = tokenResponse.AccessToken - c.RefreshToken = tokenResponse.RefreshToken - c.Expiry = time.Now().Add(time.Duration(tokenResponse.ExpiresIn-30) * time.Second) // Subtract 30 seconds to account for delay - - return c.AccessToken, nil -} diff --git a/auth_providers/keycloak/keycloak_auth_client_credentials_test.go b/auth_providers/keycloak/keycloak_auth_client_credentials_test.go deleted file mode 100644 index 64ed06f..0000000 --- a/auth_providers/keycloak/keycloak_auth_client_credentials_test.go +++ /dev/null @@ -1,59 +0,0 @@ -// 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 keycloak - -import ( - "os" - "testing" -) - -const ( - TestEnvIsClientAuth = "TEST_KEYFACTOR_KC_AUTH" -) - -func TestCommandAuthKeyCloakClientCredentials_AuthEnvironment(t *testing.T) { - - if !checkAuthEnvClientCreds() { - msg := "Skipping test because Keyfactor Command environment is not authenticated with client credentials" - t.Log(msg) - t.Skip(msg) - return - } - - // Create a new CommandAuthKeyCloakClientCredentials instance - c := &CommandAuthKeyCloakClientCredentials{} //Used environment configuration - - // Call the Authenticate method - err := c.Authenticate() - if err != nil { - t.Errorf("Authenticate() error = %v", err) - return - } - - // Check that the AuthHeader was set correctly - //expectedAuthHeader := fmt.Sprintf("Bearer %s", c.AccessToken) - //if c.AuthHeader != expectedAuthHeader { - // t.Errorf("Authenticate() AuthHeader = %v, want %v", c.AuthHeader, expectedAuthHeader) - //} else if c.HttpClient.Tra -} - -func checkAuthEnvClientCreds() bool { - if isKCAuth, ok := os.LookupEnv(TestEnvIsClientAuth); ok { - if isKCAuth == "true" || isKCAuth == "1" { - return true - } - } - return false -} diff --git a/go.mod b/go.mod index c1abd39..3c45212 100644 --- a/go.mod +++ b/go.mod @@ -16,4 +16,7 @@ module github.com/Keyfactor/keyfactor-auth-client-go go 1.22 -require golang.org/x/oauth2 v0.22.0 +require ( + golang.org/x/oauth2 v0.22.0 + gopkg.in/yaml.v2 v2.4.0 +) diff --git a/go.sum b/go.sum index 280e973..17be337 100644 --- a/go.sum +++ b/go.sum @@ -2,3 +2,7 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +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/lib/main.go b/lib/main.go new file mode 100644 index 0000000..c1f2736 --- /dev/null +++ b/lib/main.go @@ -0,0 +1,132 @@ +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/tag.sh b/tag.sh index 1f84e80..f1086ab 100755 --- a/tag.sh +++ b/tag.sh @@ -1,4 +1,4 @@ -RC_VERSION=rc.6 +RC_VERSION=rc.8 TAG_VERSION_1=v0.0.1-$RC_VERSION git tag -d $TAG_VERSION_1 || true git tag $TAG_VERSION_1 From adbb9b8d426bfcc03b9773962f4861cd5f83b444 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 16 Oct 2024 17:11:23 -0700 Subject: [PATCH 28/57] fix(tests): update test environments --- .github/config/environments.tf | 27 +++++++++++++++++++-------- .github/config/variables.tf | 10 +++++----- .github/workflows/go_tests.yml | 6 ++++-- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/.github/config/environments.tf b/.github/config/environments.tf index 7719571..0b42a3f 100644 --- a/.github/config/environments.tf +++ b/.github/config/environments.tf @@ -8,15 +8,26 @@ module "keyfactor_github_test_environment_ad_10_5_0" { keyfactor_password = var.keyfactor_password_10_5_0 } -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" +# 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 +# } - gh_environment_name = "KFC_11_5_0_KC" +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_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_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 } - diff --git a/.github/config/variables.tf b/.github/config/variables.tf index c61463b..5353f86 100644 --- a/.github/config/variables.tf +++ b/.github/config/variables.tf @@ -14,25 +14,25 @@ variable "keyfactor_password_10_5_0" { type = string } -variable "keyfactor_client_id_11_5_0" { +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_11_5_0" { +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_11_5_0_KC" { +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_hostname_11_5_0_KC" { +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 = "int-oidc-lab.eastus2.cloudapp.azure.com" + default = "https://int-oidc-lab.eastus2.cloudapp.azure.com:8444/realms/Keyfactor/protocol/openid-connect/token" } diff --git a/.github/workflows/go_tests.yml b/.github/workflows/go_tests.yml index cc67909..1d9096d 100644 --- a/.github/workflows/go_tests.yml +++ b/.github/workflows/go_tests.yml @@ -1,6 +1,8 @@ name: Go Test Workflow -on: [ push ] +on: + push: + workflow_dispatch: jobs: test: @@ -8,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - environment: [ "KFC_10_5_0", "KFC_11_5_0_KC"] + environment: [ "KFC_10_5_0", "KFC_12_3_0_KC"] environment: ${{ matrix.environment }} steps: - name: Check out code From 6f0d88467c040280f6c34e763f1a9b1a87d99482 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 16 Oct 2024 17:16:06 -0700 Subject: [PATCH 29/57] fix(tests): Only run basic auth tests against basic auth environment and oauth tests against an oauth environment --- auth_providers/auth_basic_test.go | 28 ++++++++++++++++++++++++++++ auth_providers/auth_oauth_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/auth_providers/auth_basic_test.go b/auth_providers/auth_basic_test.go index 50dcff9..d95f0d9 100644 --- a/auth_providers/auth_basic_test.go +++ b/auth_providers/auth_basic_test.go @@ -9,6 +9,12 @@ import ( ) 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{}, } @@ -24,6 +30,11 @@ func TestBasicAuthAuthenticator_GetHttpClient(t *testing.T) { } 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), @@ -36,6 +47,12 @@ func TestCommandAuthConfigBasic_ValidateAuthConfig(t *testing.T) { } 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), @@ -52,6 +69,12 @@ func TestCommandAuthConfigBasic_GetHttpClient(t *testing.T) { } 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 + } + config := &auth_providers.CommandAuthConfigBasic{} err := config.Authenticate() @@ -61,6 +84,11 @@ func TestCommandAuthConfigBasic_Authenticate(t *testing.T) { } 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), diff --git a/auth_providers/auth_oauth_test.go b/auth_providers/auth_oauth_test.go index 5b17147..82a79a8 100644 --- a/auth_providers/auth_oauth_test.go +++ b/auth_providers/auth_oauth_test.go @@ -9,6 +9,11 @@ import ( ) 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{}, } @@ -24,6 +29,11 @@ func TestOAuthAuthenticator_GetHttpClient(t *testing.T) { } 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), @@ -37,6 +47,11 @@ func TestCommandConfigOauth_ValidateAuthConfig(t *testing.T) { } 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), @@ -55,6 +70,11 @@ func TestCommandConfigOauth_GetHttpClient(t *testing.T) { } 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 + } config := &auth_providers.CommandConfigOauth{ ClientID: os.Getenv(auth_providers.EnvKeyfactorClientID), ClientSecret: os.Getenv(auth_providers.EnvKeyfactorClientSecret), @@ -69,6 +89,11 @@ func TestCommandConfigOauth_Authenticate(t *testing.T) { } 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), From a52adff16a02a62c47d47729289b864d80f9027c Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 17 Oct 2024 10:11:48 -0700 Subject: [PATCH 30/57] fix(config): Remove `AuthPort` and rename `AuthHostname` to `OAuthTokenUrl` fix(config):`AuthProvider.Parameters` is now `map[string]interface` vs `interface` chore(docs): Add docs around GitHub test environments chore(docs): Move auth config examples and schema to `lib` --- .github/config/MODULE.MD | 41 +++ .github/config/Makefile | 26 ++ .github/config/README.md | 92 +++++ README.md | 30 +- auth_config/command_config.go | 16 +- auth_config/command_config_test.go | 345 ++++++++++++++++++ .../config}/auth_config_schema.json | 0 lib/config/basic_auth_config_example.json | 18 + .../config/full_auth_config_example.json | 10 +- lib/config/oauth_config_example.json | 18 + 10 files changed, 563 insertions(+), 33 deletions(-) create mode 100644 .github/config/MODULE.MD create mode 100644 .github/config/Makefile create mode 100644 .github/config/README.md create mode 100644 auth_config/command_config_test.go rename {auth_config => lib/config}/auth_config_schema.json (100%) create mode 100644 lib/config/basic_auth_config_example.json rename auth_config/auth_config_example.json => lib/config/full_auth_config_example.json (80%) create mode 100644 lib/config/oauth_config_example.json diff --git a/.github/config/MODULE.MD b/.github/config/MODULE.MD new file mode 100644 index 0000000..4a0f4ab --- /dev/null +++ b/.github/config/MODULE.MD @@ -0,0 +1,41 @@ +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0 | +| [github](#requirement\_github) | >=6.2 | + +## Providers + +| Name | Version | +|------|---------| +| [github](#provider\_github) | 6.3.1 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [keyfactor\_github\_test\_environment\_12\_3\_0\_kc](#module\_keyfactor\_github\_test\_environment\_12\_3\_0\_kc) | git::ssh://git@github.com/Keyfactor/terraform-module-keyfactor-github-test-environment-ad.git | main | +| [keyfactor\_github\_test\_environment\_ad\_10\_5\_0](#module\_keyfactor\_github\_test\_environment\_ad\_10\_5\_0) | git::ssh://git@github.com/Keyfactor/terraform-module-keyfactor-github-test-environment-ad.git | main | + +## Resources + +| Name | Type | +|------|------| +| [github_repository.repo](https://registry.terraform.io/providers/integrations/github/latest/docs/data-sources/repository) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [keyfactor\_auth\_token\_url\_12\_3\_0\_KC](#input\_keyfactor\_auth\_token\_url\_12\_3\_0\_KC) | The hostname of the KeyCloak instance to authenticate to for a Keyfactor Command access token | `string` | `"https://int-oidc-lab.eastus2.cloudapp.azure.com:8444/realms/Keyfactor/protocol/openid-connect/token"` | no | +| [keyfactor\_client\_id\_12\_3\_0](#input\_keyfactor\_client\_id\_12\_3\_0) | The client ID to authenticate with the Keyfactor instance using Keycloak client credentials | `string` | n/a | yes | +| [keyfactor\_client\_secret\_12\_3\_0](#input\_keyfactor\_client\_secret\_12\_3\_0) | The client secret to authenticate with the Keyfactor instance using Keycloak client credentials | `string` | n/a | yes | +| [keyfactor\_hostname\_10\_5\_0](#input\_keyfactor\_hostname\_10\_5\_0) | The hostname of the Keyfactor instance | `string` | `"integrations1050-lab.kfdelivery.com"` | no | +| [keyfactor\_hostname\_12\_3\_0\_KC](#input\_keyfactor\_hostname\_12\_3\_0\_KC) | The hostname of the Keyfactor instance | `string` | `"int-oidc-lab.eastus2.cloudapp.azure.com"` | no | +| [keyfactor\_password\_10\_5\_0](#input\_keyfactor\_password\_10\_5\_0) | The password to authenticate with the Keyfactor instance | `string` | n/a | yes | +| [keyfactor\_username\_10\_5\_0](#input\_keyfactor\_username\_10\_5\_0) | The username to authenticate with the Keyfactor instance | `string` | n/a | yes | + +## Outputs + +No outputs. diff --git a/.github/config/Makefile b/.github/config/Makefile new file mode 100644 index 0000000..f67d9df --- /dev/null +++ b/.github/config/Makefile @@ -0,0 +1,26 @@ +.DEFAULT_GOAL := help + +##@ Utility +help: ## Display this help + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +deps: ## Install deps for macos + @brew install pre-commit tflint terraform terraform-docs + +docs: ## Run terraform-docs to update module docs. + @terraform-docs markdown . > MODULE.MD + @terraform-docs markdown table --output-file README.md --output-mode inject . + +lint: ## Run tflint + @tflint + +validate: ## Run terraform validate + @terraform init --upgrade + @terraform validate + +precommit/add: ## Install pre-commit hook + @pre-commit install + +precommit/remove: ## Uninstall pre-commit hook + @pre-commit uninstall + diff --git a/.github/config/README.md b/.github/config/README.md new file mode 100644 index 0000000..e1a8977 --- /dev/null +++ b/.github/config/README.md @@ -0,0 +1,92 @@ +# GitHub Test Environment Setup + +This code sets up GitHub environments for testing against Keyfactor Command instances that are configured to use +Active Directory or Keycloak for authentication. + +## Requirements + +1. Terraform >= 1.0 +2. GitHub Provider >= 6.2 +3. Keyfactor Command instance(s) configured to use Active Directory or Keycloak for authentication +4. AD or Keycloak credentials for authenticating to the Keyfactor Command instance(s) +5. A GitHub token with access and permissions to the repository where the environments will be created + +## Adding a new environment + +Modify the `environments.tf` file to include the new environment module. The module should be named appropriately. +Example: + +### Active Directory Environment + +```hcl +module "keyfactor_github_test_environment_ad_10_5_0" { + source = "git::ssh://git@github.com/Keyfactor/terraform-module-keyfactor-github-test-environment-ad.git?ref=main" + + gh_environment_name = "KFC_10_5_0" # Keyfactor Command 10.5.0 environment using Active Directory(/Basic Auth) + gh_repo_name = data.github_repository.repo.name + keyfactor_hostname = var.keyfactor_hostname_10_5_0 + keyfactor_username = var.keyfactor_username_10_5_0 + keyfactor_password = var.keyfactor_password_10_5_0 +} +``` + +### oAuth Client Environment + +```hcl +module "keyfactor_github_test_environment_12_3_0_kc" { + source = "git::ssh://git@github.com/Keyfactor/terraform-module-keyfactor-github-test-environment-kc.git?ref=main" + + gh_environment_name = "KFC_12_3_0_KC" # Keyfactor Command 12.3.0 environment using Keycloak + gh_repo_name = data.github_repository.repo.name + keyfactor_hostname = var.keyfactor_hostname_12_3_0_KC + keyfactor_auth_token_url = var.keyfactor_auth_token_url_12_3_0_KC + keyfactor_client_id = var.keyfactor_client_id_12_3_0 + keyfactor_client_secret = var.keyfactor_client_secret_12_3_0 + keyfactor_tls_skip_verify = true +} +``` + + + +## Requirements + +| Name | Version | +|---------------------------------------------------------------------------|---------| +| [terraform](#requirement\_terraform) | >= 1.0 | +| [github](#requirement\_github) | >=6.2 | + +## Providers + +| Name | Version | +|------------------------------------------------------------|---------| +| [github](#provider\_github) | 6.3.1 | + +## Modules + +| Name | Source | Version | +|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------|---------| +| [keyfactor\_github\_test\_environment\_12\_3\_0\_kc](#module\_keyfactor\_github\_test\_environment\_12\_3\_0\_kc) | git::ssh://git@github.com/Keyfactor/terraform-module-keyfactor-github-test-environment-ad.git | main | +| [keyfactor\_github\_test\_environment\_ad\_10\_5\_0](#module\_keyfactor\_github\_test\_environment\_ad\_10\_5\_0) | git::ssh://git@github.com/Keyfactor/terraform-module-keyfactor-github-test-environment-ad.git | main | + +## Resources + +| Name | Type | +|---------------------------------------------------------------------------------------------------------------------------|-------------| +| [github_repository.repo](https://registry.terraform.io/providers/integrations/github/latest/docs/data-sources/repository) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|--------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------|----------|---------------------------------------------------------------------------------------------------------|:--------:| +| [keyfactor\_auth\_token\_url\_12\_3\_0\_KC](#input\_keyfactor\_auth\_token\_url\_12\_3\_0\_KC) | The hostname of the KeyCloak instance to authenticate to for a Keyfactor Command access token | `string` | `"https://int-oidc-lab.eastus2.cloudapp.azure.com:8444/realms/Keyfactor/protocol/openid-connect/token"` | no | +| [keyfactor\_client\_id\_12\_3\_0](#input\_keyfactor\_client\_id\_12\_3\_0) | The client ID to authenticate with the Keyfactor instance using Keycloak client credentials | `string` | n/a | yes | +| [keyfactor\_client\_secret\_12\_3\_0](#input\_keyfactor\_client\_secret\_12\_3\_0) | The client secret to authenticate with the Keyfactor instance using Keycloak client credentials | `string` | n/a | yes | +| [keyfactor\_hostname\_10\_5\_0](#input\_keyfactor\_hostname\_10\_5\_0) | The hostname of the Keyfactor instance | `string` | `"integrations1050-lab.kfdelivery.com"` | no | +| [keyfactor\_hostname\_12\_3\_0\_KC](#input\_keyfactor\_hostname\_12\_3\_0\_KC) | The hostname of the Keyfactor instance | `string` | `"int-oidc-lab.eastus2.cloudapp.azure.com"` | no | +| [keyfactor\_password\_10\_5\_0](#input\_keyfactor\_password\_10\_5\_0) | The password to authenticate with the Keyfactor instance | `string` | n/a | yes | +| [keyfactor\_username\_10\_5\_0](#input\_keyfactor\_username\_10\_5\_0) | The username to authenticate with the Keyfactor instance | `string` | n/a | yes | + +## Outputs + +No outputs. + \ No newline at end of file diff --git a/README.md b/README.md index 32c4445..50aa115 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,8 @@ Client library for authenticating to Keyfactor Command - [Environment Variables](#environment-variables) * [Global](#global) - * [Active Directory](#active-directory) - * [Keycloak](#keycloak) - + [Client Credentials](#client-credentials) + * [Basic Auth](#basic-auth) + * [oAuth Client Credentials](#oauth-client-credentials) - [Test Environment Variables](#test-environment-variables) @@ -25,7 +24,7 @@ Client library for authenticating to Keyfactor Command | 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 | | -### Active Directory +### Basic Auth | Name | Description | Default | |--------------------|---------------------------------------------------------------------------------------------|---------| @@ -33,22 +32,15 @@ Client library for authenticating to Keyfactor Command | 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 `\\` | | -### Keycloak +### oAuth Client Credentials -| Name | Description | Default | -|-------------------------|---------------------------------------------------------------------------------------------------------------------------------|-------------| -| KEYFACTOR_AUTH_HOSTNAME | Hostname of Keycloak instance to authenticate to Keyfactor Command | | -| KEYFACTOR_AUTH_REALM | Keyfactor Auth Realm | `Keyfactor` | -| KEYFACTOR_AUTH_SCOPES | Scopes to request when authenticating to Keyfactor Command API | `openid` | -| KEYFACTOR_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 | | - -#### Client Credentials - -| Name | Description | Default | -|------------------------------|------------------------------|---------| -| KEYFACTOR_AUTH_CLIENT_ID | Keyfactor Auth Client ID | | -| KEYFACTOR_AUTH_CLIENT_SECRET | Keyfactor Auth Client Secret | | +| Name | Description | Default | +|------------------------------|---------------------------------------------------------------------------------------------------------------------------------|----------| +| KEYFACTOR_AUTH_CLIENT_ID | Keyfactor Auth Client ID | | +| KEYFACTOR_AUTH_CLIENT_SECRET | Keyfactor Auth Client Secret | | +| KEYFACTOR_AUTH_SCOPES | Scopes to request when authenticating to Keyfactor Command API | `openid` | +| KEYFACTOR_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 diff --git a/auth_config/command_config.go b/auth_config/command_config.go index 4fb5bd4..c696b13 100644 --- a/auth_config/command_config.go +++ b/auth_config/command_config.go @@ -10,15 +10,15 @@ import ( // 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. - AuthHostname string `json:"auth_hostname,omitempty" yaml:"auth_hostname,omitempty"` // AuthHostname is the authentication hostname. - AuthPort int `json:"auth_port,omitempty" yaml:"auth_port,omitempty"` // AuthPort is the authentication port. + 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. - Domain string `json:"domain,omitempty" yaml:"domain,omitempty"` // Domain is the domain for authentication. + OAuthTokenUrl string `json:"oauth_token_url,omitempty" yaml:"auth_hostname,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. @@ -28,9 +28,9 @@ omitempty"` // CACertPath is the path to the CA certificate to trust. // 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 interface{} `json:"parameters,omitempty" yaml:"parameters,omitempty"` // Parameters are additional parameters for the authentication provider. + 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. diff --git a/auth_config/command_config_test.go b/auth_config/command_config_test.go new file mode 100644 index 0000000..229799e --- /dev/null +++ b/auth_config/command_config_test.go @@ -0,0 +1,345 @@ +package authconfig_test + +import ( + "encoding/json" + "os" + "testing" + + "gopkg.in/yaml.v2" + + "github.com/Keyfactor/keyfactor-auth-client-go/auth_config" +) + +func TestReadServerFromJSON(t *testing.T) { + filePath := "test_config.json" + server := &authconfig.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 := authconfig.WriteServerToJSON(filePath, server) + if err != nil { + t.Fatalf("failed to write server to JSON: %v", err) + } + defer os.Remove(filePath) + + readServer, err := authconfig.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 := &authconfig.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 := authconfig.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 authconfig.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 := &authconfig.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 := authconfig.WriteServerToYAML(filePath, server) + if err != nil { + t.Fatalf("failed to write server to YAML: %v", err) + } + defer os.Remove(filePath) + + readServer, err := authconfig.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 := &authconfig.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 := authconfig.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 authconfig.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 := &authconfig.Config{ + Servers: map[string]authconfig.Server{ + "server1": { + Host: "localhost", + Port: 8080, + }, + }, + } + + err := authconfig.WriteConfigToJSON(filePath, config) + if err != nil { + t.Fatalf("failed to write config to JSON: %v", err) + } + defer os.Remove(filePath) + + newConfig := &authconfig.Config{ + Servers: map[string]authconfig.Server{ + "server2": { + Host: "remotehost", + Port: 9090, + }, + }, + } + + err = authconfig.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 := &authconfig.Config{ + Servers: map[string]authconfig.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: authconfig.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: authconfig.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 authconfig.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 := &authconfig.Config{ + Servers: map[string]authconfig.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 authconfig.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 := &authconfig.Config{ + Servers: map[string]authconfig.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 authconfig.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 *authconfig.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 *authconfig.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/auth_config/auth_config_schema.json b/lib/config/auth_config_schema.json similarity index 100% rename from auth_config/auth_config_schema.json rename to lib/config/auth_config_schema.json 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/auth_config/auth_config_example.json b/lib/config/full_auth_config_example.json similarity index 80% rename from auth_config/auth_config_example.json rename to lib/config/full_auth_config_example.json index d2f4fac..4bfc7eb 100644 --- a/auth_config/auth_config_example.json +++ b/lib/config/full_auth_config_example.json @@ -1,9 +1,8 @@ { "servers": { - "default_profile": { + "default": { "host": "keyfactor.command.kfdelivery.com", - "auth_hostname": "idp.keyfactor.command.kfdelivery.com", - "auth_port": 8444, + "oauth_token_url": "idp.keyfactor.command.kfdelivery.com", "username": "keyfactor", "password": "password", "client_id": "client-id", @@ -19,10 +18,9 @@ } } }, - "alternative_profile": { + "server2": { "host": "keyfactor2.command.kfdelivery.com", - "auth_hostname": "idp.keyfactor2.command.kfdelivery.com", - "auth_port": 8444, + "oauth_token_url": "idp.keyfactor2.command.kfdelivery.com", "username": "keyfactor2", "password": "password2", "client_id": "client-id2", diff --git a/lib/config/oauth_config_example.json b/lib/config/oauth_config_example.json new file mode 100644 index 0000000..40f097f --- /dev/null +++ b/lib/config/oauth_config_example.json @@ -0,0 +1,18 @@ +{ + "servers": { + "default": { + "host": "keyfactor.command.kfdelivery.com", + "oauth_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", + "oauth_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 From d8f274d2827aa6d1a2b74e2c087d672e2e6559ef Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 17 Oct 2024 13:01:25 -0700 Subject: [PATCH 31/57] chore: add license headers fix(core): Source environmental variables before config validation --- README.md | 22 +++-- auth_config/command_config.go | 14 +++ auth_config/command_config_test.go | 14 +++ auth_providers/auth_basic.go | 41 ++++++++ auth_providers/auth_basic_test.go | 86 ++++++++++++++++- auth_providers/auth_core.go | 106 +++++++++++++++++---- auth_providers/auth_core_test.go | 14 +++ auth_providers/auth_oauth.go | 145 +++++++++++++++++++++++------ auth_providers/auth_oauth_test.go | 14 +++ go.mod | 2 +- go.sum | 2 + lib/main.go | 14 +++ pkg/version.go | 14 +++ 13 files changed, 429 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 50aa115..4265d76 100644 --- a/README.md +++ b/README.md @@ -16,16 +16,21 @@ Client library for authenticating to Keyfactor Command ### 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 | | +| Name | Description | Default | +|-------------------------------|-----------------------------------------------------------------------------------------------------------------|----------------------------------------| +| KEYFACTOR_HOSTNAME | Keyfactor Command hostname without protocol and port | | +| KEYFACTOR_PORT | Keyfactor Command port | `443` | +| KEYFACTOR_API_PATH | Keyfactor Command API Path | `KeyfactorAPI` | +| KEYFACTOR_SKIP_VERIFY | Skip TLS verification when connecting to Keyfactor Command | `false` | +| KEYFACTOR_CA_CERT | Either a file path or PEM encoded string to a CA certificate to trust when communicating with Keyfactor Command | | +| KEYFACTOR_CLIENT_TIMEOUT | Timeout for HTTP client requests to Keyfactor Command | `60s` | +| KEYFACTOR_AUTH_CONFIG_FILE | Path to a JSON file containing the authentication configuration | `$HOME/.keyfactor/command_config.json` | +| KEYFACTOR_AUTH_CONFIG_PROFILE | Profile to use from the authentication configuration file | `default` | ### Basic Auth +Currently, only Active Directory `Basic` authentication is supported. + | Name | Description | Default | |--------------------|---------------------------------------------------------------------------------------------|---------| | KEYFACTOR_USERNAME | Active Directory username to authenticate to Keyfactor Command API | | @@ -38,8 +43,9 @@ Client library for authenticating to Keyfactor Command |------------------------------|---------------------------------------------------------------------------------------------------------------------------------|----------| | KEYFACTOR_AUTH_CLIENT_ID | Keyfactor Auth Client ID | | | KEYFACTOR_AUTH_CLIENT_SECRET | Keyfactor Auth Client Secret | | +| KEYFACTOR_AUTH_TOKEN_URL | URL to request an access token from Keyfactor Auth | | | KEYFACTOR_AUTH_SCOPES | Scopes to request when authenticating to Keyfactor Command API | `openid` | -| KEYFACTOR_ACCESS_TOKEN | Access token to use to authenticate to Keyfactor Command API. This can be supplied directly or generated via client credentials | | +| 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 diff --git a/auth_config/command_config.go b/auth_config/command_config.go index c696b13..e6d035e 100644 --- a/auth_config/command_config.go +++ b/auth_config/command_config.go @@ -1,3 +1,17 @@ +// 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 authconfig import ( diff --git a/auth_config/command_config_test.go b/auth_config/command_config_test.go index 229799e..1b0861a 100644 --- a/auth_config/command_config_test.go +++ b/auth_config/command_config_test.go @@ -1,3 +1,17 @@ +// 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 authconfig_test import ( diff --git a/auth_providers/auth_basic.go b/auth_providers/auth_basic.go index e308535..5f5cad9 100644 --- a/auth_providers/auth_basic.go +++ b/auth_providers/auth_basic.go @@ -19,11 +19,13 @@ import ( "fmt" "net/http" "os" + "strings" ) const ( EnvKeyfactorUsername = "KEYFACTOR_USERNAME" EnvKeyfactorPassword = "KEYFACTOR_PASSWORD" + EnvKeyfactorDomain = "KEYFACTOR_DOMAIN" ) // Basic Authenticator @@ -49,6 +51,9 @@ type CommandAuthConfigBasic struct { // 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 @@ -116,6 +121,7 @@ func (a *CommandAuthConfigBasic) ValidateAuthConfig() error { serverConfig, _ := a.CommandAuthConfig.LoadConfig( a.CommandAuthConfig.ConfigProfile, a.CommandAuthConfig.ConfigFilePath, + false, ) if a.Username == "" { if username, ok := os.LookupEnv(EnvKeyfactorUsername); ok { @@ -140,6 +146,18 @@ func (a *CommandAuthConfigBasic) ValidateAuthConfig() error { } } + 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() } @@ -170,3 +188,26 @@ func (a *CommandAuthConfigBasic) Authenticate() error { 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 +} diff --git a/auth_providers/auth_basic_test.go b/auth_providers/auth_basic_test.go index d95f0d9..4e994ea 100644 --- a/auth_providers/auth_basic_test.go +++ b/auth_providers/auth_basic_test.go @@ -1,6 +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 auth_providers_test import ( + "fmt" "net/http" "os" "testing" @@ -75,12 +90,30 @@ func TestCommandAuthConfigBasic_Authenticate(t *testing.T) { return } - config := &auth_providers.CommandAuthConfigBasic{} + noParamsConfig := &auth_providers.CommandAuthConfigBasic{} + authBasicTest(t, "with complete Environmental variables", false, noParamsConfig) - err := config.Authenticate() - if err != nil { - t.Fatalf("expected no error, got %v", err) + // Environment variables are not set + t.Log("Unsetting environment variables") + username, password, domain := exportBasicEnvVariables() + unsetBasicEnvVariables() + + incompleteEnvConfig := &auth_providers.CommandAuthConfigBasic{} + authBasicTest(t, "with incomplete Environmental variables", true, incompleteEnvConfig) + + usernameOnlyConfig := &auth_providers.CommandAuthConfigBasic{ + Username: "test-username", + } + authBasicTest(t, "Username Only", true, usernameOnlyConfig) + + fullParamsConfig := &auth_providers.CommandAuthConfigBasic{ + Username: username, + Password: password, + Domain: domain, } + authBasicTest(t, "w/ full params variables", false, fullParamsConfig) + t.Log("Resetting environment variables") + setBasicEnvVariables(username, password, domain) } func TestCommandAuthConfigBasic_Build(t *testing.T) { @@ -103,3 +136,48 @@ func TestCommandAuthConfigBasic_Build(t *testing.T) { 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) { + 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 + } + 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 index ad49bae..e71bf6a 100644 --- a/auth_providers/auth_core.go +++ b/auth_providers/auth_core.go @@ -38,6 +38,7 @@ const ( DefaultAPIClientName = "APIClient" DefaultProductVersion = "10.5.0.0" DefaultConfigFilePath = "~/.keyfactor/command_config.json" + DefaultConfigProfile = "default" DefaultClientTimeout = 60 EnvKeyfactorHostName = "KEYFACTOR_HOSTNAME" @@ -46,6 +47,8 @@ const ( EnvKeyfactorSkipVerify = "KEYFACTOR_SKIP_VERIFY" EnvKeyfactorCACert = "KEYFACTOR_CA_CERT" EnvKeyfactorAuthProvider = "KEYFACTOR_AUTH_PROVIDER" + EnvKeyfactorAuthProfile = "KEYFACTOR_AUTH_CONFIG_PROFILE" + EnvKeyfactorConfigFile = "KEYFACTOR_AUTH_CONFIG_FILE" EnvKeyfactorClientTimeout = "KEYFACTOR_CLIENT_TIMEOUT" ) @@ -104,8 +107,21 @@ type CommandAuthConfig struct { HttpClient *http.Client } +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 } @@ -144,7 +160,12 @@ func (c *CommandAuthConfig) WithHttpClient(client *http.Client) *CommandAuthConf func (c *CommandAuthConfig) WithConfigFile(configFilePath string) *CommandAuthConfig { if c.ConfigProfile == "" { - c.ConfigProfile = "default" + // check if profile is set in environment + if profile, ok := os.LookupEnv(EnvKeyfactorAuthProfile); ok { + c.ConfigProfile = profile + } else { + c.ConfigProfile = DefaultConfigProfile + } } c.ConfigFilePath = configFilePath @@ -153,7 +174,16 @@ func (c *CommandAuthConfig) WithConfigFile(configFilePath string) *CommandAuthCo // WithConfigProfile sets the configuration profile for authentication to Keyfactor Command API. func (c *CommandAuthConfig) WithConfigProfile(profile string) *CommandAuthConfig { - c.ConfigProfile = profile + if profile == "" { + // check if profile is set in environment + if profile, ok := os.LookupEnv(EnvKeyfactorAuthProfile); ok { + c.ConfigProfile = profile + } else { + c.ConfigProfile = DefaultConfigProfile + } + } else { + c.ConfigProfile = profile + } return c } @@ -167,10 +197,10 @@ func (c *CommandAuthConfig) WithClientTimeout(timeout int) *CommandAuthConfig { func (c *CommandAuthConfig) ValidateAuthConfig() error { if c.CommandHostName == "" { if hostName, ok := os.LookupEnv(EnvKeyfactorHostName); ok { - c.CommandHostName = hostName + c.CommandHostName = cleanHostName(hostName) } else { - if c.FileConfig != nil { - c.CommandHostName = c.FileConfig.Host + 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) } @@ -482,51 +512,91 @@ func DecodePEMBytes(buf []byte) ([]*pem.Block, []byte, error) { } // LoadConfig loads the configuration file and returns the server configuration. -func (c *CommandAuthConfig) LoadConfig(profile string, configFilePath string) (*authconfig.Server, error) { +func (c *CommandAuthConfig) LoadConfig(profile string, configFilePath string, silentLoad bool) ( + *authconfig.Server, + error, +) { if configFilePath == "" { - if c.ConfigFilePath != "" { - configFilePath = c.ConfigFilePath + // check if config file is set in environment + if config, ok := os.LookupEnv(EnvKeyfactorConfigFile); ok { + configFilePath = config } else { configFilePath = DefaultConfigFilePath } + } else { + c.ConfigFilePath = configFilePath } expandedPath, err := expandPath(configFilePath) if err != nil { - return nil, err + 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 { - return nil, err + if !silentLoad { + return nil, err + } + // if silentLoad is true then eat the error and return nil + return nil, nil } defer file.Close() var config authconfig.Config decoder := json.NewDecoder(file) if jErr := decoder.Decode(&config); jErr != nil { - return nil, jErr + 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 = "default" + profile = DefaultConfigProfile } } server, ok := config.Servers[profile] if !ok { - return nil, fmt.Errorf("profile %s not found in config file", profile) + 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 - c.CommandHostName = server.Host - c.CommandPort = server.Port - c.CommandAPIPath = server.APIPath - c.CommandCACert = server.CACertPath // TODO: Implement CACert in config file - 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 } diff --git a/auth_providers/auth_core_test.go b/auth_providers/auth_core_test.go index 5a3e938..efcf2a9 100644 --- a/auth_providers/auth_core_test.go +++ b/auth_providers/auth_core_test.go @@ -1,3 +1,17 @@ +// 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 ( diff --git a/auth_providers/auth_oauth.go b/auth_providers/auth_oauth.go index ace3776..6c455f7 100644 --- a/auth_providers/auth_oauth.go +++ b/auth_providers/auth_oauth.go @@ -5,8 +5,10 @@ import ( "crypto/x509" "fmt" "net/http" + "os" "time" + authconfig "github.com/Keyfactor/keyfactor-auth-client-go/auth_config" "golang.org/x/oauth2" "golang.org/x/oauth2/clientcredentials" ) @@ -28,7 +30,7 @@ const ( EnvKeyfactorAuthTokenURL = "KEYFACTOR_AUTH_TOKEN_URL" // EnvKeyfactorAccessToken is the environment variable used to set the access token for oauth Client credentials authentication - EnvKeyfactorAccessToken = "KEYFACTOR_ACCESS_TOKEN" + EnvKeyfactorAccessToken = "KEYFACTOR_AUTH_ACCESS_TOKEN" // EnvKeyfactorAuthAudience is the environment variable used to set the audience for oauth Client credentials //authentication @@ -37,12 +39,6 @@ const ( // EnvKeyfactorAuthScopes is the environment variable used to set the scopes for oauth Client credentials authentication EnvKeyfactorAuthScopes = "KEYFACTOR_AUTH_SCOPES" - // EnvKeyfactorAuthHostname is the environment variable used to set the hostname for oauth Client credentials authentication - EnvKeyfactorAuthHostname = "KEYFACTOR_AUTH_HOSTNAME" - - // EnvKeyfactorAuthPort is the environment variable used to set the port for oauth Client credentials authentication - EnvKeyfactorAuthPort = "KEYFACTOR_AUTH_PORT" - // EnvAuthCACert is a path to a CA certificate for the OAuth Client credentials authentication EnvAuthCACert = "KEYFACTOR_AUTH_CA_CERT" ) @@ -92,11 +88,11 @@ type CommandConfigOauth struct { // TokenURL is the token URL for Keycloak authentication TokenURL string `json:"token_url"` - // AuthPort - AuthPort string `json:"auth_port,omitempty"` + //// AuthPort + //AuthPort string `json:"auth_port,omitempty"` - // AuthType is the type of Keycloak auth to use such as client_credentials, password, etc. - AuthType string `json:"auth_type,omitempty"` + //// AuthType is the type of Keycloak auth to use such as client_credentials, password, etc. + //AuthType string `json:"auth_type,omitempty"` } // NewOAuthAuthenticatorBuilder creates a new CommandConfigOauth instance. @@ -134,25 +130,48 @@ func (b *CommandConfigOauth) WithAudience(audience string) *CommandConfigOauth { return b } -// WithCACertificatePath sets the CA certificate path for Keycloak authentication. +// WithCaCertificatePath sets the CA certificate path for Keycloak authentication. func (b *CommandConfigOauth) WithCaCertificatePath(caCertificatePath string) *CommandConfigOauth { b.CACertificatePath = caCertificatePath return b } -// WithCACertificates sets the CA certificates for Keycloak authentication. +// WithCaCertificates sets the CA certificates for Keycloak authentication. func (b *CommandConfigOauth) WithCaCertificates(caCertificates []*x509.Certificate) *CommandConfigOauth { b.CACertificates = caCertificates return b } +// WithAccessToken sets the access token for Keycloak authentication. +func (b *CommandConfigOauth) WithAccessToken(accessToken string) *CommandConfigOauth { + if accessToken != "" { + b.AccessToken = accessToken + } + + return b +} + +// GetHttpClient returns an HTTP client for oAuth authentication. func (b *CommandConfigOauth) GetHttpClient() (*http.Client, error) { - //validate the configuration cErr := b.ValidateAuthConfig() if cErr != nil { return nil, cErr } + if b.AccessToken != "" { + return &http.Client{ + Transport: &oauth2.Transport{ + Base: http.DefaultTransport, + Source: oauth2.StaticTokenSource( + &oauth2.Token{ + AccessToken: b.AccessToken, + TokenType: DefaultTokenPrefix, + }, + ), + }, + }, nil + } + config := &clientcredentials.Config{ ClientID: b.ClientID, ClientSecret: b.ClientSecret, @@ -166,9 +185,7 @@ func (b *CommandConfigOauth) GetHttpClient() (*http.Client, error) { if b.Audience != "" { config.EndpointParams = map[string][]string{ - "Audience": { - b.Audience, - }, + "Audience": {b.Audience}, } } @@ -199,22 +216,93 @@ func (b *CommandConfigOauth) Build() (Authenticator, error) { return &OAuthAuthenticator{Client: client}, nil } -func (b *CommandConfigOauth) ValidateAuthConfig() error { - if b.ClientID == "" { - return fmt.Errorf("Client ID is required") +func (b *CommandConfigOauth) LoadConfig(profile, path string, silentLoad bool) (*authconfig.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 b.ClientSecret == "" { - return fmt.Errorf("Client secret is required") - } + if !silentLoad { + b.ClientID = serverConfig.ClientID + b.ClientSecret = serverConfig.ClientSecret + b.TokenURL = serverConfig.OAuthTokenUrl + b.CACertificatePath = serverConfig.CACertPath - if b.TokenURL == "" { - return fmt.Errorf("token URL is required") + } 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 + } } - //if len(b.Scopes) == 0 { - // return fmt.Errorf("at least one scope is required") - //} + return serverConfig, nil +} + +func (b *CommandConfigOauth) ValidateAuthConfig() error { + // silently load the server config + _, _ = b.CommandAuthConfig.LoadConfig( + b.CommandAuthConfig.ConfigProfile, + b.CommandAuthConfig.ConfigFilePath, + true, + ) + 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 { + return fmt.Errorf("client ID is required") + } + } + + if b.ClientSecret == "" { + if clientSecret, sOk := os.LookupEnv(EnvKeyfactorClientSecret); sOk { + b.ClientSecret = clientSecret + } else { + return fmt.Errorf("client secret is required") + } + } + + if b.TokenURL == "" { + if tokenUrl, uOk := os.LookupEnv(EnvKeyfactorAuthTokenURL); uOk { + b.TokenURL = tokenUrl + } else { + return fmt.Errorf("token URL is required") + } + } + } + } return b.CommandAuthConfig.ValidateAuthConfig() } @@ -232,6 +320,7 @@ func (b *CommandConfigOauth) Authenticate() error { WithClientId(b.ClientID). WithClientSecret(b.ClientSecret). WithTokenUrl(b.TokenURL). + WithAccessToken(b.AccessToken). Build() if err != nil { diff --git a/auth_providers/auth_oauth_test.go b/auth_providers/auth_oauth_test.go index 82a79a8..0962f37 100644 --- a/auth_providers/auth_oauth_test.go +++ b/auth_providers/auth_oauth_test.go @@ -1,3 +1,17 @@ +// 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 ( diff --git a/go.mod b/go.mod index 3c45212..e83a7c5 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,6 @@ module github.com/Keyfactor/keyfactor-auth-client-go go 1.22 require ( - golang.org/x/oauth2 v0.22.0 + golang.org/x/oauth2 v0.23.0 gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index 17be337..b65756f 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,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.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/lib/main.go b/lib/main.go index c1f2736..e41bf2c 100644 --- a/lib/main.go +++ b/lib/main.go @@ -1,3 +1,17 @@ +// 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 ( diff --git a/pkg/version.go b/pkg/version.go index 25ba0e1..1bc3d80 100644 --- a/pkg/version.go +++ b/pkg/version.go @@ -1,3 +1,17 @@ +// 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 ( From 79969ab79b97bb7ec4cceaca691994dd3b87f37f Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 17 Oct 2024 13:59:14 -0700 Subject: [PATCH 32/57] fix(core): Add `WithDomain` to basicauth chore(tests): Add negative tests for basic auth --- auth_providers/auth_basic.go | 6 ++++++ auth_providers/auth_basic_test.go | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/auth_providers/auth_basic.go b/auth_providers/auth_basic.go index 5f5cad9..5befc45 100644 --- a/auth_providers/auth_basic.go +++ b/auth_providers/auth_basic.go @@ -73,6 +73,12 @@ func (a *CommandAuthConfigBasic) WithPassword(password string) *CommandAuthConfi 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 diff --git a/auth_providers/auth_basic_test.go b/auth_providers/auth_basic_test.go index 4e994ea..25f45a7 100644 --- a/auth_providers/auth_basic_test.go +++ b/auth_providers/auth_basic_test.go @@ -18,6 +18,7 @@ import ( "fmt" "net/http" "os" + "strings" "testing" "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers" @@ -112,6 +113,23 @@ func TestCommandAuthConfigBasic_Authenticate(t *testing.T) { Domain: domain, } authBasicTest(t, "w/ full params variables", false, fullParamsConfig) + + noDomainConfig := &auth_providers.CommandAuthConfigBasic{ + Username: username, + Password: password, + } + authBasicTest(t, "w/ no domain", false, noDomainConfig) + + // remove domain from 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) + t.Log("Resetting environment variables") setBasicEnvVariables(username, password, domain) } From 8b1ac49de14e9f777c4a6bbab6d97e51880c81d5 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Fri, 18 Oct 2024 11:25:30 -0700 Subject: [PATCH 33/57] chore(tests): Finish basicauth tests --- auth_config/command_config.go | 23 +++++++ auth_providers/auth_basic.go | 15 ++++- auth_providers/auth_basic_test.go | 105 ++++++++++++++++++++++++++++-- auth_providers/auth_core.go | 12 ++-- 4 files changed, 143 insertions(+), 12 deletions(-) diff --git a/auth_config/command_config.go b/auth_config/command_config.go index e6d035e..da19475 100644 --- a/auth_config/command_config.go +++ b/auth_config/command_config.go @@ -62,6 +62,29 @@ type Config struct { // return yaml.Unmarshal(data, c) //} +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 +} + // ReadServerFromJSON reads a Server configuration from a JSON file. func ReadServerFromJSON(filePath string) (*Server, error) { file, err := os.Open(filePath) diff --git a/auth_providers/auth_basic.go b/auth_providers/auth_basic.go index 5befc45..0713300 100644 --- a/auth_providers/auth_basic.go +++ b/auth_providers/auth_basic.go @@ -124,11 +124,21 @@ func (a *CommandAuthConfigBasic) Build() (Authenticator, error) { // ValidateAuthConfig validates the configuration func (a *CommandAuthConfigBasic) ValidateAuthConfig() error { - serverConfig, _ := a.CommandAuthConfig.LoadConfig( + 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, - false, + silentLoad, ) + if !silentLoad && cErr != nil { + return cErr + } + if a.Username == "" { if username, ok := os.LookupEnv(EnvKeyfactorUsername); ok { a.Username = username @@ -178,6 +188,7 @@ func (a *CommandAuthConfigBasic) Authenticate() error { authy, err := NewBasicAuthAuthenticatorBuilder(). WithUsername(a.Username). WithPassword(a.Password). + WithDomain(a.Domain). Build() if err != nil { diff --git a/auth_providers/auth_basic_test.go b/auth_providers/auth_basic_test.go index 25f45a7..86abba3 100644 --- a/auth_providers/auth_basic_test.go +++ b/auth_providers/auth_basic_test.go @@ -21,6 +21,7 @@ import ( "strings" "testing" + auth_config "github.com/Keyfactor/keyfactor-auth-client-go/auth_config" "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers" ) @@ -91,6 +92,35 @@ func TestCommandAuthConfigBasic_Authenticate(t *testing.T) { return } + userHome, hErr := os.UserHomeDir() + if hErr != nil { + userHome = os.Getenv("HOME") + } + + configFilePath := fmt.Sprintf("%s/%s", userHome, auth_providers.DefaultConfigFilePath) + configFromFile, cErr := auth_config.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_config.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) @@ -98,15 +128,30 @@ func TestCommandAuthConfigBasic_Authenticate(t *testing.T) { 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{} - authBasicTest(t, "with incomplete Environmental variables", true, incompleteEnvConfig) + 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", } - authBasicTest(t, "Username Only", true, usernameOnlyConfig) + 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, @@ -114,13 +159,23 @@ func TestCommandAuthConfigBasic_Authenticate(t *testing.T) { } 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) - // remove domain from username + 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{ @@ -130,8 +185,34 @@ func TestCommandAuthConfigBasic_Authenticate(t *testing.T) { //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) - t.Log("Resetting environment variables") - setBasicEnvVariables(username, password, domain) + // Write the config file back + t.Logf("Writing config file: %s", configFilePath) + fErr := auth_config.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 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) { @@ -177,7 +258,10 @@ func unsetBasicEnvVariables() { os.Unsetenv(auth_providers.EnvKeyfactorDomain) } -func authBasicTest(t *testing.T, testName string, allowFail bool, config *auth_providers.CommandAuthConfigBasic) { +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) { @@ -188,6 +272,15 @@ func authBasicTest(t *testing.T, testName string, allowFail bool, config *auth_p 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 } diff --git a/auth_providers/auth_core.go b/auth_providers/auth_core.go index e71bf6a..bf45a5a 100644 --- a/auth_providers/auth_core.go +++ b/auth_providers/auth_core.go @@ -37,7 +37,7 @@ const ( DefaultAPIVersion = "1" DefaultAPIClientName = "APIClient" DefaultProductVersion = "10.5.0.0" - DefaultConfigFilePath = "~/.keyfactor/command_config.json" + DefaultConfigFilePath = ".keyfactor/command_config.json" DefaultConfigProfile = "default" DefaultClientTimeout = 60 @@ -176,8 +176,8 @@ func (c *CommandAuthConfig) WithConfigFile(configFilePath string) *CommandAuthCo func (c *CommandAuthConfig) WithConfigProfile(profile string) *CommandAuthConfig { if profile == "" { // check if profile is set in environment - if profile, ok := os.LookupEnv(EnvKeyfactorAuthProfile); ok { - c.ConfigProfile = profile + if p, ok := os.LookupEnv(EnvKeyfactorAuthProfile); ok { + c.ConfigProfile = p } else { c.ConfigProfile = DefaultConfigProfile } @@ -521,7 +521,11 @@ func (c *CommandAuthConfig) LoadConfig(profile string, configFilePath string, si if config, ok := os.LookupEnv(EnvKeyfactorConfigFile); ok { configFilePath = config } else { - configFilePath = DefaultConfigFilePath + homedir, err := os.UserHomeDir() + if err != nil { + homedir = os.Getenv("HOME") + } + configFilePath = fmt.Sprintf("%s/%s", homedir, DefaultConfigFilePath) } } else { c.ConfigFilePath = configFilePath From f15a3e172bb060c5dc1460833933d8ef54eee273 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Fri, 18 Oct 2024 12:42:26 -0700 Subject: [PATCH 34/57] chore(tests): Finish oauth tests --- auth_config/command_config.go | 2 +- auth_providers/auth_basic_test.go | 25 +++- auth_providers/auth_oauth.go | 42 ++++++- auth_providers/auth_oauth_test.go | 194 ++++++++++++++++++++++++++++-- 4 files changed, 243 insertions(+), 20 deletions(-) diff --git a/auth_config/command_config.go b/auth_config/command_config.go index da19475..2addac1 100644 --- a/auth_config/command_config.go +++ b/auth_config/command_config.go @@ -32,7 +32,7 @@ type Server struct { Domain string `json:"domain,omitempty" yaml:"domain,omitempty"` // Domain is the domain for authentication. ClientID string `json:"client_id,omitempty" yaml:"client_id,omitempty"` // ClientID is the client ID for OAuth. ClientSecret string `json:"client_secret,omitempty" yaml:"client_secret,omitempty"` // ClientSecret is the client secret for OAuth. - OAuthTokenUrl string `json:"oauth_token_url,omitempty" yaml:"auth_hostname,omitempty"` // OAuthTokenUrl is full URL for OAuth token request endpoint. + 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. diff --git a/auth_providers/auth_basic_test.go b/auth_providers/auth_basic_test.go index 86abba3..7f3af4a 100644 --- a/auth_providers/auth_basic_test.go +++ b/auth_providers/auth_basic_test.go @@ -124,6 +124,12 @@ func TestCommandAuthConfigBasic_Authenticate(t *testing.T) { 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() @@ -207,11 +213,20 @@ func TestCommandAuthConfigBasic_Authenticate(t *testing.T) { invProfileCreds.WithConfigProfile("invalid_username") authBasicTest(t, "with invalid creds implicit config file", true, invProfileCreds, invalidCredsExpectedError...) - 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...) + 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...) } diff --git a/auth_providers/auth_oauth.go b/auth_providers/auth_oauth.go index 6c455f7..8fb786a 100644 --- a/auth_providers/auth_oauth.go +++ b/auth_providers/auth_oauth.go @@ -266,12 +266,24 @@ func (b *CommandConfigOauth) LoadConfig(profile, path string, silentLoad bool) ( } func (b *CommandConfigOauth) ValidateAuthConfig() error { - // silently load the server config - _, _ = b.CommandAuthConfig.LoadConfig( + + 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, - true, + 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 { @@ -282,7 +294,11 @@ func (b *CommandConfigOauth) ValidateAuthConfig() error { if clientId, idOk := os.LookupEnv(EnvKeyfactorClientID); idOk { b.ClientID = clientId } else { - return fmt.Errorf("client ID is required") + if serverConfig != nil && serverConfig.ClientID != "" { + b.ClientID = serverConfig.ClientID + } else { + return fmt.Errorf("client ID or environment variable %s is required", EnvKeyfactorClientID) + } } } @@ -290,7 +306,14 @@ func (b *CommandConfigOauth) ValidateAuthConfig() error { if clientSecret, sOk := os.LookupEnv(EnvKeyfactorClientSecret); sOk { b.ClientSecret = clientSecret } else { - return fmt.Errorf("client secret is required") + if serverConfig != nil && serverConfig.ClientSecret != "" { + b.ClientSecret = serverConfig.ClientSecret + } else { + return fmt.Errorf( + "client secret or environment variable %s is required", + EnvKeyfactorClientSecret, + ) + } } } @@ -298,7 +321,14 @@ func (b *CommandConfigOauth) ValidateAuthConfig() error { if tokenUrl, uOk := os.LookupEnv(EnvKeyfactorAuthTokenURL); uOk { b.TokenURL = tokenUrl } else { - return fmt.Errorf("token URL is required") + if serverConfig != nil && serverConfig.OAuthTokenUrl != "" { + b.TokenURL = serverConfig.OAuthTokenUrl + } else { + return fmt.Errorf( + "token URL or environment variable %s is required", + EnvKeyfactorAuthTokenURL, + ) + } } } } diff --git a/auth_providers/auth_oauth_test.go b/auth_providers/auth_oauth_test.go index 0962f37..a017493 100644 --- a/auth_providers/auth_oauth_test.go +++ b/auth_providers/auth_oauth_test.go @@ -15,10 +15,13 @@ package auth_providers_test import ( + "fmt" "net/http" "os" + "strings" "testing" + auth_config "github.com/Keyfactor/keyfactor-auth-client-go/auth_config" "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers" ) @@ -89,17 +92,135 @@ func TestCommandConfigOauth_Authenticate(t *testing.T) { 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"}, + userHome, hErr := os.UserHomeDir() + if hErr != nil { + userHome = os.Getenv("HOME") } - err := config.Authenticate() - if err != nil { - t.Fatalf("expected no error, got %v", err) + configFilePath := fmt.Sprintf("%s/%s", userHome, auth_providers.DefaultConfigFilePath) + configFromFile, cErr := auth_config.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_config.WriteConfigToJSON(configFilePath, configFromFile) + if fErr != nil { + t.Errorf("unable to write auth config to file %s: %v", configFilePath, fErr) + } + }() + + t.Log("Testing oAuth with Environmental variables") + noParamsConfig := &auth_providers.CommandConfigOauth{} + authOauthTest(t, "with complete Environmental variables", false, noParamsConfig) + + 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") + 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, + } + 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, + } + 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_config.WriteConfigToJSON(configFilePath, configFromFile) + if fErr != nil { + t.Errorf("unable to write auth config to file %s: %v", configFilePath, fErr) } + + t.Log("Testing oAuth with valid implicit config file") + wConfigFile := &auth_providers.CommandConfigOauth{} + wConfigFile.WithConfigProfile("oauth") + authOauthTest(t, "with valid implicit config file", false, wConfigFile) + + 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") + 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") + invHostExpectedError := []string{"no such host"} + authOauthTest(t, "with invalid creds implicit config file", true, invCmdHost, invHostExpectedError...) } func TestCommandConfigOauth_Build(t *testing.T) { @@ -124,3 +245,60 @@ func TestCommandConfigOauth_Build(t *testing.T) { 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) +} + +// 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) +} From 6dc5efdf2c43fa901c8fee1ce4ddaaa816a4f04f Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Fri, 18 Oct 2024 12:55:20 -0700 Subject: [PATCH 35/57] fix(ci): Add ability to test command auth config file --- .github/config/environments.tf | 2 ++ .github/workflows/go_tests.yml | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/config/environments.tf b/.github/config/environments.tf index 0b42a3f..d26fab7 100644 --- a/.github/config/environments.tf +++ b/.github/config/environments.tf @@ -6,6 +6,7 @@ module "keyfactor_github_test_environment_ad_10_5_0" { 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" { @@ -30,4 +31,5 @@ module "keyfactor_github_test_environment_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/workflows/go_tests.yml b/.github/workflows/go_tests.yml index 1d9096d..337caf9 100644 --- a/.github/workflows/go_tests.yml +++ b/.github/workflows/go_tests.yml @@ -22,10 +22,16 @@ jobs: go-version: 1.22 - name: Run tests - run: go test -v -cover ./auth_providers/... + 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_HOSTNAME: ${{ vars.KEYFACTOR_HOSTNAME }} From e02a467f90b36ee6eafe74062c61e5346d2b3005 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Mon, 21 Oct 2024 06:10:36 -0700 Subject: [PATCH 36/57] fix(conf): Add `GetAuthType` --- auth_config/command_config.go | 18 +++++++++++++++--- auth_config/command_config_test.go | 4 +--- tag.sh | 2 +- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/auth_config/command_config.go b/auth_config/command_config.go index 2addac1..b17edd3 100644 --- a/auth_config/command_config.go +++ b/auth_config/command_config.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package authconfig +package auth_config import ( "encoding/json" @@ -36,8 +36,9 @@ type Server struct { 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. + 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. @@ -208,3 +209,14 @@ func MergeConfigFromFile(filePath string, config *Config) error { return nil } + +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 +} diff --git a/auth_config/command_config_test.go b/auth_config/command_config_test.go index 1b0861a..8f5ec4e 100644 --- a/auth_config/command_config_test.go +++ b/auth_config/command_config_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package authconfig_test +package auth_config_test import ( "encoding/json" @@ -20,8 +20,6 @@ import ( "testing" "gopkg.in/yaml.v2" - - "github.com/Keyfactor/keyfactor-auth-client-go/auth_config" ) func TestReadServerFromJSON(t *testing.T) { diff --git a/tag.sh b/tag.sh index f1086ab..42ccefa 100755 --- a/tag.sh +++ b/tag.sh @@ -1,4 +1,4 @@ -RC_VERSION=rc.8 +RC_VERSION=rc.9 TAG_VERSION_1=v0.0.1-$RC_VERSION git tag -d $TAG_VERSION_1 || true git tag $TAG_VERSION_1 From aa2b90d34137f5144191b30070ca756324bd6bef Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Mon, 21 Oct 2024 06:26:00 -0700 Subject: [PATCH 37/57] fix(conf): Add `UserAgent` param --- auth_providers/auth_core.go | 3 +++ tag.sh | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/auth_providers/auth_core.go b/auth_providers/auth_core.go index bf45a5a..f24ca3d 100644 --- a/auth_providers/auth_core.go +++ b/auth_providers/auth_core.go @@ -103,6 +103,9 @@ type CommandAuthConfig struct { // 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"` + // HttpClient is the http Client to be used for authentication to Keyfactor Command API HttpClient *http.Client } diff --git a/tag.sh b/tag.sh index 42ccefa..bfc802c 100755 --- a/tag.sh +++ b/tag.sh @@ -1,4 +1,4 @@ -RC_VERSION=rc.9 +RC_VERSION=rc.10 TAG_VERSION_1=v0.0.1-$RC_VERSION git tag -d $TAG_VERSION_1 || true git tag $TAG_VERSION_1 From bf152565e5a1f35294414a5039e6c68a4a012cde Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Mon, 21 Oct 2024 06:30:00 -0700 Subject: [PATCH 38/57] fix(conf): Add `Debug` param --- auth_providers/auth_core.go | 3 +++ tag.sh | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/auth_providers/auth_core.go b/auth_providers/auth_core.go index f24ca3d..d5669b0 100644 --- a/auth_providers/auth_core.go +++ b/auth_providers/auth_core.go @@ -106,6 +106,9 @@ type CommandAuthConfig struct { // 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 } diff --git a/tag.sh b/tag.sh index bfc802c..08d9cf4 100755 --- a/tag.sh +++ b/tag.sh @@ -1,4 +1,4 @@ -RC_VERSION=rc.10 +RC_VERSION=rc.11 TAG_VERSION_1=v0.0.1-$RC_VERSION git tag -d $TAG_VERSION_1 || true git tag $TAG_VERSION_1 From 2ebd039126680fbe1091b849ee9f83a1048477b6 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Mon, 21 Oct 2024 08:17:06 -0700 Subject: [PATCH 39/57] feat(auth): Add `GetServerConfig` that returns `*authconfig.Server` --- auth_providers/auth_basic.go | 22 ++++++++++++++++++++++ auth_providers/auth_core.go | 6 +++--- auth_providers/auth_oauth.go | 16 ++++++++++++++++ tag.sh | 2 +- 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/auth_providers/auth_basic.go b/auth_providers/auth_basic.go index 0713300..6d77afb 100644 --- a/auth_providers/auth_basic.go +++ b/auth_providers/auth_basic.go @@ -20,6 +20,8 @@ import ( "net/http" "os" "strings" + + "github.com/Keyfactor/keyfactor-auth-client-go/auth_config" ) const ( @@ -228,3 +230,23 @@ func (a *CommandAuthConfigBasic) parseUsernameDomain() error { return nil } + +// GetServerConfig returns the server configuration +func (a *CommandAuthConfigBasic) GetServerConfig() *auth_config.Server { + server := auth_config.Server{ + Host: a.CommandHostName, + Port: a.CommandPort, + Username: a.Username, + Password: a.Password, + Domain: a.Domain, + ClientID: "", + ClientSecret: "", + OAuthTokenUrl: "", + APIPath: a.CommandAPIPath, + //AuthProvider: auth_config.AuthProvider{}, + SkipTLSVerify: a.SkipVerify, + CACertPath: a.CommandCACert, + AuthType: "basic", + } + return &server +} diff --git a/auth_providers/auth_core.go b/auth_providers/auth_core.go index d5669b0..4e2d728 100644 --- a/auth_providers/auth_core.go +++ b/auth_providers/auth_core.go @@ -83,13 +83,13 @@ type CommandAuthConfig struct { AuthHeader string `json:"auth_header"` // CommandHostName is the hostname of the Keyfactor Command API - CommandHostName string `json:"command_host_name"` + CommandHostName string `json:"host"` // CommandPort is the port of the Keyfactor Command API - CommandPort int `json:"command_port"` + CommandPort int `json:"port"` // CommandAPIPath is the path of the Keyfactor Command API, default is "KeyfactorAPI" - CommandAPIPath string `json:"command_api_path"` + CommandAPIPath string `json:"api_path"` // CommandAPIVersion is the version of the Keyfactor Command API, default is "1" CommandVersion string `json:"command_version"` diff --git a/auth_providers/auth_oauth.go b/auth_providers/auth_oauth.go index 8fb786a..00bcc61 100644 --- a/auth_providers/auth_oauth.go +++ b/auth_providers/auth_oauth.go @@ -372,3 +372,19 @@ func (b *CommandConfigOauth) Authenticate() error { return nil } + +func (b *CommandConfigOauth) GetServerConfig() *authconfig.Server { + server := authconfig.Server{ + Host: b.CommandHostName, + Port: b.CommandPort, + ClientID: b.ClientID, + ClientSecret: b.ClientSecret, + OAuthTokenUrl: b.TokenURL, + APIPath: b.CommandAPIPath, + //AuthProvider: authconfig.AuthProvider{}, + SkipTLSVerify: b.SkipVerify, + CACertPath: b.CommandCACert, + AuthType: "oauth", + } + return &server +} diff --git a/tag.sh b/tag.sh index 08d9cf4..d18b07d 100755 --- a/tag.sh +++ b/tag.sh @@ -1,4 +1,4 @@ -RC_VERSION=rc.11 +RC_VERSION=rc.13 TAG_VERSION_1=v0.0.1-$RC_VERSION git tag -d $TAG_VERSION_1 || true git tag $TAG_VERSION_1 From 6c5dd1a18a954f64d8c7e2c267828e9a1bdf57ab Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Mon, 21 Oct 2024 08:21:25 -0700 Subject: [PATCH 40/57] feat(auth): Add `GetServerConfig` that returns `*authconfig.Server` --- auth_providers/auth_core.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/auth_providers/auth_core.go b/auth_providers/auth_core.go index 4e2d728..b4569d4 100644 --- a/auth_providers/auth_core.go +++ b/auth_providers/auth_core.go @@ -621,3 +621,22 @@ func expandPath(path string) (string, error) { } return path, nil } + +func (c *CommandAuthConfig) GetServerConfig() *authconfig.Server { + server := authconfig.Server{ + Host: c.CommandHostName, + Port: c.CommandPort, + Username: "", + Password: "", + Domain: "", + ClientID: "", + ClientSecret: "", + OAuthTokenUrl: "", + APIPath: c.CommandAPIPath, + AuthProvider: authconfig.AuthProvider{}, + SkipTLSVerify: c.SkipVerify, + CACertPath: c.CommandCACert, + AuthType: "", + } + return &server +} From 08cbfe230161949409b48765324d659ae7e09664 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 22 Oct 2024 15:27:16 -0700 Subject: [PATCH 41/57] feat(conf): Add ability to construct client configs based on Server params --- auth_config/command_config.go | 88 +++++++++++++++++++++++++++++++---- tag.sh | 2 +- 2 files changed, 79 insertions(+), 11 deletions(-) diff --git a/auth_config/command_config.go b/auth_config/command_config.go index b17edd3..a700d94 100644 --- a/auth_config/command_config.go +++ b/auth_config/command_config.go @@ -20,6 +20,8 @@ import ( "os" "gopkg.in/yaml.v2" + + "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers" ) // Server represents the server configuration for authentication. @@ -53,16 +55,6 @@ type Config struct { Servers map[string]Server `json:"servers,omitempty" yaml:"servers,omitempty"` // Servers is a map of server configurations. } -// UnmarshalJSON unmarshals the JSON data into the Config struct. -//func (c *Config) UnmarshalJSON(data []byte) error { -// return json.Unmarshal(data, c) -//} - -// UnmarshalYAML unmarshals the YAML data into the Config struct. -//func (c *Config) UnmarshalYAML(data []byte) error { -// return yaml.Unmarshal(data, c) -//} - func NewConfig() *Config { return &Config{ Servers: make(map[string]Server), @@ -86,6 +78,21 @@ func ReadConfigFromJSON(filePath string) (*Config, error) { 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) @@ -220,3 +227,64 @@ func (s *Server) GetAuthType() string { } return s.AuthType } + +// GetBasicAuthClientConfig returns the basic auth configuration for the client. +func (s *Server) GetBasicAuthClientConfig() (*auth_providers.CommandAuthConfigBasic, error) { + configType := s.GetAuthType() + if configType != "basic" { + return nil, fmt.Errorf("invalid auth type: %s", configType) + } + + baseConfig := auth_providers.CommandAuthConfig{} + baseConfig. + WithCommandHostName(s.Host). + WithCommandPort(s.Port). + WithCommandAPIPath(s.APIPath). + WithCommandCACert(s.CACertPath). + WithSkipVerify(s.SkipTLSVerify) + + basicConfig := auth_providers.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() (*auth_providers.CommandConfigOauth, error) { + configType := s.GetAuthType() + if configType != "oauth" { + return nil, fmt.Errorf("invalid auth type: %s", configType) + } + baseConfig := auth_providers.CommandAuthConfig{} + baseConfig. + WithCommandHostName(s.Host). + WithCommandPort(s.Port). + WithCommandAPIPath(s.APIPath). + WithCommandCACert(s.CACertPath). + WithSkipVerify(s.SkipTLSVerify) + + oauthConfig := auth_providers.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 +} diff --git a/tag.sh b/tag.sh index d18b07d..f82972b 100755 --- a/tag.sh +++ b/tag.sh @@ -1,4 +1,4 @@ -RC_VERSION=rc.13 +RC_VERSION=rc.15 TAG_VERSION_1=v0.0.1-$RC_VERSION git tag -d $TAG_VERSION_1 || true git tag $TAG_VERSION_1 From df84e8928f4d4b764663b7bc24d04ba19f85ed3b Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 24 Oct 2024 14:18:39 -0700 Subject: [PATCH 42/57] feat(auth): Basic auth to include domain in username if present --- auth_providers/auth_basic.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/auth_providers/auth_basic.go b/auth_providers/auth_basic.go index 6d77afb..5da1f21 100644 --- a/auth_providers/auth_basic.go +++ b/auth_providers/auth_basic.go @@ -90,7 +90,12 @@ func (a *CommandAuthConfigBasic) GetHttpClient() (*http.Client, error) { } // Encode the username and password in Base64 - auth := a.Username + ":" + a.Password + 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 From ae2aaf9775d8173559d0610140140282d2de5a77 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 24 Oct 2024 14:56:57 -0700 Subject: [PATCH 43/57] fix(pkg): Flatten package structure --- auth_providers/auth_basic.go | 8 +-- auth_providers/auth_core.go | 14 ++-- auth_providers/auth_oauth.go | 9 ++- auth_providers/auth_oauth_test.go | 7 +- .../command_config.go | 16 ++--- .../command_config_test.go | 65 ++++++++++--------- tag.sh | 2 +- 7 files changed, 57 insertions(+), 64 deletions(-) rename {auth_config => auth_providers}/command_config.go (94%) rename {auth_config => auth_providers}/command_config_test.go (83%) diff --git a/auth_providers/auth_basic.go b/auth_providers/auth_basic.go index 5da1f21..38829a7 100644 --- a/auth_providers/auth_basic.go +++ b/auth_providers/auth_basic.go @@ -20,8 +20,6 @@ import ( "net/http" "os" "strings" - - "github.com/Keyfactor/keyfactor-auth-client-go/auth_config" ) const ( @@ -237,8 +235,8 @@ func (a *CommandAuthConfigBasic) parseUsernameDomain() error { } // GetServerConfig returns the server configuration -func (a *CommandAuthConfigBasic) GetServerConfig() *auth_config.Server { - server := auth_config.Server{ +func (a *CommandAuthConfigBasic) GetServerConfig() *Server { + server := Server{ Host: a.CommandHostName, Port: a.CommandPort, Username: a.Username, @@ -248,7 +246,7 @@ func (a *CommandAuthConfigBasic) GetServerConfig() *auth_config.Server { ClientSecret: "", OAuthTokenUrl: "", APIPath: a.CommandAPIPath, - //AuthProvider: auth_config.AuthProvider{}, + //AuthProvider: AuthProvider{}, SkipTLSVerify: a.SkipVerify, CACertPath: a.CommandCACert, AuthType: "basic", diff --git a/auth_providers/auth_core.go b/auth_providers/auth_core.go index b4569d4..f2cd6ef 100644 --- a/auth_providers/auth_core.go +++ b/auth_providers/auth_core.go @@ -27,8 +27,6 @@ import ( "strconv" "strings" "time" - - authconfig "github.com/Keyfactor/keyfactor-auth-client-go/auth_config" ) const ( @@ -77,7 +75,7 @@ type CommandAuthConfig struct { ConfigFilePath string // FileConfig - FileConfig *authconfig.Server + FileConfig *Server // AuthHeader is the header to be used for authentication to Keyfactor Command API AuthHeader string `json:"auth_header"` @@ -519,7 +517,7 @@ func DecodePEMBytes(buf []byte) ([]*pem.Block, []byte, error) { // LoadConfig loads the configuration file and returns the server configuration. func (c *CommandAuthConfig) LoadConfig(profile string, configFilePath string, silentLoad bool) ( - *authconfig.Server, + *Server, error, ) { if configFilePath == "" { @@ -555,7 +553,7 @@ func (c *CommandAuthConfig) LoadConfig(profile string, configFilePath string, si } defer file.Close() - var config authconfig.Config + var config Config decoder := json.NewDecoder(file) if jErr := decoder.Decode(&config); jErr != nil { if !silentLoad { @@ -622,8 +620,8 @@ func expandPath(path string) (string, error) { return path, nil } -func (c *CommandAuthConfig) GetServerConfig() *authconfig.Server { - server := authconfig.Server{ +func (c *CommandAuthConfig) GetServerConfig() *Server { + server := Server{ Host: c.CommandHostName, Port: c.CommandPort, Username: "", @@ -633,7 +631,7 @@ func (c *CommandAuthConfig) GetServerConfig() *authconfig.Server { ClientSecret: "", OAuthTokenUrl: "", APIPath: c.CommandAPIPath, - AuthProvider: authconfig.AuthProvider{}, + AuthProvider: AuthProvider{}, SkipTLSVerify: c.SkipVerify, CACertPath: c.CommandCACert, AuthType: "", diff --git a/auth_providers/auth_oauth.go b/auth_providers/auth_oauth.go index 00bcc61..639f1bc 100644 --- a/auth_providers/auth_oauth.go +++ b/auth_providers/auth_oauth.go @@ -8,7 +8,6 @@ import ( "os" "time" - authconfig "github.com/Keyfactor/keyfactor-auth-client-go/auth_config" "golang.org/x/oauth2" "golang.org/x/oauth2/clientcredentials" ) @@ -216,7 +215,7 @@ func (b *CommandConfigOauth) Build() (Authenticator, error) { return &OAuthAuthenticator{Client: client}, nil } -func (b *CommandConfigOauth) LoadConfig(profile, path string, silentLoad bool) (*authconfig.Server, error) { +func (b *CommandConfigOauth) LoadConfig(profile, path string, silentLoad bool) (*Server, error) { serverConfig, sErr := b.CommandAuthConfig.LoadConfig(profile, path, silentLoad) if sErr != nil { if !silentLoad { @@ -373,15 +372,15 @@ func (b *CommandConfigOauth) Authenticate() error { return nil } -func (b *CommandConfigOauth) GetServerConfig() *authconfig.Server { - server := authconfig.Server{ +func (b *CommandConfigOauth) GetServerConfig() *Server { + server := Server{ Host: b.CommandHostName, Port: b.CommandPort, ClientID: b.ClientID, ClientSecret: b.ClientSecret, OAuthTokenUrl: b.TokenURL, APIPath: b.CommandAPIPath, - //AuthProvider: authconfig.AuthProvider{}, + //AuthProvider: AuthProvider{}, SkipTLSVerify: b.SkipVerify, CACertPath: b.CommandCACert, AuthType: "oauth", diff --git a/auth_providers/auth_oauth_test.go b/auth_providers/auth_oauth_test.go index a017493..0ee1667 100644 --- a/auth_providers/auth_oauth_test.go +++ b/auth_providers/auth_oauth_test.go @@ -21,7 +21,6 @@ import ( "strings" "testing" - auth_config "github.com/Keyfactor/keyfactor-auth-client-go/auth_config" "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers" ) @@ -98,7 +97,7 @@ func TestCommandConfigOauth_Authenticate(t *testing.T) { } configFilePath := fmt.Sprintf("%s/%s", userHome, auth_providers.DefaultConfigFilePath) - configFromFile, cErr := auth_config.ReadConfigFromJSON(configFilePath) + configFromFile, cErr := auth_providers.ReadConfigFromJSON(configFilePath) if cErr != nil { t.Errorf("unable to load auth config from file %s: %v", configFilePath, cErr) } @@ -114,7 +113,7 @@ func TestCommandConfigOauth_Authenticate(t *testing.T) { defer func() { // Write the config file back t.Logf("Writing config file: %s", configFilePath) - fErr := auth_config.WriteConfigToJSON(configFilePath, configFromFile) + fErr := auth_providers.WriteConfigToJSON(configFilePath, configFromFile) if fErr != nil { t.Errorf("unable to write auth config to file %s: %v", configFilePath, fErr) } @@ -195,7 +194,7 @@ func TestCommandConfigOauth_Authenticate(t *testing.T) { // Write the config file back t.Logf("Writing config file: %s", configFilePath) - fErr := auth_config.WriteConfigToJSON(configFilePath, configFromFile) + fErr := auth_providers.WriteConfigToJSON(configFilePath, configFromFile) if fErr != nil { t.Errorf("unable to write auth config to file %s: %v", configFilePath, fErr) } diff --git a/auth_config/command_config.go b/auth_providers/command_config.go similarity index 94% rename from auth_config/command_config.go rename to auth_providers/command_config.go index a700d94..3d5d382 100644 --- a/auth_config/command_config.go +++ b/auth_providers/command_config.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package auth_config +package auth_providers import ( "encoding/json" @@ -20,8 +20,6 @@ import ( "os" "gopkg.in/yaml.v2" - - "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers" ) // Server represents the server configuration for authentication. @@ -229,13 +227,13 @@ func (s *Server) GetAuthType() string { } // GetBasicAuthClientConfig returns the basic auth configuration for the client. -func (s *Server) GetBasicAuthClientConfig() (*auth_providers.CommandAuthConfigBasic, error) { +func (s *Server) GetBasicAuthClientConfig() (*CommandAuthConfigBasic, error) { configType := s.GetAuthType() if configType != "basic" { return nil, fmt.Errorf("invalid auth type: %s", configType) } - baseConfig := auth_providers.CommandAuthConfig{} + baseConfig := CommandAuthConfig{} baseConfig. WithCommandHostName(s.Host). WithCommandPort(s.Port). @@ -243,7 +241,7 @@ func (s *Server) GetBasicAuthClientConfig() (*auth_providers.CommandAuthConfigBa WithCommandCACert(s.CACertPath). WithSkipVerify(s.SkipTLSVerify) - basicConfig := auth_providers.CommandAuthConfigBasic{ + basicConfig := CommandAuthConfigBasic{ CommandAuthConfig: baseConfig, } basicConfig. @@ -260,12 +258,12 @@ func (s *Server) GetBasicAuthClientConfig() (*auth_providers.CommandAuthConfigBa } // GetOAuthClientConfig returns the OAuth configuration for the client. -func (s *Server) GetOAuthClientConfig() (*auth_providers.CommandConfigOauth, error) { +func (s *Server) GetOAuthClientConfig() (*CommandConfigOauth, error) { configType := s.GetAuthType() if configType != "oauth" { return nil, fmt.Errorf("invalid auth type: %s", configType) } - baseConfig := auth_providers.CommandAuthConfig{} + baseConfig := CommandAuthConfig{} baseConfig. WithCommandHostName(s.Host). WithCommandPort(s.Port). @@ -273,7 +271,7 @@ func (s *Server) GetOAuthClientConfig() (*auth_providers.CommandConfigOauth, err WithCommandCACert(s.CACertPath). WithSkipVerify(s.SkipTLSVerify) - oauthConfig := auth_providers.CommandConfigOauth{ + oauthConfig := CommandConfigOauth{ CommandAuthConfig: baseConfig, } oauthConfig. diff --git a/auth_config/command_config_test.go b/auth_providers/command_config_test.go similarity index 83% rename from auth_config/command_config_test.go rename to auth_providers/command_config_test.go index 8f5ec4e..0a43068 100644 --- a/auth_config/command_config_test.go +++ b/auth_providers/command_config_test.go @@ -12,19 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -package auth_config_test +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 := &authconfig.Server{ + server := &auth_providers.Server{ Host: "localhost", Port: 8080, OAuthTokenUrl: "https://auth.localhost:8443/openid/token", @@ -36,13 +37,13 @@ func TestReadServerFromJSON(t *testing.T) { APIPath: "api", } - err := authconfig.WriteServerToJSON(filePath, server) + 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 := authconfig.ReadServerFromJSON(filePath) + readServer, err := auth_providers.ReadServerFromJSON(filePath) if err != nil { t.Fatalf("failed to read server from JSON: %v", err) } @@ -54,7 +55,7 @@ func TestReadServerFromJSON(t *testing.T) { func TestWriteServerToJSON(t *testing.T) { filePath := "test_server.json" - server := &authconfig.Server{ + server := &auth_providers.Server{ Host: "localhost", Port: 8080, OAuthTokenUrl: "https://auth.localhost:8443/openid/token", @@ -66,7 +67,7 @@ func TestWriteServerToJSON(t *testing.T) { APIPath: "api", } - err := authconfig.WriteServerToJSON(filePath, server) + err := auth_providers.WriteServerToJSON(filePath, server) if err != nil { t.Fatalf("failed to write server to JSON: %v", err) } @@ -77,7 +78,7 @@ func TestWriteServerToJSON(t *testing.T) { t.Fatalf("failed to read file: %v", err) } - var readServer authconfig.Server + var readServer auth_providers.Server err = json.Unmarshal(file, &readServer) if err != nil { t.Fatalf("failed to unmarshal JSON: %v", err) @@ -90,7 +91,7 @@ func TestWriteServerToJSON(t *testing.T) { func TestReadServerFromYAML(t *testing.T) { filePath := "test_server.yaml" - server := &authconfig.Server{ + server := &auth_providers.Server{ Host: "localhost", Port: 8080, OAuthTokenUrl: "https://auth.localhost:8443/openid/token", @@ -102,13 +103,13 @@ func TestReadServerFromYAML(t *testing.T) { APIPath: "api", } - err := authconfig.WriteServerToYAML(filePath, server) + 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 := authconfig.ReadServerFromYAML(filePath) + readServer, err := auth_providers.ReadServerFromYAML(filePath) if err != nil { t.Fatalf("failed to read server from YAML: %v", err) } @@ -120,7 +121,7 @@ func TestReadServerFromYAML(t *testing.T) { func TestWriteServerToYAML(t *testing.T) { filePath := "test_server.yaml" - server := &authconfig.Server{ + server := &auth_providers.Server{ Host: "localhost", Port: 8080, OAuthTokenUrl: "https://auth.localhost:8443/openid/token", @@ -132,7 +133,7 @@ func TestWriteServerToYAML(t *testing.T) { APIPath: "api", } - err := authconfig.WriteServerToYAML(filePath, server) + err := auth_providers.WriteServerToYAML(filePath, server) if err != nil { t.Fatalf("failed to write server to YAML: %v", err) } @@ -143,7 +144,7 @@ func TestWriteServerToYAML(t *testing.T) { t.Fatalf("failed to read file: %v", err) } - var readServer authconfig.Server + var readServer auth_providers.Server err = yaml.Unmarshal(file, &readServer) if err != nil { t.Fatalf("failed to unmarshal YAML: %v", err) @@ -156,8 +157,8 @@ func TestWriteServerToYAML(t *testing.T) { func TestMergeConfigFromFile(t *testing.T) { filePath := "test_config.json" - config := &authconfig.Config{ - Servers: map[string]authconfig.Server{ + config := &auth_providers.Config{ + Servers: map[string]auth_providers.Server{ "server1": { Host: "localhost", Port: 8080, @@ -165,14 +166,14 @@ func TestMergeConfigFromFile(t *testing.T) { }, } - err := authconfig.WriteConfigToJSON(filePath, config) + err := auth_providers.WriteConfigToJSON(filePath, config) if err != nil { t.Fatalf("failed to write config to JSON: %v", err) } defer os.Remove(filePath) - newConfig := &authconfig.Config{ - Servers: map[string]authconfig.Server{ + newConfig := &auth_providers.Config{ + Servers: map[string]auth_providers.Server{ "server2": { Host: "remotehost", Port: 9090, @@ -180,7 +181,7 @@ func TestMergeConfigFromFile(t *testing.T) { }, } - err = authconfig.MergeConfigFromFile(filePath, newConfig) + err = auth_providers.MergeConfigFromFile(filePath, newConfig) if err != nil { t.Fatalf("failed to merge config from file: %v", err) } @@ -200,8 +201,8 @@ func TestMergeConfigFromFile(t *testing.T) { func TestReadFullAuthConfigExample(t *testing.T) { filePath := "../lib/config/full_auth_config_example.json" - expectedConfig := &authconfig.Config{ - Servers: map[string]authconfig.Server{ + expectedConfig := &auth_providers.Config{ + Servers: map[string]auth_providers.Server{ "default": { Host: "keyfactor.command.kfdelivery.com", OAuthTokenUrl: "idp.keyfactor.command.kfdelivery.com", @@ -211,7 +212,7 @@ func TestReadFullAuthConfigExample(t *testing.T) { ClientSecret: "client-secret", Domain: "command", APIPath: "KeyfactorAPI", - AuthProvider: authconfig.AuthProvider{ + AuthProvider: auth_providers.AuthProvider{ Type: "azid", Profile: "azure", Parameters: map[string]interface{}{ @@ -229,7 +230,7 @@ func TestReadFullAuthConfigExample(t *testing.T) { ClientSecret: "client-secret2", Domain: "command", APIPath: "KeyfactorAPI", - AuthProvider: authconfig.AuthProvider{ + AuthProvider: auth_providers.AuthProvider{ Type: "azid", Profile: "azure", Parameters: map[string]interface{}{ @@ -246,7 +247,7 @@ func TestReadFullAuthConfigExample(t *testing.T) { t.Fatalf("failed to read file: %v", err) } - var config authconfig.Config + var config auth_providers.Config err = json.Unmarshal(file, &config) if err != nil { t.Fatalf("failed to unmarshal JSON: %v", err) @@ -259,8 +260,8 @@ func TestReadFullAuthConfigExample(t *testing.T) { func TestReadOAuthConfigExample(t *testing.T) { filePath := "../lib/config/oauth_config_example.json" - expectedConfig := &authconfig.Config{ - Servers: map[string]authconfig.Server{ + 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", @@ -283,7 +284,7 @@ func TestReadOAuthConfigExample(t *testing.T) { t.Fatalf("failed to read file: %v", err) } - var config authconfig.Config + var config auth_providers.Config err = json.Unmarshal(file, &config) if err != nil { t.Fatalf("failed to unmarshal JSON: %v", err) @@ -296,8 +297,8 @@ func TestReadOAuthConfigExample(t *testing.T) { func TestReadBasicAuthConfigExample(t *testing.T) { filePath := "../lib/config/basic_auth_config_example.json" - expectedConfig := &authconfig.Config{ - Servers: map[string]authconfig.Server{ + expectedConfig := &auth_providers.Config{ + Servers: map[string]auth_providers.Server{ "default": { Host: "keyfactor.command.kfdelivery.com", Username: "keyfactor", @@ -320,7 +321,7 @@ func TestReadBasicAuthConfigExample(t *testing.T) { t.Fatalf("failed to read file: %v", err) } - var config authconfig.Config + var config auth_providers.Config err = json.Unmarshal(file, &config) if err != nil { t.Fatalf("failed to unmarshal JSON: %v", err) @@ -331,7 +332,7 @@ func TestReadBasicAuthConfigExample(t *testing.T) { } } -func compareConfigs(a, b *authconfig.Config) bool { +func compareConfigs(a, b *auth_providers.Config) bool { if len(a.Servers) != len(b.Servers) { return false } @@ -344,7 +345,7 @@ func compareConfigs(a, b *authconfig.Config) bool { return true } -func compareServers(a, b *authconfig.Server) bool { +func compareServers(a, b *auth_providers.Server) bool { return a.Host == b.Host && a.Port == b.Port && a.OAuthTokenUrl == b.OAuthTokenUrl && diff --git a/tag.sh b/tag.sh index f82972b..7c04e1f 100755 --- a/tag.sh +++ b/tag.sh @@ -1,4 +1,4 @@ -RC_VERSION=rc.15 +RC_VERSION=rc.17 TAG_VERSION_1=v0.0.1-$RC_VERSION git tag -d $TAG_VERSION_1 || true git tag $TAG_VERSION_1 From 892ec73467fad6debbb0158dba15d2524e7f0135 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 24 Oct 2024 15:08:15 -0700 Subject: [PATCH 44/57] fix(tests): Basic auth tests don't ref `auth_config` anymore --- auth_providers/auth_basic_test.go | 7 +++---- tag.sh | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/auth_providers/auth_basic_test.go b/auth_providers/auth_basic_test.go index 7f3af4a..5c404ae 100644 --- a/auth_providers/auth_basic_test.go +++ b/auth_providers/auth_basic_test.go @@ -21,7 +21,6 @@ import ( "strings" "testing" - auth_config "github.com/Keyfactor/keyfactor-auth-client-go/auth_config" "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers" ) @@ -98,7 +97,7 @@ func TestCommandAuthConfigBasic_Authenticate(t *testing.T) { } configFilePath := fmt.Sprintf("%s/%s", userHome, auth_providers.DefaultConfigFilePath) - configFromFile, cErr := auth_config.ReadConfigFromJSON(configFilePath) + configFromFile, cErr := auth_providers.ReadConfigFromJSON(configFilePath) if cErr != nil { t.Errorf("unable to load auth config from file %s: %v", configFilePath, cErr) } @@ -114,7 +113,7 @@ func TestCommandAuthConfigBasic_Authenticate(t *testing.T) { defer func() { // Write the config file back t.Logf("Writing config file: %s", configFilePath) - fErr := auth_config.WriteConfigToJSON(configFilePath, configFromFile) + fErr := auth_providers.WriteConfigToJSON(configFilePath, configFromFile) if fErr != nil { t.Errorf("unable to write auth config to file %s: %v", configFilePath, fErr) } @@ -193,7 +192,7 @@ func TestCommandAuthConfigBasic_Authenticate(t *testing.T) { // Write the config file back t.Logf("Writing config file: %s", configFilePath) - fErr := auth_config.WriteConfigToJSON(configFilePath, configFromFile) + fErr := auth_providers.WriteConfigToJSON(configFilePath, configFromFile) if fErr != nil { t.Errorf("unable to write auth config to file %s: %v", configFilePath, fErr) } diff --git a/tag.sh b/tag.sh index 7c04e1f..c350f84 100755 --- a/tag.sh +++ b/tag.sh @@ -1,4 +1,4 @@ -RC_VERSION=rc.17 +RC_VERSION=rc.18 TAG_VERSION_1=v0.0.1-$RC_VERSION git tag -d $TAG_VERSION_1 || true git tag $TAG_VERSION_1 From 0249a731b4d04328534362cb18a257e7c4ecae49 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Mon, 28 Oct 2024 10:49:12 -0700 Subject: [PATCH 45/57] chore(docs): Add example usage for auth providers. --- auth_providers/auth_basic.go | 39 ++++++++++++-- auth_providers/auth_core.go | 88 ++++++++++++++++++++++++++------ auth_providers/auth_oauth.go | 51 ++++++++++++++++-- auth_providers/command_config.go | 54 ++++++++++++++++++++ integration-manifest.json | 11 ++++ tag.sh | 4 +- 6 files changed, 222 insertions(+), 25 deletions(-) create mode 100644 integration-manifest.json diff --git a/auth_providers/auth_basic.go b/auth_providers/auth_basic.go index 38829a7..eb3f58a 100644 --- a/auth_providers/auth_basic.go +++ b/auth_providers/auth_basic.go @@ -23,9 +23,14 @@ import ( ) 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 = "KEYFACTOR_DOMAIN" + + // EnvKeyfactorDomain is the environment variable for the Keyfactor domain + EnvKeyfactorDomain = "KEYFACTOR_DOMAIN" ) // Basic Authenticator @@ -127,7 +132,7 @@ func (a *CommandAuthConfigBasic) Build() (Authenticator, error) { return &BasicAuthAuthenticator{Client: client}, nil } -// ValidateAuthConfig validates the configuration +// ValidateAuthConfig validates the basic authentication configuration. func (a *CommandAuthConfigBasic) ValidateAuthConfig() error { silentLoad := true if a.CommandAuthConfig.ConfigProfile != "" { @@ -182,7 +187,7 @@ func (a *CommandAuthConfigBasic) ValidateAuthConfig() error { return a.CommandAuthConfig.ValidateAuthConfig() } -// Authenticate authenticates the user +// Authenticate authenticates the request using basic authentication. func (a *CommandAuthConfigBasic) Authenticate() error { cErr := a.ValidateAuthConfig() if cErr != nil { @@ -253,3 +258,31 @@ func (a *CommandAuthConfigBasic) GetServerConfig() *Server { } 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_core.go b/auth_providers/auth_core.go index f2cd6ef..f8f9420 100644 --- a/auth_providers/auth_core.go +++ b/auth_providers/auth_core.go @@ -30,23 +30,55 @@ import ( ) const ( - DefaultCommandPort = 443 + // DefaultCommandPort is the default port for Keyfactor Command API + DefaultCommandPort = 443 + + // DefaultCommandAPIPath is the default path for Keyfactor Command API DefaultCommandAPIPath = "KeyfactorAPI" - DefaultAPIVersion = "1" - DefaultAPIClientName = "APIClient" + + // 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 = "default" - DefaultClientTimeout = 60 - - EnvKeyfactorHostName = "KEYFACTOR_HOSTNAME" - EnvKeyfactorPort = "KEYFACTOR_PORT" - EnvKeyfactorAPIPath = "KEYFACTOR_API_PATH" - EnvKeyfactorSkipVerify = "KEYFACTOR_SKIP_VERIFY" - EnvKeyfactorCACert = "KEYFACTOR_CA_CERT" - EnvKeyfactorAuthProvider = "KEYFACTOR_AUTH_PROVIDER" - EnvKeyfactorAuthProfile = "KEYFACTOR_AUTH_CONFIG_PROFILE" - EnvKeyfactorConfigFile = "KEYFACTOR_AUTH_CONFIG_FILE" + + // 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" ) @@ -111,8 +143,9 @@ type CommandAuthConfig struct { HttpClient *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 + // 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 @@ -620,6 +653,7 @@ func expandPath(path string) (string, error) { return path, nil } +// GetServerConfig returns the server configuration. func (c *CommandAuthConfig) GetServerConfig() *Server { server := Server{ Host: c.CommandHostName, @@ -638,3 +672,27 @@ func (c *CommandAuthConfig) GetServerConfig() *Server { } 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_oauth.go b/auth_providers/auth_oauth.go index 639f1bc..c113614 100644 --- a/auth_providers/auth_oauth.go +++ b/auth_providers/auth_oauth.go @@ -50,11 +50,14 @@ type OAuthAuthenticator struct { Client *http.Client } +// GetHttpClient returns the http client func (a *OAuthAuthenticator) GetHttpClient() (*http.Client, error) { return a.Client, nil } +// CommandConfigOauth represents the configuration needed for authentication to Keyfactor Command API using OAuth2. type CommandConfigOauth struct { + // CommandAuthConfig is a reference to the base configuration needed for authentication to Keyfactor Command API CommandAuthConfig // ClientID is the Client ID for Keycloak authentication @@ -76,16 +79,16 @@ type CommandConfigOauth struct { CACertificates []*x509.Certificate `json:"-"` // AccessToken is the access token for Keycloak authentication - AccessToken string `json:"access_token;omitempty"` + AccessToken string `json:"access_token,omitempty"` // RefreshToken is the refresh token for Keycloak authentication - RefreshToken string `json:"refresh_token;omitempty"` + RefreshToken string `json:"refresh_token,omitempty"` // Expiry is the expiry time of the access token - Expiry time.Time `json:"expiry;omitempty"` + Expiry time.Time `json:"expiry,omitempty"` // TokenURL is the token URL for Keycloak authentication - TokenURL string `json:"token_url"` + TokenURL string `json:"token_url,omitempty"` //// AuthPort //AuthPort string `json:"auth_port,omitempty"` @@ -178,7 +181,7 @@ func (b *CommandConfigOauth) GetHttpClient() (*http.Client, error) { Scopes: b.Scopes, } - if b.Scopes == nil || len(b.Scopes) == 0 { + if len(b.Scopes) == 0 { b.Scopes = []string{"openid", "profile", "email"} } @@ -205,6 +208,7 @@ func (b *CommandConfigOauth) GetHttpClient() (*http.Client, error) { } +// Build creates an OAuth authenticator. func (b *CommandConfigOauth) Build() (Authenticator, error) { client, cErr := b.GetHttpClient() @@ -215,6 +219,7 @@ func (b *CommandConfigOauth) Build() (Authenticator, error) { 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 { @@ -264,6 +269,7 @@ func (b *CommandConfigOauth) LoadConfig(profile, path string, silentLoad bool) ( return serverConfig, nil } +// ValidateAuthConfig validates the configuration for Keyfactor Command API using OAuth2. func (b *CommandConfigOauth) ValidateAuthConfig() error { silentLoad := true @@ -336,6 +342,7 @@ func (b *CommandConfigOauth) ValidateAuthConfig() error { return b.CommandAuthConfig.ValidateAuthConfig() } +// Authenticate authenticates to Keyfactor Command API using OAuth2. func (b *CommandConfigOauth) Authenticate() error { // validate auth config @@ -372,6 +379,7 @@ func (b *CommandConfigOauth) Authenticate() error { return nil } +// GetServerConfig returns the server configuration for Keyfactor Command API using OAuth2. func (b *CommandConfigOauth) GetServerConfig() *Server { server := Server{ Host: b.CommandHostName, @@ -387,3 +395,36 @@ func (b *CommandConfigOauth) GetServerConfig() *Server { } return &server } + +// Example usage of CommandConfigOauth +// +// This example demonstrates how to use CommandConfigOauth to authenticate to the Keyfactor Command API using OAuth2. +// +// func ExampleCommandConfigOauth_Authenticate() { +// authConfig := &CommandConfigOauth{ +// CommandAuthConfig: 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, +// }, +// ClientID: "exampleClientID", +// ClientSecret: "exampleClientSecret", +// TokenURL: "https://example.com/oauth/token", +// Scopes: []string{"openid", "profile", "email"}, +// Audience: "exampleAudience", +// CACertificatePath: "/path/to/ca-cert.pem", +// AccessToken: "exampleAccessToken", +// } +// +// err := authConfig.Authenticate() +// if err != nil { +// fmt.Println("Authentication failed:", err) +// } else { +// fmt.Println("Authentication successful") +// } +// } diff --git a/auth_providers/command_config.go b/auth_providers/command_config.go index 3d5d382..45532c4 100644 --- a/auth_providers/command_config.go +++ b/auth_providers/command_config.go @@ -53,6 +53,7 @@ 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), @@ -215,6 +216,7 @@ func MergeConfigFromFile(filePath string, config *Config) error { return nil } +// GetAuthType returns the type of authentication to use based on the configuration params. func (s *Server) GetAuthType() string { if s.ClientID != "" && s.ClientSecret != "" { s.AuthType = "oauth" @@ -286,3 +288,55 @@ func (s *Server) GetOAuthClientConfig() (*CommandConfigOauth, error) { } 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/integration-manifest.json b/integration-manifest.json new file mode 100644 index 0000000..a01d975 --- /dev/null +++ b/integration-manifest.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://keyfactor.github.io/integration-manifest-schema.json", + "integration_type": "api-client", + "name": "Keyfactor Auth Client - Golang", + "status": "production", + "description": "The Keyfactor Auth Client - Golang is a Go module that handles authentication and authorization for the Keyfactor API.", + "support_level": "kf-community", + "link_github": false, + "update_catalog": false +} + diff --git a/tag.sh b/tag.sh index c350f84..d42fdd5 100755 --- a/tag.sh +++ b/tag.sh @@ -1,5 +1,5 @@ -RC_VERSION=rc.18 -TAG_VERSION_1=v0.0.1-$RC_VERSION +RC_VERSION=rc.1 +TAG_VERSION_1=v1.0.0-$RC_VERSION git tag -d $TAG_VERSION_1 || true git tag $TAG_VERSION_1 git push origin $TAG_VERSION_1 \ No newline at end of file From ef80dc1ad502292448d841bbe5589afb6620c4d2 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Mon, 28 Oct 2024 11:01:16 -0700 Subject: [PATCH 46/57] fix(config): Fix invalid yaml param config --- auth_providers/command_config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth_providers/command_config.go b/auth_providers/command_config.go index 45532c4..fee0820 100644 --- a/auth_providers/command_config.go +++ b/auth_providers/command_config.go @@ -37,7 +37,7 @@ type Server struct { 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. + AuthType string `json:"auth_type,omitempty" yaml:"auth_type,omitempty"` // AuthType is the type of authentication to use. } From a5e80f431b28a426e67671f2fe3c31d177dc3788 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Mon, 28 Oct 2024 11:17:33 -0700 Subject: [PATCH 47/57] fix(tests): `TestReadFullAuthConfigExample` --- auth_providers/command_config_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/auth_providers/command_config_test.go b/auth_providers/command_config_test.go index 0a43068..2aeae9f 100644 --- a/auth_providers/command_config_test.go +++ b/auth_providers/command_config_test.go @@ -201,7 +201,7 @@ func TestMergeConfigFromFile(t *testing.T) { func TestReadFullAuthConfigExample(t *testing.T) { filePath := "../lib/config/full_auth_config_example.json" - expectedConfig := &auth_providers.Config{ + expectedConfig := auth_providers.Config{ Servers: map[string]auth_providers.Server{ "default": { Host: "keyfactor.command.kfdelivery.com", @@ -253,7 +253,7 @@ func TestReadFullAuthConfigExample(t *testing.T) { t.Fatalf("failed to unmarshal JSON: %v", err) } - if !compareConfigs(&config, expectedConfig) { + if !compareConfigs(&config, &expectedConfig) { t.Fatalf("expected %v, got %v", expectedConfig, config) } } From 31fa0340609597f2dca42bf67ea8d786557c2823 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Mon, 28 Oct 2024 12:48:15 -0700 Subject: [PATCH 48/57] chore(docs): Add docs around config file. --- README.md | 98 ++++++++++++++++++++++- lib/config/auth_config_schema.json | 99 +++++++++++++++++------- lib/config/full_auth_config_example.json | 4 +- lib/config/oauth_config_example.json | 4 +- 4 files changed, 171 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 4265d76..c92a028 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,12 @@ Client library for authenticating to Keyfactor Command * [Global](#global) * [Basic Auth](#basic-auth) * [oAuth Client Credentials](#oauth-client-credentials) +- [Configuration File](#configuration-file) + * [Basic Auth](#basic-auth) + * [oAuth Client Credentials](#oauth-client-credentials) + * [Auth Providers](#auth-providers) + + [Azure Id](#azure-id) + + [Azure KeyVault](#azure-keyvault) - [Test Environment Variables](#test-environment-variables) @@ -48,11 +54,99 @@ Currently, only Active Directory `Basic` authentication is supported. | 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 +### 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 | \ No newline at end of file +| TEST_KEYFACTOR_KC_AUTH | Set to `true` to test Keycloak authentication | false | + +## Configuration File +A JSON or YAML file can be used to store authentication configuration. A configuration file can contain references to +multiple Keyfactor Command environments and can be referenced by a `profile` name. The `default` profile will be used +when no profile is specified. Keyfactor tools will look for a config file located at `$HOME/.keyfactor/command_config.json` +by default. The config file should be structured as follows: + +### Basic Auth + +#### JSON +```json +{ + "servers": { + "default": { + "host": "keyfactor.command.kfdelivery.com", + "username": "keyfactor", + "password": "password", + "domain": "command", + "api_path": "KeyfactorAPI" + }, + "server2": { + "host": "keyfactor2.command.kfdelivery.com", + "username": "keyfactor2", + "password": "password2", + "domain": "command", + "api_path": "Keyfactor/API" + } + } +} +``` + +#### YAML +```yaml +servers: + default: + host: keyfactor.command.kfdelivery.com + username: keyfactor + password: password + domain: command + api_path: KeyfactorAPI + server2: + host: keyfactor2.command.kfdelivery.com + username: keyfactor2 + password: password2 + domain: command + api_path: Keyfactor/API +``` + +### oAuth Client Credentials + +#### JSON +```json +{ + "servers": { + "default": { + "host": "keyfactor.command.kfdelivery.com", + "token_url": "https://idp.keyfactor.command.kfdelivery.com/oauth2/token", + "client_id": "client-id", + "client_secret": "client-secret", + "api_path": "KeyfactorAPI" + }, + "server2": { + "host": "keyfactor.command.kfdelivery.com", + "token_url": "https://idp.keyfactor.command.kfdelivery.com/oauth2/token", + "client_id": "client-id", + "client_secret": "client-secret", + "api_path": "KeyfactorAPI" + } + } +} +``` + +#### YAML +```yaml +servers: + default: + host: keyfactor.command.kfdelivery.com + token_url: https://idp.keyfactor.command.kfdelivery.com/oauth2/token + client_id: client-id + client_secret: client-secret + api_path: KeyfactorAPI + server2: + host: keyfactor.command.kfdelivery.com + token_url: https://idp.keyfactor.command.kfdelivery.com/oauth2/token + client_id: client-id + client_secret: client-secret + api_path: KeyfactorAPI +``` \ No newline at end of file diff --git a/lib/config/auth_config_schema.json b/lib/config/auth_config_schema.json index 7b9bfc9..2af7d48 100644 --- a/lib/config/auth_config_schema.json +++ b/lib/config/auth_config_schema.json @@ -1,6 +1,8 @@ { "$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", @@ -9,55 +11,71 @@ "type": "object", "properties": { "host": { - "type": "string" - }, - "auth_hostname": { - "type": "string" + "type": "string", + "description": "The hostname of the Keyfactor Command API server" }, "auth_port": { - "type": "integer" + "type": "integer", + "description": "The port of the Keyfactor Command API server" }, "username": { - "type": "string" + "type": "string", + "description": "The username to authenticate with using basic auth" }, "password": { - "type": "string" + "type": "string", + "description": "The password to authenticate with using basic auth" }, "client_id": { - "type": "string" + "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" + "type": "string", + "description": "The client secret to authenticate with using OAuth2" }, "domain": { - "type": "string" + "type": "string", + "description": "The Active Directory domain to authenticate with using basic auth" }, "api_path": { - "type": "string" + "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" + "type": "string", + "enum": [ + "azid", + "akv" + ] }, "profile": { - "type": "string" + "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" + "type": "string", + "description": "The name of the secret to use in the Azure KeyVault auth provider configuration" }, "vault_name": { - "type": "string" + "type": "string", + "description": "The name of the vault to use in the Azure KeyVault auth provider configuration" } }, - "required": [ - "secret_name", - "vault_name" - ] + "required": [] } }, "required": [ @@ -69,33 +87,58 @@ }, "oneOf": [ { - "required": ["username", "password"], + "required": [ + "username", + "password" + ], "not": { - "required": ["client_id", "client_secret"] + "required": [ + "client_id", + "client_secret" + ] } }, { - "required": ["client_id", "client_secret"], + "required": [ + "client_id", + "client_secret", + "token_url" + ], "not": { - "required": ["username", "password"] + "required": [ + "username", + "password" + ] } } ], "if": { - "required": ["auth_provider"] + "required": [ + "auth_provider" + ] }, "then": { - "required": ["auth_provider"] + "required": [ + "auth_provider" + ] }, "else": { "if": { - "required": ["client_id", "client_secret"] + "required": [ + "client_id", + "client_secret" + ] }, "then": { - "required": ["auth_hostname", "host"] + "required": [ + "token_url", + "host" + ] }, "else": { - "required": ["host"] + "required": [ + "host" + ] } } } diff --git a/lib/config/full_auth_config_example.json b/lib/config/full_auth_config_example.json index 4bfc7eb..c2bb73c 100644 --- a/lib/config/full_auth_config_example.json +++ b/lib/config/full_auth_config_example.json @@ -2,7 +2,7 @@ "servers": { "default": { "host": "keyfactor.command.kfdelivery.com", - "oauth_token_url": "idp.keyfactor.command.kfdelivery.com", + "token_url": "idp.keyfactor.command.kfdelivery.com", "username": "keyfactor", "password": "password", "client_id": "client-id", @@ -20,7 +20,7 @@ }, "server2": { "host": "keyfactor2.command.kfdelivery.com", - "oauth_token_url": "idp.keyfactor2.command.kfdelivery.com", + "token_url": "idp.keyfactor2.command.kfdelivery.com", "username": "keyfactor2", "password": "password2", "client_id": "client-id2", diff --git a/lib/config/oauth_config_example.json b/lib/config/oauth_config_example.json index 40f097f..9c6ae86 100644 --- a/lib/config/oauth_config_example.json +++ b/lib/config/oauth_config_example.json @@ -2,14 +2,14 @@ "servers": { "default": { "host": "keyfactor.command.kfdelivery.com", - "oauth_token_url": "https://idp.keyfactor.command.kfdelivery.com/oauth2/token", + "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", - "oauth_token_url": "https://idp.keyfactor.command.kfdelivery.com/oauth2/token", + "token_url": "https://idp.keyfactor.command.kfdelivery.com/oauth2/token", "client_id": "client-id", "client_secret": "client-secret", "api_path": "KeyfactorAPI" From 8b7ccc0f527fc9f10b4081d7f0e550f43d3c8ae3 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 29 Oct 2024 10:09:25 -0700 Subject: [PATCH 49/57] fix(ci): Use hosted AKS runners --- .github/workflows/go_tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/go_tests.yml b/.github/workflows/go_tests.yml index 337caf9..642623c 100644 --- a/.github/workflows/go_tests.yml +++ b/.github/workflows/go_tests.yml @@ -7,7 +7,7 @@ on: jobs: test: name: Run tests - runs-on: ubuntu-latest + runs-on: kf-auth-client-runner-set strategy: matrix: environment: [ "KFC_10_5_0", "KFC_12_3_0_KC"] From 73a9dc6aa2bf1a400840dd0659cd064e46a3de15 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 29 Oct 2024 10:35:26 -0700 Subject: [PATCH 50/57] fix(ci): Get public IP of runner --- .github/workflows/go_tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/go_tests.yml b/.github/workflows/go_tests.yml index 642623c..a9e2137 100644 --- a/.github/workflows/go_tests.yml +++ b/.github/workflows/go_tests.yml @@ -21,6 +21,9 @@ jobs: with: go-version: 1.22 + - name: Get Public IP + run: curl -s https://api.ipify.org + - name: Run tests run: | if [ -n "${{ secrets.KEYFACTOR_AUTH_CONFIG_B64 }}" ]; then From 54983b9b8c20367b6376ae8e015f0633f0f0ba04 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 30 Oct 2024 17:22:36 -0700 Subject: [PATCH 51/57] fix(core): Support for CaCertFilePath --- auth_providers/auth_core.go | 176 ++++++++++++++++++------------ auth_providers/auth_oauth.go | 119 ++++++++++++++------ auth_providers/auth_oauth_test.go | 83 +++++++++++++- 3 files changed, 267 insertions(+), 111 deletions(-) diff --git a/auth_providers/auth_core.go b/auth_providers/auth_core.go index f8f9420..e79a8c2 100644 --- a/auth_providers/auth_core.go +++ b/auth_providers/auth_core.go @@ -270,70 +270,70 @@ func (c *CommandAuthConfig) ValidateAuthConfig() error { c.HttpClientTimeout = DefaultClientTimeout } } - c.SetClient(nil) + + if c.CommandCACert == "" { + // check if CommandCACert is set in environment + if caCert, ok := os.LookupEnv(EnvKeyfactorCACert); ok { + c.CommandCACert = caCert + } else { + return nil + } + } // check for skip verify in environment if skipVerify, ok := os.LookupEnv(EnvKeyfactorSkipVerify); ok { c.SkipVerify = skipVerify == "true" || skipVerify == "1" } - if c.SkipVerify { - c.HttpClient.Transport = &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } - return nil - } - - caErr := c.updateCACerts() - if caErr != nil { - return caErr - } + //TODO: This should be part of BuildTransport + //if c.SkipVerify { + // c.HttpClient.Transport = &http.Transport{ + // TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + // } + // //return nil + //} + // + //caErr := c.updateCACerts() + //if caErr != nil { + // return caErr + //} return nil } // BuildTransport creates a custom http Transport for authentication to Keyfactor Command API. func (c *CommandAuthConfig) BuildTransport() (*http.Transport, error) { - output := &http.Transport{ - Proxy: http.ProxyFromEnvironment, - TLSClientConfig: &tls.Config{ - Renegotiation: tls.RenegotiateOnceAsClient, - }, - TLSHandshakeTimeout: 10 * time.Second, - } - if c.SkipVerify { - output.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + var output *http.Transport + if c.HttpClient == nil { + c.SetClient(nil) } - - // Load the system certs - if c.CommandCACert != "" { - rootCAs, pErr := x509.SystemCertPool() - if pErr != nil { - return nil, 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 nil, ioErr - } - // Append your custom cert to the pool - if ok := rootCAs.AppendCertsFromPEM(cert); !ok { - return nil, fmt.Errorf("failed to append custom CA cert to pool") - } + // check if c already has a transport and if it does, assign it to output else create a new transport + if c.HttpClient.Transport != nil { + if transport, ok := c.HttpClient.Transport.(*http.Transport); ok { + output = transport } else { - // Append your custom cert to the pool - if ok := rootCAs.AppendCertsFromPEM([]byte(c.CommandCACert)); !ok { - return nil, fmt.Errorf("failed to append custom CA cert to pool") + output = &http.Transport{ + TLSClientConfig: &tls.Config{}, } } + } else { + output = &http.Transport{ + Proxy: http.ProxyFromEnvironment, + TLSClientConfig: &tls.Config{ + Renegotiation: tls.RenegotiateOnceAsClient, + }, + TLSHandshakeTimeout: 10 * time.Second, + } + } + + if c.SkipVerify { + output.TLSClientConfig.InsecureSkipVerify = true + } - output.TLSClientConfig.RootCAs = rootCAs + if c.CommandCACert != "" { + _ = c.updateCACerts() } + return output, nil } @@ -343,7 +343,7 @@ func (c *CommandAuthConfig) SetClient(client *http.Client) *http.Client { c.HttpClient = client } if c.HttpClient == nil { - c.HttpClient = &http.Client{} + c.HttpClient = http.DefaultClient } return c.HttpClient } @@ -389,20 +389,37 @@ func (c *CommandAuthConfig) updateCACerts() error { } } - // Trust the augmented cert pool in our Client - c.HttpClient.Transport = &http.Transport{ - TLSClientConfig: &tls.Config{ - RootCAs: rootCAs, - }, + //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 { - // call /Status/Endpoints API to validate credentials - c.SetClient(nil) //create headers for request headers := map[string]string{ @@ -428,6 +445,7 @@ func (c *CommandAuthConfig) Authenticate() error { if rErr != nil { return rErr } + // Set headers from the map for key, value := range headers { req.Header.Set(key, value) @@ -615,29 +633,45 @@ func (c *CommandAuthConfig) LoadConfig(profile string, configFilePath string, si c.FileConfig = &server - if !silentLoad { + 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 - } 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 - } } + + //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 } diff --git a/auth_providers/auth_oauth.go b/auth_providers/auth_oauth.go index c113614..308efe0 100644 --- a/auth_providers/auth_oauth.go +++ b/auth_providers/auth_oauth.go @@ -2,6 +2,7 @@ package auth_providers import ( "context" + "crypto/tls" "crypto/x509" "fmt" "net/http" @@ -153,25 +154,42 @@ func (b *CommandConfigOauth) WithAccessToken(accessToken string) *CommandConfigO 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() + var client http.Client + if b.CommandAuthConfig.HttpClient != nil { + client = *b.CommandAuthConfig.HttpClient + } if cErr != nil { return nil, cErr } + if client.Transport == nil { + transport, tErr := b.BuildTransport() + if tErr != nil { + return nil, tErr + } + client.Transport = transport + } + if b.AccessToken != "" { - return &http.Client{ - Transport: &oauth2.Transport{ - Base: http.DefaultTransport, - Source: oauth2.StaticTokenSource( - &oauth2.Token{ - AccessToken: b.AccessToken, - TokenType: DefaultTokenPrefix, - }, - ), - }, - }, nil + baseTransport := cloneHTTPTransport(client.Transport.(*http.Transport)) + client.Transport = &oauth2.Transport{ + Base: baseTransport, + Source: oauth2.StaticTokenSource( + &oauth2.Token{ + AccessToken: b.AccessToken, + TokenType: DefaultTokenPrefix, + }, + ), + } + return &client, nil } config := &clientcredentials.Config{ @@ -191,21 +209,17 @@ func (b *CommandConfigOauth) GetHttpClient() (*http.Client, error) { } } - transport, tErr := b.BuildTransport() - if tErr != nil { - return nil, tErr - } + ctx := context.WithValue(context.Background(), oauth2.HTTPClient, client) - tokenSource := config.TokenSource(context.Background()) - oauthTransport := &oauth2.Transport{ - Base: transport, + tokenSource := config.TokenSource(ctx) + baseTransport := cloneHTTPTransport(client.Transport.(*http.Transport)) + oauthTransport := oauth2.Transport{ + Base: baseTransport, Source: tokenSource, } + client.Transport = &oauthTransport - return &http.Client{ - Transport: oauthTransport, - }, nil - + return &client, nil } // Build creates an OAuth authenticator. @@ -352,24 +366,15 @@ func (b *CommandConfigOauth) Authenticate() error { } // create oauth Client - oauthy, err := NewOAuthAuthenticatorBuilder(). - WithClientId(b.ClientID). - WithClientSecret(b.ClientSecret). - WithTokenUrl(b.TokenURL). - WithAccessToken(b.AccessToken). - Build() + oauthy, err := b.GetHttpClient() if err != nil { return err + } else if oauthy == nil { + return fmt.Errorf("unable to create http client") } - if oauthy != nil { - oClient, oerr := oauthy.GetHttpClient() - if oerr != nil { - return oerr - } - b.SetClient(oClient) - } + b.SetClient(oauthy) aErr := b.CommandAuthConfig.Authenticate() if aErr != nil { @@ -428,3 +433,47 @@ func (b *CommandConfigOauth) GetServerConfig() *Server { // fmt.Println("Authentication successful") // } // } + +func cloneHTTPTransport(original *http.Transport) *http.Transport { + if original == nil { + return nil + } + + return &http.Transport{ + Proxy: original.Proxy, + DialContext: original.DialContext, + ForceAttemptHTTP2: original.ForceAttemptHTTP2, + MaxIdleConns: original.MaxIdleConns, + IdleConnTimeout: original.IdleConnTimeout, + TLSHandshakeTimeout: original.TLSHandshakeTimeout, + ExpectContinueTimeout: original.ExpectContinueTimeout, + ResponseHeaderTimeout: original.ResponseHeaderTimeout, + TLSClientConfig: cloneTLSConfig(original.TLSClientConfig), + DialTLSContext: original.DialTLSContext, + DisableKeepAlives: original.DisableKeepAlives, + DisableCompression: original.DisableCompression, + MaxIdleConnsPerHost: original.MaxIdleConnsPerHost, + MaxConnsPerHost: original.MaxConnsPerHost, + WriteBufferSize: original.WriteBufferSize, + ReadBufferSize: original.ReadBufferSize, + } +} + +func cloneTLSConfig(original *tls.Config) *tls.Config { + if original == nil { + return nil + } + + return &tls.Config{ + InsecureSkipVerify: original.InsecureSkipVerify, + MinVersion: original.MinVersion, + MaxVersion: original.MaxVersion, + CipherSuites: original.CipherSuites, + PreferServerCipherSuites: original.PreferServerCipherSuites, + NextProtos: original.NextProtos, + ServerName: original.ServerName, + ClientAuth: original.ClientAuth, + RootCAs: original.RootCAs, + // Deep copy the rest of the TLS fields as needed + } +} diff --git a/auth_providers/auth_oauth_test.go b/auth_providers/auth_oauth_test.go index 0ee1667..5e5fb87 100644 --- a/auth_providers/auth_oauth_test.go +++ b/auth_providers/auth_oauth_test.go @@ -107,7 +107,7 @@ func TestCommandConfigOauth_Authenticate(t *testing.T) { t.FailNow() } - // Delete the config file + //Delete the config file t.Logf("Deleting config file: %s", configFilePath) os.Remove(configFilePath) defer func() { @@ -118,9 +118,15 @@ func TestCommandConfigOauth_Authenticate(t *testing.T) { 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") t.Log("Testing oAuth with Environmental variables") noParamsConfig := &auth_providers.CommandConfigOauth{} + noParamsConfig. + WithSkipVerify(true). + WithCommandCACert("lib/certs/int-oidc-lab.eastus2.cloudapp.azure.com.pem") authOauthTest(t, "with complete Environmental variables", false, noParamsConfig) t.Log("Testing oAuth with invalid config file path") @@ -131,6 +137,7 @@ func TestCommandConfigOauth_Authenticate(t *testing.T) { // Environment variables are not set t.Log("Unsetting environment variables") + //keyfactorEnvVars := exportEnvVarsWithPrefix("KEYFACTOR_") clientID, clientSecret, tokenURL := exportOAuthEnvVariables() unsetOAuthEnvVariables() defer func() { @@ -168,6 +175,7 @@ func TestCommandConfigOauth_Authenticate(t *testing.T) { 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") @@ -199,11 +207,51 @@ func TestCommandConfigOauth_Authenticate(t *testing.T) { t.Errorf("unable to write auth config to file %s: %v", configFilePath, fErr) } - t.Log("Testing oAuth with valid implicit config file") - wConfigFile := &auth_providers.CommandConfigOauth{} - wConfigFile.WithConfigProfile("oauth") - authOauthTest(t, "with valid implicit config file", false, wConfigFile) + //unsetOAuthEnvVariables() + t.Log("Testing oAuth with valid implicit config file profile param, caCert, and skiptls") + wCaCertConfigFile := &auth_providers.CommandConfigOauth{} + wCaCertConfigFile. + WithConfigProfile("oauth"). + WithCommandCACert("lib/certs/int-oidc-lab.eastus2.cloudapp.azure.com.pem"). + 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") + 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) + 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", "certificate is not trusted"} + authOauthTest( + t, "oAuth with valid implicit config file https fail", true, httpsFailConfigFile, + httpsFailConfigFileExpected..., + ) + + os.Setenv(auth_providers.EnvKeyfactorSkipVerify, "true") t.Log("Testing oAuth with invalid profile implicit config file") invProfile := &auth_providers.CommandConfigOauth{} invProfile.WithConfigProfile("invalid-profile") @@ -287,6 +335,21 @@ func setOAuthEnvVariables(client_id, client_secret, token_url string) { 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) @@ -300,4 +363,14 @@ 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) + } From 1fb370e1fae37f56057379ad98d79b91c6bd00f7 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 31 Oct 2024 11:46:35 -0700 Subject: [PATCH 52/57] fix(oauth): OAuth clients not to inherit DefaultClient config --- auth_providers/auth_core.go | 98 +++++++++++--------- auth_providers/auth_oauth.go | 117 ++++++------------------ auth_providers/auth_oauth_test.go | 72 +++++++++++++-- main.go | 143 +++++++++++++++++++++++++++++- 4 files changed, 287 insertions(+), 143 deletions(-) diff --git a/auth_providers/auth_core.go b/auth_providers/auth_core.go index e79a8c2..390646b 100644 --- a/auth_providers/auth_core.go +++ b/auth_providers/auth_core.go @@ -141,6 +141,7 @@ type CommandAuthConfig struct { // 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. @@ -275,8 +276,6 @@ func (c *CommandAuthConfig) ValidateAuthConfig() error { // check if CommandCACert is set in environment if caCert, ok := os.LookupEnv(EnvKeyfactorCACert); ok { c.CommandCACert = caCert - } else { - return nil } } @@ -284,46 +283,17 @@ func (c *CommandAuthConfig) ValidateAuthConfig() error { if skipVerify, ok := os.LookupEnv(EnvKeyfactorSkipVerify); ok { c.SkipVerify = skipVerify == "true" || skipVerify == "1" } - - //TODO: This should be part of BuildTransport - //if c.SkipVerify { - // c.HttpClient.Transport = &http.Transport{ - // TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - // } - // //return nil - //} - // - //caErr := c.updateCACerts() - //if caErr != nil { - // return caErr - //} - return nil } // BuildTransport creates a custom http Transport for authentication to Keyfactor Command API. func (c *CommandAuthConfig) BuildTransport() (*http.Transport, error) { - var output *http.Transport - if c.HttpClient == nil { - c.SetClient(nil) - } - // check if c already has a transport and if it does, assign it to output else create a new transport - if c.HttpClient.Transport != nil { - if transport, ok := c.HttpClient.Transport.(*http.Transport); ok { - output = transport - } else { - output = &http.Transport{ - TLSClientConfig: &tls.Config{}, - } - } - } else { - output = &http.Transport{ - Proxy: http.ProxyFromEnvironment, - TLSClientConfig: &tls.Config{ - Renegotiation: tls.RenegotiateOnceAsClient, - }, - TLSHandshakeTimeout: 10 * time.Second, - } + output := http.Transport{ + Proxy: http.ProxyFromEnvironment, + TLSClientConfig: &tls.Config{ + Renegotiation: tls.RenegotiateOnceAsClient, + }, + TLSHandshakeTimeout: 10 * time.Second, } if c.SkipVerify { @@ -331,10 +301,28 @@ func (c *CommandAuthConfig) BuildTransport() (*http.Transport, error) { } if c.CommandCACert != "" { - _ = c.updateCACerts() + 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 + return &output, nil } // SetClient sets the http Client for authentication to Keyfactor Command API. @@ -343,8 +331,34 @@ func (c *CommandAuthConfig) SetClient(client *http.Client) *http.Client { c.HttpClient = client } if c.HttpClient == nil { - c.HttpClient = http.DefaultClient + //// 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 } @@ -356,6 +370,7 @@ func (c *CommandAuthConfig) updateCACerts() error { if caCert, ok := os.LookupEnv(EnvKeyfactorCACert); ok { c.CommandCACert = caCert } else { + // nothing to do return nil } } @@ -452,7 +467,6 @@ func (c *CommandAuthConfig) Authenticate() error { } c.HttpClient.Timeout = time.Duration(c.HttpClientTimeout) * time.Second - cResp, cErr := c.HttpClient.Do(req) if cErr != nil { return cErr @@ -645,7 +659,7 @@ func (c *CommandAuthConfig) LoadConfig(profile string, configFilePath string, si if c.CommandCACert == "" { c.CommandCACert = server.CACertPath } - if c.SkipVerify { + if !c.SkipVerify { c.SkipVerify = server.SkipTLSVerify } diff --git a/auth_providers/auth_oauth.go b/auth_providers/auth_oauth.go index 308efe0..8a8c940 100644 --- a/auth_providers/auth_oauth.go +++ b/auth_providers/auth_oauth.go @@ -2,7 +2,6 @@ package auth_providers import ( "context" - "crypto/tls" "crypto/x509" "fmt" "net/http" @@ -51,6 +50,11 @@ 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 @@ -162,24 +166,17 @@ func (b *CommandConfigOauth) WithHttpClient(httpClient *http.Client) *CommandCon // GetHttpClient returns an HTTP client for oAuth authentication. func (b *CommandConfigOauth) GetHttpClient() (*http.Client, error) { cErr := b.ValidateAuthConfig() - var client http.Client - if b.CommandAuthConfig.HttpClient != nil { - client = *b.CommandAuthConfig.HttpClient - } if cErr != nil { return nil, cErr } - if client.Transport == nil { - transport, tErr := b.BuildTransport() - if tErr != nil { - return nil, tErr - } - client.Transport = transport + var client http.Client + baseTransport, tErr := b.BuildTransport() + if tErr != nil { + return nil, tErr } if b.AccessToken != "" { - baseTransport := cloneHTTPTransport(client.Transport.(*http.Transport)) client.Transport = &oauth2.Transport{ Base: baseTransport, Source: oauth2.StaticTokenSource( @@ -209,15 +206,15 @@ func (b *CommandConfigOauth) GetHttpClient() (*http.Client, error) { } } - ctx := context.WithValue(context.Background(), oauth2.HTTPClient, client) - + ctx := context.WithValue(context.Background(), oauth2.HTTPClient, &http.Client{Transport: baseTransport}) tokenSource := config.TokenSource(ctx) - baseTransport := cloneHTTPTransport(client.Transport.(*http.Transport)) - oauthTransport := oauth2.Transport{ - Base: baseTransport, - Source: tokenSource, + + client = http.Client{ + Transport: &oauth2Transport{ + base: baseTransport, + src: tokenSource, + }, } - client.Transport = &oauthTransport return &client, nil } @@ -375,6 +372,7 @@ func (b *CommandConfigOauth) Authenticate() error { } b.SetClient(oauthy) + //b.DefaultHttpClient = oauthy aErr := b.CommandAuthConfig.Authenticate() if aErr != nil { @@ -401,79 +399,16 @@ func (b *CommandConfigOauth) GetServerConfig() *Server { return &server } -// Example usage of CommandConfigOauth -// -// This example demonstrates how to use CommandConfigOauth to authenticate to the Keyfactor Command API using OAuth2. -// -// func ExampleCommandConfigOauth_Authenticate() { -// authConfig := &CommandConfigOauth{ -// CommandAuthConfig: 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, -// }, -// ClientID: "exampleClientID", -// ClientSecret: "exampleClientSecret", -// TokenURL: "https://example.com/oauth/token", -// Scopes: []string{"openid", "profile", "email"}, -// Audience: "exampleAudience", -// CACertificatePath: "/path/to/ca-cert.pem", -// AccessToken: "exampleAccessToken", -// } -// -// err := authConfig.Authenticate() -// if err != nil { -// fmt.Println("Authentication failed:", err) -// } else { -// fmt.Println("Authentication successful") -// } -// } - -func cloneHTTPTransport(original *http.Transport) *http.Transport { - if original == nil { - return nil +// 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) } - return &http.Transport{ - Proxy: original.Proxy, - DialContext: original.DialContext, - ForceAttemptHTTP2: original.ForceAttemptHTTP2, - MaxIdleConns: original.MaxIdleConns, - IdleConnTimeout: original.IdleConnTimeout, - TLSHandshakeTimeout: original.TLSHandshakeTimeout, - ExpectContinueTimeout: original.ExpectContinueTimeout, - ResponseHeaderTimeout: original.ResponseHeaderTimeout, - TLSClientConfig: cloneTLSConfig(original.TLSClientConfig), - DialTLSContext: original.DialTLSContext, - DisableKeepAlives: original.DisableKeepAlives, - DisableCompression: original.DisableCompression, - MaxIdleConnsPerHost: original.MaxIdleConnsPerHost, - MaxConnsPerHost: original.MaxConnsPerHost, - WriteBufferSize: original.WriteBufferSize, - ReadBufferSize: original.ReadBufferSize, - } -} + // Clone the request to avoid mutating the original + reqCopy := req.Clone(req.Context()) + token.SetAuthHeader(reqCopy) -func cloneTLSConfig(original *tls.Config) *tls.Config { - if original == nil { - return nil - } - - return &tls.Config{ - InsecureSkipVerify: original.InsecureSkipVerify, - MinVersion: original.MinVersion, - MaxVersion: original.MaxVersion, - CipherSuites: original.CipherSuites, - PreferServerCipherSuites: original.PreferServerCipherSuites, - NextProtos: original.NextProtos, - ServerName: original.ServerName, - ClientAuth: original.ClientAuth, - RootCAs: original.RootCAs, - // Deep copy the rest of the TLS fields as needed - } + return t.base.RoundTrip(reqCopy) } diff --git a/auth_providers/auth_oauth_test.go b/auth_providers/auth_oauth_test.go index 5e5fb87..c2c68ad 100644 --- a/auth_providers/auth_oauth_test.go +++ b/auth_providers/auth_oauth_test.go @@ -120,14 +120,62 @@ func TestCommandConfigOauth_Authenticate(t *testing.T) { }() //os.Setenv(auth_providers.EnvKeyfactorConfigFile, configFilePath) //os.Setenv(auth_providers.EnvKeyfactorAuthProfile, "oauth") - //os.Setenv(auth_providers.EnvKeyfactorSkipVerify, "true") + os.Setenv(auth_providers.EnvKeyfactorSkipVerify, "true") + os.Setenv(auth_providers.EnvKeyfactorCACert, "lib/certs/int-oidc-lab.eastus2.cloudapp.azure.com.pem") - t.Log("Testing oAuth with Environmental variables") + // 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{} - noParamsConfig. - WithSkipVerify(true). - WithCommandCACert("lib/certs/int-oidc-lab.eastus2.cloudapp.azure.com.pem") - authOauthTest(t, "with complete Environmental variables", false, noParamsConfig) + 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, "lib/certs/int-oidc-lab.eastus2.cloudapp.azure.com.pem") + 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", "certificate is not trusted"} + 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{} @@ -184,6 +232,7 @@ func TestCommandConfigOauth_Authenticate(t *testing.T) { ClientSecret: "invalid-client-secret", TokenURL: tokenURL, } + fullParamsInvalidPassConfig.WithSkipVerify(true) invalidCredsExpectedError := []string{ "oauth2", "unauthorized_client", "Invalid client or Invalid client credentials", } @@ -234,11 +283,13 @@ func TestCommandConfigOauth_Authenticate(t *testing.T) { 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") @@ -251,7 +302,6 @@ func TestCommandConfigOauth_Authenticate(t *testing.T) { httpsFailConfigFileExpected..., ) - os.Setenv(auth_providers.EnvKeyfactorSkipVerify, "true") t.Log("Testing oAuth with invalid profile implicit config file") invProfile := &auth_providers.CommandConfigOauth{} invProfile.WithConfigProfile("invalid-profile") @@ -260,12 +310,16 @@ func TestCommandConfigOauth_Authenticate(t *testing.T) { t.Log("Testing oAuth with invalid creds implicit config file") invProfileCreds := &auth_providers.CommandConfigOauth{} - invProfileCreds.WithConfigProfile("oauth_invalid_creds") + 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") + invCmdHost. + WithConfigProfile("oauth_invalid_host"). + WithSkipVerify(true) invHostExpectedError := []string{"no such host"} authOauthTest(t, "with invalid creds implicit config file", true, invCmdHost, invHostExpectedError...) } diff --git a/main.go b/main.go index f1615e2..96f5009 100644 --- a/main.go +++ b/main.go @@ -17,11 +17,152 @@ package main import ( "fmt" - "github.com/Keyfactor/keyfactor-auth-client-go/pkg" // Correct + "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) +//} From c91587315b62ef5a877b92024c37c59274a6ccd8 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 31 Oct 2024 11:53:14 -0700 Subject: [PATCH 53/57] fix(core): If client isn't set when `Authenticate()` is called, created an empty client. --- auth_providers/auth_core.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/auth_providers/auth_core.go b/auth_providers/auth_core.go index 390646b..97d1295 100644 --- a/auth_providers/auth_core.go +++ b/auth_providers/auth_core.go @@ -436,6 +436,9 @@ func (c *CommandAuthConfig) updateCACerts() error { // 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", From 0d8cee6010b2f2691999440ecdc3cfddba36e16f Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 31 Oct 2024 11:56:09 -0700 Subject: [PATCH 54/57] fix(tests): Add oidc lab cert --- ...nt-oidc-lab.eastus2.cloudapp.azure.com.pem | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 lib/certs/int-oidc-lab.eastus2.cloudapp.azure.com.pem diff --git a/lib/certs/int-oidc-lab.eastus2.cloudapp.azure.com.pem b/lib/certs/int-oidc-lab.eastus2.cloudapp.azure.com.pem new file mode 100644 index 0000000..6ef8014 --- /dev/null +++ b/lib/certs/int-oidc-lab.eastus2.cloudapp.azure.com.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIENTCCAp2gAwIBAgIUXiYBpLlOyvIAuP9iZ8NmIfftHq0wDQYJKoZIhvcNAQEL +BQAwMDEXMBUGCgmSJomT8ixkAQEMBzI0Mjc4MzcxFTATBgNVBAMMDE1hbmFnZW1l +bnRDQTAeFw0yNDEwMTYxNjUxMzdaFw0yNTExMTcxNjQyMzVaMDIxMDAuBgNVBAMM +J2ludC1vaWRjLWxhYi5lYXN0dXMyLmNsb3VkYXBwLmF6dXJlLmNvbTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAO9bxuPyHOytYPN7KIz2IBuuVuKMh7YU +yuZHJ2CiFfOOOnZlojaKjsp5j8E50h4tc5aPBWDZ0Cma7Ty3pdsssRsa78JwQ8cG +oOUumrRYySzAET6OJA8Pn00t+lxLxBf4St3buobhQTEMesCsKdCSWbAydiIgkKA8 +E02zPKzXJU6yMmV/JnMPqEQHUBC8yb4NmfXsMplkVYkdhqESL+0xYYJLqireDuR3 +LfDdvZkh0XovgdN9zJpf10KP2os2tOzaa35dXVCBsl7y/IPaDt+zYb0OX88TGOPl +1AysYO35lS+6CGiENn2ACTZSmvqOcwjOLB5dJd95b1lnUwsJgmMcSesCAwEAAaOB +xDCBwTAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFOOYYJwB1O/MQlfbORHp1YqM +Uy0MMEwGA1UdEQRFMEOCJ2ludC1vaWRjLWxhYi5lYXN0dXMyLmNsb3VkYXBwLmF6 +dXJlLmNvbYIMaW50LW9pZGMtbGFihwQUAYhEhwQKCgAEMBMGA1UdJQQMMAoGCCsG +AQUFBwMBMB0GA1UdDgQWBBTBsBvKKfjz/P14o0c3V/fLm/hXmDAOBgNVHQ8BAf8E +BAMCBaAwDQYJKoZIhvcNAQELBQADggGBAEizjLNGgjGNML6L4On1EhcDAtGIH6SY +mkiP/mcMXcxESx/wAZAQO04ERbVX3mnewdmq1+TsnbfxEtetAobaxlRRej6tzFQp +BBqGqz7uK8O3CpzBZzuBtR2d+GZw6+amBbxGyZvsIW0f2RQqaHp+FgBgJjZc3t+/ +UmaRoS2h26VBXuv40bEgErih/cI80xnJtRKtxS1/hJNvzeDMobgZ0KkZz1j/tHbi +DzaX8CDCg0fhMH5BuSleG9MSYcW36TL30pv/h92kN8EldQgpWwMW0C3aTZBEpiCw +NGeiUmbciia//rOi4Rin4uHCe7sllb10MTaLcy251S9vAmmHvSBj33iLlCms2WS7 +02BwsPPGilydfjKqekoC6M1AwxiSDp8ckiXNx4LvW8uyqwHCIOsW3kc7CR0LGvTn +qLN7Sy5keB9uVs4e69nmU2GG6V0SN1QreuQfroIkgsgYyfz6MvMVhmVVLObELkrY +RfrOSKEyZnB0+kBfyGykvnQcSaUsqFwXmg== +-----END CERTIFICATE----- From 9347b1a0f743c99a26ca2c7ac83bb5ce23dcd0ff Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 31 Oct 2024 12:12:28 -0700 Subject: [PATCH 55/57] fix(ci): Reference token_url for oauth tests --- .github/workflows/go_tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/go_tests.yml b/.github/workflows/go_tests.yml index a9e2137..ad6f2cb 100644 --- a/.github/workflows/go_tests.yml +++ b/.github/workflows/go_tests.yml @@ -37,6 +37,7 @@ jobs: 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_URL: ${{ var.KEYFACTOR_AUTH_URL }} KEYFACTOR_HOSTNAME: ${{ vars.KEYFACTOR_HOSTNAME }} KEYFACTOR_AUTH_HOSTNAME: ${{ vars.KEYFACTOR_AUTH_HOSTNAME }} KEYFACTOR_SKIP_VERIFY: ${{ vars.KEYFACTOR_SKIP_VERIFY }} From 2eb783fc30fe4b463c4db9f8cd975c8d49718e59 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 31 Oct 2024 12:15:13 -0700 Subject: [PATCH 56/57] fix(ci): Reference token_url for oauth tests --- .github/workflows/go_tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/go_tests.yml b/.github/workflows/go_tests.yml index ad6f2cb..b61e926 100644 --- a/.github/workflows/go_tests.yml +++ b/.github/workflows/go_tests.yml @@ -37,7 +37,7 @@ jobs: 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_URL: ${{ var.KEYFACTOR_AUTH_URL }} + 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 }} From 16766812e5cc5a8a8037c78dc401e2b8567f44fc Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 31 Oct 2024 12:30:43 -0700 Subject: [PATCH 57/57] fix(ci): Check that lab cert is present --- .github/workflows/go_tests.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/go_tests.yml b/.github/workflows/go_tests.yml index b61e926..a40e8ba 100644 --- a/.github/workflows/go_tests.yml +++ b/.github/workflows/go_tests.yml @@ -24,6 +24,14 @@ jobs: - name: Get Public IP run: curl -s https://api.ipify.org + - name: Validate lab cert is present + run: | + pwd + ls -la + ls -la lib + ls -la lib/certs + cat lib/certs/int-oidc-lab.eastus2.cloudapp.azure.com.pem + - name: Run tests run: | if [ -n "${{ secrets.KEYFACTOR_AUTH_CONFIG_B64 }}" ]; then