Comments (6)
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.
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()
orAddDevelopmentSigningCertificate()
- or the other overloads that accept aX509Certificate2
or aSecurityKey
- 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 toOpenIddictClientRegistration.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.
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.
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.
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.
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
andBackchannelAccessToken
.
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)
- Data Protection causes claim value type to default to string HOT 3
- Signing Key Store HOT 1
- openiddict.entityframeworkcore is missing NuGet package README file HOT 1
- How to configure certificate validation? HOT 6
- integration openiddict identityServer .net core 7 with client .net framework 4.8 HOT 1
- Oracle doesn't support `RepeatableRead` isolation level. HOT 3
- I'm unable to get the authorization code when trying to authenticate an application with OpenIdDict HOT 4
- How to make the BFF Proxy Sample Dantooine work? HOT 65
- SPA and BFF on different sub-domains, BFFs for mobile apps HOT 6
- Getting The security token is missing during login validation on postman client authorization or web client authorization HOT 2
- InvalidOperationException: Cannot redirect to the authorization endpoint, the configuration may be missing or invalid. HOT 7
- OpenIddict + SPA UI Question HOT 9
- Remove `Uri.IsWellFormedOriginalString()`/`Uri.IsWellFormedUriString()`
- The specified token is invalid since we renewed SSL certificate in Azure key vault HOT 14
- invalid_token returned by authorization callback HOT 14
- Claims not found in token of external provider (Microsoft) HOT 5
- unauthorized_client when changing url HOT 6
- Using "role" claims from external providers in access tokens HOT 7
- Update the OpenIddict client ASP.NET Core/OWIN integrations to support overriding the requested scopes via `AuthenticationProperties`
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from openiddict-core.