Giter Club home page Giter Club logo

Comments (6)

karlschriek avatar karlschriek commented on June 2, 2024 1

Yes, that is much clearer thank you. I can also see this implemented clearly in the Zirku example. Directly after Zirku.Client1 calls AuthenticateInteractivelyAsync the response includes tokens, BackchannelIdentityToken and BackchannelAccessToken.

BackchannelAccessToken is signed and encrypted and includes scopes authorizing its use at the "api1" and "api2" resource servers. "api2" can decrypt it directly (it has the symmetric key), "api1" decrypts it using introspection (which I think means it asks the auth server to decrypt and send back the decrypted token). Both validate the signature using discovery.

BackchannelIdentityToken is just signed. The client can use it to validate that the principal has logged in, and can scrutinize it for claims about the principal's identity. It validates the token using discovery.

Thanks for taking the time. It is now much clearer to me what the implications of AddSigningKey / AddEncryptionKey at server/client/api are.

from openiddict-core.

kevinchalet avatar kevinchalet commented on June 2, 2024

Hey,

Thanks for sponsoring the project 👍🏻

How do we add a KeyVaultSecurityKey as a signing key?

This type is not supported as it doesn't inherit from AsymmetricSecurityKey.

FWIW I considered replacing the is AsymmetricSecurityKey check by IsAlgorithmSupported(RSA+SHA/ECDSA+SHA) calls, but the ICryptoProvider that you're supposed to use with that type - KeyVaultCryptoProvider - doesn't implement any real validation (it just returns true if a valid JWS or JWE algorithm is specified, independently of the type or capabilities of the KeyVault key: https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/e1f029f99a2c8d90874641de5a407c95e1172443/src/Microsoft.IdentityModel.KeyVaultExtensions/KeyVaultCryptoProvider.cs#L77-L87).

That said, it doesn't mean you can't use AKV keys - software or HSM - in OpenIddict. In fact, it's very simple: here's an example with RSA (HSM) keys:

var client = new KeyClient(new Uri("https://your-vault-id.vault.azure.net/"), new DefaultAzureCredential());

options.AddEncryptionKey(new RsaSecurityKey(client.GetCryptographyClient("rsa-hsm-encryption-key").CreateRSA()));
options.AddSigningKey(new RsaSecurityKey(client.GetCryptographyClient("rsa-hsm-signing-key").CreateRSA()));

Note: this requires using the Azure.Security.KeyVault.Keys SDK. If you're still using the old/legacy/deprecated Microsoft.Azure.KeyVault SDK, see https://kevinchalet.com/2017/08/15/using-azure-key-vault-with-asos-and-openiddict/ for a different approach using RSAKeyVaultProvider.

How do we add it to clients/apis for validation? Do we need to export its public key then add that using AddSigningKey? Do we need to create a custom validation? Or could we just set SetIssuer and UseSystemNetHttp as described here: https://documentation.openiddict.com/configuration/encryption-and-signing-credentials.html#using-openid-connect-discovery-asymmetric-signing-keys-only

Clients can download the public part of the AKV-protected RSA signing key via OIDC discovery and they don't need an access to the server RSA encryption key used to protect the access tokens (since they are not supposed to read them) so no specific configuration is required: setting the issuer and using the System.Net.Http integration - as you mentioned - is enough.

APIs can also download the RSA signing key via OIDC but not the encryption key. You'll need to register it using services.AddOpenIddict().AddValidation().AddEncryptionKey(new RsaSecurityKey(client.GetCryptographyClient("rsa-hsm-encryption-key").CreateRSA())).

In the Velusia sample there is an AddClient() section in Velusa.Server (and also in Velusia.Client of course). In that section we also see encryption and signing certs being registered (with options.AddDevelopmentEncryptionCertificate().AddDevelopmentSigningCertificate();). Just to be clear, these are supposed to correspond with the certs in AddServer(), right? I.e. the client doesn't actually use them for any signing/encryption, just for validation/decryption? We ask because the words "Encryption" and "Signing" have a very specific one-way meaning, but in the context of a client/api it must surely mean the reverse unless we have fundamentally misunderstood something.

There are multiple keys/certificates used by the client:

  • The global, non-registration-specific keys/certificates added by AddDevelopmentEncryptionCertificate() or AddDevelopmentSigningCertificate() - or the other overloads that accept a X509Certificate2 or a SecurityKey - are used by the client to sign, encrypt, validate and decrypt the state tokens it creates to prevent CSRF/session fixation attacks. These keys are not used to validate or decrypt tokens returned by the remote server.

  • For scenarios where client assertions are used, per-registration encryption and signing keys must be added via OpenIddictClientRegistration.Encryption/SigningCredentials.

  • The signing keys used to verify tokens returned by the authorization server are not attached to OpenIddictClientOptions but are dynamically downloaded via OIDC discovery or manually attached to OpenIddictClientRegistration.Configuration.SigningKeys for servers that don't support discovery.

For the first type of keys/certificates, nothing prevents you from reusing the same keys as the server stack, but I'd recommend using a separate/different set of credentials. That's what the AddDevelopmentEncryptionCertificate()/AddDevelopmentSigningCertificate() helpers do internally.

Again here, if we use SetIssuer and UseSystemNetHttp() then we can leave out explicitly adding the signer key; but for encryption (if using a symmetric key) we'll alway have to add it?

Clients don't need (and actually should never attempt) to read access tokens and identity tokens are not encrypted so they don't need to decrypt any server-returned token. So yeah, calling these two APIs is enough.

Lastly a small question about about the difference between "AddValidation" and "AddClient". Can we think about this as simply as "AddValidation" is meant for APIs and "AddClient" is meant for UIs?

Yeah, pretty much (tho' the client can also be used for non-interactive flows like the client credentials grant, which is mostly used to retrieve an access token for machine-to-machine communication, so there's no real "UI" in that case).

Hope it'll help clarify things a bit 😄

from openiddict-core.

kevinchalet avatar kevinchalet commented on June 2, 2024

Note: you're probably already aware of that, but AKV enforces strict limits that can make it unusable for public/high-throughput scenarios: https://learn.microsoft.com/en-us/azure/key-vault/general/service-limits. (e.g for 2048-bit RSA HSM keys: only 2000 operations in 10 seconds). Depending on the flow you're using, a bunch of operations will be required (e.g for the hybrid flow, up to 6 tokens can be generated), so it's definitely something to keep in mind.

from openiddict-core.

karlschriek avatar karlschriek commented on June 2, 2024

Thanks for the comprehensive answer! Yes, we are aware of the AKV service limits, but the fact that up to six tokens could generated is news to us, and might in that case change the calculation a bit. I will attempt your suggested approach using var client = new KeyClient(new Uri("https://your-vault-id.vault.azure.net/"), new DefaultAzureCredential()); in the next few days.

I would like to ask more clarifying questions about the AddSigningKey / AddEncryptionKey though. You state that:

The global, non-registration-specific keys/certificates added by AddDevelopmentEncryptionCertificate() or AddDevelopmentSigningCertificate() - or the other overloads that accept a X509Certificate2 or a SecurityKey - are used by the client to sign, encrypt, validate and decrypt the state tokens it creates to prevent CSRF/session fixation attacks. These keys are not used to validate or decrypt tokens returned by the remote server.

So, if on the client/api I specify AddSigningKey(...), then this key has nothing to do with validating the tokens that were signed by by the server? How would I approach it then if we for some reason do not which to use key discovery. How would I manually register the public key to use for validation? You mention one could attach them to OpenIddictClientRegistration.Configuration.SigningKeys, but it is not clear to me what this means in practice. Could you provide an example?

Further, you also mention:

Clients don't need (and actually should never attempt) to read access tokens and identity tokens are not encrypted so they don't need to decrypt any server-returned token.

If this is true I have misunderstood something pretty fundamental. It was my understanding that the Server signs the token (asymmetrically with private key) and then encrypts it (symmetrically) and that the client then uses the (symmetric) encryption key to decrypt it and then validates the signature (asymmetrically with public key). Also if a client does not read the token, how does it gain access to the payload (claims etc.)?

If I pull the token from the web browser (or from httpcontext for that) matter, I definitely get an encrypted JWE. If the server did not encrypt this, then was it encrypted by the client instead? What would be the point of AddEncryptionKey on the Server side in that case?

from openiddict-core.

kevinchalet avatar kevinchalet commented on June 2, 2024

So, if on the client/api I specify AddSigningKey(...), then this key has nothing to do with validating the tokens that were signed by by the server?

Yes!

How would I approach it then if we for some reason do not which to use key discovery. How would I manually register the public key to use for validation? You mention one could attach them to OpenIddictClientRegistration.Configuration.SigningKeys, but it is not clear to me what this means in practice. Could you provide an example?

Sure, here's an example of a client registration using a static configuration (with one RSA signing key attached):

options.AddRegistration(new OpenIddictClientRegistration
{
    Issuer = new Uri("https://localhost:44395/", UriKind.Absolute),
    ProviderName = "Local",
    ProviderDisplayName = "Local OIDC server",

    ClientId = "mvc",
    Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" },

    RedirectUri = new Uri("callback/login/local", UriKind.Relative),
    PostLogoutRedirectUri = new Uri("callback/logout/local", UriKind.Relative),

    Configuration = new OpenIddictConfiguration
    {
        Issuer = new Uri("https://localhost:44395/"),
        AuthorizationEndpoint = new Uri("https://localhost:44395/connect/authorize"),
        TokenEndpoint = new Uri("https://localhost:44395/connect/token"),
        IntrospectionEndpoint = new Uri("https://localhost:44395/connect/introspect"),
        EndSessionEndpoint = new Uri("https://localhost:44395/connect/logout"),
        RevocationEndpoint = new Uri("https://localhost:44395/connect/revoke"),
        UserinfoEndpoint = new Uri("https://localhost:44395/connect/userinfo"),
        DeviceAuthorizationEndpoint = new Uri("https://localhost:44395/connect/device"),
        ScopesSupported = { Scopes.OpenId },
        ResponseTypesSupported = { ResponseTypes.Code },
        CodeChallengeMethodsSupported = { CodeChallengeMethods.Sha256 },
        AuthorizationResponseIssParameterSupported = true,
        SigningKeys =
        {
            JsonWebKey.Create($$"""
            {
                "kid": "BEF5F37DBE811EBB191C81A2DB5F0D5E826AC3CF",
                "use": "sig",
                "kty": "RSA",
                "alg": "RS256",
                "e": "AQAB",
                "n": "0YFZB4JLbzUkHkimnI70kM40PPOPb5RFbE0dKzZJndLl97lwFu5QlX126FFuqK0KWXd2YFv_w9u48vrMXhvgA1FW4YA2CsmabaoQ7dMeOZxg3GJem-BWWX3RUI8nS2XlLy-SNBkd3Eq1JVqsuFYZbA4pY5ybx0cKKVmf3TiLcugSxCI-btjxeBj9TARrEwerOiZd3iXAMBpBUxKiKWYyMqw_5MskQALtlRxCvu6l-C3IItUIJQSeedrq1fz_Y2iAQ-eAe4IvPQYki6LavCKu7W2AcgwLE4K3C1yJRA5qbAHHBZ_S5Rim36qdjh5hCqR6Zf2TyIQdGMU_sjeGeK1HKQ",
                "x5t": "vvXzfb6BHrsZHIGi218NXoJqw88",
                "x5c": [
                "MIIC9DCCAdygAwIBAgIIRWvT0LbQnxQwDQYJKoZIhvcNAQELBQAwMDEuMCwGA1UEAxMlT3BlbklkZGljdCBTZXJ2ZXIgU2lnbmluZyBDZXJ0aWZpY2F0ZTAeFw0yMzEyMjExNDEyMDNaFw0yNTEyMjExNDEyMDNaMDAxLjAsBgNVBAMTJU9wZW5JZGRpY3QgU2VydmVyIFNpZ25pbmcgQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDRgVkHgktvNSQeSKacjvSQzjQ8849vlEVsTR0rNkmd0uX3uXAW7lCVfXboUW6orQpZd3ZgW//D27jy+sxeG+ADUVbhgDYKyZptqhDt0x45nGDcYl6b4FZZfdFQjydLZeUvL5I0GR3cSrUlWqy4VhlsDiljnJvHRwopWZ/dOIty6BLEIj5u2PF4GP1MBGsTB6s6Jl3eJcAwGkFTEqIpZjIyrD/kyyRAAu2VHEK+7qX4Lcgi1QglBJ552urV/P9jaIBD54B7gi89BiSLotq8Iq7tbYByDAsTgrcLXIlEDmpsAccFn9LlGKbfqp2OHmEKpHpl/ZPIhB0YxT+yN4Z4rUcpAgMBAAGjEjAQMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQsFAAOCAQEAW61UYk6cjoFyNsU+CrwP0an4avf0vovWep1kYOz+4lfxarBYRAd33vHVll80Pu+wCv0upinLIQIyls/E6eqFN2ddWpszSYJH5XtJ1w+701gpEkmI5VwtD0Ozrrq0D+C+n9C+FlkhJWUa3IzwBPQg7dMeVqTSTaCWDKZ/5zK9QpQKCbqJ1Mksyy/6Oo+PGPv01EFGvnsNuceoYYjdDss0MmCb8FZz/mg01LJ1bqJdzkpcJLBnbXMje+zCHcY7oEjBlGRNx0BM1TBcha0QwApQ2hvZstlEA83z31l8Q44eb03VKKrr5QEAkFJ7oB4rYYiX5NKSeTIliG+w+hK0BqWalQ=="
                ]
            }
            """)
        }
    }
});

If this is true I have misunderstood something pretty fundamental. It was my understanding that the Server signs the token (asymmetrically with private key) and then encrypts it (symmetrically) and that the client then uses the (symmetric) encryption key to decrypt it and then validates the signature (asymmetrically with public key). Also if a client does not read the token, how does it gain access to the payload (claims etc.)?

If I pull the token from the web browser (or from httpcontext for that) matter, I definitely get an encrypted JWE. If the server did not encrypt this, then was it encrypted by the client instead? What would be the point of AddEncryptionKey on the Server side in that case?

You're very likely confusing yourself by saying "the token": there's no unique token involved, there are multiple types of tokens. Two of them are relevant here:

  • Access tokens, that are meant to only be read by resource servers (i.e .AddValidation() for an OpenIddict-based app). They are received by clients that can attach them to API requests: clients should never try to read their content (that may not be a standard format or may be opaque depending on the implementation). By default, OpenIddict encrypts these tokens using a server key that must be shared with the API servers.

  • Identity tokens, that are meant to only be read by clients and serve as a way to identify the user. While these tokens could be encrypted using a client-specific key, in practice most implementations don't encrypt them so the clients don't need to decrypt them to be able to read the claims they contain.

Hope it's clearer 😃

from openiddict-core.

kevinchalet avatar kevinchalet commented on June 2, 2024

Yes, that is much clearer thank you. I can also see this implemented clearly in the Zirku example. Directly after Zirku.Client1 calls AuthenticateInteractivelyAsync the response includes tokens, BackchannelIdentityToken and BackchannelAccessToken.

BackchannelAccessToken is signed and encrypted and includes scopes authorizing its use at the "api1" and "api2" resource servers. "api2" can decrypt it directly (it has the symmetric key), "api1" decrypts it using introspection (which I think means it asks the auth server to decrypt and send back the decrypted token). Both validate the signature using discovery.

BackchannelIdentityToken is just signed. The client can use it to validate that the principal has logged in, and can scrutinize it for claims about the principal's identity. It validates the token using discovery.

Your understanding is 100% exact 👍🏻

Thanks for taking the time.

My pleasure! Thanks for sponsoring the project! ❤️

from openiddict-core.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.