From 0b04c3898408bf001cd599bfc96383aa5e41612d Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Thu, 13 Nov 2025 11:26:13 -0600 Subject: [PATCH 1/5] feat: add username override functionality to jfrog-oauth and fix username extraction from oauth --- .../coder/modules/jfrog-oauth/main.test.ts | 25 +++++++++++++++++++ registry/coder/modules/jfrog-oauth/main.tf | 22 ++++++++++++++-- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/registry/coder/modules/jfrog-oauth/main.test.ts b/registry/coder/modules/jfrog-oauth/main.test.ts index 940d166bd..f5c899f87 100644 --- a/registry/coder/modules/jfrog-oauth/main.test.ts +++ b/registry/coder/modules/jfrog-oauth/main.test.ts @@ -12,6 +12,7 @@ describe("jfrog-oauth", async () => { jfrog_url: string; package_managers: string; + username?: string; username_field?: string; jfrog_server_id?: string; external_auth_id?: string; @@ -182,4 +183,28 @@ EOF`; 'if [ -z "YES" ]; then\n not_configured maven', ); }); + + it("accepts manual username override with special characters", async () => { + const customUsername = "john.smith"; + const state = await runTerraformApply(import.meta.dir, { + agent_id: "some-agent-id", + jfrog_url: fakeFrogUrl, + username: customUsername, + package_managers: JSON.stringify({ + npm: ["npm"], + pypi: ["pypi"], + docker: ["docker.jfrog.io"], + }), + }); + + const coderScript = findResourceInstance(state, "coder_script"); + + expect(coderScript.script).toContain( + `docker login "$repo" --username ${customUsername}`, + ); + + expect(coderScript.script).toContain(`https://${customUsername}:`); + + expect(coderScript.script).toContain("cat << EOF > ~/.npmrc"); + }); }); diff --git a/registry/coder/modules/jfrog-oauth/main.tf b/registry/coder/modules/jfrog-oauth/main.tf index 922f64421..9ffd668cf 100644 --- a/registry/coder/modules/jfrog-oauth/main.tf +++ b/registry/coder/modules/jfrog-oauth/main.tf @@ -25,6 +25,16 @@ variable "jfrog_server_id" { default = "0" } +variable "username" { + type = string + description = <<-EOF + Override JFrog username. Leave empty for automatic extraction from OAuth token. + The module automatically extracts your JFrog username from the OAuth token. + Only set this if automatic extraction fails or you need to use a different username. + EOF + default = null +} + variable "username_field" { type = string description = "The field to use for the artifactory username. i.e. Coder username or email." @@ -76,8 +86,11 @@ variable "package_managers" { } locals { - # The username field to use for artifactory - username = var.username_field == "email" ? data.coder_workspace_owner.me.email : data.coder_workspace_owner.me.name + username = coalesce( + var.username, + try(data.external.jfrog_username[0].result.username != "" ? data.external.jfrog_username[0].result.username : null, null), + var.username_field == "email" ? data.coder_workspace_owner.me.email : data.coder_workspace_owner.me.name + ) jfrog_host = split("://", var.jfrog_url)[1] common_values = { JFROG_URL = var.jfrog_url @@ -116,6 +129,11 @@ data "coder_workspace_owner" "me" {} data "coder_external_auth" "jfrog" { id = var.external_auth_id } +data "external" "jfrog_username" { + count = var.username == null ? 1 : 0 + + program = ["bash", "-c", "TOKEN='${data.coder_external_auth.jfrog.access_token}'; PAYLOAD=$(echo \"$TOKEN\" | cut -d. -f2); LEN=$(printf '%s' \"$PAYLOAD\" | wc -c); MOD=$((LEN % 4)); if [ $MOD -eq 2 ]; then PAYLOAD=\"$PAYLOAD==\"; elif [ $MOD -eq 3 ]; then PAYLOAD=\"$PAYLOAD=\"; fi; USERNAME=$(echo \"$PAYLOAD\" | base64 -d 2>/dev/null | grep -oP '\"/users/\\K[^\"]+' 2>/dev/null | head -1 || echo \"\"); if [ -z \"$USERNAME\" ]; then echo '{\"username\":\"\"}'; else USERNAME=$(echo \"$USERNAME\" | sed 's/\\\\/\\\\\\\\/g; s/\"/\\\\\"/g'); echo \"{\\\"username\\\":\\\"$USERNAME\\\"}\"; fi"] +} resource "coder_script" "jfrog" { agent_id = var.agent_id From 374a5cb38c0ede2f52f8e03b6c683e483bf46506 Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Thu, 13 Nov 2025 12:16:18 -0600 Subject: [PATCH 2/5] refactor: remove username override from jfrog-oauth module and enhance username extraction --- registry/coder/modules/jfrog-oauth/README.md | 9 +++++++ .../coder/modules/jfrog-oauth/main.test.ts | 25 ------------------- registry/coder/modules/jfrog-oauth/main.tf | 16 ++---------- 3 files changed, 11 insertions(+), 39 deletions(-) diff --git a/registry/coder/modules/jfrog-oauth/README.md b/registry/coder/modules/jfrog-oauth/README.md index eaddfee6a..6daf04f19 100644 --- a/registry/coder/modules/jfrog-oauth/README.md +++ b/registry/coder/modules/jfrog-oauth/README.md @@ -39,6 +39,15 @@ module "jfrog" { This module is usable by JFrog self-hosted (on-premises) Artifactory as it requires configuring a custom integration. This integration benefits from Coder's [external-auth](https://coder.com/docs/v2/latest/admin/external-auth) feature and allows each user to authenticate with Artifactory using an OAuth flow and issues user-scoped tokens to each user. For configuration instructions, see this [guide](https://coder.com/docs/v2/latest/guides/artifactory-integration#jfrog-oauth) on the Coder documentation. +## Username Handling + +The module automatically extracts your JFrog username directly from the OAuth token's JWT payload. This preserves special characters like dots (`.`), hyphens (`-`), and accented characters that Coder normalizes in usernames. + +**Priority order:** + +1. **JWT extraction** (default) - Extracts username from OAuth token, preserving special characters +2. **Fallback to `username_field`** - If JWT extraction fails, uses Coder username or email + ## Examples Configure the Python pip package manager to fetch packages from Artifactory while mapping the Coder email to the Artifactory username. diff --git a/registry/coder/modules/jfrog-oauth/main.test.ts b/registry/coder/modules/jfrog-oauth/main.test.ts index f5c899f87..940d166bd 100644 --- a/registry/coder/modules/jfrog-oauth/main.test.ts +++ b/registry/coder/modules/jfrog-oauth/main.test.ts @@ -12,7 +12,6 @@ describe("jfrog-oauth", async () => { jfrog_url: string; package_managers: string; - username?: string; username_field?: string; jfrog_server_id?: string; external_auth_id?: string; @@ -183,28 +182,4 @@ EOF`; 'if [ -z "YES" ]; then\n not_configured maven', ); }); - - it("accepts manual username override with special characters", async () => { - const customUsername = "john.smith"; - const state = await runTerraformApply(import.meta.dir, { - agent_id: "some-agent-id", - jfrog_url: fakeFrogUrl, - username: customUsername, - package_managers: JSON.stringify({ - npm: ["npm"], - pypi: ["pypi"], - docker: ["docker.jfrog.io"], - }), - }); - - const coderScript = findResourceInstance(state, "coder_script"); - - expect(coderScript.script).toContain( - `docker login "$repo" --username ${customUsername}`, - ); - - expect(coderScript.script).toContain(`https://${customUsername}:`); - - expect(coderScript.script).toContain("cat << EOF > ~/.npmrc"); - }); }); diff --git a/registry/coder/modules/jfrog-oauth/main.tf b/registry/coder/modules/jfrog-oauth/main.tf index 9ffd668cf..d0fd35b67 100644 --- a/registry/coder/modules/jfrog-oauth/main.tf +++ b/registry/coder/modules/jfrog-oauth/main.tf @@ -25,16 +25,6 @@ variable "jfrog_server_id" { default = "0" } -variable "username" { - type = string - description = <<-EOF - Override JFrog username. Leave empty for automatic extraction from OAuth token. - The module automatically extracts your JFrog username from the OAuth token. - Only set this if automatic extraction fails or you need to use a different username. - EOF - default = null -} - variable "username_field" { type = string description = "The field to use for the artifactory username. i.e. Coder username or email." @@ -87,8 +77,7 @@ variable "package_managers" { locals { username = coalesce( - var.username, - try(data.external.jfrog_username[0].result.username != "" ? data.external.jfrog_username[0].result.username : null, null), + try(data.external.jfrog_username.result.username != "" ? data.external.jfrog_username.result.username : null, null), var.username_field == "email" ? data.coder_workspace_owner.me.email : data.coder_workspace_owner.me.name ) jfrog_host = split("://", var.jfrog_url)[1] @@ -129,9 +118,8 @@ data "coder_workspace_owner" "me" {} data "coder_external_auth" "jfrog" { id = var.external_auth_id } -data "external" "jfrog_username" { - count = var.username == null ? 1 : 0 +data "external" "jfrog_username" { program = ["bash", "-c", "TOKEN='${data.coder_external_auth.jfrog.access_token}'; PAYLOAD=$(echo \"$TOKEN\" | cut -d. -f2); LEN=$(printf '%s' \"$PAYLOAD\" | wc -c); MOD=$((LEN % 4)); if [ $MOD -eq 2 ]; then PAYLOAD=\"$PAYLOAD==\"; elif [ $MOD -eq 3 ]; then PAYLOAD=\"$PAYLOAD=\"; fi; USERNAME=$(echo \"$PAYLOAD\" | base64 -d 2>/dev/null | grep -oP '\"/users/\\K[^\"]+' 2>/dev/null | head -1 || echo \"\"); if [ -z \"$USERNAME\" ]; then echo '{\"username\":\"\"}'; else USERNAME=$(echo \"$USERNAME\" | sed 's/\\\\/\\\\\\\\/g; s/\"/\\\\\"/g'); echo \"{\\\"username\\\":\\\"$USERNAME\\\"}\"; fi"] } From a17a52a578f3a5740d282ce7ad063f28c03f4745 Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Fri, 14 Nov 2025 10:59:53 -0600 Subject: [PATCH 3/5] refactor: use native terraform functions for username extraction --- registry/coder/modules/jfrog-oauth/main.tf | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/registry/coder/modules/jfrog-oauth/main.tf b/registry/coder/modules/jfrog-oauth/main.tf index d0fd35b67..c666af9e9 100644 --- a/registry/coder/modules/jfrog-oauth/main.tf +++ b/registry/coder/modules/jfrog-oauth/main.tf @@ -76,8 +76,20 @@ variable "package_managers" { } locals { + jwt_parts = split(".", data.coder_external_auth.jfrog.access_token) + jwt_payload = try(local.jwt_parts[1], "") + payload_padding = local.jwt_payload == "" ? "" : (length(local.jwt_payload) % 4 == 0 ? "" : length(local.jwt_payload) % 4 == 2 ? "==" : "=") + + jwt_username = try( + regex( + "/users/(.+)", + jsondecode(base64decode("${local.jwt_payload}${local.payload_padding}"))["sub"] + )[0], + "" + ) + username = coalesce( - try(data.external.jfrog_username.result.username != "" ? data.external.jfrog_username.result.username : null, null), + local.jwt_username != "" ? local.jwt_username : null, var.username_field == "email" ? data.coder_workspace_owner.me.email : data.coder_workspace_owner.me.name ) jfrog_host = split("://", var.jfrog_url)[1] @@ -119,10 +131,6 @@ data "coder_external_auth" "jfrog" { id = var.external_auth_id } -data "external" "jfrog_username" { - program = ["bash", "-c", "TOKEN='${data.coder_external_auth.jfrog.access_token}'; PAYLOAD=$(echo \"$TOKEN\" | cut -d. -f2); LEN=$(printf '%s' \"$PAYLOAD\" | wc -c); MOD=$((LEN % 4)); if [ $MOD -eq 2 ]; then PAYLOAD=\"$PAYLOAD==\"; elif [ $MOD -eq 3 ]; then PAYLOAD=\"$PAYLOAD=\"; fi; USERNAME=$(echo \"$PAYLOAD\" | base64 -d 2>/dev/null | grep -oP '\"/users/\\K[^\"]+' 2>/dev/null | head -1 || echo \"\"); if [ -z \"$USERNAME\" ]; then echo '{\"username\":\"\"}'; else USERNAME=$(echo \"$USERNAME\" | sed 's/\\\\/\\\\\\\\/g; s/\"/\\\\\"/g'); echo \"{\\\"username\\\":\\\"$USERNAME\\\"}\"; fi"] -} - resource "coder_script" "jfrog" { agent_id = var.agent_id display_name = "jfrog" From 2ec569b06cebd941c1a64f9803af374fcfd252c6 Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Fri, 14 Nov 2025 15:00:39 -0600 Subject: [PATCH 4/5] fix: enhance JWT handling --- registry/coder/modules/jfrog-oauth/main.tf | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/registry/coder/modules/jfrog-oauth/main.tf b/registry/coder/modules/jfrog-oauth/main.tf index c666af9e9..416ca3dc8 100644 --- a/registry/coder/modules/jfrog-oauth/main.tf +++ b/registry/coder/modules/jfrog-oauth/main.tf @@ -76,13 +76,18 @@ variable "package_managers" { } locals { - jwt_parts = split(".", data.coder_external_auth.jfrog.access_token) - jwt_payload = try(local.jwt_parts[1], "") - payload_padding = local.jwt_payload == "" ? "" : (length(local.jwt_payload) % 4 == 0 ? "" : length(local.jwt_payload) % 4 == 2 ? "==" : "=") + jwt_parts = try(split(".", data.coder_external_auth.jfrog.access_token), []) + jwt_payload = try(local.jwt_parts[1], "") + payload_padding = local.jwt_payload == "" ? "" : ( + length(local.jwt_payload) % 4 == 0 ? "" : + length(local.jwt_payload) % 4 == 2 ? "==" : + length(local.jwt_payload) % 4 == 3 ? "=" : + "" + ) jwt_username = try( regex( - "/users/(.+)", + "/users/([^/]+)", jsondecode(base64decode("${local.jwt_payload}${local.payload_padding}"))["sub"] )[0], "" From 26d27b1f906591d6033891e33c1ca51425f4b458 Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Mon, 17 Nov 2025 07:50:26 -0600 Subject: [PATCH 5/5] chore: update module version for bump after 535 merge --- registry/coder/modules/jfrog-oauth/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/registry/coder/modules/jfrog-oauth/README.md b/registry/coder/modules/jfrog-oauth/README.md index 6daf04f19..50311de75 100644 --- a/registry/coder/modules/jfrog-oauth/README.md +++ b/registry/coder/modules/jfrog-oauth/README.md @@ -16,7 +16,7 @@ Install the JF CLI and authenticate package managers with Artifactory using OAut module "jfrog" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jfrog-oauth/coder" - version = "1.2.0" + version = "1.2.2" agent_id = coder_agent.example.id jfrog_url = "https://example.jfrog.io" username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username" @@ -56,7 +56,7 @@ Configure the Python pip package manager to fetch packages from Artifactory whil module "jfrog" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jfrog-oauth/coder" - version = "1.2.0" + version = "1.2.2" agent_id = coder_agent.example.id jfrog_url = "https://example.jfrog.io" username_field = "email" @@ -85,7 +85,7 @@ The [JFrog extension](https://open-vsx.org/extension/JFrog/jfrog-vscode-extensio module "jfrog" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jfrog-oauth/coder" - version = "1.2.0" + version = "1.2.2" agent_id = coder_agent.example.id jfrog_url = "https://example.jfrog.io" username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username"