This blogpost was written by the team at CleverAnalytics about their use of Stormpath and is reprinted from them with permission (and our thanks!). The updated Java SDK now includes token based authentication, and more!
CleverAnalytics is a location intelligence cloud platform. It allows you to easily create interactive and highly responsive business-oriented maps based on internal and external data. For example, you could calculate a store catchment area from order records and relate it to demographics and purchasing power. Business users can map their businesses in context and make informed decisions.
Providing this type of platform for enterprise businesses comes with lots of mainly non-functional requirements:
- User Management – advanced tools for user provisioning via LDAP or Active Directory
- Authentication – support standard authentication protocols for accessing the API
- Scalability – grow with our customers
- Data Security – guarantee data security and strong password management
- Service Availability – SLA 99%
- Flexibility – allow customers to customize their projects
- Integration – easily integrate CleverAnalytics into customer’s business environment, for example:
- Single Sign-on
- Embedding maps as iFrame widget
- Customer Workflow integration – data loading, scheduled exports etc.
Internally, we have added following requirements:
- Performace – it’s always feature number one, the most visible feature for every customer
- Multi-tenancy – Host thousands of projects in one shared AWS environment
- Developer effectiveness – invest our development time in developing our core business and outsource all the general functionality
Our Stack
Fortunately, we started the project from scratch in February 2014, so we could easily choose technologies optimized for our requirements. We chose the AWS stack as a primary environment and a microservice oriented architecture to meet our scalability and flexibility needs.
HATEOAS REST API architecture is also a core architecture concept for communication between the javascript client and the server backend, as well as for internal communication between microservices and for any external application or public REST APIs.
Backend
As a core framework for our microservice, we decided to use a new Spring Boot framework that comes with completely self-contained executable jar. Getting rid of shared server container and running executable jars as a competely separated system processes, is an important backend principle. Spring Boots adds a powerful APIs for health check monitoring.
We also use other Spring frameworks in our stack, such as Spring MVC for REST API, Spring Security for application authentication and authorization, Spring Data for relational database access.
For storing persistent data there is a Postgres DB with Postgis extension for spatial data. Geo-spatial data are served by GeoServer – an open source server. It publishes a standard WFS (Web Feature Service) API for serving geospatial data in vector form, so we can transform the DB data to GeoJSON, add metadata and support operations like bounding box.
Frontend
Our web client is based on AngularJS MVW Framework and loads all data and metadata through a public REST API from CleverAnaltytics backend. The interactive map uses a Leaflet library. All business mapping data are transferred as vectors (GeoJSON), only the background map is consumed as an outsourced WMS (raster) service from MapBox.com.
REST Authentication API
This section describes how CleverAnalytics authentication works and describes the whole authentication workflow.
We have chosen OAuth 2.0 as an authentication protocol, because it suits SaaS application use cases best. A REST API can be used by many different types of clients: * CleverAnalytics web client * Third party SaaS applications * Consultant tools * Some operational and management tools
OAuth 2.0 focuses on developer simplicity while providing specific authorization flows for web applications, desktop applications, mobile phones, and living room devices.
The protocol uses two types of tokens: refresh and access token. Firstly, a refresh token needs to be obtained and to do this, client must send valid authorization credentials to a login resource. When obtained, this token can be exchanged for a fresh access token (Bearer) that is used for authentication all HTTP requests.
Workflow
To see an authentication workflow in more detail, this section describes the REST API for the resources that participate in the authentication workflow.
Login – Refresh Token
The login resource handles requests with user name and password. If credentials pass verification, a new refresh token is generated and the client gets detail about the authenticated account. The following example shows it from the HTTP perspective:
POST /rest/oauth/login HTTP/1.1
> User-Agent: curl/7.29.0
> Host: localhost:9010
> Accept: application/json
> Content-Type: application/json
> Cache-Control: no-cache
> Content-Length: 60
>
{ "email": "john", "password": "PASSWORD", "rememberMe": true }
*Response*
< HTTP/1.1 200 OK
< Server: Apache-Coyote/1.1
< Set-Cookie: CAN-RefreshToken=R1VVS0RBSk42UTVSNTJYUFJKFHDIDJOIJlkKLJDKIEOU9BbUxpKzV5NEhTRXlR; Expires=Thu, 19-Feb-2015 13:27:19 GMT; Domain=your.domain.com; Path=/rest; Secure; HttpOnly
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Fri, 21 Nov 2014 13:27:19 GMT
<
{"id":"O3fdjikfoiSIMRej4q0","fullName":"John Testing","email":"john@cleveranalytics.com","status":"ENABLED"}
As you can see, this login request succeeded and the server generated a new refresh token. It is returned as HTTP response header and the web browser will store it’s value as a cookie with given expiration time. The browser will completely maintain sending this refresh token with each request to our https://our.domain.com/rest
url. The cookie’s flag HttpOnly
means that the cookie cannot be accessed through client side script – a good practice for mitigating the majority of Cross Site Scripting (XSS) attacks.
The request body is returned with some basic information about the authenticated account.
Remember Me
Allowing the user to stay logged in when working with the web application is a “must-have” functionality and requires storing authentication credentials in a browser. For security reasons, it is not a good practice to store user credentials in browser cookie. OAuth 2.0 offers a better option – store the obtained refresh token, which is a better choice because it does not contain any user data, has limited validity and can be independently invalidated. This means a user can have more than one valid refresh token at the same time – the unique token is then stored at user’s laptop and tablet web browser.
When filling in the credentials in login screen, there is an option for enabling the “Remember Me” option. If enabled, the user will not be asked for credentials by current web browser for the next 90 days. Otherwise, the authentication is remembered only for the current browser session – it expires when user closes the browser application.
In both cases, the refresh token is stored in browser cookie and what differs is a setting of the max age:
// A negative value means that the cookie is not stored persistently and it is deleted when a web browser is closed cookie.setMaxAge(request.isRememberMe() ? (60 * 60 * 24 * 90) : -1);
Access Token
When REST API client gets a refresh token, it can ask for access token what is used for each request authentication. To obtain access token, REST API client only sends a GET request into /rest/oauth/token
resource:
> GET /rest/oauth/token HTTP/1.1
> Host: localhost:9010
> Cookie: CAN-RefreshToken=R1VVS0RBSk42UTVSNTJYUFJKFHDIDJOIJlkKLJDKIEOU9BbUxpKzV5NEhTRXlR
> Accept: application/json
> Content-Type: application/json
> Cache-Control: no-cache
*Response*
< HTTP/1.1 200 OK
< Server: Apache-Coyote/1.1
< Content-Type: application/json;charset=UTF-8
< Content-Length: 329
< Date: Fri, 21 Nov 2014 13:46:30 GMT
<
{"expires_in":900,"token_type":"Bearer","access_token":"eyJhbGciOiJIUzI1Ni...OiJIUzIKeM"}
As you can see, the request above was successfully authenticated – and the web browser correctly set a cookie HTTP request header. The server returns a new access token with expiration time 15 minutes.
Access Authenticated Resource
The majority of CleverAnalytics REST API resources require authentication. The following example shows how to use the access token to make authenticated call on a REST API resource:
> GET /rest/bootstrap HTTP/1.1
> Host: localhost:9010
> Authorization: Bearer eyJhbGciOiJIUzI1Ni...OiJIUzIKeM
> Cookie: CAN-RefreshToken=R1VVS0RBSk42UTVSNTJYUFJKFHDIDJOIJlkKLJDKIEOU9BbUxpKzV5NEhTRXlR
> Accept: application/json
> Content-Type: application/json
> Cache-Control: no-cache
*Response*
< HTTP/1.1 200 OK
< Server: Apache-Coyote/1.1
< Content-Type: application/json;charset=UTF-8
< Content-Length: 329
< Date: Fri, 21 Nov 2014 13:46:30 GMT
<
{"account":{"id":"O3fdjikfoiSIMRej4q0","fullName":"John testing","email":"john@cleveranalytics.com","status":"ENABLED"`
The authenticated request must have an Authentication
header and the header value must be set to a valid access token with prefix Bearer
. As you can see, the web browser sets the Cookie header too, but it is not evaluated on server side.
If the authentication fails, the response is:
< HTTP/1.1 401 Unauthorized
< Server: Apache-Coyote/1.1
< WWW-Authenticate: Bearer realm="CleverAnalytics API" error="invalid_token"
< Pragma: no-cache
< Cache-Control: no-store, no-cache, must-revalidate, max-age=0
< Expires: Thu, 01 Jan 1970 00:00:00 GMT
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Fri, 21 Nov 2014 14:13:20 GMT
<
{"timestamp":1416579200612,"status":401,"error":"Unauthorized","message":"Unauthorized","path":"/rest/bootstrap"}
Server returns 401 Unauthorized
response status and sets response WWW-Authenticate
header to Bearer realm="CleverAnalytics API" error="invalid_token"
. In this case, the access token was expired and REST API client will ask for the new Bearer token by GET /rest/oauth/token
and retry the failed request again.
Logout
The final but still very important functionality in authetication workflow is a logout operation. For OAuth 2.0, this means the user wants to invalidate the refresh token. In a REST API, it is implemented as authenticated resource with method DELETE /rest/oauth/login
> DELETE /rest/oauth/login HTTP/1.1
> User-Agent: curl/7.29.0
> Host: localhost:9010
> Accept: application/json
> Authorization: Bearer eyJhbGciOiJIUzI1Ni...OiJIUzIKeM
> Cache-Control: no-cache
> Content-Length: 0
> Content-Type: application/x-www-form-urlencoded
< HTTP/1.1 204 No Content
< Server: Apache-Coyote/1.1
< Date: Tue, 25 Nov 2014 09:02:07 GMT
Server response is 204 No Content
if the request was successfully processed.
Implementation
OAuth 2.0 focuses on client developer simplicity while providing specific authentication flows for web applications, tools, third party applications etc. But on the server side, implementation of OAuth 2.0 is quite a complex task. Every mistake in the implementation could cause a security vulnerability. Further to our requirement of offloading non-core functionality, we sought out a service that could handle all the hard work connected with authentication and user management for us.
Stormpath
After some research, we chose Stormpath, as it is strongly oriented to developers and offers exactly what we need. Stormpath announced support for OAuth 2.0 authentication this summer, and we successfully started to use this service in beta immediately after the launch. Visit the Stormpath web for more details about this service: http://docs.stormpath.com/guides/api-key-management/
To use Stormpath SDK in a Java Maven project, you need add following dependencies:
<dependency>
<groupId>com.stormpath.sdk</groupId>
<artifactId>stormpath-sdk-api</artifactId>
</dependency>
<dependency>
<groupId>com.stormpath.sdk</groupId>
<artifactId>stormpath-sdk-impl</artifactId>
</dependency>
<dependency>
<groupId>com.stormpath.sdk</groupId>
<artifactId>stormpath-sdk-httpclient</artifactId>
</dependency>
<dependency>
<groupId>com.stormpath.sdk</groupId>
<artifactId>stormpath-sdk-oauth</artifactId>
</dependency>
Spring Security
Spring security is a really powerful and highly customizable authentication and access-control framework. This section describes how to integrate Stormpath authentication into Spring Boot application.
Setting Security Configuration
To enable Spring Security you need add a new Maven dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
The next step is to enable integration Spring Security with Spring MVC in a Configuration with the annotation @EnableWebMvcSecurity
. You can see complete example of configuration of WebSecurityConfigurerAdapter for hosting stateless REST API below. Look out for more comments inside the code:
@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private StormpathAuthenticationFilter stormpathFilter;
@Autowired
private RestAuthenticationEntryPoint entryPoint;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// defines the authentication for application entrypoints
.authorizeRequests()
// POST to /rest/oauth/login is not authenticated
.antMatchers(HttpMethod.POST, "/rest/oauth/login").permitAll()
// GET /rest/oauth/token is not authenticated
.antMatchers(HttpMethod.GET, "/rest/oauth/token").permitAll()
// the other REST APIs are authenticated
.antMatchers("/rest/**").authenticated()
.and()
// never use server side sessions (stateless mode)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.anonymous()
.and()
.securityContext()
.and()
.headers().disable()
.rememberMe().disable()
.requestCache().disable()
.x509().disable()
.csrf().disable()
.httpBasic().disable()
.formLogin().disable()
.logout().disable()
// add custom authentication filter
.addFilterBefore(stormpathFilter, AnonymousAuthenticationFilter.class)
// register custom authentication exception handler
.exceptionHandling().authenticationEntryPoint(entryPoint);
}
}
The configuration enforces authentication for all resources except the following, two of which are available for an anonymous user: * POST on /rest/oauth/login * GET on /rest/oauth/token
A complete authentication flow with authentication filter is vizualized in a following sequence diagram:
Stormpath Client
Stormpath Client is a SDK wrapper for Stormpath REST API. It is a primary class for communication with Stormpath from a Java application. In the Spring ecosystem, you can simply inject the Client bean anywhere you need it, but first it needs to be properly initialized. Stormpath provides a ClientBuilder for constructing the Client instance. An example below demonstrates setting up Stormpath Administrator Key and configuring cache manager:
ApiKey apiKey = ApiKeys.builder().setId(rootStormpathKeyId).setSecret(rootStormpathKeySecret).build();
client = Clients.builder()
.setApiKey(apiKey)
.setCacheManager(
newCacheManager()
.withDefaultTimeToLive(1, TimeUnit.DAYS) //general default
.withDefaultTimeToIdle(2, TimeUnit.HOURS) //general default
.withCache(forResource(Account.class) //Account-specific cache settings
.withTimeToLive(1, TimeUnit.HOURS)
.withTimeToIdle(30, TimeUnit.MINUTES))
.withCache(forResource(Group.class) //Group-specific cache settings
.withTimeToLive(2, TimeUnit.HOURS))
.build() //build the CacheManager
)
.build();
In following resources examples is not injected the bean Client directly but a concrete Application bean for CleverAnalytics application. You can read more about Stormpath Application here: http://docs.stormpath.com/rest/product-guide/#applications
Login Resource
The login controller verifies the given user credentials by calling the Stormpath service. If verification passes, the resource generates a new refresh token. Here is a simplified fragment of code without any exception handling, logging etc.:
import com.stormpath.sdk.account.Account;
import com.stormpath.sdk.api.ApiKey;
import com.stormpath.sdk.authc.AuthenticationRequest;
import com.stormpath.sdk.application.Application;
import com.stormpath.sdk.authc.UsernamePasswordRequest;
import javax.servlet.http.Cookie;
@RestController
@RequestMapping("/rest/oauth/login")
public class LoginController {
@Autowired
private Application stormpathApplication;
@RequestMapping(method = {RequestMethod.POST, RequestMethod.HEAD})
public LoginResponse login(@RequestBody LoginRequest request, HttpServletResponse response) throws Exception {
AuthenticationRequest authReq = new UsernamePasswordRequest(request.email, request.password);
Account account = stormpathApplication.authenticateAccount(authReq).getAccount();
// create a new api key and set it store it in a cookie
ApiKey apiKey = account.createApiKey();
String concat = apiKey.getId() + ":" + apiKey.getSecret();
String securityToken = Base64.encodeBase64String(concat.getBytes("UTF-8"));
// set ApiKeyToken
Cookie cookie = new Cookie("CAN-RefreshToken", securityToken);
cookie.setPath("/rest");
cookie.setHttpOnly(true);
cookie.setSecure(true);
// A negative value means that the cookie is not stored persistently and will be deleted when the Web browser exits.
cookie.setMaxAge(request.isRememberMe() ? (60 * 60 * 24 * 90) : -1);
cookie.setDomain(SERVER_DOMAIN);
response.addCookie(cookie);
// return a LoginResponse DTO as a response body
}
}
Token Resource
The controller of the token resource reads the value of the cookie CAN-RefreshToken
, verifies validity of the refresh token and generates a new access token. The Stormpath REST API generates a new access token is provided on, but the API is looking for the refresh token in Authorization
header. By contrast, we get a request that has a refresh token in cookie header.
Because we do not want to manipulate with refresh token on the client side (read this cookie is forbidden by the javascript HttpOnly
flag), it is necessary to do this work on server. Instead of simply resending the incoming HTTP request, the server creates a new HTTP request and sets the refresh header to the Authorization
request header to fit the expectations of the Stormpath API.
The following example is again a simplified fragment of code. Lots of the logic should be definitely be part of some Service class, and proper exception handling and logging are also omitted:
@RestController
@RequestMapping("/rest/oauth/token")
public class TokenController {
@Autowired
private Application stormpathApplication;
@RequestMapping(method = {RequestMethod.GET})
public String createToken(@CookieValue("CAN-RefreshToken") String apiKeyToken) {
// Set up HTTP Headers
Map<String, String[]> headers = new HashMap();
headers.put(HttpHeaders.AUTHORIZATION, new String[]{"Basic " + apiKeyToken});
headers.put(HttpHeaders.CONTENT_TYPE, new String[]{"application/x-www-form-urlencoded"});
headers.put(HttpHeaders.ACCEPT, new String[]{"application/json"});
String[] param = {"client_credentials"};
Map<String, String[]> params = new HashMap();
params.put("grant_type", param);
// Create a new HttpRequest that Stormpath SDK understands:
HttpRequest requestCustom = HttpRequests.method(HttpMethod.POST).headers(headers).parameters(params).build();
// Obtain access token from Stormpath
AccessTokenResult resultToken = (AccessTokenResult) stormpathApplication.authenticateOauthRequest(request).withTtl(60 *15).execute();
return resultToken.getTokenResponse().toJson();
}
}
Logout Resource
The logout operation is an invalidation of the refresh token. This could mean an invalidation of all existing refresh tokens (ie. logout user from all it’s devices) or invalidating only the selected ones. In this example, all active refresh tokens are removed:
@ResponseStatus(HttpStatus.NO_CONTENT)
@RequestMapping(method = {RequestMethod.DELETE})
public void logout(@AuthenticationPrincipal Principal principal) throws Exception {
// get current Stormpath Account from Principal details
Account account = ((Account) ((UsernamePasswordAuthenticationToken)principal).getDetails());
// remove all existing API keys for the account
ApiKeyList apiKeys = account.getApiKeys();
for (ApiKey key : apiKeys) {
key.delete();
}
}
Authentication Filter
All incoming HTTP requests are handled by authentication filter before being dispatched to MVC Controllers. The authentication filter StormpathAuthenticationFilter
is registered in the filters chain by the WebSecurityConfig
configuration. The filter verifies the account token present from Authorization
HTTP header. If the authentication succeeds, Stormpath API returns the ApiAuthenticationResult
instance withdetails of the authenticated Stormpath account.
The Account
is then set into SecurityContextHolder
to be available anywhere as a ThreadLocal variable.
Here is how filter authenticates a request and sets result to SecurityContextHolder
:
@Component
public class StormpathAuthenticationFilter extends GenericFilterBean {
@Autowired
private Application stormpathApplication;
@Autowired
private RestAuthenticationEntryPoint entryPoint;
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
try {
// authenticate by Stormpath SDK
ApiAuthenticationResult result = stormpathApplication.authenticateApiRequest(request);
// set authentication success into securityContextHolder
Set<GrantedAuthority> authorities = new HashSet<>();
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(result.getAccount().getEmail(), "secret_pass", authorities);
auth.setDetails(result.getAccount());
SecurityContextHolder.getContext().setAuthentication(auth);
} catch (ResourceException ex) {
// authentication failed, log the exception and continue processing the request
// Principal is set to anonymous user
}
// continue processing the request
chain.doFilter(request, response);
}
}
If authentication fails, user is authenticated as anonymously and only URLs with permitAll
settings in WebSecurityConfig
are available.
Injecting Account into Controller
It is very useful to inject the current Account directly into Controller’s method. Spring Security supports injecting the current Principal general instance, but it is a bit ugly when you want to get the Account object from it:
@RequestMapping(method = RequestMethod.GET)
public SomeDTO getResource(Principal principal) {
Account account = (Account)((UsernamePasswordAuthenticationToken)principal).getDetails();
...
}
Much more elegant is defining the custom annotation that directly provides the Account
object:
@RequestMapping(method = RequestMethod.GET)
public SomeDTO getResource(@ActiveAccount Account account) {
...
}
To do this, you need define a custom annotation:
/** Custom annotation ActiveAccount for injecting Account from SecurityContextHolder detail */
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ActiveAccount {}
implement custom `HandlerMethodArgumentResolver` for resolving method argument:
/** ActiveAccount annotation resolver. */
@Component
public class ActiveAccountHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
final static Logger logger = LoggerFactory.getLogger(ActiveAccountHandlerMethodArgumentResolver.class);
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
return methodParameter.getParameterAnnotation(ActiveAccount.class) != null
&& methodParameter.getParameterType().equals(Account.class);
}
@Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
if (this.supportsParameter(methodParameter)) {
Principal principal = webRequest.getUserPrincipal();
return (Account) ((Authentication) principal).getDetails();
} else {
return WebArgumentResolver.UNRESOLVED;
}
}
}
and register HandlerMethodArgumentResolver
by ActiveAccountConfigurerAdapter:
:
/** Register custom annotation resolver */
@Configuration
public class ActiveAccountConfigurerAdapter extends WebMvcConfigurerAdapter {
@Bean
public ActiveAccountHandlerMethodArgumentResolver activeAccountHandlerMethodArgumentResolver() {
return new ActiveAccountHandlerMethodArgumentResolver();
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(activeAccountHandlerMethodArgumentResolver());
super.addArgumentResolvers(argumentResolvers);
}
}
Summary
This article wraps up how we have integrated Stompath OAuth 2.0 authentication into Spring Boot backend application. Thanks to outsourcing all the hard work to Stormpath, it only required configuring Spring Security and calling a few Stormpath SDK methods. The authentication logic is encapsulated into the Authentication Filter and resource access rules are configured as a standard HttpSecurity
object in WebSecurityConfig
.
Hopefully, you can now imagine how to write a custom resource for getting or updating an Account or creating a user request for password reset. It is again only about injecting the Stormpath Application
instance and calling the SDK method. You can read complete SDK documentation here: http://docs.stormpath.com/java/apidocs/