In the Authentication section, we showed how user-provided credentials (username/password) are compared with application-stored ones, and if they match, the user is authenticated.
To boost security, we can limit the user's access to application resources. This is where authorization comes into the picture—the question of who should access which application's resources.
Spring Security provides very comprehensive authorization features. We can categorize these features into these three authorization groups:
For example, a customer should be able to see his own order and profile data, whereas an admin should be able to see all the customers' orders plus the data that is not visible to any customer.
Since version 3.0 of Spring Security, Spring has added Spring EL expressions to its authorization features. Spring EL lets you convert complex authorization logic into simple expressions. In this section, we use Spring EL for authorization.
GrandAuthority
in Spring Security is the object for including a string value that is interchangeably called an authority, right, or permission (refer to the Authentication section, where the AuthenticationProvider
interface is explained, to see how GrandAuthority
is created). By default, if this string value starts with the prefix ROLE_
(for example, ROLE_ADMIN
), it will be considered as a user's role. So, it is also flexible enough to be used as a permission if it does not start with the prefix. Spring Security uses this object for web, method, and domain object authorization.
For web request authorization, we can limit user access based on the user's role in Spring Security, as follows (we will see later in this section how to do this in a controller):
public void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("*.jsp").denyAll() .antMatchers("/", "/login").permitAll() .antMatchers("/user*//**").access("hasRole('USER') or hasRole('ADMIN')") .antMatchers("/admin*//**").access("hasRole('ADMIN')") .antMatchers("/accountant*//**").access("hasRole('ADMIN') or hasRole('ACCOUNTANT')") .failureUrl("/nonAuthorized") ... .permitAll(); }
Since we use spring MVC, we deny all URLs that end with .jsp
(*.jsp
) and let MVC map the URL to the JSP page. We permit anybody to have access to the login page using (.antMatchers("/", /login").permitAll()
).
We limit user access to accountant resources to the admin and accountant roles (for example, antMatchers("/accountant*//**").access("hasRole('ADMIN') or hasRole('ACCOUNTANT')")
). We set an error URL and forward a user to it if he fails authentication or tries to access non-authorized resources with failureUrl("/nonAuthorized")
.
You need to add @EnableGlobalMethodSecurity(prePostEnabled=true)
to be able to apply method/domain-level authorization:
@EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled=true) @ComponentScan(basePackages = "com.springessentialsbook.chapter5") public class MultiWebSecurityConfigurator {
We already described how to limit access to URLs using a configuration file. You can do the same thing in the controller's methods too:
@PreAuthorize("hasRole('ADMIN') or hasRole('ACCOUNTANT')" @RequestMapping(value = "/accountant", method = RequestMethod.GET) public String dbaPage(ModelMap model) { ... }
For method-invoking authorization, you can configure Spring Security at the method level and define who can run a particular method in your application's service layer:
@PreAuthorize("hasRole('ADMIN') or hasRole('ACCOUNTANT')" ) public void migrateUsers(id){...};
For domain object access, you can apply method-invoking authorization and have a service method to fine-tune who can see which data in the application. For example, in the service layer, you can limit access if the username parameter is equal to the logged-in username or the user has an admin role (refer to bussinessServiceImpl
in the code):
@PreAuthorize("@businessServiceImpl.isEligibleToSeeUserData(principal, #username)") @RequestMapping("/userdata/{username}") public String getUserPage(@PathVariable String username,ModelMap model) { {...}
The OAuth2 Authorization Framework is simply a way to let third-party applications access your protected resources without you sharing your user credentials (username/password). You will have faced this situation when a website such as LinkedIn asks you to share your e-mail contacts, and when you agree, you are forwarded to your mail provider's login page (for example, Yahoo!).
When you log in, the mail provider asks for your permission to share your contacts with LinkedIn. Then, LinkedIn can get the list of your contacts in order to send them an invitation.
OAuth2 relies on the following entities:
Many leading providers (for example, Google and Facebook) have both authorization and resource servers.
This diagram illustrates how the OAuth2 framework works in a simple form:
Spring facilitates the OAuth2 framework by reusing Spring Security concepts for authentication and authorization and includes new features to implement authorization and resource servers. To use Spring OAuth2 in your project, you need the following dependency:
<dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> <version>2.0.8.RELEASE</version> </dependency>
What we explained in the Authentication section with respect to validating the user and protecting resources remains the same here. The new things are the authorization and resource server settings.
The OAuth 2.0 service includes authorization and resource servers. Spring Security lets you have separate applications as authorization and resource servers, on which one authorization server could be shared by one or many resource servers, or have both types of servers in a single application. For simplicity, we implement authorization and resource servers within the same application.
In the class MultiOAuth2ResourceAndAuthorizationConfigurator
, we define resource and authorization servers. @EnableResourceServer
tags the class ResourceServerConfiguration
as a resource server, which defines resources with the URL /public
as non-protected and ones with the /protected/**
URL as secure resources that require a valid token to access.
@EnableAuthorizationServer
tags AuthorizationServerConfiguration
as an authorization server that grants tokens to third-party clients. TokenStore
is a Spring interface; its implementation classes (InMemoryTokenStore
, JdbcTokenStore
, and JwtTokenStore
) keep track of tokens.
JdbcTokenStore
uses a database to store tokens and has a Spring-JDBC dependency. JdbcTokenStore
is suitable when you want to have a history of tokens, recovery after server failure, or the sharing of tokens among several servers.
JwtTokenStore
encodes token-related data into the token itself. JwtTokenStore
does not make tokens persistent and requires JwtAccessTokenConverter
as a translator between a JWT-encoded token and OAuth authentication information.
For simplicity, we use the InMemoryTokenStore
implementation class, but in real applications, using JdbcTokenStore
/JwtTokenStore
is a better practice.
We reuse the AuthenticationManager
class that was detailed in the Authentication section.
The method configure(ClientDetailsServiceConfigurer clients)
is the location in which we configure token generation settings, as follows:
withClient
tells us which client can access resources (this is separate from user authentication)secret
is the client's passwordauthorities
tells us which user roles are eligible to access the resourceauthorizedGrantType
specifies which grant type the client has (for example, the refresh and access token)accessTokenValiditySeconds
sets the token's time to liveThe settings are mentioned in the following code:
@Configuration public class MultiOAuth2ResourceAndAuthorizationConfigurator { @Configuration @EnableResourceServer protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http .headers() .frameOptions().disable() .authorizeRequests() .antMatchers("/public/").permitAll() .antMatchers("/protected/**").authenticated(); } } @Configuration @EnableAuthorizationServer protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter implements EnvironmentAware { private static final String CLIENT_ID = "myClientId"; private static final String CLIENT_PASSWORD = "myClientPassword"; private static final int TOKEN_TIME_TO_LIVE = 1800; private static final String ROLE_USER = "ROLE_USER"; private static final String ROLE_ACCOUNTANT = "ROLE_ACCOUNTANT"; private static final String READ_ACCESS = "read"; private static final String WRITE_ACCESS = "write"; private static final String GRANT_TYPE_PASSWORD = "password"; private static final String GRANT_TYPE_REFRESH_TOKEN = "refresh_token"; @Bean public TokenStore tokenStore() { return new InMemoryTokenStore(); } @Autowired private AuthenticationManager authenticationManager; @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints .tokenStore(tokenStore()) .authenticationManager(authenticationManager); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients .inMemory() .withClient(CLIENT_ID) .secret(CLIENT_PASSWORD) .scopes(READ_ACCESS, WRITE_ACCESS) .authorities(ROLE_USER, ROLE_ACCOUNTANT) .authorizedGrantTypes(GRANT_TYPE_PASSWORD, GRANT_TYPE_REFRESH_TOKEN) .accessTokenValiditySeconds( TOKEN_TIME_TO_LIVE); } @Override public void setEnvironment(Environment environment) { } } }
The resources we granted access to using the token are included in a controller. Here, we define a very simple resource:
@RequestMapping(value = "/protected", method = RequestMethod.GET) @ResponseBody public String getProtectedResources(ModelMap model) { return "this is from getProtectedResources"; } @RequestMapping(value = "/public", method = RequestMethod.GET) @ResponseBody public String getPublicResources(ModelMap model) { return "this is from getPublicResources"; }
You can run the project with the following command, which builds and runs the resource and authorization server:
mvn clean package spring-boot:run -Dserver.contextPath=/myapp -Dserver.port=9090
If you try the following, you can see the resource because this URL is unprotected:
curl -i http://localhost:9090/myapp/public
However, if you try the next command, you get a "non-authorized" error and you need a valid token to access this resource:
curl -i http://localhost:9090/myapp/protected
You need to get a token first to be able to access protected resources. Spring MVC exposes an endpoint, TokenEndpoint
, in order to get the token with the /oauth/token
URL by default. The following command gives you an authorization token:
curl -X POST -vu myClientId:myClientPassword 'http://localhost:9090/myapp/oauth/token?username=operator&password=password&grant_type=password'
Now, you can provide the token and access the secure resource:
curl -i -H "Authorization: Bearer [access_token]" http://localhost:9090/myapp/protected
Notice that we set a time to live for the token and we need to refresh the token if it expires. The following command renews the token by calling the /oauth/token
endpoint and passing refresh_token
as the grant_type
parameter:
curl -X POST -vu myClientId:myClientPassword 'http://localhost:9090/myapp/oauth/token?grant_type=refresh_token&refresh_token=[refresh_token]'