Skip to content
This repository was archived by the owner on Nov 20, 2025. It is now read-only.

feat: Implement X509 Cert Based Authentication#2036

Merged
vverman merged 15 commits intogoogleapis:x509_cert_authenticationfrom
vverman:feat/x509-certificate
Jul 14, 2025
Merged

feat: Implement X509 Cert Based Authentication#2036
vverman merged 15 commits intogoogleapis:x509_cert_authenticationfrom
vverman:feat/x509-certificate

Conversation

@vverman
Copy link
Contributor

@vverman vverman commented Jun 12, 2025

With this PR, we are now able to use X509 Certificates to authenticate via Google Auth Library.

Testing

Added unit tests and all integration tests passing as per this testing document

Additional Information

Change also includes trust chain verification to be passed to STS endpoint

@vverman vverman requested a review from a team June 12, 2025 18:47
@vverman vverman requested a review from a team as a code owner June 12, 2025 18:47
@vverman vverman force-pushed the feat/x509-certificate branch from 8d225a9 to 750e24a Compare June 12, 2025 20:35
Copy link

@nbayati nbayati Jun 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed the use of synchronous file operations (e.g., fs.readFileSync in CertificateSubjectTokenSupplier and fs.existsSync/lstatSync in util.ts). To prevent potential event loop blocking, especially in a library context, have you considered using asynchronous alternatives like fs.promises?
If there was a specific reason for the synchronous approach, I'd be interested to hear it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good question,

The synchronous file operations are needed because reading the certificate and key files are needed for creating the IdentityPoolClient. For e.g. to create the mtlsAgent. Since the constructor cannot be async, we need to do it synchronously.

I have changed the instances of file reads in processChainFromPaths() to async since that is called from the async getSubjectToken().

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we technically defer the creation of the mTLS https.Agent and the specific Gaxios transporter instance until they are about to be used with the token exchange methods, allowing the file reads to be done asynchronously?

/**
* Location to fetch trust chain from to send to STS endpoint.
* in case no location is provided, we will just send the leaf certificate as the
* trust chain
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not really accurate. If a trust chain file is not provided, we just send the leaf certificate as the "subject token" and not the trust chain. I'd suggest focusing on what this field IS rather than the details of how we'll use it in the implementation. For instance, in the go library we have the following comments for these three fields:

type CertificateConfig struct {
	// UseDefaultCertificateConfig, if true, attempts to load the default
	// certificate configuration. It checks the GOOGLE_API_CERTIFICATE_CONFIG
	// environment variable first, then a conventional default file location.
	// Cannot be true if CertificateConfigLocation is set.
	UseDefaultCertificateConfig bool `json:"use_default_certificate_config"`
	// CertificateConfigLocation specifies the path to the client certificate
	// and private key file. Must be set if UseDefaultCertificateConfig is
	// false or unset.
	CertificateConfigLocation string `json:"certificate_config_location"`
	// TrustChainPath specifies the path to a PEM-formatted file containing
	// the X.509 certificate trust chain. This file should contain any
	// intermediate certificates required to complete the trust chain between
	// the leaf certificate (used for mTLS) and the root certificate(s) in
	// your workload identity pool's trust store. The leaf certificate and
	// any certificates already present in the workload identity pool's trust
	// store are optional in this file. Certificates should be ordered with the
	// leaf certificate (or the certificate which signed the leaf) first.
	TrustChainPath string `json:"trust_chain_path"`
}

* and returns a JSON array of base64-encoded certificates.
* @returns A stringified JSON array of the certificate chain.
*/
#processChainFromPaths(): string {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adopting a more granular parsing loop similar to the Java implementation, where each certificate block is parsed in its own try...catch. This would enable more precise error messages if a single certificate in the chain is invalid.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added that and error now shows which index the malformed certificate has in the chain.

