-
-
Notifications
You must be signed in to change notification settings - Fork 500
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
Authorization code flow with multi-tenancy, database per tenant and claim-based tenant resolution #1699
Comments
Hi @InFarAday, Thanks for your interest! FYI, as mentioned on the sponsors page, there's no support offered for professional projects in the first tier (it's reserved to tier 4 and higher): Feel free to email me for additional details (or if your company would prefer a more traditional support option). |
Thanks for the feedback @kevinchalet, perhaps this is the better option If this qualifies as professional I will pay the t4 but I'll have to do it of my own pocket |
@kevinchalet With that out of the way I'm eager to hear what we can do to solve the issue at hand. |
Hey,
Thanks, much appreciated! 👍🏻
First, it's important to note that Unfortunately, the situation you describe is basically a chicken-and-egg problem, where your Finbuckle configuration depends on the result of There are generally two common ways to approach multi-tenancy with OpenIddict:
Other forms of multi-tenancy - such as the one you describe - are going to be much more complicated to implement, and more fragile. For some scenarios, you could probably use the OpenIddict events model (e.g for the code flow, to resolve the tenant ID from the token payload during the token request, you could likely add a custom handler that would set an TL;DR, you'll probably want to reconsider your approach. Adopting something hostname or Hope that helped. |
This was my exact line of reasoning as well. I was hoping that maybe there was something I would have missed. I found claim-based resolution with a single unified login screen more streamlined in terms of UX but I guess I'll settle for hostname-based resolution, which IMO is the next best option. And indeed it works now with the solution you posted on SO. That does mean I will have to store the tenants' signing keys somewhere though, and I'm not sure I feel comfortable having them as plaintext in the database. Do you have recommendations on that? The only viable non-expensive solution I see is https://github.com/hashicorp/vault (used by GitLab as well IIRC). To wrap it up, I mostly used https://github.com/robinvanderknaap/authorization-server-openiddict/blob/main/AuthorizationServer/Controllers/AccountController.cs as a sample to implement to base myself upon for my solution. My typical user flow with hostname-based tenancy resolution would be:
Does that sound correct to you? Thanks. |
Vault is an excellent option for on-premises deployments. Alternatively, if you host your app on IIS, you could use Windows' built-in X.509 certificates store and deploy certificates yourself, but it's likely more work than going with Vault.
Yep, sounds good. Depending on the scenario, you can also implement the tenant selection logic at the client level if the client itself is tenant-aware, so that the user is redirected to the correct OpenIddict instance without having to set the tenant ID via a form. Cheers. |
Sorry for the late reply, I got sidetracked a bit.
That certainly is an excellent idea, but my client app probably won't be tenant-aware. In any case I'll go ahead and close the issue since I have my answer and I won't have the time to finish this project in the near future. I would still have a final question before closing it however. Thanks and good evening. |
In the typical scenario, the client application is either the frontend (for a SPA) or the backend (a server-side app), but not the two at the same time. In the first case, the OIDC flow is purely handled by the frontend and the identity is generally maintained in the local storage. In the second case, the OIDC flow is purely handled by the backend and the identity is generally stored as an authentication cookie (the backend-for-frontend pattern is an implementation of this approach). The scenario you describe is way more rare in practice and we tend to use an alternative flow to implement it: the hybrid flow (which is basically a code flow where an identity and an access token can be directly returned during the first step, as part of the callback URI). In this scenario, the frontend generally starts the OIDC flow and handles the callback: it extracts the access/identity token(s) from the callback parameters and sends the authorization code to the backend, that will security redeem it to get separate access and identity tokens that won't be shared with the frontend (depending on the scenarios, the tokens retrieved via the backchannel call can give access to more resources than the tokens extracted by the frontend). There's unfortunately a downside with this approach: since the frontend doesn't get a refresh token, you can't get a new access token after the original one expired. In the past, we used
The main advantage with the backchannel part is that confidential web applications (i.e server-side web apps that have been given a client secret) can redeem the authorization code without the access/identity token(s) ever being visible by either the browser or the user himself, since the backchannel call is a direct HTTP request between the web app and the authorization server. Assuming the client secret never leaks, you can be fairly sure that the access token can only be used with the legitimate client itself, and not the user himself or any other party. Hope it was clear 😃 Thanks again for sponsoring the project! |
I see. I typically do my things in microservices sitting behind a gateway, and ideally I'd want the authorization to be centralized on the gateway. Not all of them are to be exposed directly to the user either. For instance, I can have three microservices that are only available with my local network (or VPC) with a rigid definition and no frontend, like APIs, and a fourth that is exposed to the internet through the gateway at app1.example.com and would be an actual proper backend. I was eyeing Google Cloud as a provider since I find it simpler and more interesting to learn. When the user tries to access app1.example.com he would either be logged in already or redirected to the authorization server that would serve him a login page. My idea was that app1 would retrieve the authorization code (so the frontend would have a callback route, get the code and send it to the backend) and exchange it for an access token that is valid for all apps sitting behind the gateway. The apps and the gateway would know each other so that I could have an "admin" app to create users for your tenant, etc. Non-exposed services wouldn't check authentication since they would assume the calling application did it for them. Maybe I should open another issue and explain my project in more detail? If you don't want to mix questions. I'm not sure my initial choice of authorization code flow is the right one anymore. Also, no problem! OpenIddict is great and I'm glad I don't have pay for IdentityServer. I need extensive customization and all the turnkey offerings have proven more complicated than doing it myself given my constraints. |
Woops, sorry for the late reply, I somehow missed the notification 😅
Sure, feel free.
I would personally keep it simple by choosing either the SPA or the backend as the unique client handling the authorization dance from A to Z rather than having an "hybrid" client (for the reasons I mentioned in my previous post):
Cheers. |
Confirm you've already contributed to this project or that you sponsor it
Version
4.x
Question
Hello,
First of all as a disclaimer I must point out that I am neither an expert nor very experienced at all in OAuth or OIDC and that I may sometimes commit mistakes in the process itself ; eventually if my projects are to go public I will arrange to have it audited at minimal cost or will make sure that I did everything correctly. If however you see a simple oversight it would be very kind of you to point it out.
This is a very specific issue that I have been unable to solve so far, and I'll try to be as detailed as possible. I am building, or rather making a POC at this point, of my own authentication server for my future projects. I have not been satisfied with turnkey solutions both because of their price and because I require extensive customization of the user model.
My tool stack regarding the authentication server is:
With this tool stack I intend to use a database per tenant, and a single "tenants" database to hold cross-tenant data. The tenants database should not hold any authentication data, it should only contain a single table with the tenant ids and the connection strings to the related databases. This is a constraint that I have as I want to keep a robust data isolation. This means that, for each tenant, the tenant's authentication data (roles, usernames, passwords...) should be kept strictly within its own database.
I have setup a custom claim called
<namespace>:tenant_id
that should hold the tenant's identifier. Finbuckle is set to resolve the tenant from that. I have two DbContexts:MultiTenancyContext
with a singleTenant
entity that relates to the tenants database as mentioned above. It is only used by Finbuckle to get the tenants' connection strings from their identifiersAuthenticationContext
with the full authentication dataI have setup my DI services so that the tenancy info is dynamically injected when the context is resolved. This looks like this:
ProviderConfiguration
is a custom class which allows me to be provider agnostic by injecting the provider-dependent configuration only at the aggregation root (in Program.cs).HashOrchestrator
is a custom helper because I use Argon2 as my hashing method and I need to have it in myOnConfiguring
to seed some initial data for my migrations.TenantInfo
is, as I said, what allows me to properly register the connection string.For reference OpenIddict is setup this way:
What happens from a linear POV is that when the user is redirected towards the login page, he is asked for three informations: his tenant id, his username and his password. The last two fields are thus unique only within the context of the first one. At this point the
AuthenticationContext
is injected into theLoginController
but its connection string is null. Then the user submits the form and Finbuckle extracts the tenant id from that, as it uses a custom resolution strategy as fallback, like this:As you can see, Finbuckle first tries to extract the tenant name from the OpenIddict's authentication scheme. It falls back on the cookie authentication scheme that is used before retrieving the authorization code (but after checking the credentials), and if that fails it tries to extract it from the form data. This means that once the user has submitted the form I have a working
AuthenticationContext
injected into myLoginController
. I then check that the credentials are correct, and if it is the case I redirect the user to its return url (if whitelisted of course). Excerpt from myLoginController
:After that the user is signed in with a cookie, and goes to
/authorize
to get an authorization code. Once again Finbuckle resolves the tenant from the cookie's claim and a workingAuthenticationContext
is injected in theAuthorizationController
. I can provide the code but the issue message is already long so I won't include it here, it's pretty standard. The user is simply signed-in usingOpenIddictServerAspNetCoreDefaults.AuthenticationScheme
.So far everything works and has been successfully implemented and tested under the condition that I remove the first claim strategy (the one that uses OpenIddict's autentication scheme). But eventually the tenant will have to be resolved from something else than the cookie ; this is the case with the call at
/token
. Finbuckle has a middleware to inject the tenant called in Program.cs withUseMultiTenant()
. This middleware iterates over each strategy and tries to find one that returns a proper tenant identifier. For claim strategies, it tries to authenticate the user usingHttpContext.AuthenticateAsync()
. For cookies the authentication silently fails and Finbuckle tries the next strategy. With OpenIddict however I get the following error:While misguided this provides a clue as to what OpenIddict expects: that no authentication should happen before the authentication middleware
UseAuthentication
has run. Obviously I tried placingUseAuthentication()
higher thanUseMultiTenant()
but this cannot work because OpenIddict tries to make a database call with a context that has anull
connection string, as Finbuckle hasn't retrieved it yet.Just to be clear:
WithClaimStrategy(MultiTenancyClaims.TenantId, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)
is kept the authentication server does not even launch, or rather I get the error above when ASP tries injecting theAuthenticationContext
in theLoginController
that is used to both display the login form and validate the credentials./token
, that is once the user is signed-in using OpenIddict's scheme. After that Finbuckle can't resolve the tenant from the cookie anymore.I hope my issue is as clear as it can be. I don't think we will be able to solve it exactly the way I want, so I'm open to suggestions or alternative ways to do it. But I very much would like to keep my tenancy claim-based, whilst also having a database per tenant in regard to authentication data. I've thought of moving OpenIddict to the cross-tenant database but this would require moving
UseAuthentication()
aboveUseMultiTenant()
and I'm not sure it would solve anything or be secure.Thank you very much.
The text was updated successfully, but these errors were encountered: