Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature Request] Help certificate rotation of client certificates #956

Closed
jmprieur opened this issue Feb 11, 2021 · 12 comments
Closed

[Feature Request] Help certificate rotation of client certificates #956

jmprieur opened this issue Feb 11, 2021 · 12 comments
Assignees
Labels
enhancement New feature or request
Milestone

Comments

@jmprieur
Copy link
Collaborator

Is your feature request related to a problem? Please describe.
Today, Microsoft.Identity.Web:

  • gets certificates from KeyVault
  • helps rotating decrypt certificates (by enabling passing several certificates)

For the client certificates, it also supports send5xc, however, it could help a bit more by fetching from keyvault a newer version of a certificate when the confidential client flow fail because of certificate expiry or invalidation.

Describe the solution you'd like
When:

  • the certificate proposed by Microsoft.Identity.Web to prove the app identity is rejected by Azure AD (for instance because it was revoked in the app registration, or a newer version is available in KeyVault), Microsoft.Identity.Web should try to refetch the certificate from KeyVault.

Describe alternatives you've considered
Subscribe to KeyVault events, but this would require too much configuration from customers

@jmprieur jmprieur added the enhancement New feature or request label Feb 11, 2021
@jmprieur jmprieur added this to the 1.9.0 milestone Mar 4, 2021
@jmprieur
Copy link
Collaborator Author

jmprieur commented Mar 15, 2021

