Skip to content

Conversation

lennartj
Copy link

Fixes #33122

My first stab at describing multitenancy application setup and concepts.
@sberyozkin - feel free to comment and improve.

@github-actions
Copy link

github-actions bot commented May 14, 2023

🎊 PR Preview c23c27c has been successfully built and deployed to https://quarkus-pr-main-33355-preview.surge.sh/version/main/guides/

@sberyozkin
Copy link
Member

Hey @lennartj
Thanks for the quality PR, very appreciated. I'll comment tomorrow.
I'd also like to ask my colleagues from the Doc team to have a look
Cheers Sergey

@sberyozkin
Copy link
Member

@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:
Copy link
Member

@sberyozkin sberyozkin May 15, 2023

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 ?

Copy link
Member

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

Copy link
Author

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 ?

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.
Copy link
Member

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)

Copy link
Author

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?

Copy link
Member

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.
Copy link
Member

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 ?

Copy link
Author

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

Copy link
Member

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.
Copy link
Member

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.
Copy link
Member

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

====


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.
Copy link
Member

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
Copy link
Member

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
Copy link
Member

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.
Copy link
Member

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:
Copy link
Member

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
Copy link
Member

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

@geoand
Copy link
Contributor

geoand commented Jun 27, 2023

What's the status of this?

@sberyozkin
Copy link
Member

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

OIDC Documentation: Multitenancy TenantConfigResolver Setup
4 participants