feat: Implement X509 Cert Based Authentication#2036
feat: Implement X509 Cert Based Authentication#2036vverman merged 15 commits intogoogleapis:x509_cert_authenticationfrom
Conversation
8d225a9 to
750e24a
Compare
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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().
There was a problem hiding this comment.
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?
src/auth/identitypoolclient.ts
Outdated
| /** | ||
| * 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 |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
protected? seems we don't expose much in NodeJS
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Here are the test names added in test.identitypoolclient.ts
-
Malformed cert/key:
1.1 . 'should throw if cert has invalid PEM format'
1.2 'should throw if key has invalid private key format' -
Malformed trust chains:
2.1 'should throw when one or more certs in trust chain is malformed' -
Empty cert_config.json
3.1 'should throw in case cert config is empty or malformed' -
Missing cert_path:
4.1 'should throw in case cert config has missing cert path'
* 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>
704e808 to
5b7359e
Compare
…orter logic to IdentityPoolClients Transporter
med certificate in trust chain.
5b7359e to
4782b23
Compare
| return `gl-node/${nodeVersion} auth/${pkg.version} google-byoid-sdk source/${credentialSourceType} sa-impersonation/${saImpersonation} config-lifetime/${this.configLifetimeRequested}`; | ||
| } | ||
|
|
||
| getTokenUrl(): string { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
nit: maybe group these based on visibility
There was a problem hiding this comment.
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?
|
Feel free to merge into the feature branch and open a new PR with the refactor. Thank you! |
167490f
into
googleapis:x509_cert_authentication
* 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>
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