return `gl-node/${nodeVersion} auth/${pkg.version} google-byoid-sdk source/${credentialSourceType} sa-impersonation/${saImpersonation} config-lifetime/${this.configLifetimeRequested}`;
}

getTokenUrl(): string {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

protected? seems we don't expose much in NodeJS

Copy link
Contributor Author

@vverman vverman Jul 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could make this method public as well. The tokenUrl was a private readonly member without a getter which is why I thought to give the getter minimum privilege

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe in TypeScript members of a class (properties and methods) are public by default if no access modifier is explicitly specified. To make a method private, you need to use the private keyword. So let's mark this as protected.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add tests:

  • Malformed cert/key files: Tests for scenarios where the files pointed to by cert_path or key_path exist but are not valid PEM or key formats.
  • Malformed trust chain certs. While the order is tested, a test where one of the certificates within the trust chain PEM file is malformed should be added (this is related to the suggestion of more granular parsing I mentioned)
  • Empty cert_config.json: Test the behavior when the cert_config.json file is empty or not valid JSON.
  • Missing cert_path: Similar to the missing key_path test, add one for a missing cert_path in the config.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here are the test names added in test.identitypoolclient.ts

  1. Malformed cert/key:
    1.1 . 'should throw if cert has invalid PEM format'
    1.2 'should throw if key has invalid private key format'

  2. Malformed trust chains:
    2.1 'should throw when one or more certs in trust chain is malformed'

  3. Empty cert_config.json
    3.1 'should throw in case cert config is empty or malformed'

  4. Missing cert_path:
    4.1 'should throw in case cert config has missing cert path'

gcf-owl-bot bot and others added 4 commits June 27, 2025 12:17
* chore: install higher version of Python

* chore: update to python 3.15

* update lagging dependency

* fix vulnerability

* change the version

Source-Link: googleapis/synthtool@ca4c7ce
Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:6062c519ce78ee08490e7ac7330eca80f00f139ef1a241c5c2b306550b60c728

Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
* chore(deps): upgrade sinon to 21

* specify which timers to fake

* use @feywind's util for timers

* add crucial file
Source-Link: googleapis/synthtool@1218bc2
Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:66c44f0ad8f6caaa4eb3fbe74f8c2b4de5a97c2b930cee069e712c447723ba95

Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
Co-authored-by: Leah E. Cole <6719667+leahecole@users.noreply.github.com>
@product-auto-label product-auto-label bot added the size: xl Pull request size is extra large. label Jul 9, 2025
@vverman vverman force-pushed the feat/x509-certificate branch from 704e808 to 5b7359e Compare July 9, 2025 22:26
@vverman vverman changed the base branch from x509_cert_authentication to main July 9, 2025 23:45
@vverman vverman force-pushed the feat/x509-certificate branch from 5b7359e to 4782b23 Compare July 10, 2025 00:00
return `gl-node/${nodeVersion} auth/${pkg.version} google-byoid-sdk source/${credentialSourceType} sa-impersonation/${saImpersonation} config-lifetime/${this.configLifetimeRequested}`;
}

getTokenUrl(): string {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe in TypeScript members of a class (properties and methods) are public by default if no access modifier is explicitly specified. To make a method private, you need to use the private keyword. So let's mark this as protected.

public projectNumber: string | null;
private readonly configLifetimeRequested: boolean;
protected credentialSourceType?: string;
private readonly tokenUrl: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe group these based on visibility

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we technically defer the creation of the mTLS https.Agent and the specific Gaxios transporter instance until they are about to be used with the token exchange methods, allowing the file reads to be done asynchronously?

@vverman vverman changed the base branch from main to x509_cert_authentication July 10, 2025 17:51
Copy link

@nbayati nbayati left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, Good job! :)

@lsirac
Copy link
Contributor

lsirac commented Jul 14, 2025

Feel free to merge into the feature branch and open a new PR with the refactor. Thank you!

