Spring Cloud Security offers a set of primitives for building secure applications and services with minimum fuss. A declarative model which can be heavily configured externally (or centrally) lends itself to the implementation of large systems of co-operating, remote components, usually with a central indentity management service. It is also extremely easy to use in a service platform like Cloud Foundry. Building on Spring Boot and Spring Security OAuth2 we can quickly create systems that implement common patterns like single sign on, token relay and token exchange.
Quickstart
OAuth2 Single Sign On
Here’s a Spring Cloud "Hello World" app with HTTP Basic authentication and a single user account:
@Grab('spring-boot-starter-security')
@Controller
class Application {
@RequestMapping('/')
String home() {
'Hello World'
}
}
You can run it with spring run app.groovy
and watch the logs for the password (username is "user"). So far this is just the default for a Spring Boot app.
Here’s a Spring Cloud app with OAuth2 SSO:
@Controller
@EnableOAuth2Sso
class Application {
@RequestMapping('/')
String home() {
'Hello World'
}
}
Spot the difference? This app will actually behave exactly the same as the previous one, because it doesn’t know it’s OAuth2 credentals yet.
You can register an app in github quite easily, so try that if you want a production app on your own domain. If you are happy to test on localhost:8080, then set up these properties in your application configuration:
spring:
oauth2:
client:
clientId: bd1c0a783ccdd1c9b9e4
clientSecret: 1a9030fbca47a5b2c28e92f19050bb77824b5ad1
accessTokenUri: https://github.com/login/oauth/access_token
userAuthorizationUri: https://github.com/login/oauth/authorize
clientAuthenticationScheme: form
resource:
userInfoUri: https://api.github.com/user
preferTokenInfo: false
run the app above and it will redirect to github for authorization. If you are already signed into github you won’t even notice that it has authenticated. These credentials will only work if your app is running on port 8080.
To limit the scope that the client asks for when it obtains an access token
you can set spring.oauth2.client.scope
(comma separated or an array in YAML). By
default the scope is empty and it is up to to Authorization Server to
decide what the defaults should be, usually depending on the settings in
the client registration that it holds.
Note
|
The examples above are all Groovy scripts. If you want to write the same code in Java (or Groovy) you need to add Spring Security OAuth2 to the classpath (e.g. see the sample here). |
OAuth2 Protected Resource
You want to protect an API resource with an OAuth2 token? Here’s a simple example (paired with the client above):
@Grab('spring-cloud-starter-security')
@RestController
@EnableOAuth2Resource
class Application {
@RequestMapping('/')
def home() {
[message: 'Hello World']
}
}
and
spring:
oauth2:
resource:
userInfoUri: https://api.github.com/user
preferTokenInfo: false
More Detail
Single Sign On
An app will activate @EnableOAuth2Sso
if you bind provide the
following properties in the Environment
:
-
spring.oauth2.client.*
with*
equal toclientId
,clientSecret
,accessTokenUri
,userAuthorizationUri
and one of:-
spring.oauth2.resource.userInfoUri
to use the "/me" resource (e.g. "https://uaa.run.pivotal.io/userinfo" on PWS), or -
spring.oauth2.resource.tokenInfoUri
to use the token decoding endpoint (e.g. "https://uaa.run.pivotal.io/check_token" on PWS).
If you specify both the
userInfoUri
and thetokenInfoUri
then you can set a flag to say that one is preferred over the other (preferTokenInfo=true
is the default). Or -
-
spring.oauth2.resource.jwt.keyValue
to decode a JWT token locally, where the key is a verification key. The verification key value is either a symmetric secret or PEM-encoded RSA public key. If you don’t have the key and it’s public you can provide a URI where it can be downloaded (as a JSON object with a "value" field) withspring.oauth2.resource.jwt.keyUri
. E.g. on PWS:$ curl https://uaa.run.pivotal.io/token_key {"alg":"SHA256withRSA","value":"-----BEGIN PUBLIC KEY-----\nMIIBI...\n-----END PUBLIC KEY-----\n"}
Warning
|
If you use the spring.oauth2.resource.jwt.keyUri the authorization
server needs to be running when your application starts up. It will
log a warning if it can’t find the key, and tell you what to do to fix
it.
|
You can set the preferred scope (as a comma-separated list or YAML
array) in spring.oauth2.client.scope
. It defaults to empty, in which case
most Authorization Servers will ask the user for approval for the
maximum allowed scope for the client.
There is also a setting for spring.oauth2.client.clientAuthenticationScheme
which
defaults to "header" (but you might need to set it to "form" if, like
Github for instance, your OAuth2 provider doesn’t like header
authentication). The spring.oauth2.client.*
properties are bound to an instance
of AuthorizationCodeResourceDetails
so all its properties can be specified.
Token Type in User Info
Google (and certain other 3rd party identity providers) is more strict
about the token type name that is sent in the headers to the user info
endpoint. The default is "Bearer" which suits most providers and
matches the spec, but if you need to change it you can set
spring.oauth2.resource.tokenType
.
Customizing the RestTemplate
The SSO (and Resource Server) features use an OAuth2RestTemplate
internally to fetch user details for authentication. This is provided
as a qualified @Bean
with id "userInfoRestTemplate", but you
shouldn’t need to know that to just use it. The default should be fine
for most providers, but occasionally you might need to add additional
interceptors, or change the request authenticator (which is how the
token gets attached to outgoing requests). To add a customization just
create a bean of type UserInfoRestTemplateCustomizer
- it has a
single method that will be called after the bean is created but before
it is initialized. The rest template that is being customized here is
only used internally to carry out authentication (in the SSO or
Resource Server use cases).
A second {@link OAuth2RestTemplate} is available for autowiring if you want to use it for back channel calls, and if there is a token-authenticated user (in a web application) it will have the token injected for you.
Tip
|
To set an RSA key value in YAML use the "pipe" continuation marker to split it over multiple lines ("|") and remember to indent the key value (it’s a standard YAML language feature). Example:
|
Access Decision Rules
By default the whole application will be secured with OAuth2 with the
same access rule ("authenticated"). This includes the Actuator
endpoints, which you might prefer to be secured differently, so Spring
Cloud Security provides a configurer callback that lets you change the
matching and access rules for OAuth2 authentication. Any bean of type
OAuth2SsoConfigurer
(there is a convenient empty base class) will
get 2 callbacks, one to set the request matchers for the OAuth2
filter, and one with the full HttpSecurity
builder (so you can set
up all sorts of behaviour, but the main application is to control
access rules).
The default login path, i.e. the one that triggers the redirect to the
OAuth2 Authorization Server, is "/login". It will always be added to
the matching patterns for the OAuth2 SSO, even if you have
OAuth2SsoConfigurer
beans as well. The default logout path is
"/logout" and it gets similar treatment, as does the "home" page
(which is the logout success page, defaults to "/"). Those paths can
be overriden by setting spring.oauth2.sso.*' (`loginPath
, logoutPath
and
home.path
).
For example if you want the resources under "/ui/**" to be protected with OAuth2:
@Configuration
@EnableOAuth2Sso
@EnableAutoConfiguration
protected static class TestConfiguration extends OAuth2SsoConfigurerAdapter {
@Override
public void match(RequestMatchers matchers) {
matchers.antMatchers("/ui/**");
}
}
In this case the rest of the application will default to the normal Spring Boot access control (Basic authentication, or whatever custom filters you put in place).
Integrating with the Actuator Endpoints
The Spring Boot Actuator endpoints ("/env", "/metrics", etc.) if
present will, by default, be protected by the standard Spring Boot
basic authentication. The SSO authentication filter is added in a
position directly behind the filter that intercepts requests to the
Actuator endpoints by default (i.e.
ManagementProperties.BASIC_AUTH_ORDER + 1
which is
Ordered.LOWEST_PRECEDENCE-9
or 2147483636
). If you want to change
the order you can set spring.oauth2.sso.filterOrder
. If you do that
and the value is less than the default, then you will need to consider
setting the access rules for the Actuator, since they will become
accessible to all authenticated users who sign on with the external
provider. One way to do that would be to set
management.contextPath=/admin
(for instance) and use an
OAuth2SsoConfigurer
to set the access rules, e.g.
@Configuration
@EnableOAuth2Sso
@EnableAutoConfiguration
protected static class TestConfiguration extends OAuth2SsoConfigurerAdapter {
@Override
public void configure(HttpSecurity http) {
http.authorizeRequests()
.antMatchers("/admin/**").role("ADMIN")
.anyRequest().authenticated();
}
}
Resource Server
The @EnableOAuth2Resource
annotation will protect your API endpoints
if you have the same environment settings as the SSO client, except
that it doesn’t need a tokenUri
or authorizationUri
, and it also
doesn’t need a clientId
and clientSecret
if it isn’t using the
tokenInfoUri
(i.e. if it has jwt.*
or userInfoUri
).
By default all your endpoints are protected (i.e. "/**") but you can
pick and choose by adding a ResourceServerConfigurerAdapter
(standard
Spring OAuth feature), e.g. to protect only the "/api/**" resources
@RestController
@EnableOAuth2Resource
class Application extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.requestMatchers()
.antMatchers("/api/**")
.and()
.authorizeRequests()
.anyRequest().authenticated();
}
@RequestMapping("/api")
public String home() {
return "Hello World";
}
}
Customizing the JWT Token Converter
When a resource server accepts an access token as a JWT, it has to
convert it to an Authentication
so that Spring Security can do its
access decisions. Different token providers might support JWT tokens
with different contents, so Spring OAuth2 has an abstraction for
converting the token into security domain objects
(AccessTokenConverter
). You can modify the default behaviour easily
by providing a @Bean
of type JwtAccessTokenConverterConfigurer
,
e.g.
@Component
public class JwtCustomization extends DefaultAccessTokenConverter implements
JwtAccessTokenConverterConfigurer {
@Override
public void configure(JwtAccessTokenConverter converter) {
converter.setAccessTokenConverter(this);
}
... // implement custom AccessTokenConverter here
}
Token Relay
A Token Relay is where an OAuth2 consumer acts as a Client and forwards the incoming token to outgoing resource requests. The consumer can be a pure Client (like an SSO application) or a Resource Server.
Client Token Relay
If your app has a
Spring
Cloud Zuul embedded reverse proxy (using @EnableZuulProxy
) then you
can ask it to forward OAuth2 access tokens downstream to the services
it is proxying. Thus the SSO app above can be enhanced simply like this:
@Controller
@EnableOAuth2Sso
@EnableZuulProxy
class Application {
}
and it will (in addition to loggin the user in and grabbing a token)
pass the authentication token downstream to the /proxy/*
services. If those services are implemented with
@EnableOAuth2Resource
then they will get a valid token in the
correct header.
How does it work? The @EnableOAuth2Sso
annotation pulls in
spring-cloud-starter-security
(which you could do manually in a
traditional app), and that in turn triggers some autoconfiguration for
a ZuulFilter
, which itself is activated because Zuul is on the
classpath (via @EnableZuulProxy
). The
filter
just extracts an access token from the currently authenticated user,
and puts it in a request header for the downstream requests.
Resource Server Token Relay
If your app has @EnableOAuth2Resource
and also is a Client (i.e. it
has a spring.oauth2.client.clientId
, even if it doesn’t use it),
then the OAuth2RestOperations
that is provided for @Autowired
users by Spring Cloud (it is declared as @Primary
) will also forward
tokens. If you don’t want to forward tokens (and that is a valid
choice, since you might want to act as yourself, rather than the
client that sent you the token), then you only need to create your own
OAuth2RestOperations
instead of autowiring the default one. Here’s
a basic example showing the use of the autowired rest template ("foo.com"
is a Resource Server accepting the same tokens as the surrounding app):
@Autowired
private OAuth2RestOperations restTemplate;
@RequestMapping("/relay")
public String relay() {
ResponseEntity<String> response =
restTemplate.getForEntity("https://foo.com/bar", String.class);
return "Success! (" + response.getBody() + ")";
}
Configuring Authentication Downstream of a Zuul Proxy
You can control the authorization behaviour downstream of an
@EnableZuulProxy
through the proxy.auth.*
settings. Example:
proxy:
auth:
routes:
customers: oauth2
stores: passthru
recommendations: none
In this example the "customers" service gets an OAuth2 token relay, the "stores" service gets a passthrough (the authorization header is just passed downstream), and the "recommendations" service has its authorization header removed. The default behaviour is to do a token relay if there is a token available, and passthru otherwise.
See ProxyAuthenticationProperties for full details.