What happens:

  1. Create a self signed certificate which will expire soon. For instance if I want the cert to expire today at 4:05 pm. The date/time needs to be adapted to your locale (you might need to wrote "03/15/2021 4:05 pm" in the US
 $cert=New-SelfSignedCertificate -Subject "Expired" -CertStoreLocation "Cert:\CurrentUser\My"  -KeyExportPolicy Exportable -KeySpec Signature -NotAfter "15/03/2021 16:05"
  1. Using the certificate manager, export with private key (.pfx) and without (.cer).

  2. Add the certificate (.cer) to the client certificate of the app to test before the certificate expires.

  3. Upload the certificate with private key (.pfx) to KeyVault.

  4. Upload the appsettings.json

    // Thumbprint = F5222C7A7A03ED1C4CA51AADA4D931D02CDC5365, CN=Expired-03-16-21
    "ClientCertificates": [
    {
        "SourceType": "KeyVault",
        "KeyVaultUrl": "https://webappsapistests.vault.azure.net",
        "KeyVaultCertificateName": "Expired-03-16-21"
     }
    ]
  5. After the certificate expires, run the app, and observe that there is an MsalUiRequiredException:
    StatusCode: 401 ResponseBody: {"error":"invalid_client","error_description":"[Reason - The key used is expired., Thumbprint of key used by client: 'F5222C7A7A03ED1C4CA51AADA4D931D02CDC5365', Found key 'Start=03/16/2021 12:32:24, End=03/16/2021 13:00:00', "

Proposal

  1. When we load the certificates from KeyVault, if they are not yet valid, or no longer valid, log a warning, and try the next certificate in the array. For this we should return null when the certificate is invalid from a time point of view, at this line:

    Time Validity

    With certificate defined here:

    KeyVaultCertificateWithPolicy certificate = certificateClient.GetCertificate(certificateName);

    compare certificate.Properties.NotBefore and certificate.Properties.ExpiresOn against the current date

  2. Change the logic of LoadFirstCertificate so that it loads the first valid certificate in the array

    CertificateDescription certDescription = certificateDescription.First();
    defaultCertificateLoader.LoadIfNeeded(certDescription);

    The 2 lines above would be replaced by

    CertificateDescription certDescription = certificateDescription.First( defaultCertificateLoader.LoadIfNeeded(certDescription) );

    where CertificateLoader.LoadIfNeeded(certDescription) would return a bool. We might need a new method, as this would be a breaking change in the interface?

  3. When Azure AD returns the AADSTS700027 exception (with "Reason - The key used is expired."), we would reset the certificate description (null the certificate in the certificate description), and retry. The certificate would be re-loaded if needed, which would take the rotated certificate, or fail that time because none of the certificate is valid. For example, in web apps, in:

    we could add handling MsalUiRequiredException:

    catch(MsalUiRequiredException ex2)
    {
     DefaultCertificateLoaderResetCertificates(_microsoftIdentityOptions.ClientCertificates);
    
     // Retry
     AddAccountToCacheFromAuthorizationCodeAsync(context, scopes);
    }

@wuwei8372
Copy link

A few questions:

  1. Looks like proposed method will only re-load the certificate when the current certificate is expired or invalid. If a new version of certificate is available and the old version is still valid, could we get the new certificate when calling LoadIfNeeded? (Probably not, right?)

  2. Will this proposed method also work for non-aad certificate? For example a client certificate used for service to service call?

  3. Could we possibly still check the certificate.Properties.NotBefore after line 117 in DefaultCertificateLoader.cs even CertificateDescription.Certificate is not null when calling LoadIfNeeded? Then if MIW finds the newest certificate from AKV has a newer expire date than the current CertificateDescription.Certificate, it can replace CertificateDescription.Certificate with the newer version from AKV. Could it be way to roll forward to the new cert even the current CertificateDescription.Certificate is still valid?

Thanks!

@jmprieur
Copy link
Collaborator Author

Thanks for these questions, @wuwei8372

  1. Would you think that we'd want to re-check the certificate periodically?
  2. You can use the DefaultCertificateLoader to load certificates.
  3. I think that this is related to question 1). I'd think that, in that case, we'd want to also keep a LKG (Last known good) certificate in case Azure AD's application was not yet updated ?

@wuwei8372
Copy link

Thanks for replay @jmprieur!

  1. It would be great if we could re-check the and update certificateDescription.Certificate periodically to make it up-to-date.
  2. Good to know we could use DefaultCertificateLoader load client certificates for S2S call as well.
  3. Yes it is related to question 1. I assume LKG means the latest valid certificate in AKV, correct? If we can periodically check and put the latest certificate from AKV in certificateDescription.Certificate, then the App referring to certificateDescription.Certificate would always use the lasted cert from AKV, which will be great.

@jmprieur
Copy link
Collaborator Author

Yes @wuwei8372, LKG = Last Known Good (the latest valid certificate in AKV). The issue is, the certificate could be valid in AKV, but not registered as a client credential in the app registration for the app.

@wuwei8372
Copy link

@jmprieur, could we do the check to update certificateDescription.Certificate when LoadIfNeeded(CertificateDescription) is called? So that we could use CertificateDescription.KeyVaultCertificateName to find the cert in AKV even if the cert is not registered in the app registration? Then in our code we could call loadIfNeeded periodically, or just before sending each request to update certificateDescription.Certificate.

@jmprieur
Copy link
Collaborator Author

@wuwei8372, we could add a boolean to do the check. I don't think we'd want to check keyvault each time we call LoadIfNeeded, because this would be basically each time we validate a token or acquire a token ...

@wuwei8372
Copy link

@jmprieur it makes sense to add a boolean to LoadIfNeeded to determine if checking in AKV is needed or not. I think it would work for us.

@wuwei8372
Copy link

@jmprieur could we have a cache behind LoadIfNeeded for certificate when checking certificate in AVK, so we don't have to call AKV every time if the cache is valid?

@jmprieur
Copy link
Collaborator Author

yes, that's what I mean, @wuwei8372 by LKG :)

@jennyf19 jennyf19 modified the milestones: 1.9.0, 1.10.0 Mar 31, 2021
@jmprieur
Copy link
Collaborator Author

jmprieur commented May 5, 2021

Things to test: The certificates:

  • can be invalid (expired)
  • can be not loadable (from KeyVault for instance)
  • can be loadable, but not added to the app registration

Depending on all these cases, the error message should help the developer to troubleshoot.

Certificates pattern in ClientCertificates In KeyVault in app registration Outcome
1 valid certificate Yes Yes Works
1 expired certificate, 1 valid certificate Yes Yes Works
1 valid certificate No Yes Exception when acquiring token: RequestFailedException: Service request failed. Status: 400 (Bad Request) coming from KeyVault with some HTML explication.
1 valid certificate Yes No Exception when acquiring token: Client assertion contains an invalid signature. [Reason - The key was not found.
1 expired certificate Yes Yes Exception when acquiring token: ArgumentException: All client certificates passed-in in the configuration have expired or can't be loaded.
1 expired certificate, 1 valid certificate Yes No Exception when acquiring token: Client assertion contains an invalid signature. [Reason - The key was not found.

This was referenced May 6, 2021
@jennyf19
Copy link
Collaborator

included in 1.10.0 release

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants