Replies: 4 comments 3 replies
-
|
Not sure I fully understand how a dynamic scope will work, and if it's fully the protocol mappers that has to support this capability. Considering an example where I want to have a dynamic scope that works based on groups, and I want it to select groups that are sub-groups of "organizations". So I have the following groups:
When a client sends "groups:*" what determines the choices presented to the user? Is it a special protocol mapper? I presume when a client sends "groups:org-1" a special protocol mapper selects the correct child-group from "organizations" and filters what groups are included in the token. Another example say I want to have the same example as above, but with a slight twist. I want to include either all groups, or a specific group. So "groups:*" includes all child groups of organizations, while "groups:org-1" only one group. Would that be something I'd configure in the protocol mapper or the client scope? |
Beta Was this translation helpful? Give feedback.
-
|
Wonder if "?scope=groups#one" would be a better syntax, rather than "?scope=groups:one". Also, wonder if "groups" should be used by the client instead of "groups:*". So a client can use "?scope=groups" or "?scope=groups:one". One reason for that is a client can be configured to use dynamic scopes, without changing the client, and will also work with default scopes where the client just sends "?scope=something-else", but the groups scope has been configured as a default scope. |
Beta Was this translation helpful? Give feedback.
-
|
Currently you have to define client scope entities upfront. The dynamic scopes support should first of all be an extension mechanism which doesn't require upfront client scope definition. It should be possible to register custom "dynamic client scope" handler for scope values which start with a prefix (or match a regex). Dynamic Scope handlersA handler for prefix "group:" could handle scope resolution for all scopes which start with this string.
To do this work handler may needs some specific config, similar to the authenticator implementations. A dynamic scope handler would need to be assigned to a client like client scopes. However not with default and optional separation. Keycloak could come with one or more dynamic scope handlers, e.g. for the case that a tenant/company is represented as group. Selection of scopes by userThe feature where the user neds to select one of multiple dynamic scopes (as described in the inital concept in this discussion) is an additional requirement. It should come together with a general possibility for the user to select a subset of grantble scopes. This selection could be different per handler. One handler would need to enforce a "select only one scope" e.g. under section titled "For which company/group do you want to work?". Another handler could ask "For which devices you want to grant access?" and want to allows a free subset to be selected. |
Beta Was this translation helpful? Give feedback.
-
|
I've come to the conclusion that we must not extend "scopes" without considering RAR, and at the same time we must not implement RAR as a separate thing. The two have very similar needs: a) client includes some additional details in authz request, b) some policies decide if it's permitted or not, c) the user optionally consents to some stuff, and d) some claims are added to tokens. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Note: This discussion will be unified in: #8532, please continue any contributions in the new one.
Motivation
With more complex use cases, comes more complex authorization requirements. OAuth2 and OIDC are the specs used for authentication but they are known to lack the mechanisms to handle the most complex authorization requirements.
These specs make use of JWTs to carry AuthN and AuthZ data to distributed systems. Without getting into too much detail, these mechanisms have an important limitation: Size.
JWTs are sent to other systems over HTTP as headers and may be stored as cookies, but they offer a limited amount of space to send or store this information.
In a B2B, multi-tenant context, where a user can be part of more than one different tenants, each with their set of authorization data, it not uncommon to end up with a JWT containing this information for all the tenants combined.
There's a need to scope the tokens to a specific context from within the system for which the user will be authorized for.
This has the following benefits:
Avoid including information about other tenants that the user is a member of.
If an application has a way to further reduce the scope of their systems, you may even generate a token for a specific team within the tenant.
There are also security implications in this, similar to the ones with static scopes: If you generate a token with scopes that won't allow you to perform any scalation of priviledges actions, if the token is leaked, the attacker would be blocked from such actions.
With dynamic scopes, in case of a token leak, if the token was sufficiently scoped, the damage may be limited to a specific tenant or group instead of a full-blown compromise.
Use-case
1. Significantly reducing the amount of information added to JWTs
Let's work with a hypotetical JWT generated for a multi-tenant, B2B application where they don't scope tokens to a specific organization:
{ "sub": "<snip>", "name": "John Doe", "subjectType": "user", "scopes": "openid tenants", "tenants": { "tenant_1": { "groups": { "groupG": ["role1", "role2"], "groupF": ["role1", "role2"], "groupE": ["role1", "role2"], "groupD": ["role1", "role2"], "groupA": ["role1", "role2"] }, "tenantRoles": ["tenantRole1", "tenantRole2"] }, "tenant_2": { "groups": { "group1": ["role1", "role2"], "group2": ["role1", "role2"], "group3": ["role1", "role2"] }, "tenantRoles": ["tenantRole1", "tenantRole2"] }, "tenant_3": { "groups": { "group1": ["role1", "role2"], "group2": ["role1", "role2"], "group3": ["role1", "role2"], "group4": ["role1", "role2"], "group5": ["role1", "role2"] }, "tenantRoles": ["tenantRole1", "tenantRole2", "tenantRole4"] }, "tenant_4": { "groups": { "group1": ["role1", "role2"] }, "tenantRoles": ["tenantRole1"] } }, "iss": "https://cool.app.com/", "jti": "<snip>", "iat": <snip>, "nbf": <snip>, "exp": <snip> }There are a few problems with this token:
It can be used for every tenant, regardless of the one the user is currently working with, so in the case of a leak, this token has access to everything.
If using an RBAC model, it's possible to experience role explosion, where the user may be assigned a huge amount of different roles. As the amount of roles or groups increases, we'll eventually reach the size where we will have problems with cookies, header sizes etc.
Using dynamic scopes, we can reduce the scope of the generated token, to a specific token, resulting in:
{ "sub": "<snip>", "name": "John Doe", "subjectType": "user", "scopes": "openid tenant:tenant_1", "tenants": { "tenant_1": { "groups": { "groupG": ["role1", "role2"], "groupF": ["role1", "role2"], "groupE": ["role1", "role2"], "groupD": ["role1", "role2"], "groupA": ["role1", "role2"] }, "tenantRoles": ["tenantRole1", "tenantRole2"] }, }, "iss": "https://cool.app.com/", "jti": "<snip>", "iat": <snip>, "nbf": <snip>, "exp": <snip> }As shown, the scope is now "tenant:tenant_1" instead of the generic "tenants". Resource Servers can use this scope to restrict actions to this specific tenant, reducing the security exposure area.
We can go even further and scope by a specific group by adding the group:groupG scope:
{ "sub": "<snip>", "name": "John Doe", "subjectType": "user", "scopes": "openid tenant:tenant_1 group:groupG", "current_tenant": "tenant_1", "current_group": "groupG", "tenants": { "tenant_1": { "groups": { "groupG": ["role1", "role2"], }, "tenantRoles": ["tenantRole1", "tenantRole2"] }, }, "iss": "https://cool.app.com/", "jti": "<snip>", "iat": <snip>, "nbf": <snip>, "exp": <snip> }Defining the format of dynamic scopes
The objective of this feature is to have a static part of the scope that represents an entity in the Keycloak server and a variable part that identifies the entity.
Notation: <static-part>:<variable-part>
Matching Regexp sample: (.*):(.*) or, if we go with a subset of initially supported static entities: (org|workspace|account):(.*).
With this notation, we always have the static part to match against any kind of entity that we want to filter by when building the tokens.
The variable part will be the identifier for the entity that we want to filter by.
Alternatively, when requesting scopes, a wildcard can be used in place of the variable part of the scope. The system should interpret this as:
Look for all entities of this entity type, if there's only one, default to that, if there are more, ask the user to choose.Examples:
group:* -> group:singleGroup. If there are more than one, ask the user.Client Scope Definition and Configuration in the Admin UI
Client scopes definition and configuration can be straight forward by reusiung the existing client scopes screen in Keycloak. What defines a dynamic scope vs a static scope can either be a scope attribute that you can select in the UI or it can be inferred if the scope contains a regex wildcard or the format conforms to the previously stated notation.
Regardless of the way we use to define the scope, we may need to disable some configuration values such as:
Scopeclaim. As a token will be scoped to a specific entity, we need it to be there.Grant access to tenant: <dynamic_scope_variable_part>Grant access to group ${scope.variable} for a given tenantClient Scope Evaluation in the Admin UI
When configuring a Client in Keycloak, you have the ability to select which Client Scopes will be default or optional client scopes and evaluate the end result of an authentication with certain scopes for a given user.
We'll need to expand the
Evaluatescreen to support Dynamic Scopes. Right now a user is able to select a number of static scopes and get an answer without any further input.With Dynamic Scopes, we'll need the user to also provide the variable part of any dynamic scopes added to the evaluation, so the claims computation can be done correctly for a given value.
It would also be possible to let the user choose from a list of entities when running evaluate if the requested scope contains a wildcard in the variable part, exactly like the system will do when a user sends a wildcard dynamic scope to the authentication endpoint and it resolves more than one entities for a given scope type.
Custom Token Mapper
In order to make these scopes add the correct data to the tokens, a new token mapper will need to be added.
In this design proposal I'm only covering the definition, configuration and evaluation of dynamic scopes, and how the resulting information will be available for token mappers that users want to create to extend the system, so the custom token mapper is out of scope.
Using Dynamic Scopes
Requesting a dynamic scope with a specified variable part
Calling
authwith a correctly defined scope:(
scopeis decoded for clarity)We're requesting the Keycloak server to accept a scope with the type
groupand a variable parametergroup-a. Keycloak will now look for an entity of typegroupand namegroup-a. With a custom SPI to validate certain dynamic scope types, we'll be able to use this information to generate the tokens with the correct information.In this case, we'd want to add this group to the token if the user is a member of it, otherwise we may generate an
invalid_scopeerror or proceed to the consent screen.Consent screen
The server has already validated that the scope is dynamic, it matches the defined regular expression of a client scope defined for the specified client and resolved
${scope.type} is groupand${scope.variable} is group-a. As the request already contains a totally qualified dynamic scope, the server will not ask the user to choose form a list of possible entities of typegroup.In the consent screen, the server will show this scope as being requested. If the user specified a custom text, that text will contain the type and variable parts embedded in a custom text, otherwise the server will have to show either the scope itself or a default text containing both.
If the user specified a custom text
Otherwise, the server would show
group:group-aor other default text.Requesting a dynamic scope with a wildcard variable
Calling
authwith a wildcard dynamic scope:(
scopeis decoded for clarity)When the user requests a dynamic scope type but doesn't provide a variable value, and instead uses a wildcard
*such as `group:*, two things could happen:There's only one entity that matches the entity type, so the system chooses that only entity and returns the fully qualified scope. In this case, if there's only
group-a,group:*turns intogroup:group-a.If there are more than one, then the system asks the user to choose one:
Once one is chosen, this information will be stored in the user session and shown in the consent screen as shown before.
Requesting multiple dynamic scopes with wildcards
Calling
authwith more than one wildcard dynamic scopes:(
scopeis decoded for clarity)In this case, the system will loop through all the dynamic scopes that it needs an answer from and either choose automatically if there's a single entity or choose the user once per type.
Other supporting claims
Sometimes, systems will need to not only scope a token to a specific entity, but also it will need to know the alternatives to switch contexts.
In a multi-tenant application, the following request may be made:
(
scopeis decoded for clarity)So the system will generate a token set with this information.
Now, the requesting system wants to have a way to let the user switch tenants from an embedded control like a dropdown. For this to happen the system needs to know all the possible tenants that the user can switch to.
In this case, a claim called
available_\<scope-type\>with a list of all possible entities of the specific type that the user has access to will also be added to the id-token.Note: we only want this claim in the id-token and the user-info endpoint because this information is needed for clients and not for resource servers.
So, if User A is a member of
tenant-1andtenant-2, and they request a token scoped fortenant-1, that's the claim that is going to be handled by token mappers to add any tenant specific information, but it will also add to the id-token a claim such as:available_tenants: ["tenant-1", "tenant-2"]Other supporting claim that could be necessary is
current_\<scope-type\>to have a way to tell what specific entity the token is now being scoped to.In this case, we'd have the following claim added to all tokens:
current_tenant: "tenant-1"So the token example shown above would look like the following if it was an id-token:
{ "sub": "<snip>", "name": "John Doe", "subjectType": "user", "available_tenants": ["tenant_1", "tenant_2", "tenant_3", "tenant_4"], "current_tenant": "tenant_1", "available_groups": ["groupA", "groupD", "groupE", "groupF", "groupG"], "current_group": "groupG", "tenants": { "tenant_1": { "groups": { "groupG": ["role1", "role2"], }, "tenantRoles": ["tenantRole1", "tenantRole2"] }, }, "iss": "https://cool.app.com/", "jti": "<snip>", "iat": <snip>, "nbf": <snip>, "exp": <snip> }And as following if it was an access-token:
{ "sub": "<snip>", "name": "John Doe", "subjectType": "user", "scopes": "openid tenant:tenant_1 group:groupG", "current_tenant": "tenant_1", "current_group": "groupG", "tenants": { "tenant_1": { "groups": { "groupG": ["role1", "role2"], }, "tenantRoles": ["tenantRole1", "tenantRole2"] }, }, "iss": "https://cool.app.com/", "jti": "<snip>", "iat": <snip>, "nbf": <snip>, "exp": <snip> }Implementation considerations
Implementation of the scope validation code
There's an open PR already to make the scopes validation an SPI:
#7721
Following through with this PR, we'd be able to define a dynamic scopes validation SPI that would implement most of the details of this design.
We'd also need to consider whether we want to start implementing this dynamic enough to let users search by any kind of type or implement certain type-specific SPIs so we have a more controlled experience.
For example, we'd implement a GroupsDynamicScopeValidationSPI that would validate dynamic scopes that are groups only.
Or we'd let users have a generic validation where they can scope by anything within Keycloak, or just validate to true if we just need the scope to pass through without needing to filter by anything.
Implementation of the choice screen
In order to make this available to all authentication flows, it wouldn't be feasible to implement it as custom execution in an authenticator.
We'd ideally need to improve the consent screen to find all of these dynamic scopes and performm all the actions detailed above to choose the correct entities, either automatically or with choice screens and lastly show the consent screen.
Backward Compatibility
We may find that Keycloak users may have defined custom, static scopes with the specified notation, so in order to keep dynamic scopes backward compatible we may need to add a new property to scopes so we don't erroneously consider existing static scopes as dynamic ones.
Otherwise, new dynamic scopes shouldn't break backwards compatibility.
Extensibility
With the Scopes validation SPI and the custom token mappers to include any data to tokens, we're covered for many use cases that may come in the future.
Beta Was this translation helpful? Give feedback.
All reactions