-
Notifications
You must be signed in to change notification settings - Fork 3k
WIP: Describe concepts of MultiTenancy applications #33355
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
base: main
Are you sure you want to change the base?
Conversation
🎊 PR Preview c23c27c has been successfully built and deployed to https://quarkus-pr-main-33355-preview.surge.sh/version/main/guides/ |
Hey @lennartj |
@lennartj This is a great effort, very informative, reviewing now. @michelle-purcell @sheilamjones Can you have a look please at this PR as well as you have worked with OIDC docs before and because the OIDC multi-tenancy doc was one of the candidates for the refactoring ? Thanks |
:categories: security | ||
:summary: This overview explains OpenID Connect multi-tenancy for Quarkus applications | ||
|
||
The goal of OIDC multi-tenancy is developing an application which can provide functionality to users from different organisations. Each enabled organisation could store its registered users and their credentials in its own `realm`, and use OIDC to supply security-related user information with the application. Each call to the application must therefore contain at least two kinds of information: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we replace realm
with tenant
as realm
is quite a Keycloak specific term. And add a Note clarifying that if you work with Keycloak, realm
typically represents a tenant
, something like that ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Each call to the application must therefore contain at least two kinds of information
- the tenant id and ID token listed below are only available when Quarkus itself does the authorization code flow and the user is already authenticated - but the tenant specific authentication should also be activated. And as you mention below, in a Bearer authentication case, ID token won't be available.
I wonder, at this point, should the doc focus on the necessity of providing a tenant identifier and clarify in the sentence you already added about the tenant id below how it can be obtained, please see my comment below
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we replace
realm
withtenant
asrealm
is quite a Keycloak specific term. And add a Note clarifying that if you work with Keycloak,realm
typically represents atenant
, something like that ?
Good feedback! Will fix wording on tenant, and add an info blurb on the domain terms of tenants in some IDP:s
🤠
|
||
The goal of OIDC multi-tenancy is developing an application which can provide functionality to users from different organisations. Each enabled organisation could store its registered users and their credentials in its own `realm`, and use OIDC to supply security-related user information with the application. Each call to the application must therefore contain at least two kinds of information: | ||
|
||
* A `tenant identifier` which uniquely identifies the realm where the end user was authenticated / logged in. While you can technically use any means of sending the `tenant identifier` from the user's browser or other client-side application, it is convenient to insert it as a string within the context path of the service. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a very nice, compact summary - but it is only the case, where a tenant id has to be extracted (typically) from the HTTP request URL, is for bearer token authentication requests or initial
non-authenticated requests to Quarkus quarkus.oidc.application-type=web-app
endpoints which drive the code flow themselves (example, we'd like users authenticate to Google or Azure, or specific Keycloak realms, etc).
Once a Quarkus quarkus.oidc.application-type=web-app
user has authenticated for example to Google, then they can access the application URL space without google
being encoded in URLs - it will be encoded in the session cookie name and Quarkus will itself extract it and make it available to tenant resolves (as a Routing Context tenant-id
attribute - this is a too technical detail though for this doc)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good feedback, here too. I Will rephrase a tad.
However - the case of the CORS preflight request comes to mind. The session cookie Will not be sent by a browser to the resource server. So the preflight call Will not be interpreted correctly by the TenantResolver if using a session Cookie.
Am i missing parts of the flow here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lennartj But the preflight request should end at the CORS filter level and return, so it should work
|
||
. Each user has logged in using their organisation's own single-sign ("SSO"), which is frequently called OIDC Authentication Provider (called `"OpenID Provider"`, `"Identity Provider"` or `"IdP"` in OIDC literature). Upon login, the SSO has produced an `IdToken` JWT with user data and delivered that to the user's web browser. | ||
. The two user will call the calendar service with _different resource paths_ within the service URL to reach the calendar service. The application uses the `tenant identifier` part of the URI to identify the caller's organisation. Should an authenticated user from organisation 1 accidentally use a resource path allocated to organisation 2, the call will yield an unauthenticated HTTP response. | ||
. The `IdToken` data retreived from the own organisation's SSO is normally sent to the multi-tenant application with each call as an HTTP header, called `"authorization:"`. In this respect, an IdToken works as a session cookie which contains user information. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure about IdToken
references. SPA will get both ID and access tokens with the authorization code flow, with the traditional approach being is that it will itself use ID token to interact with the user but use the access token to access Quarkus to fetch something for the user.
SPA can surely post an ID token to Quarkus too, but unless a dedicated call like useThisIDToken
is made to the application, as far as SPA passing ID token to Quarkus is concerned, it travels as a Bearer token - i.e Quarkus will treat it as the access token.
Note we have OIDC bearer and code flow guides - and I feel we should leave the aspects of dealing with the actual tokens to those guides (improve it as needed there) - here, in a nice new OIDC multi-tenancy concept doc you introduce with this PR, I believe we should highlight the multi-tenancy itself - which is done very well.
What do you think ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could reference other guides if appropriate.
I would suggest We spin this guide as a story through multi-tenancy, as sen from the developer's perspective.
Will take a look and nudge descriptions in other guides so they harmomize
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lennartj What can make it simpler is for this concept doc to focus purely on explaining the multitenancy idea; a lot what you typed in that regard would make it a perfect concept doc wh8ch would accompany the already existing multitenancy how to doc.
Everything related to the actual token treatment, should be applied where necessary in follow up PRs to the bearer and code flow guides IMHO, it will be easier to merge this PR that way 🙂
. The two user will call the calendar service with _different resource paths_ within the service URL to reach the calendar service. The application uses the `tenant identifier` part of the URI to identify the caller's organisation. Should an authenticated user from organisation 1 accidentally use a resource path allocated to organisation 2, the call will yield an unauthenticated HTTP response. | ||
. The `IdToken` data retreived from the own organisation's SSO is normally sent to the multi-tenant application with each call as an HTTP header, called `"authorization:"`. In this respect, an IdToken works as a session cookie which contains user information. | ||
|
||
With every call, the multi-tenant application can translate the `tenant identifier` "org1" to imply that the user is logged in to the SSO of organisation 1, and "org2" to the SSO of organisation 2. The multi-tenant application can also verify that the user information in the `IdToken` is not changed, and extract the data within it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The actual token verification (signature, etc) is controlled by Quarkus, users can affect the way the verification is done but typically, it would be implicit for a typical user application :-)
|
||
Quarkus multi-tenant applications need to perform several tasks with each call from a client to provide a simple and consistent development experience. Understanding the sequence of those steps helps designing multi-tenant applications correctly; the conceptual models described in the OIDC and OAuth2 specifications is huge and illustrating the steps required by development teams helps to clarify multi-tenant application development. | ||
|
||
In this example, our multi-tenant application is designed to serve RESTful resources to already logged-in users. This mode of operation is called `Bearer authentication` in OIDC and indicates that an HTTP header called `"authorization:"` with the value `"Bearer *encoded JWT token*"` must be present in each request from the client to the application. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A good number of providers (Auth0, Google, Github, etc) would only have opaque/binary access tokens. So I'd clarify it
docs/src/main/asciidoc/security-openid-connect-multitenancy-concept.adoc
Outdated
Show resolved
Hide resolved
docs/src/main/asciidoc/security-openid-connect-multitenancy-concept.adoc
Outdated
Show resolved
Hide resolved
docs/src/main/asciidoc/security-openid-connect-multitenancy-concept.adoc
Show resolved
Hide resolved
docs/src/main/asciidoc/security-openid-connect-multitenancy-concept.adoc
Show resolved
Hide resolved
…ncept.adoc Co-authored-by: Sergey Beryozkin <[email protected]>
…ncept.adoc Co-authored-by: Sergey Beryozkin <[email protected]>
==== | ||
|
||
|
||
The only required method to implement is `Uni<OidcTenantConfig> resolve(RoutingContext routingContext, OidcRequestContext<OidcTenantConfig> requestContext);`. If possible, cache realm IDs and their corresponding `OidcTenantConfig` at application launch to yield good performance during application runtime. A simple structure for resolving tenant configuration is shown below. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a good example but I believe we should clarify here it is a Keycloak specific example and that users can apply a similar technique when working with other providers
|
||
[source,kotlin] | ||
---- | ||
@Singleton |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is interesting, what I'm not sure, if the example should be in Java... @gsmet, @maxandersen - should we have tabs like we have for Maven/Gradle/CLI for code as well, Java/Kotlin/etc ?
|
||
This implementation concludes steps `1` in Figure 2 above. Quarkus is now aware of the tenant identifier/realm, and has resolved the configuration for the OIDC provider which authenticated the calling user. | ||
|
||
=== 2. Retrieve injected IdToken and user information |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an interesting summary that follows below, but I think it is off-topic for the multi-tenancy concept you introduce here, things like how to access the tokens, difference between them, etc. I'd like to propose for you to have a look at the current OIDC Bearer and Code flow tokens and reapply some of your proposed text below, does it sound reasonable ? Here it gets a bit technical while the main purpose is to explain the multi-tenancy concept
|
||
. `Inject the AccessToken` typecast to a `JsonWebToken`, which holds information from OIDC provider with simple access methods. This is a `@RequestScoped` CDI bean, which is automatically provided by the `quarkus-oidc` extension. As discussed above, the access token holds user authorization information, so it is the best option to use when validating that the user is authorized to perform operations within the multi-tenant service. | ||
. _(Preferred):_ `Inject a custom JsonWebToken wrapper` exemplified by a TokenBasedCallerInfo below. Depending on your corporate/business requirements | ||
. _(Discouraged):_ `Inject the IdToken` typecast to a `JsonWebToken`, which holds information from the IdToken generated by the OIDC provider. This is a `@RequestScoped` CDI bean, which is automatically provided by the `quarkus-oidc` extension. You need to qualify the injection point with `@IdToken` - but OAuth2 authentication information is not necessarily part of the IdToken as discussed above. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I said above, I think all the text related to the treatment of tokens should be adapted to the bearer and code guides. Also I'd not qualify it as Discouraged
- for most users not working with Oauth2 but certified OIDC providers it will be a real ID token.
|
||
=== 3. Validate user authorization to retrieve or create data. | ||
|
||
Multi-tenant applications of some complexity tend to require both annotated and programmatic authorization for users calling resource methods. Since we now have user information injected (assuming we chose to use the `TokenBasedCallerInfo` request scoped bean defined above), we can use its properties in any `AbstractCalendarResource` subclass - and any other class in which we inject the `TokenBasedCallerInfo`. The common/shared information about the service's resource path and used roles is extracted to a shared singleton holding constants: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the concept doc like this one, I'd not base it on the custom token info example, it complicates things IMHO
. https://auth0.com/blog/id-token-access-token-what-is-the-difference[IdToken vs AccessToken] | ||
. https://cloud.google.com/docs/authentication/token-types[Token Types] | ||
|
||
== References |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add a link to the current OIDC multi-tenant tutorial and have that one cross-link to this new concept doc
What's the status of this? |
Hey @lennartj, would you like to give it a try and shorten this PR, focusing only on describing the multi-tenancy concept ? Your description of it is quite perfect, but the PR goes on with some very specific recommendations how to validate things, etc, and it feels it complicates things - we can certainly iterate if you'd like, you can follow with other PRs - would be easier to review too, thanks |
Fixes #33122
My first stab at describing multitenancy application setup and concepts.
@sberyozkin - feel free to comment and improve.