Complete Java chapters

This commit is contained in:
Stian Thorgersen 2016-06-03 12:35:36 +02:00
parent d1f3dc049f
commit 43ffe3c962
9 changed files with 183 additions and 194 deletions

View file

@ -20,7 +20,7 @@
{% endif %}
... link:topics/oidc/java/servlet-filter-adapter.adoc[Java Servlet Filter Adapter]
... link:topics/oidc/java/jaas.adoc[JAAS plugin]
... link:topics/oidc/java/adapter-context.adoc[Keycloak Security Context]
... link:topics/oidc/java/adapter-context.adoc[Security Context]
... link:topics/oidc/java/adapter_error_handling.adoc[Error Handling]
... link:topics/oidc/java/logout.adoc[Logout]
... link:topics/oidc/java/multi-tenancy.adoc[Multi Tenancy]

View file

@ -1,12 +1,19 @@
=== Keycloak Security Context
=== Security Context
The `KeycloakSecurityContext` interface is available if you need to look at the access token directly.
This context is also useful if you need to get the encoded access token so you can make additional REST invocations.
In servlet environments it is available in secured invocations as an attribute in HttpServletRequest.
Or, it is available in secure and insecure requests in the HttpSession for browser apps.
The `KeycloakSecurityContext` interface is available if you need to access to the tokens directly. This could be useful if you want to retrieve additional
details from the token (such as user profile information) or you want to invoke a RESTful service that is protected by {{book.project.title}}.
[source]
In servlet environments it is available in secured invocations as an attribute in HttpServletRequest:
[source,java]
----
httpServletRequest.getAttribute(KeycloakSecurityContext.class.getName());
httpServletRequest.getSession().getAttribute(KeycloakSecurityContext.class.getName());
httpServletRequest
.getAttribute(KeycloakSecurityContext.class.getName());
----
Or, it is available in secure and insecure requests in the HttpSession:
[source,java]
----
httpServletRequest.getSession()
.getAttribute(KeycloakSecurityContext.class.getName());
----

View file

@ -2,59 +2,33 @@
[[_adapter_error_handling]]
=== Error Handling
Keycloak has some error handling facilities for servlet based client adapters.
When an error is encountered in authentication, keycloak will call `HttpServletResponse.sendError()`.
{{book.project.name}} has some error handling facilities for servlet based client adapters.
When an error is encountered in authentication, {{book.project.title}} will call `HttpServletResponse.sendError()`.
You can set up an error-page within your `web.xml` file to handle the error however you want.
Keycloak may throw 400, 401, 403, and 500 errors.
{{book.project.title}} may throw 400, 401, 403, and 500 errors.
[source]
[source,xml]
----
<error-page>
<error-code>404</error-code>
<location>/ErrorHandler</location>
</error-page>
----
Keycloak also sets an `HttpServletRequest` attribute that you can retrieve.
The attribute name is `org.keycloak.adapters.spi.AuthenticationError`.
Typecast this object to: `org.keycloak.adapters.OIDCAuthenticationError`.
This class can tell you exactly what happened.
If this attribute is not set, then the adapter was not responsible for the error code.
{{book.project.name}} also sets a `HttpServletRequest` attribute that you can retrieve.
The attribute name is `org.keycloak.adapters.spi.AuthenticationError`, which should be casted to to `org.keycloak.adapters.OIDCAuthenticationError`.
For example:
[source]
[source,java]
----
import org.keycloak.adapters.OIDCAuthenticationError;
import org.keycloak.adapters.OIDCAuthenticationError.Reason;
...
public class OIDCAuthenticationError implements AuthenticationError {
public static enum Reason {
NO_BEARER_TOKEN,
NO_REDIRECT_URI,
INVALID_STATE_COOKIE,
OAUTH_ERROR,
SSL_REQUIRED,
CODE_TO_TOKEN_FAILURE,
INVALID_TOKEN,
STALE_TOKEN,
NO_AUTHORIZATION_HEADER
}
OIDCAuthenticationError error = (OIDCAuthenticationError) httpServletRequest
.getAttribute('org.keycloak.adapters.spi.AuthenticationError');
private Reason reason;
private String description;
public OIDCAuthenticationError(Reason reason, String description) {
this.reason = reason;
this.description = description;
}
public Reason getReason() {
return reason;
}
public String getDescription() {
return description;
}
}
Reason reason = error.getReason();
System.out.println(reason.name());
----

View file

@ -1,131 +1,113 @@
[[_applicationclustering]]
=== Application Clustering
This chapter is focused on clustering support for your own AS7, EAP6 or Wildfly applications, which are secured by Keycloak.
We support various deployment scenarios according if your application is:
This chapter is related to supporting clustered applications deployed to JBoss EAP{% if book.community %}, WildFly and JBoss AS{% endif %}.
* stateless or stateful
* distributable (replicated http session) or non-distributable and just relying on sticky sessions provided by loadbalancer
* deployed on same or different cluster hosts where keycloak servers are deployed
There are several options depending on if your application is:
The situation is a bit tricky as application communicates with Keycloak directly within user's browser (for example redirecting to login screen), but there is also backend (out-of-bound) communication between keycloak and application, which is hidden from end-user and his browser and hence can't rely on sticky sessions.
* Stateless or stateful
* Distributable (replicated http session) or non-distributable
* Relying on sticky sessions provided by load balancer
* Hosted on same domain as {{book.project.name}}
NOTE: To enable distributable (replicated) HTTP Sessions in your application, you may need to do some additional steps.
Usually you need to put tag into `WEB-INF/web.xml` file of your application and possibly do some additional steps to configure underlying cluster cache (In case of Wildfly, the implementation of cluster cache is based on Infinispan). These steps are server specific, so consult documentation of your application server for more details.
Dealing with clustering is not quite as simple as for a regular application. Mainly due to the fact that both the browser and the server-side application
sends requests to {{book.project.name}}, so it's not as simple as enabling sticky sessions on your load balancer.
==== Stateless token store
By default, the servlet web application secured by Keycloak uses HTTP session to store information about authenticated user account.
This means that this info could be replicated across cluster and your application will safely survive failover of some cluster node.
By default, the web application secured by {{book.project.name}} uses the HTTP session to store security context. This means that you either have to
enable sticky sessions or replicate the HTTP session.
However if you don't need or don't want to use HTTP Session, you may alternatively save all info about authenticated account into cookie.
This is useful especially if your application is:
As an alternative to storing the security context in the HTTP session the adapter can be configured to store this in a cookie instead. This is useful if you want
to make your application stateless or if you don't want to store the security context in the HTTP session.
* stateless application without need of HTTP Session, but with requirement to be safe to failover of some cluster node
* stateful application, but you don't want sensitive token data to be saved in HTTP session
* stateless application relying on loadbalancer, which is not aware of sticky sessions (in this case cookie is your only way)
To configure this, you can add this line to configuration of your adapter in `WEB-INF/keycloak.json` of your application:
[source]
To use the cookie store for saving the security context, edit your applications `WEB-INF/keycloak.json` and add:
[source,json]
----
"token-store": "cookie"
----
Default value of `token-store` is `session`, hence saving data in HTTP session.
NOTE: The default value for `token-store` is `session`, which stores the security context in the HTTP session.
One limitation of cookie store is, that whole info about account is passed in cookie KEYCLOAK_ADAPTER_STATE in each HTTP request.
Hence it's not the best for network performance.
Another small limitation is limited support for Single-Sign out.
It works without issues if you init servlet logout (HttpServletRequest.logout) from this application itself as the adapter will delete the KEYCLOAK_ADAPTER_STATE cookie.
But back-channel logout initialized from different application can't be propagated by Keycloak to this application with cookie store.
Hence it's recommended to use very short value of access token timeout (1 minute for example).
One limitation of using the cookie store is that the whole security context is passed in the cookie for every HTTP request. This may impact performance.
Another small limitation is limited support for Single-Sign Out. It works without issues if you init servlet logout (HttpServletRequest.logout) from the
application itself as the adapter will delete the KEYCLOAK_ADAPTER_STATE cookie. However, back-channel logout initialized from a different application isn't
propagated by {{book.project.name}} to applications using cookie store. Hence it's recommended to use a short value for the access token timeout (for example 1 minute).
==== Relative URI optimization
In many deployment scenarios will be Keycloak and secured applications deployed on same cluster hosts.
For this case Keycloak already provides option to use relative URI as value of option _auth-server-url_ in `WEB-INF/keycloak.json` . In this case, the URI of Keycloak server is resolved from the URI of current request.
In deployment scenarios where {{book.project.name}} and the application is hosted on the same domain (through a reverse proxy or load balancer) it can be
convenient to use relative URI options in your client configuration.
For example if your loadbalancer is on _https://loadbalancer.com/myapp_ and auth-server-url is _/auth_, then relative URI of Keycloak is resolved to be _https://loadbalancer.com/auth_ .
With relative URIs the URI is resolved as relative to the URL of the URL used to access {{book.project.name}}.
For cluster setup, it may be even better to use option _auth-server-url-for-backend-request_ . This allows to configure that backend requests between Keycloak and your application will be sent directly to same cluster host without additional round-trip through loadbalancer.
So for this, it's good to configure values in `WEB-INF/keycloak.json` like this:
[source]
----
"auth-server-url": "/auth",
"auth-server-url-for-backend-requests": "http://${jboss.host.name}:8080/auth"
----
This would mean that browser requests (like redirecting to Keycloak login screen) will be still resolved relatively to current request URI like _https://loadbalancer.com/myapp_, but backend (out-of-bound) requests between keycloak and your app are sent always to same cluster host with application .
Note that additionally to network optimization, you may not need "https" in this case as application and keycloak are communicating directly within same cluster host.
For example if the URL to your application is `https://acme.org/myapp` and the URL to {{book.project.name}} is `https://acme.org/auth`, then you can use
the redirect-uri `/myapp` instead of `https://acme.org/myapp`.
==== Admin URL configuration
Admin URL for particular application can be configured in Keycloak admin console.
It's used by Keycloak server to send backend requests to application for various tasks, like logout users or push revocation policies.
Admin URL for a particular application can be configured in the {{book.project.name}} Administration Console.
It's used by the {{book.project.name}} server to send backend requests to the application for various tasks, like logout users or push revocation policies.
For example logout of user from Keycloak works like this:
For example the way backchannel logout works is:
. User sends logout request from one of applications where he is logged.
. Then application will send logout request to Keycloak
. Keycloak server logout user in itself, and then it re-sends logout request by backend channel to all applications where user is logged.
Keycloak is using admin URL for this.
So logout is propagated to all apps.
You may again use relative values for admin URL, but in cluster it may not be the best similarly like in <<_relative_uri_optimization,previous section>> .
. User sends logout request from one application
. The application sends logout request to {{book.project.name}}
. The {{book.project.name}} server invalidates the user session
. The {{book.project.name}} server then sends a backchannel request to application with a admin url that are associated with the session
. When an application receives the logout request it invalidates the corresponding HTTP session
Some examples of possible values of admin URL are:
http://${jboss.host.name}:8080/myapp::
This is best choice if "myapp" is deployed on same cluster hosts like Keycloak and is distributable.
In this case Keycloak server sends logout request to itself, hence no communication with loadbalancer or other cluster nodes and no additional network traffic.
http://${application.session.host}:8080/myapp::
Keycloak will track hosts where is particular HTTP Session served and it will send session invalidation message to proper cluster node.
{{book.project.name}} tracks hosts associated with the HTTP Session and will send session invalidation message to the associated node.
[[_registration_app_nodes]]
==== Registration of application nodes to Keycloak
==== Registration of application nodes
Previous section describes how can Keycloak send logout request to proper application node.
However in some cases admin may want to propagate admin tasks to all registered cluster nodes, not just one of them.
For example push new notBefore for realm or application, or logout all users from all applications on all cluster nodes.
The previous describes how {{book.project.name}} can send logout request to node associated with a specific HTTP session.
However, in some cases admin may want to propagate admin tasks to all registered cluster nodes, not just one of them.
For example to push a new not before policy to the application or to logout all users from the application.
In this case Keycloak should be aware of all application cluster nodes, so it could send event to all of them.
In this case {{book.project.name}} needs to be aware of all application cluster nodes, so it can send the event to all of them.
To achieve this, we support auto-discovery mechanism:
. Once new application node joins cluster, it sends registration request to Keycloak server
. The request may be re-sent to Keycloak in configured periodic intervals
. If Keycloak won't receive re-registration request within specified timeout (should be greater than period from point 2) then it automatically unregister particular node
. Node is also unregistered in Keycloak when it sends unregistration request, which is usually during node shutdown or application undeployment.
This may not work properly for forced shutdown when undeployment listeners are not invoked, so here you need to rely on automatic unregistration from point 3 .
. When a new application node joins the cluster, it sends a registration request to the {{book.project.name}} server
. The request may be re-sent to {{book.project.name}} in configured periodic intervals
. If the {{book.project.name}} server doesn't receive a re-registration request within a specified timeout then it automatically unregisters the specific node
. The node is also unregistered in {{book.project.name}} when it sends an unregistration request, which is usually during node shutdown or application undeployment.
This may not work properly for forced shutdown when undeployment listeners are not invoked, which results in the need for automatic unregistration
Sending startup registrations and periodic re-registration is disabled by default, as it's main usecase is just cluster deployment.
In `WEB-INF/keycloak.json` of your application, you can specify:
Sending startup registrations and periodic re-registration is disabled by default as it's only required for some clustered applications.
To enable the feature edit the `WEB-INF/keycloak.json` file for your application and add:
[source]
----
"register-node-at-startup": true,
"register-node-period": 600,
----
which means that registration is sent at startup (accurately when 1st request is served by the application node) and then it's resent each 10 minutes.
In Keycloak admin console you can specify the maximum node re-registration timeout (makes sense to have it bigger than _register-node-period_ from adapter configuration for particular application). Also you can manually add and remove cluster nodes in admin console, which is useful if you don't want to rely on adapter's automatic registration or if you want to remove stale application nodes, which weren't unregistered (for example due to forced shutdown).
This means the adapter will send the registration request on startup and re-register every 10 minutes.
In the {{book.project.name}} Administration Console you can specify the maximum node re-registration timeout (should be larger than _register-node-period_ from
the adapter configuration). You can also manually add and remove cluster nodes in through the Adminstration Console, which is useful if you don't want to rely
on the automatic registration feature or if you want to remove stale application nodes in the event your not using the automatic unregistration feature.
[[_refresh_token_each_req]]
==== Refresh token in each request
By default, application adapter tries to refresh access token when it's expired (period can be specified as <<_token_timeouts,Access Token Lifespan>>) . However if you don't want to rely on the fact, that Keycloak is able to successfully propagate admin events like logout to your application nodes, then you have possibility to configure adapter to refresh access token in each HTTP request.
By default the application adapter will only refresh the access token when it's expired. However, you can also configure the adapter to refresh the token on every
request. This may have a performance impact as your application will send more requests to the {{book.project.name}} server.
In `WEB-INF/keycloak.json` you can configure:
To enable the feature edit the `WEB-INF/keycloak.json` file for your application and add:
[source]
----
"always-refresh-token": true
----
Note that this has big performance impact.
It's useful just if performance is not priority, but security is critical and you can't rely on logout and push notBefore propagation from Keycloak to applications.
NOTE: This may have a significant impact on performance. Only enable this feature if you can't rely on backchannel messages to propagate logout and not before
policies. Another thing to consider is that by default access tokens has a short expiration so even if logout is not propagated the token will expire within
minutes of the logout.

View file

@ -1,27 +1,30 @@
[[_jaas_adapter]]
=== JAAS plugin
It's generally not needed to use JAAS for most of the applications, especially if they are HTTP based, but directly choose one of our adapters.
However some applications and systems may still rely on pure legacy JAAS solution.
Keycloak provides couple of login modules to help with such use cases.
Some login modules provided by Keycloak are:
It's generally not needed to use JAAS for most of the applications, especially if they are HTTP based, and you should most likely choose one of our other adapters.
However, some applications and systems may still rely on pure legacy JAAS solution.
{{book.project.title}} provides two login modules to help in these situations.
The provided login modules are:
org.keycloak.adapters.jaas.DirectAccessGrantsLoginModule::
This login module allows to authenticate with username/password from Keycloak database.
It's using <<_direct_access_grants,Direct Access Grants>> Keycloak endpoint to validate on Keycloak side if provided username/password is valid.
It's useful especially for non-web based systems, which need to rely on JAAS and want to use Keycloak credentials, but can't use classic browser based authentication flow due to their non-web nature.
Example of such application could be messaging application or SSH system.
This login module allows to authenticate with username/password from {{book.project.title}}.
It's using <<fake/../../oidc-generic.adoc#_resource_owner_password_credentials_flow,Resource Owner Password Credentials>> flow to validate if the provided
username/password is valid. It's useful for non-web based systems, which need to rely on JAAS and want to use Keycloak, but can't use the standard browser
based flows due to their non-web nature. Example of such application could be messaging or SSH.
org.keycloak.adapters.jaas.BearerTokenLoginModule::
This login module allows to authenticate with Keycloak access token passed to it through CallbackHandler as password.
It may be useful for example in case, when you have Keycloak access token from classic web based authentication flow and your web application then needs to talk to external non-web based system, which rely on JAAS.
For example to JMS/messaging system.
This login module allows to authenticate with {{book.project.title}} access token passed to it through CallbackHandler as password.
It may be useful for example in case, when you have {{book.project.title}} access token from standard based authentication flow and your web application then
needs to talk to external non-web based system, which rely on JAAS. For example a messaging system.
Both login modules have configuration property `keycloak-config-file` where you need to provide location of keycloak.json configuration file.
It could be either provided from filesystem or from classpath (in that case you may need value like `classpath:/folder-on-classpath/keycloak.json` ).
Both modules use the following configuration properties:
Second property `role-principal-class` allows to specify alternative class for Role principals attached to JAAS Subject.
Default value for Role principal is `org.keycloak.adapters.jaas.RolePrincipal` . Note that class should have constructor with single String argument.
keycloak-config-file::
The location of the `keycloak.json` configuration file. The configuration file can either be located on the filesystem or on the classpath. If it's located
on the classpath you need to prefix the location with `classpath:` (for example `classpath:/path/keycloak.json`).
This is _REQUIRED._
`role-principal-class`::
Configure alternative class for Role principals attached to JAAS Subject.
Default value is `org.keycloak.adapters.jaas.RolePrincipal`. Note: The class is required to have a constructor with a single `String` argument.

View file

@ -1,6 +1,5 @@
=== Logout
There are multiple ways you can logout from a web application.
For Java EE servlet containers, you can call HttpServletRequest.logout(). For any other browser application, you can point the browser at the url `http://auth-server/auth/realms/{realm-name}/tokens/logout?redirect_uri=encodedRedirectUri`.
This will log you out if you have an SSO session with your browser.
For Java EE servlet containers, you can call HttpServletRequest.logout(). For any other browser application, you can redirect the browser to
`http://auth-server/auth/realms/{realm-name}/tokens/logout?redirect_uri=encodedRedirectUri`. This will log you out if you have a SSO session with your browser.

View file

@ -1,28 +1,54 @@
=== Multi Tenancy
Multi Tenancy, in our context, means that one single target application (WAR) can be secured by a single (or clustered) Keycloak server, authenticating its users against different realms.
In practice, this means that one application needs to use different `keycloak.json` files.
For this case, there are two possible solutions:
Multi Tenancy, in our context, means that a single target application (WAR) can be secured with multiple {{book.project.name}} realms. The realms can be located
one the same {{book.project.name}} instance or on different instances.
* The same WAR file deployed under two different names, each with its own Keycloak configuration (probably via the Keycloak Subsystem).
This scenario is suitable when the number of realms is known in advance or when there's a dynamic provision of application instances.
One example would be a service provider that dynamically creates servers/deployments for their clients, like a PaaS.
* client1.acme.com
+`client2.acme.com`
+`/app/client1/`
+`/app/client2/` This chapter of the reference guide focus on this second scenario.
In practice, this means that the application needs to have multiple `keycloak.json` adapter configuration files.
Keycloak provides an extension point for applications that need to evaluate the realm on a request basis.
During the authentication and authorization phase of the incoming request, Keycloak queries the application via this extension point and expects the application to return a complete representation of the realm.
With this, Keycloak then proceeds the authentication and authorization process, accepting or refusing the request based on the incoming credentials and on the returned realm.
For this scenario, an application needs to:
You could have multiple instances of your WAR with different adapter configuration files deployed to different context-paths. However, this may be inconvenient
and you may also want to select the realm based on something else than context-path.
* web.xml
+`keycloak.config.resolver`
+`org.keycloak.adapters.KeycloakConfigResolver`
* org.keycloak.adapters.KeycloakConfigResolver
+`resolve(org.keycloak.adapters.spi.HttpFacade.Request)`
+`org.keycloak.adapters.KeycloakDeployment`
{{book.project.name}} makes it possible to have a custom config resolver so you can choose what adapter config is used for each request.
An implementation of this feature can be found in the examples.
To achieve this first you need to create an implementation of `org.keycloak.adapters.KeycloakConfigResolver`. For example:
[source,java]
----
package example;
import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
public class PathBasedKeycloakConfigResolver implements KeycloakConfigResolver {
@Override
public KeycloakDeployment resolve(OIDCHttpFacade.Request request) {
if (path.startsWith("alternative")) {
KeycloakDeployment deployment = cache.get(realm);
if (null == deployment) {
InputStream is = getClass().getResourceAsStream("/tenant1-keycloak.json");
return KeycloakDeploymentBuilder.build(is);
}
} else {
InputStream is = getClass().getResourceAsStream("/default-keycloak.json");
return KeycloakDeploymentBuilder.build(is);
}
}
}
----
You also need to configure which `KeycloakConfigResolver` implementation to use with the `keycloak.config.resolver` context-param in your `web.xml`:
[source,xml]
----
<web-app>
...
<context-param>
<param-name>keycloak.config.resolver</param-name>
<param-value>example.PathBasedKeycloakConfigResolver</param-value>
</context-param>
</web-app>
----