@vverman vverman merged commit 167490f into googleapis:x509_cert_authentication Jul 14, 2025
16 checks passed
sofisl pushed a commit that referenced this pull request Jul 18, 2025
* feat: Implement X509 Cert Based Authentication (#2036)

* chore(owlbot-nodejs): install 3.13.5 Python (#2042)

* chore: install higher version of Python

* chore: update to python 3.15

* update lagging dependency

* fix vulnerability

* change the version

Source-Link: googleapis/synthtool@ca4c7ce
Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:6062c519ce78ee08490e7ac7330eca80f00f139ef1a241c5c2b306550b60c728

Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>

* chore(deps): upgrade sinon to 21 (#2050)

* chore(deps): upgrade sinon to 21

* specify which timers to fake

* use @feywind's util for timers

* add crucial file

* fix(deps): update dependency @googleapis/iam to v30 (#2052)

* chore: add node 24 in node ci test (#2051)

Source-Link: googleapis/synthtool@1218bc2
Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:66c44f0ad8f6caaa4eb3fbe74f8c2b4de5a97c2b930cee069e712c447723ba95

Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
Co-authored-by: Leah E. Cole <6719667+leahecole@users.noreply.github.com>

* Included initial interfaces and options for creating x509client.

* Added implementation for x509provider

* Augmented logic for well-known cert config.

* Added changes to create CertificateSubjectTokenSupplier

* Added feature to call STS endpoint with the leaf certificate as trust chain.

* Added logic to use trust chains.

* Cleaned up certificateSubjectTokenSupplier and added mtlsClientTransporter logic to IdentityPoolClients Transporter

* Added tests for certificateConfig type externalClient

* All x509 auth logic in src/auth/certificatesubjecttokensupplier.ts

* Added tests for malformed cert_config file, malfor
med certificate in trust chain.

* Added unit tests for util

---------

Co-authored-by: gcf-owl-bot[bot] <78513119+gcf-owl-bot[bot]@users.noreply.github.com>
Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
Co-authored-by: Leah E. Cole <6719667+leahecole@users.noreply.github.com>
Co-authored-by: Mend Renovate <renovate@whitesourcesoftware.com>

* feat: Async X509 file Operations (#2054)

* chore(owlbot-nodejs): install 3.13.5 Python (#2042)

* chore: install higher version of Python

* chore: update to python 3.15

* update lagging dependency

* fix vulnerability

* change the version

Source-Link: googleapis/synthtool@ca4c7ce
Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:6062c519ce78ee08490e7ac7330eca80f00f139ef1a241c5c2b306550b60c728

Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>

* chore: add node 24 in node ci test (#2051)

Source-Link: googleapis/synthtool@1218bc2
Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest@sha256:66c44f0ad8f6caaa4eb3fbe74f8c2b4de5a97c2b930cee069e712c447723ba95

Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
Co-authored-by: Leah E. Cole <6719667+leahecole@users.noreply.github.com>

* X509 Cert Auth now does only async file reads

* Fixed any linter error in util

---------

Co-authored-by: gcf-owl-bot[bot] <78513119+gcf-owl-bot[bot]@users.noreply.github.com>
Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
Co-authored-by: Leah E. Cole <6719667+leahecole@users.noreply.github.com>

* feat: x509 async Readme Update (#2056)


* Added readme changes.

* Addressed PR comments.

* 🦉 Updates from OwlBot post-processor

See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md

* Readme changes transferred from Readme.md to readme-partials.yaml for Yoshi bot compliance

* 🦉 Updates from OwlBot post-processor

See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md

---------

Co-authored-by: gcf-owl-bot[bot] <78513119+gcf-owl-bot[bot]@users.noreply.github.com>
Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
Co-authored-by: Leah E. Cole <6719667+leahecole@users.noreply.github.com>
Co-authored-by: Mend Renovate <renovate@whitesourcesoftware.com>
@vverman vverman deleted the feat/x509-certificate branch November 11, 2025 02:32
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

size: xl Pull request size is extra large.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants