diff --git a/README.md b/README.md index 486be60c0..c1670d089 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ - [Security Updates](#security-updates) - [Input Parameters](#input-parameters) - [`client-id`](#client-id) + - [`mask-client-id`](#mask-client-id) - [`subscription-id`](#subscription-id) - [`tenant-id`](#tenant-id) - [`creds`](#creds) @@ -144,6 +145,7 @@ Customers using v1 should migrate to v3. End-of-life releases no longer receive |allow-no-subscriptions|false|boolean|false|if login without subscription is allowed| |audience|false|string|api://AzureADTokenExchange|the audience to get the JWT ID token from GitHub OIDC provider| |auth-type|false|string|SERVICE_PRINCIPAL|the auth type| +|mask-client-id|false|boolean|true|if the `client-id` value is masked in workflow logs| ### `client-id` @@ -156,7 +158,28 @@ It's better to create a GitHub Action secret for this parameter when using it. R Refer to [Login With OpenID Connect (OIDC)](#login-with-openid-connect-oidc-recommended) and [Login With User-assigned Managed Identity](#login-with-user-assigned-managed-identity) for its usage. > [!NOTE] -> The action registers the `client-id` value as a secret (via `core.setSecret`) so it is masked in workflow logs. Some enterprises treat the client ID as sensitive, and masking also prevents it from being printed accidentally, which matters in public repositories. `tenant-id` and `subscription-id` are not masked. +> By default the action registers the `client-id` value as a secret (via `core.setSecret`) so it is masked in workflow logs. Some enterprises treat the client ID as sensitive, and masking also prevents it from being printed accidentally, which matters in public repositories. `tenant-id` and `subscription-id` are not masked. Set [`mask-client-id`](#mask-client-id) to `false` to opt out of the masking. + +### `mask-client-id` + +The input parameter `mask-client-id` controls whether the login client id is registered as a secret and masked in the workflow logs. It defaults to `true`. + +Set it to `false` when the client id is not treated as sensitive and masking gets in the way, for example when the same value appears in log output or command results that you need to read. + +The client-id is effectively a username: it is low sensitivity on its own, and only useful to an attacker who already holds the client secret or certificate. Disabling masking is therefore reasonable when the value is treated as configuration rather than as a secret. + +```yaml + - name: Azure login + uses: azure/login@v3 + with: + tenant-id: ${{ vars.AZURE_TENANT_ID }} + subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} + client-id: ${{ vars.AZURE_CLIENT_ID }} + mask-client-id: false +``` + +> [!NOTE] +> The value is only unmasked when it is not a GitHub Action secret. A value passed from `${{ secrets.* }}` is still masked by GitHub itself, regardless of this input. ### `subscription-id` diff --git a/__tests__/LoginConfig.test.ts b/__tests__/LoginConfig.test.ts index ee5d67b2c..497592454 100644 --- a/__tests__/LoginConfig.test.ts +++ b/__tests__/LoginConfig.test.ts @@ -175,6 +175,53 @@ describe("LoginConfig Test", () => { expect(loginConfig.subscriptionId).toBe("subscription-id-aa"); }); + test('initialize with mask-client-id=true', async () => { + setEnv('environment', 'azureusgovernment'); + setEnv('enable-AzPSSession', 'false'); + setEnv('allow-no-subscriptions', 'true'); + setEnv('auth-type', 'SERVICE_PRINCIPAL'); + setEnv('tenant-id', 'tenant-id'); + setEnv('subscription-id', 'subscription-id'); + setEnv('client-id', 'client-id'); + setEnv('mask-client-id', 'true'); + + let loginConfig = new LoginConfig(); + await loginConfig.initialize(); + expect(loginConfig.maskClientId).toBeTruthy(); + expect(loginConfig.servicePrincipalId).toBe("client-id"); + }); + + test('initialize with mask-client-id=false', async () => { + setEnv('environment', 'azureusgovernment'); + setEnv('enable-AzPSSession', 'false'); + setEnv('allow-no-subscriptions', 'true'); + setEnv('auth-type', 'SERVICE_PRINCIPAL'); + setEnv('tenant-id', 'tenant-id'); + setEnv('subscription-id', 'subscription-id'); + setEnv('client-id', 'client-id'); + setEnv('mask-client-id', 'false'); + + let loginConfig = new LoginConfig(); + await loginConfig.initialize(); + expect(loginConfig.maskClientId).toBeFalsy(); + expect(loginConfig.servicePrincipalId).toBe("client-id"); + }); + + test('initialize without mask-client-id', async () => { + setEnv('environment', 'azureusgovernment'); + setEnv('enable-AzPSSession', 'false'); + setEnv('allow-no-subscriptions', 'true'); + setEnv('auth-type', 'SERVICE_PRINCIPAL'); + setEnv('tenant-id', 'tenant-id'); + setEnv('subscription-id', 'subscription-id'); + setEnv('client-id', 'client-id'); + + let loginConfig = new LoginConfig(); + await loginConfig.initialize(); + expect(loginConfig.maskClientId).toBeTruthy(); + expect(loginConfig.servicePrincipalId).toBe("client-id"); + }); + test('validate with wrong environment', async () => { setEnv('environment', 'aWrongCloud'); setEnv('enable-AzPSSession', 'false'); diff --git a/action.yml b/action.yml index bf52a453b..4897be543 100644 --- a/action.yml +++ b/action.yml @@ -34,6 +34,10 @@ inputs: description: 'The type of authentication. Supported values are SERVICE_PRINCIPAL, IDENTITY. Default value is SERVICE_PRINCIPAL' required: false default: 'SERVICE_PRINCIPAL' + mask-client-id: + description: 'Set this value to false to stop registering the client-id as a secret, so it is not masked in workflow logs' + required: false + default: true branding: icon: 'login.svg' color: 'blue' diff --git a/src/common/LoginConfig.ts b/src/common/LoginConfig.ts index b9939c588..4a2a9020f 100644 --- a/src/common/LoginConfig.ts +++ b/src/common/LoginConfig.ts @@ -25,6 +25,7 @@ export class LoginConfig { enableAzPSSession: boolean; audience: string; federatedToken: string; + maskClientId: boolean; async initialize() { this.environment = core.getInput("environment").toLowerCase(); @@ -42,7 +43,10 @@ export class LoginConfig { this.audience = core.getInput('audience', { required: false }); this.federatedToken = null; - this.mask(this.servicePrincipalId); + this.maskClientId = core.getInput('mask-client-id').toLowerCase() !== "false"; + if (this.maskClientId) { + this.mask(this.servicePrincipalId); + } this.mask(this.servicePrincipalSecret); }