View file

@ -1,25 +1,22 @@
[[_servlet_filter_adapter]]
=== Java Servlet Filter Adapter
If you want to use Keycloak with a Java servlet application that doesn't have an adapter for that servlet platform, you can opt to use the servlet filter adapter that Keycloak has.
This adapter works a little differently than the other adapters.
You do not define security constraints in web.xml.
Instead you define a filter mapping using the Keycloak servlet filter adapter to secure the url patterns you want to secure.
If you are deploying your Java Servlet application on a platform where there is no {{book.project.title}} adapter you opt to use the the servlet filter adapter.
This adapter works a bit differently than the other adapters. You do not define security constraints in web.xml.
Instead you define a filter mapping using the {{book.project.title}} servlet filter adapter to secure the url patterns you want to secure.
WARNING: Backchannel logout works a bit differently than the standard adapters.
Instead of invalidating the http session it instead marks the session id as logged out.
There's just no way of arbitrarily invalidating an http session based on a session id.
Instead of invalidating the HTTP session it marks the session id as logged out.
There's no way standard way to invalidate an HTTP session based on a session id.
[source]
[source,xml]
----
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<module-name>customer-portal</module-name>
<module-name>application</module-name>
<filter>
<filter-name>Keycloak Filter</filter-name>
@ -33,20 +30,20 @@ There's just no way of arbitrarily invalidating an http session based on a sessi
</web-app>
----
If you notice above, there are two url-patterns.
`/protected/*` are just the files we want protected. `/keycloak/*` url-pattern will handle callback from the keycloak server.
Note that you should configure your client in the Keycloak Admin Console with an Admin URL that points to a secured section covered by the filter's url-pattern.
In the snippet above there are two url-patterns.
`/protected/*` are the files we want protected, while the `/keycloak/*` url-pattern handles callbacks from the {{book.project.title}} server.
Note that you should configure your client in the {{book.project.title}} Admin Console with an Admin URL that points to a secured section covered by the filter's url-pattern.
The Admin URL will make callbacks to the Admin URL to do things like backchannel logout.
So, the Admin URL in this example should be `http[s]://hostname/{context-root}/keycloak`.
There is an example of this in the distribution.
The Keycloak filter has the same configuration parameters available as the other adapters except you must define them as filter init params instead of context params.
The {{book.project.title}} filter has the same configuration parameters as the other adapters except you must define them as filter init params instead of context params.
To use this filter, include this maven artifact in your WAR poms
To use this filter, include this maven artifact in your WAR poms:
[source]
[source,xml]
----
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-servlet-filter-adapter</artifactId>

View file

@ -14,6 +14,7 @@ TODO
==== Implicit
[[_resource_owner_password_credentials_flow]]
==== Resource Owner Password Credentials
==== Client Credentials