Spring security integrates really well with OAuth2 and OpenID-Connect. However the docs are not very clear on one point.
To set up security rules, you provide a bean that subclasses
WebSecurityConfigurerAdapter. This has a method
protected void configure(HttpSecurity http) which you override and make various calls.
In this method:
http.authorizeRequests()turns on session-based authentication
http.oauth2ResourceServer()turns on oauth2 bearer token support
authorizeRequests also allows OAuth2 bearer tokens to be used as an alternative to “login” via the authorization-code flow.
The gory details follow..
I’m using Keycloak as an OAuth2/OpenID-Connect server, and have the corresponding libraries enabled in Spring. The call to
authorizeRequests adds an instance of class
KeycloakAuthenticationProcessingFilter to the spring security filter chain for the corresponding URLs.
SpringSecurityRequestAuthenticator.authenticate which checks whether a bearer-token is present. If not, it continues to the redirect-based login flow. However when the bearer-token is present, and valid, then
AuthOutcome.AUTHENTICATED is returned. A “security context” is then created as usual, and saved in the http-session as usual (if session-based storage is enabled). Future calls with the same session-id then continue to use this token (until it expires).
I find it rather weird that bearer-tokens are allowed in this way. Bearer-tokens certainly have their uses, but an application which is designed around the concept of “login” should not, IMO, be accepting both kinds of authorization. Unfortunately I can’t currently see any standard mechanism that forbids using bearer-tokens in this way; it should however be relatively easy to override the standard classes and just check whether
token.isInteractive is true (normal redirect-based login) or false (bearer-token-based login).
In addition to the possible security implications of accepting both, there is a problem on token expiry. When using the standard “login” flows, expiry of the token in the session typically triggers a
302-redirect response which sends the caller back to the auth-server to get a new auth-code from which the server can get a new token (and this is normally transparent to the user). However when using bearer-tokens, expiry usually results in a
401-unauthorized which tells the caller to renew their bearer-token. With this frankenstein-like mixed mode, expired bearer tokens will also result in a 302 which is hard for a bearer-token-centric caller to handle.
The config settings for the auth-server are wrapped in a “Deployment” class, and this offers a “bearerTokenOnly” option. However AIUI, that will disable the login-flow behaviour but not the bearer-token behaviour.
One thing that strikes me as odd in spring-oauth is that I can find no code that looks for ID tokens. There is a distinct difference between authentication and authorization, and a quick internet search brings up lots of warnings to not confuse the two. However I can’t see where spring deals with that; as far as I can see it accepts an access-token as being sufficient in all cases.
Spring vs Keycloak
The spring-oauth integration consists of code from spring itself together with a plugin matching the auth-server implementation being used. Different behaviour will therefore be seen with different plugins (auth-servers). However the critical code that accepts bearer-tokens appears to be in SpringSecurityRequestAuthenticator and therefore it seems likely that this accept-bearer-token-for-authorizedRequests is not keycloak-specific.
Keycloak is an open-source auth-server implementation funded mostby by RedHat and sold commercially as “RedHat SSO”.
Session-Centric vs Stateless Authorization
Some servers keep per-user state, particularly ones that deal in presentation. In this case it makes sense to support the concept of “login” which stores the user’s security context in their session, and then have later requests simply reference the session (eg provide a session-id cookie). Security checks can be done against the security context in the session.
Other servers are stateless, which makes them much easier to scale and less error-prone. In this case, authorization should be done by providing a bearer-token with each request; in the usual case that the bearer-token is a JWT then the server needs to verify the signature on the token for each request (somewhat CPU-intensive) but does not need to fetch/update user state.
REST endpoints can potentially be secured via either of these mechanisms.
A rest service which is called from UI pages that are served from the same application is usually authorized via sessionid; this means the caller (running on the user’s machine) does not need access-tokens or other sensitive data, just a session-id. And it is more CPU-efficient as described above. It may seem odd to consider “session-based” authorization for rest services, but these are quite likely to be stateful rest services, ie services that take and return JSON but still depend on user state (read or write).
However a rest service that is called from a UI that is not provided by the same application should instead use stateless authorization (bearer token). And a rest service that needs to be scalable should be stateless and also use stateless authorization.
There is a danger when developing a single-tier application that combines both presentation logic and business logic that these two concepts get mixed and that either all services require login (inconvenient for remote commandline tools etc), or that the application just decides to accept both kinds of authorization. It would be better to instead have separate endpoints for “own ui use” (stateful) and “external service use” (stateless) and for these to then delegate to common business logic. However when using Spring-oauth, this would mean setting up different rules for the different endpoints, and ensuring that
authorizeRequests does not support bearer-tokens.
Warning For Code Researchers
If you are interested in all of this, and want to place breakpoints in the Spring code to follow the flow of authorization, then just a warning: if the access-tokens you are using have short expiry times then debugging can affect the code-flow. There are several places in spring where the expiry-time of the current token is checked; spend too long stepping through the code and a path that really should succeed will fail due to token expiry.
Useful places to put breakpoints: