Azure / azure-sdk-for-js

This repository is for active development of the Azure SDK for JavaScript (NodeJS & Browser). For consumers of the SDK we recommend visiting our public developer docs at https://docs.microsoft.com/javascript/azure/ or our versioned developer docs at https://azure.github.io/azure-sdk-for-js.
MIT License
2.03k stars 1.19k forks source link

Docs: DefaultAzureCredential using Service principal with certificate #26806

Open kkazala opened 1 year ago

kkazala commented 1 year ago

Describe the bug The msalClientCertificate.parseCertificate incorrectly validates the PEM private key file.

The certificatePattern requires /(-+BEGIN CERTIFICATE-+)(\n\r?|\r\n?)([A-Za-z0-9+/\n\r]+=*)(\n\r?|\r\n?)(-+END CERTIFICATE-+)/g but the private key file has -----BEGIN PRIVATE KEY----- [...] -----END PRIVATE KEY-----

The comment correctly states that

 /**
   * The PEM encoded private key (string should contain -----BEGIN PRIVATE KEY----- ... -----END PRIVATE KEY-----
   */

To Reproduce Steps to reproduce the behavior:

  1. create pem certificate
    openssl req -x509 -newkey rsa:2048 -keyout keytmp.pem -out cert.pem -days 365 -passout pass:PASSWORD -subj '/CN=SUBJECT'
    openssl rsa -in keytmp.pem -out key.pem -passin pass:PASSWORD 
  2. Open the key.pem. The file has -----BEGIN PRIVATE KEY----- [...] -----END PRIVATE KEY----- content
  3. Ensure the following environment variables are defined: AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_CLIENT_CERTIFICATE_PATH, AZURE_CLIENT_CERTIFICATE_PASSWORD : path to key.pem
  4. Run
    
    import { DefaultAzureCredential } from "@azure/identity";
    import { setLogLevel } from "@azure/logger";
    setLogLevel("info");
    // We're using DefaultAzureCredential but the credential can be any valid `Credential Type`
    const credential = new DefaultAzureCredential({
        loggingOptions: { allowLoggingAccountIdentifiers: true },
    });
    console.log(credential);
    credential.getToken("https://graph.microsoft.com/.default")
        .then(token => console.log(`graphToken: ${token}`))
        .catch(err => {
            console.error("graphToken error:")
            console.error(err)
        });
    credential.getToken("https://graph.microsoft.com/.default")
        .then(token => console.log(`spToken: ${token}`))
        .catch(err => {
            console.error("spToken error:")
            console.error(err)
        });
See errors: 
```bash
AuthenticationError: EnvironmentCredential authentication failed. To troubleshoot, visit https://aka.ms/azsdk/js/identity/environmentcredential/troubleshoot. Status code: 400
More details:
The file at the specified path does not contain a PEM-encoded certificate.

Expected behavior Authentication using Service principal with certificate should work correctly.

Additional context I'm not sure if fixing the regex will fix the problem, but it certainly looks like this might be it =)

xirzec commented 1 year ago

I'm a little rusty in my openssl command line skills, but in your above example wouldn't cert.pem be the file you want to pass along to the credential rather than the key file?

kkazala commented 1 year ago

Thank you for looking into it :) The cert.pem (public key) is uploaded to Service Principal’s certificates, the private key (key.pem) should be referenced in AZURE_CLIENT_CERTIFICATE_PATH https://learn.microsoft.com/en-us/dotnet/api/overview/azure/identity-readme?view=azure-dotnet#service-principal-with-certificate :)


From: Jeff Fisher @.> Sent: Monday, August 14, 2023 7:26:32 PM To: Azure/azure-sdk-for-js @.> Cc: Kinga @.>; Author @.> Subject: Re: [Azure/azure-sdk-for-js] DefaultAzureCredential using Service principal with certificate (Issue #26806)

I'm a little rusty in my openssl command line skills, but in your above example wouldn't cert.pem be the file you want to pass along to the credential rather than the key file?

— Reply to this email directly, view it on GitHubhttps://github.com/Azure/azure-sdk-for-js/issues/26806#issuecomment-1677762754, or unsubscribehttps://github.com/notifications/unsubscribe-auth/AFLD3H7IUQEOKCLAYP2NB7DXVJNURANCNFSM6AAAAAA3PSNUM4. You are receiving this because you authored the thread.Message ID: @.***>

kkazala commented 1 year ago

@xirzec It seems I finally made it work. 💪 Let me share my findings.

By specifying AZURE_CLIENT_CERTIFICATE_PATH we use the following DefaultAzureCredential -> EnvironmentCredential -> ClientCertificateCredential -> MsalClientCertificate

MsalClientCertificate in the init method is:

The certificate under AZURE_CLIENT_CERTIFICATE_PATH must contain both: certificate and private key. Depending on whether the password (AZURE_CLIENT_CERTIFICATE_PASSWORD) is used, it must be either decrypted (no password) or encoded (password).

For example, the following commands:

md temp
cd temp

openssl req `
    -x509 `
    -days 365 `
    -newkey rsa:2048 `
    -keyout keytmp.pem `
    -out cert.pem `
    -subj '/CN=AuthTestWithPassword/C=CH/ST=Zurich/L=Zurich' `
    -passout pass:HereIsMySuperPass

... generate:

To succesfully use the DefaultAzureCredential

Certificates

Set the environment variables:

Either set the env variables using PS, or in the root of your project create .env.cert file with the following content:

TenantName= "{tenant-name}"
AZURE_CLIENT_ID= "{client-id}"
AZURE_TENANT_ID= "{tenant-id}"
AZURE_CLIENT_CERTIFICATE_PATH= "./temp/BOTH.pem"
AZURE_CLIENT_CERTIFICATE_PASSWORD= "HereIsMySuperPass"

AZURE_CLOUD_INSTANCE= "https://login.microsoftonline.com/"
GRAPH_API_ENDPOINT= "https://graph.microsoft.com/"

Test

import dotenv from "dotenv"
import { AccessToken, DefaultAzureCredential } from "@azure/identity";

dotenv.config({ path: "./.env.cert"})
const credential = new DefaultAzureCredential()

const resultGraph: AccessToken = await credential.getToken("https://graph.microsoft.com/.default")
const resultSPO: AccessToken = await credential.getToken(`https://${process.env.TenantName}.sharepoint.com/.default`)

console.log("Auth Graph: ", resultGraph.token.slice(0, 10) + "...")
console.log("Auth SPO: ", resultSPO.token.slice(0, 10) + "...")

You should now see the first 10 digits of the token printed out in the console:

Auth Graph:  eyJ0eXAiOi...
Auth SPO:  eyJ0eXAiOi...

May I ask you a favor?

@xirzec I understand that it's difficult to keep the documentation up to date, but perhaps the team could update it some time soon? =)

Or maybe you want to change the implementation of the MsalClientCertificate and add configuration that accepts thumbprint (we can take it from "Certificates & secrets" and private key?

I'm not closing it yet because it would be absolutely fantastic if the documentation was updated. But please feel free to close it if you want to =)

KarishmaGhiya commented 1 year ago

@kkazala Thanks for the feedback! I'll update the documentation! :)