A Secure Single Page Application
In this tutorial we show some nice features of Spring Security, Spring Boot and Angular working together to provide a pleasant and secure user experience. It should be accessible to beginners with Spring and Angular, but there also is plenty of detail that will be of use to experts in either. This is actually the first in a series of sections on Spring Security and Angular, with new features exposed in each one successively. We’ll improve on the application in the second and subsequent installments, but the main changes after this are architectural rather than functional.
Spring and the Single Page Application
HTML5, rich browser-based features, and the "single page application" are extremely valuable tools for modern developers, but any meaningful interactions will involve a backend server, so as well as static content (HTML, CSS and JavaScript) we are going to need a backend server. The backend server can play any or all of a number of roles: serving static content, sometimes (but not so often these days) rendering dynamic HTML, authenticating users, securing access to protected resources, and (last but not least) interacting with JavaScript in the browser through HTTP and JSON (sometimes referred to as a REST API).
Spring has always been a popular technology for building the backend features (especially in the enterprise), and with the advent of Spring Boot things have never been easier. Let’s have a look at how to build a new single page application from nothing using Spring Boot, Angular and Twitter Bootstrap. There’s no particular reason to choose that specific stack, but it is quite popular, especially with the core Spring constituency in enterprise Java shops, so it’s a worthwhile starting point.
Create a New Project
We are going to step through creating this application in some detail, so that anyone who isn’t completely au fait with Spring and Angular can follow what is happening. If you prefer to cut to the chase, you can skip to the end where the application is working, and see how it all fits together. There are various options for creating a new project:
Using curl on the command line
Using Spring Boot CLI
Using the Spring Initializr website
Using Spring Tool Suite
The source code for the complete project we are going to build is in Github here, so you can just clone the project and work directly from there if you want. Then jump to the next section.
Using Curl
The easiest way to create a new project to get started is via the Spring Boot Initializr. E.g. using curl on a UN*X like system:
$ mkdir ui && cd ui
$ curl https://start.spring.io/starter.tgz -d style=web \
-d style=security -d name=ui | tar -xzvf -
You can then import that project (it’s a normal Maven Java project by default) into your favourite IDE, or just work with the files and "mvn" on the command line. Then jump to the next section.
Using Spring Boot CLI
You can create the same project using the Spring Boot CLI, like this:
$ spring init --dependencies web,security ui/ && cd ui
Then jump to the next section.
Using the Initializr Website
If you prefer you can also get the same code directly as a .zip file from the Spring Boot Initializr. Just open it up in your browser and select dependencies "Web" and "Security", then click on "Generate Project". The .zip file contains a standard Maven or Gradle project in the root directory, so you might want to create an empty directory before you unpack it. Then jump to the next section.
Using Spring Tool Suite
In Spring Tool Suite (a set of Eclipse plugins) you can also create and import a project using a wizard at File->New->Spring Starter Project
. Then jump to the next section. IntelliJ IDEA and NetBeans have similar features.
Add an Angular App
The core of a single page application in Angular (or any modern front-end framework) these days is going to be a Node.js build. Angular has some tools for setting this up quickly, so lets use those, and also keep the option of building with Maven, like any other Spring Boot application. The details of how to set up the Angular app are covered elsewhere, or you can just checkout the code for this tutorial from github.
Running the Application
Once the Angular app is primed, your application will be loadable in a browser (even though it doesn’t do much yet). On the command line you can do this
$ mvn spring-boot:run
and go to a browser at http://localhost:8080. When you load the home page you should get a browser dialog asking for username and password (the username is "user" and the password is printed in the console logs on startup). There’s actually no content yet (or maybe the deafult "hero" tutorial content from the ng
CLI), so you should get essentially a blank page.
If you don’t like scraping the console log for the password just add this to the "application.properties" (in "src/main/resources"): security.user.password=password (and choose your own password). We did this in the sample code using "application.yml". |
In an IDE, just run the main()
method in the application class (there is only one class, and it is called UiApplication
if you used the "curl" command above).
To package and run as a standalone JAR, you can do this:
$ mvn package
$ java -jar target/*.jar
Customize the Angular Application
Let’s customize the "app-root" component (in "src/app/app.component.ts").
A minimal Angular application looks like this:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'Demo';
greeting = {'id': 'XXX', 'content': 'Hello World'};
}
Most of the code in this TypeScript is boiler plate. The interesting stuff is all going to be in the AppComponent
where we define the "selector" (the name of the HTML element) and a snippet of HTML to render via the @Component
annotation. We also need to edit the HTML template ("app.component.html"):
style="text-align:center"class="container">
Welcome {{title}}!
class="container">
Id: {{greeting.id}}
Message: {{greeting.content}}!
If you added those files under "src/app" and rebuilt your app it should now be secure and functional, and it will say "Hello World!". The greeting
is rendered by Angular in the HTML using the handlebar placeholders, {{greeting.id}}
and {{greeting.content}}
.
Adding Dynamic Content
So far we have an application with a greeting that is hard coded. That’s useful for learning how things fit together, but really we expect content to come from a backend server, so let’s create an HTTP endpoint that we can use to grab a greeting. In your application class (in "src/main/java/demo"), add the @RestController
annotation and define a new @RequestMapping
:
@SpringBootApplication
@RestController
public class UiApplication {
@RequestMapping("/resource")
public Map<String,Object> home() {
Map<String,Object> model = new HashMap<String,Object>();
model.put("id", UUID.randomUUID().toString());
model.put("content", "Hello World");
return model;
}
public static void main(String[] args) {
SpringApplication.run(UiApplication.class, args);
}
}
Depending on the way you created your new project it might not be called UiApplication . |
Run that application and try to curl the "/resource" endpoint and you will find that it is secure by default:
$ curl localhost:8080/resource
{"timestamp":1420442772928,"status":401,"error":"Unauthorized","message":"Full authentication is required to access this resource","path":"/resource"}
Loading a Dynamic Resource from Angular
So let’s grab that message in the browser. Modify the AppComponent
to load the protected resource using XHR:
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'Demo';
greeting = {};
constructor(private http: HttpClient) {
http.get('resource').subscribe(data => this.greeting = data);
}
}
We injected an http
service, which is provided by Angular through the http
module, and used it to GET our resource. Angular passes us the response and we pull out JSON and assign it to the greeting.
To enable the dependency injection of the http
service into our custom component, we need to declare it in the AppModule
that includes the component (it’s just one more line in the imports
compared to the initial draft):
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { HttpClientModule } from '@angular/common/http';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
HttpClientModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Run the application again (or just reload the home page in the browser), and you will see the dynamic message with its unique ID. So, even though the resource is protected and you can’t curl it directly, the browser was able to access the content. We have a secure single page application in less than a hundred lines of code!
You might need to force your browser to reload the static resources after you change them. In Chrome (and Firefox with a plugin) you can use "developer tools" (F12), and that might be enough. Or you might have to use CTRL+F5. |
How Does it Work?
The interactions between the browser and the backend can be seen in your browser if you use some developer tools (usually F12 opens this up, works in Chrome by default, may require a plugin in Firefox). Here’s a summary:
VERB | PATH | STATUS | RESPONSE |
---|---|---|---|
GET |
/ |
401 |
Browser prompts for authentication |
GET |
/ |
200 |
index.html |
GET |
/*.js |
200 |
Loads of third assets from angular |
GET |
/main.bundle.js |
200 |
Application logic |
GET |
/resource |
200 |
JSON greeting |
You might not see the 401 because the browser treats the home page load as a single interaction, and you might see 2 requests for "/resource" because there is a CORS negotiation.
Look more closely at the requests and you will see that all of them have an "Authorization" header, something like this:
Authorization: Basic dXNlcjpwYXNzd29yZA==
The browser is sending the username and password with every request (so remember to use HTTPS exclusively in production). There’s nothing "Angular" about that, so it works with your JavaScript framework or non-framework of choice.
What’s Wrong with That?
On the face of it, it seems like we did a pretty good job, it’s concise, easy to implement, all our data are secured by a secret password, and it would still work if we changed the front end or backend technologies. But there are some issues.
Basic authentication is restricted to username and password authentication.
The authentication UI is ubiquitous but ugly (browser dialog).
There is no protection from Cross Site Request Forgery (CSRF).
CSRF isn’t really an issue with our application as it stands since it only needs to GET the backend resources (i.e. no state is changed in the server). As soon as you have a POST, PUT or DELETE in your application it simply isn’t secure any more by any reasonable modern measure.
In the next section in this series we will extend the application to use form-based authentication, which is a lot more flexible than HTTP Basic. Once we have a form we will need CSRF protection, and both Spring Security and Angular have some nice out-of-the box features to help with this. Spoiler: we are going to need to use the HttpSession
.
The Login Page
In this section we continue our discussion of how to use Spring Security with Angular in a "single page application". Here we show how to use Angular to authenticate a user via a form and fetch a secure resource to render in the UI. This is the second in a series of sections, and you can catch up on the basic building blocks of the application or build it from scratch by reading the first section, or you can just go straight to the source code in Github. In the first section we built a simple application that used HTTP Basic authentication to protect the backend resources. In this one we add a login form, give the user some control over whether to authenticate or not, and fix the issues with the first iteration (principally lack of CSRF protection).
Reminder: if you are working through this section with the sample application, be sure to clear your browser cache of cookies and HTTP Basic credentials. In Chrome the best way to do that for a single server is to open a new incognito window.
Add Navigation to the Home Page
The core of an Angular application is an HTML template for the basic page layout. We already had a really basic one, but for this application we need to offer some navigation features (login, logout, home), so let’s modify it (in src/app
):
class="container">
class="container">
The main content is a
and there is a navigation bar with login and logout links.
The
selector is provided by Angular, and it needs to be wired up to a component in the main module. There is going to be one component per route (per menu link), and a helper service to glue them together, and share some state (AppService
). Here’s the implementation of the module that pulls all the pieces together:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { RouterModule, Routes } from '@angular/router';
import { AppService } from './app.service';
import { HomeComponent } from './home.component';
import { LoginComponent } from './login.component';
import { AppComponent } from './app.component';
const routes: Routes = [
{ path: '', pathMatch: 'full', redirectTo: 'home'},
{ path: 'home', component: HomeComponent},
{ path: 'login', component: LoginComponent}
];
@NgModule({
declarations: [
AppComponent,
HomeComponent,
LoginComponent
],
imports: [
RouterModule.forRoot(routes),
BrowserModule,
HttpClientModule,
FormsModule
],
providers: [AppService]
bootstrap: [AppComponent]
})
export class AppModule { }
We added a dependency on an Angular module called "RouterModule" and this allowed us to inject a magic router
into the constructor of the AppComponent
. The routes
are used inside the imports of the AppModule
to set up links to "/" (the "home" controller) and "/login" (the "login" controller).
We also sneaked the FormsModule
in there, because it will be needed later for binding data to a form that we want to submit when the user logs in.
The UI components are all "declarations" and the service glue is a "provider". The AppComponent
actually doesn’t do very much. The TypeScript component that goes with the app root is here:
import { Component } from '@angular/core';
import { AppService } from './app.service';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';
import 'rxjs/add/operator/finally';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
constructor(private app: AppService, private http: HttpClient, private router: Router) {
this.app.authenticate(undefined, undefined);
}
logout() {
this.http.post('logout', {}).finally(() => {
this.app.authenticated = false;
this.router.navigateByUrl('/login');
}).subscribe();
}
}
Salient features:
There is some more dependency injection, this time of the
AppService
There is a logout function exposed as a property of the component, which we can use later to send a logout request to the backend. It sets a flag in the
app
service, and sends the user back to the login screen (and it does this unconditionally via afinally()
callback).We are using
templateUrl
to externalize the template HTML into a separate file.The
authenticate()
function is called when the controller is loaded to see if the user is actually already authenticated (e.g. if he had refreshed the browser in the middle of a session). We need theauthenticate()
function to make a remote call because the actual authentication is done by the server, and we don’t want to trust the browser to keep track of it.
The app
service that we injected above needs a boolean flag so we can tell if the user is currently authenticated, and a function authenticate()
that can be used to authenticate with the back end server, or just to query it for the user details:
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
@Injectable()
export class AppService {
authenticated = false;
constructor(private http: HttpClient) {
}
authenticate(credentials, callback) {
const headers = new HttpHeaders(credentials ? {
authorization : 'Basic ' + btoa(credentials.username + ':' + credentials.password)
} : {});
this.http.get('user', {headers: headers}).subscribe(response => {
if (response['name']) {
this.authenticated = true;
} else {
this.authenticated = false;
}
return callback && callback();
});
}
}
The authenticated
flag is simple. The authenticate()
function sends HTTP Basic authentication credentials if they are provided, and otherwise not. It also has an optional callback
argument that we can use to execute some code if the authentication is successful.
The Greeting
The greeting content from the old home page can go right next to the "app.component.html" in "src/app":
Greeting
[hidden]="!authenticated()">
The ID is {{greeting.id}}
The content is {{greeting.content}}
[hidden]="authenticated()">
Login to see your greeting
Since the user now has the choice whether to login or not (before it was all controlled by the browser), we need to distinguish in the UI between content that is secure and that which is not. We have anticipated this by adding references to an (as yet non-existent) authenticated()
function.
The HomeComponent
has to fetch the greeting, and also provide the authenticated()
utility function that pulls the flag out of the AppService
:
import { Component, OnInit } from '@angular/core';
import { AppService } from './app.service';
import { HttpClient } from '@angular/common/http';
@Component({
templateUrl: './home.component.html'
})
export class HomeComponent {
title = 'Demo';
greeting = {};
constructor(private app: AppService, private http: HttpClient) {
http.get('resource').subscribe(data => this.greeting = data);
}
authenticated() { return this.app.authenticated; }
}
The Login Form
The login form also gets its own component:
class="alert alert-danger" [hidden]="!error">
There was a problem logging in. Please try again.
This is a very standard login form, with 2 inputs for username and password and a button for submitting the form via an Angular event handler (submit)
. You don’t need an action on the form tag, so it’s probably better not to put one in at all. There is also an error message, shown only if the angular model contains an error
. The form controls use ngModel
from Angular Forms to pass data between the HTML and the Angular controller, and in this case we are using a credentials
object to hold the username and pasword.
The Authentication Process
To support the login form we just added we need to add some more features. On the client side these will be implemented in the LoginComponent
, and on the server it will be Spring Security configuration.
Submitting the Login Form
To submit the form we need to define the login()
function that we referenced already in the form via ng-submit
, and the credentials
object that we referenced via ng-model
. Let’s flesh out the "login" component:
import { Component, OnInit } from '@angular/core';
import { AppService } from './app.service';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';
@Component({
templateUrl: './login.component.html'
})
export class LoginComponent {
credentials = {username: '', password: ''};
constructor(private app: AppService, private http: HttpClient, private router: Router) {
}
login() {
this.app.authenticate(this.credentials, () => {
this.router.navigateByUrl('/');
});
return false;
}
}
In addition to initializing the credentials
object, it defines the login()
that we need in the form.
The authenticate()
makes a GET to a relative resource (relative to the deployment root of your application) "/user". When called from the login()
function it adds the Base64-encoded credentials in the headers so on the server it does an authentication and accepts a cookie in return. The login()
function also sets a local $scope.error
flag accordingly when we get the result of the authentication, which is used to control the display of the error message above the login form.
The Currently Authenticated User
To service the authenticate()
function we need to add a new endpoint to the backend:
@SpringBootApplication
@RestController
public class UiApplication {
@RequestMapping("/user")
public Principal user(Principal user) {
return user;
}
...
}
This is a useful trick in a Spring Security application. If the "/user" resource is reachable then it will return the currently authenticated user (an Authentication
), and otherwise Spring Security will intercept the request and send a 401 response through an AuthenticationEntryPoint
.
Handling the Login Request on the Server
Spring Security makes it easy to handle the login request. We just need to add some configuration to our main application class (e.g. as an inner class):
@SpringBootApplication
@RestController
public class UiApplication {
...
@Configuration
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
protected static class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic()
.and()
.authorizeRequests()
.antMatchers("/index.html", "/", "/home", "/login").permitAll()
.anyRequest().authenticated();
}
}
}
This is a standard Spring Boot application with Spring Security customization, just allowing anonymous access to the static (HTML) resources. The HTML resources need to be available to anonymous users, not just ignored by Spring Security, for reasons that will become clear.
The last thing we need to remember is to make the JavaScript components provided by Angular available anonymously to the application. We could do that in the HttpSecurity
configuration above, but since it is static content, it’s better to simply ignore it:
security:
ignored:
- "*.bundle.*"
Adding Default HTTP Request Headers
If you run the app at this point you will find that the browser pops up a Basic authentication dialogue (for user and password). It does this because it sees a 401 reponse from the XHR requests to /user
and /resource
with a "WWW-Authenticate" header. The way to suppress this popup is to suppress the header, which is coming from Spring Security. And the way to suppress the reponse header is to send a special, conventional request header "X-Requested-With=XMLHttpRequest". It used to be the default in Angular but they took it out in 1.3.0. So here’s how to set default headers in an Angular XHR request.
First extend the default RequestOptions
provided by the Angular HTTP module:
@Injectable()
export class XhrInterceptor implements HttpInterceptor {
intercept(req: HttpRequest , next: HttpHandler) {
const xhr = req.clone({
headers: req.headers.set('X-Requested-With', 'XMLHttpRequest')
});
return next.handle(xhr);
}
}
The syntax here is boilerplate. The implements
property of the Class
is its base class, and in addition to the constructor, all we really need to do is override the intercept()
function which is always called by Angular and can be used to add additional headers.
To install this new RequestOptions
factory we need to declare it in the providers
of the AppModule
:
@NgModule({
...
providers: [AppService, { provide: HTTP_INTERCEPTORS, useClass: XhrInterceptor, multi: true }],
...
})
export class AppModule { }
Logout
The application is almost finished functionally. The last thing we need to do is implement the logout feature that we sketched in the home page. If the user is authenticated then we show a "logout" link and hook it to a logout()
function in the AppComponent
. Remember, it sends an HTTP POST to "/logout" which we now need to implement on the server. This is straightforward because it is added for us already by Spring Security (i.e. we don’t need to do anything for this simple use case). For more control over the behaviour of logout you could use the HttpSecurity
callbacks in your WebSecurityAdapter
to, for instance execute some business logic after logout.
CSRF Protection
The application is almost ready to use, and in fact if you run it you will find that everything we built so far actually works except the logout link. Try using it and look at the responses in the browser and you will see why:
POST /logout HTTP/1.1
...
Content-Type: application/x-www-form-urlencoded
username=user&password=password
HTTP/1.1 403 Forbidden
Set-Cookie: JSESSIONID=3941352C51ABB941781E1DF312DA474E; Path=/; HttpOnly
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
...
{"timestamp":1420467113764,"status":403,"error":"Forbidden","message":"Expected CSRF token not found. Has your session expired?","path":"/login"}
That’s good because it means that Spring Security’s built-in CSRF protection has kicked in to prevent us from shooting ourselves in the foot. All it wants is a token sent to it in a header called "X-CSRF". The value of the CSRF token was available server side in the HttpRequest
attributes from the initial request that loaded the home page. To get it to the client we could render it using a dynamic HTML page on the server, or expose it via a custom endpoint, or else we could send it as a cookie. The last choice is the best because Angular has built in support for CSRF (which it calls "XSRF") based on cookies.
So on the server we need a custom filter that will send the cookie. Angular wants the cookie name to be "XSRF-TOKEN" and Spring Security provides it as a request attribute by default, so we just need to transfer the value from a request attribute to a cookie. Fortunately, Spring Security (since 4.1.0) provides a special CsrfTokenRepository
that does precisely this:
@Configuration
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
protected static class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
...
.and().csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
}
With those changes in place we don’t need to do anything on the client side and the login form is now working.
How Does it Work?
The interactions between the browser and the backend can be seen in your browser if you use some developer tools (usually F12 opens this up, works in Chrome by default, may require a plugin in Firefox). Here’s a summary:
VERB | PATH | STATUS | RESPONSE |
---|---|---|---|
GET |
/ |
200 |
index.html |
GET |
/*.js |
200 |
Assets from angular |
GET |
/user |
401 |
Unauthorized (ignored) |
GET |
/home |
200 |
Home page |
GET |
/user |
401 |
Unauthorized (ignored) |
GET |
/resource |
401 |
Unauthorized (ignored) |
GET |
/user |
200 |
Send credentials and get JSON |
GET |
/resource |
200 |
JSON greeting |
The responses that are marked "ignored" above are HTML responses received by Angular in an XHR call, and since we aren’t processing that data the HTML is dropped on the floor. We do look for an authenticated user in the case of the "/user" resource, but since it isn’t there in the first call, that response is dropped.
Look more closely at the requests and you will see that they all have cookies. If you start with a clean browser (e.g. incognito in Chrome), the very first request has no cookies going off to the server, but the server sends back "Set-Cookie" for "JSESSIONID" (the regular HttpSession
) and "X-XSRF-TOKEN" (the CRSF cookie that we set up above). Subsequent requests all have those cookies, and they are important: the application doesn’t work without them, and they are providing some really basic security features (authentication and CSRF protection). The values of the cookies change when the user authenticates (after the POST) and this is another important security feature (preventing session fixation attacks).
It is not adequate for CSRF protection to rely on a cookie being sent back to the server because the browser will automatically send it even if you are not in a page loaded from your application (a Cross Site Scripting attack, otherwise known as XSS). The header is not automatically sent, so the origin is under control. You might see that in our application the CSRF token is sent to the client as a cookie, so we will see it being sent back automatically by the browser, but it is the header that provides the protection. |
Help, How is My Application Going to Scale?
"But wait…" you are saying, "isn’t it Really Bad to use session state in a single-page application?" The answer to that question is going to have to be "mostly", because it very definitely is a Good Thing to use the session for authentication and CSRF protection. That state has to be stored somewhere, and if you take it out of the session, you are going to have to put it somewhere else and manage it manually yourself, on both the server and the client. That’s just more code and probably more maintenance, and generally re-inventing a perfectly good wheel.
"But, but…" you are going to respond, "how do I scale my application horizontally now?" This is the "real" question you were asking above, but it tends to get shortened to "session state is bad, I must be stateless". Don’t panic. The main point to take on board here is that security isstateful. You can’t have a secure, stateless application. So where are you going to store the state? That’s all there is to it. Rob Winch gave a very useful and insightful talk at Spring Exchange 2014 explaining the need for state (and the ubiquity of it - TCP and SSL are stateful, so your system is stateful whether you knew it or not), which is probably worth a look if you want to look into this topic in more depth.
The good news is you have a choice. The easiest choice is to store the session data in-memory, and rely on sticky sessions in your load balancer to route requests from the same session back to the same JVM (they all support that somehow). That’s good enough to get you off the ground and will work for a really large number of use cases. The other choice is to share the session data between instances of your application. As long as you are strict and only store the security data, it is small and changes infrequently (only when users log in and out, or their session times out), so there shouldn’t be any major infrastructure problems. It’s also really easy to do with Spring Session. We’ll be using Spring Session in the next section in this series, so there’s no need to go into any detail about how to set it up here, but it is literally a few lines of code and a Redis server, which is super fast.
Another easy way to set up shared session state is to deploy your application as a WAR file to Cloud Foundry Pivotal Web Services and bind it to a Redis service. |
But, What about My Custom Token Implementation (it’s Stateless, Look)?
If that was your response to the last section, then read it again because maybe you didn’t get it the first time. It’s probably not stateless if you stored the token somewhere, but even if you didn’t (e.g. you use JWT encoded tokens), how are you going to provide CSRF protection? It’s important. Here’s a rule of thumb (attributed to Rob Winch): if your application or API is going to be accessed by a browser, you need CSRF protection. It’s not that you can’t do it without sessions, it’s just that you’d have to write all that code yourself, and what would be the point because it’s already implemented and works perfectly well on top of HttpSession
(which in turn is part of the container you are using and baked into specs since the very beginning)? Even if you decide you don’t need CSRF, and have a perfectly "stateless" (non-session based) token implementation, you still had to write extra code in the client to consume and use it, where you could have just delegated to the browser and server’s own built-in features: the browser always sends cookies, and the server always has a session (unless you switch it off). That code is not business logic, and it isn’t making you any money, it’s just an overhead, so even worse, it costs you money.
Conclusion
The application we have now is close to what a user might expect in a "real" application in a live environment, and it probably could be used as a template for building out into a more feature rich application with that architecture (single server with static content and JSON resources). We are using the HttpSession
for storing security data, relying on our clients to respect and use the cookies we send them, and we are comfortable with that because it lets us concentrate on our own business domain. In the next section we expand the architecture to a separate authentication and UI server, plus a standalone resource server for the JSON. This is obviously easily generalised to multiple resource servers. We are also going to introduce Spring Session into the stack and show how that can be used to share authentication data.
The Resource Server
In this section we continue our discussion of how to use Spring Security with Angular in a "single page application". Here we start by breaking out the "greeting" resource that we are using as the dynamic content in our application into a separate server, first as an unprotected resource, and then protected by an opaque token. This is the third in a series of sections, and you can catch up on the basic building blocks of the application or build it from scratch by reading the first section, or you can just go straight to the source code in Github, which is in two parts: one where the resource is unprotected, and one where it is protected by a token.
if you are working through this section with the sample application, be sure to clear your browser cache of cookies and HTTP Basic credentials. In Chrome the best way to do that for a single server is to open a new incognito window. |
A Separate Resource Server
Client Side Changes
On the client side there isn’t very much to do to move the resource to a different backend. Here’s the "home" component in the last section:
@Component({
templateUrl: './home.component.html'
})
export class HomeComponent {
title = 'Demo';
greeting = {};
constructor(private app: AppService, private http: HttpClient) {
http.get('resource').subscribe(data => this.greeting = data);
}
authenticated() { return this.app.authenticated; }
}
All we need to do to this is change the URL. For example, if we are going to run the new resource on localhost, it could look like this:
http.get('http://localhost:9000').subscribe(data => this.greeting = data);
Server Side Changes
The UI server is trivial to change: we just need to remove the @RequestMapping
for the greeting resource (it was "/resource"). Then we need to create a new resource server, which we can do like we did in the first section using the Spring Boot Initializr. E.g. using curl on a UN*X like system:
$ mkdir resource && cd resource
$ curl https://start.spring.io/starter.tgz -d style=web \
-d name=resource | tar -xzvf -
You can then import that project (it’s a normal Maven Java project by default) into your favourite IDE, or just work with the files and "mvn" on the command line.
Just add a @RequestMapping
to the main application class, copying the implementation from the old UI:
@SpringBootApplication
@RestController
class ResourceApplication {
@RequestMapping("/")
public Message home() {
return new Message("Hello World");
}
public static void main(String[] args) {
SpringApplication.run(ResourceApplication.class, args);
}
}
class Message {
private String id = UUID.randomUUID().toString();
private String content;
public Message(String content) {
this.content = content;
}
// ... getters and setters and default constructor
}
Once that is done your application will be loadable in a browser. On the command line you can do this
$ mvn spring-boot:run -Dserver.port=9000
and go to a browser at http://localhost:9000 and you should see JSON with a greeting. You can bake in the port change in application.properties
(in"src/main/resources"):
server.port: 9000
If you try loading that resource from the UI (on port 8080) in a browser, you will find that it doesn’t work because the browser won’t allow the XHR request.
CORS Negotiation
The browser tries to negotiate with our resource server to find out if it is allowed to access it according to the Cross Origin Resource Sharing protocol. It’s not an Angular responsibility, so just like the cookie contract it will work like this with all JavaScript in the browser. The two servers do not declare that they have a common origin, so the browser declines to send the request and the UI is broken.
To fix that we need to support the CORS protocol which involves a "pre-flight" OPTIONS request and some headers to list the allowed behaviour of the caller. Spring 4.2 has some nice fine-grained CORS support, so we can just add an annotation to our controller mapping, for example:
@RequestMapping("/")
@CrossOrigin(origins="*", maxAge=3600)
public Message home() {
return new Message("Hello World");
}
Blithely using origins=* is quick and dirty, and it works, but it is not not secure and is not in any way recommended. |
Securing the Resource Server
Great! We have a working application with a new architecture. The only problem is that the resource server has no security.
Adding Spring Security
We can also look at how to add security to the resource server as a filter layer, like in the UI server. The first step is really easy: just add Spring Security to the classpath in the Maven POM:
org.springframework.boot
spring-boot-starter-security
...
Re-launch the resource server and, hey presto! It’s secure:
$ curl -v localhost:9000
< HTTP/1.1 302 Found
< Location: http://localhost:9000/login
...
We are getting a redirect to a (whitelabel) login page because curl is not sending the same headers that our Angular client will. Modifying the command to send more similar headers:
$ curl -v -H "Accept: application/json" \
-H "X-Requested-With: XMLHttpRequest" localhost:9000
< HTTP/1.1 401 Unauthorized
...
So all we need to do is teach the client to send credentials with every request.
Token Authentication
The internet, and people’s Spring backend projects, are littered with custom token-based authentication solutions. Spring Security provides a barebones Filter
implementation to get you started on your own (see for example AbstractPreAuthenticatedProcessingFilter
and TokenService
). There is no canonical implementation in Spring Security though, and one of the reasons why is probably that there’s an easier way.
Remember from Part II of this series that Spring Security uses the HttpSession
to store authentication data by default. It doesn’t interact directly with the session though: there’s an abstraction layer (SecurityContextRepository
) in between that you can use to change the storage backend. If we can point that repository, in our resource server, to a store with an authentication verified by our UI, then we have a way to share authentication between the two servers. The UI server already has such a store (the HttpSession
), so if we can distribute that store and open it up to the resource server, we have most of a solution.
Spring Session
That part of the solution is pretty easy with Spring Session. All we need is a shared data store (Redis and JDBC are supported out of the box), and a few lines of configuration in the servers to set up a Filter
.
In the UI application we need to add some dependencies to our POM:
org.springframework.session
spring-session
org.springframework.boot
spring-boot-starter-data-redis
Spring Boot and Spring Session work together to connect to Redis and store session data centrally.
With that 1 line of code in place and a Redis server running on localhost you can run the UI application, login with some valid user credentials, and the session data (the authentication) will be stored in redis.
if you don’t have a redis server running locally you can easily spin one up with Docker (on Windows or MacOS this requires a VM). There is a docker-compose.yml file in the source code in Github which you can run really easily on the command line with docker-compose up . If you do this in a VM the Redis server will be running on a different host than localhost, so you either need to tunnel it onto localhost, or configure the app to point at the correct spring.redis.host in your application.properties . |
Sending a Custom Token from the UI
The only missing piece is the transport mechanism for the key to the data in the store. The key is the HttpSession
ID, so if we can get hold of that key in the UI client, we can send it as a custom header to the resource server. So the "home" controller would need to change so that it sends the header as part of the HTTP request for the greeting resource. For example:
constructor(private app: AppService, private http: HttpClient) {
http.get('token').subscribe(data => {
const token = data['token'];
http.get('http://localhost:9000', {headers : new HttpHeaders().set('X-Auth-Token', token)})
.subscribe(response => this.greeting = response);
}, () => {});
}
(A more elegant solution might be to grab the token as needed, and use our RequestOptionsService
to add the header to every request to the resource server.)
Instead of going directly to "http://localhost:9000[http://localhost:9000]" we have wrapped that call in the success callback of a call to a new custom endpoint on the UI server at "/token". The implementation of that is trivial:
@SpringBootApplication
@RestController
public class UiApplication {
public static void main(String[] args) {
SpringApplication.run(UiApplication.class, args);
}
...
@RequestMapping("/token")
public Map<String,String> token(HttpSession session) {
return Collections.singletonMap("token", session.getId());
}
}
So the UI application is ready and will include the session ID in a header called "X-Auth-Token" for all calls to the backend.
Authentication in the Resource Server
There is one tiny change to the resource server for it to be able to accept the custom header. The CORS configuration has to nominate that header as an allowed one from remote clients, e.g.
@RequestMapping("/")
@CrossOrigin(origins = "*", maxAge = 3600,
allowedHeaders={"x-auth-token", "x-requested-with", "x-xsrf-token"})
public Message home() {
return new Message("Hello World");
}
The pre-flight check from the browser will now be handled by Spring MVC, but we need to tell Spring Security that it is allowed to let it through:
public class ResourceApplication extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().authorizeRequests()
.anyRequest().authenticated();
}
...
There is no need to permitAll() access to all resources, and there might be a handler that inadvertently sends sensitive data because it is not aware that the request is pre-flight. The cors() configuration utility mitigates this by handling all pre-flight requests in the filter layer. |
All that remains is to pick up the custom token in the resource server and use it to authenticate our user. This turns out to be pretty straightforward because all we need to do is tell Spring Security where the session repository is, and where to look for the token (session ID) in an incoming request. First we need to add the Spring Session and Redis dependencies, and then we can set up the Filter
:
@SpringBootApplication
@RestController
class ResourceApplication {
...
@Bean
HeaderHttpSessionStrategy sessionStrategy() {
return new HeaderHttpSessionStrategy();
}
}
This Filter
created is the mirror image of the one in the UI server, so it establishes Redis as the session store. The only difference is that it uses a custom HttpSessionStrategy
that looks in the header ("X-Auth-Token" by default) instead of the default (cookie named "JSESSIONID"). We also need to prevent the browser from popping up a dialog in an unauthenticated client - the app is secure but sends a 401 with WWW-Authenticate: Basic
by default, so the browser responds with a dialog for username and password. There is more than one way to achieve this, but we already made Angular send an "X-Requested-With" header, so Spring Security handles it for us by default.
There is one final change to the resource server to make it work with our new authentication scheme. Spring Boot default security is stateless, and we want this to store authentication in the session, so we need to be explicit in application.yml
(or application.properties
):
security:
sessions: NEVER
This says to Spring Security "never create a session, but use one if it is there" (it will already be there because of the authentication in the UI).
Re-launch the resource server and open the UI up in a new browser window.
Why Doesn’t it All Work With Cookies?
We had to use a custom header and write code in the client to populate the header, which isn’t terribly complicated, but it seems to contradict the advice in Part II to use cookies and sessions wherever possible. The argument there was that not to do so introduces additional unecessary complexity, and for sure the implementation we have now is the most complex we have seen so far: the technical part of the solution far outweighs the business logic (which is admittedly tiny). This is definitely a fair criticism (and one we plan to address in the next section in this series), but let’s just briefly look at why it’s not as simple as just using cookies and sessions for everything.
At least we are still using the session, which makes sense because Spring Security and the Servlet container know how to do that with no effort on our part. But couldn’t we have continued to use cookies to transport the authentication token? It would have been nice, but there is a reason it wouldn’t work, and that is that the browser wouldn’t let us. You can just go poking around in the browser’s cookie store from a JavaScript client, but there are some restrictions, and for good reason. In particular you don’t have access to the cookies that were sent by the server as "HttpOnly" (which you will see is the case by default for session cookies). You also can’t set cookies in outgoing requests, so we couldn’t set a "SESSION" cookie (which is the Spring Session default cookie name), we had to use a custom "X-Session" header. Both these restrictions are for your own protection so malicious scripts cannot access your resources without proper authorization.
TL;DR the UI and resource servers do not have a common origin, so they cannot share cookies (even though we can use Spring Session to force them to share sessions).
Conclusion
We have duplicated the features of the application in Part II of this series: a home page with a greeting fetched from a remote backend, with login and logout links in a navigation bar. The difference is that the greeting comes from a resource server that is a standalone, instead of being embedded in the UI server. This added significant complexity to the implementation, but the good news is that we have a mostly configuration-based (and practically 100% declarative) solution. We could even make the solution 100% declarative by extracting all the new code into libraries (Spring configuration and Angular custom directives). We are going to defer that interesting task for after the next couple of installments. In the next section we are going to look at a different really great way to reduce all the complexity in the current implementation: the API Gateway Pattern (the client sends all its requests to one place and authentication is handled there).
We used Spring Session here to share sessions between 2 servers that are not logically the same application. It’s a neat trick, and it isn’t possible with "regular" JEE distributed sessions. |
The API Gateway
In this section we continue our discussion of how to use Spring Security with Angular in a "single page application". Here we show how to build an API Gateway to control the authentication and access to the backend resources using Spring Cloud. This is the fourth in a series of sections, and you can catch up on the basic building blocks of the application or build it from scratch by reading the first section, or you can just go straight to the source code in Github. In the last section we built a simple distributed application that used Spring Session to authenticate the backend resources. In this one we make the UI server into a reverse proxy to the backend resource server, fixing the issues with the last implementation (technical complexity introduced by custom token authentication), and giving us a lot of new options for controlling access from the browser client.
Reminder: if you are working through this section with the sample application, be sure to clear your browser cache of cookies and HTTP Basic credentials. In Chrome the best way to do that for a single server is to open a new incognito window.
Creating an API Gateway
An API Gateway is a single point of entry (and control) for front end clients, which could be browser based (like the examples in this section) or mobile. The client only has to know the URL of one server, and the backend can be refactored at will with no change, which is a significant advantage. There are other advantages in terms of centralization and control: rate limiting, authentication, auditing and logging. And implementing a simple reverse proxy is really simple with Spring Cloud.
If you were following along in the code, you will know that the application implementation at the end of the last section was a bit complicated, so it’s not a great place to iterate away from. There was, however, a halfway point which we could start from more easily, where the backend resource wasn’t yet secured with Spring Security. The source code for this is a separate project in Github so we are going to start from there. It has a UI server and a resource server and they are talking to each other. The resource server doesn’t have Spring Security yet so we can get the system working first and then add that layer.
Declarative Reverse Proxy in One Line
To turn it into an API Gateway, the UI server needs one small tweak. Somewhere in the Spring configuration we need to add an @EnableZuulProxy
annotation, e.g. in the main (only) application class:
@SpringBootApplication
@RestController
@EnableZuulProxy
public class UiApplication {
...
}
and in an external configuration file we need to map a local resource in the UI server to a remote one in the external configuration ("application.yml"):
security:
...
zuul:
routes:
resource:
path: /resource/**
url: http://localhost:9000
This says "map paths with the pattern /resource/** in this server to the same paths in the remote server at localhost:9000". Simple and yet effective (OK so it’s 6 lines including the YAML, but you don’t always need that)!
All we need to make this work is the right stuff on the classpath. For that purpose we have a few new lines in our Maven POM:
org.springframework.cloud
spring-cloud-dependencies
Dalston.SR4
pom
import
org.springframework.cloud
spring-cloud-starter-zuul
...
Note the use of the "spring-cloud-starter-zuul" - it’s a starter POM just like the Spring Boot ones, but it governs the dependencies we need for this Zuul proxy. We are also using
because we want to be able to depend on all the versions of transitive dependencies being correct.
Consuming the Proxy in the Client
With those changes in place our application still works, but we haven’t actually used the new proxy yet until we modify the client. Fortunately that’s trivial. We just need to revert the change we made going from the "single" to the "vanilla" samples in the last section:
constructor(private app: AppService, private http: HttpClient) {
http.get('resource').subscribe(data => this.greeting = data);
}
Now when we fire up the servers everything is working and the requests are being proxied through the UI (API Gateway) to the resource server.
Further Simplifications
Even better: we don’t need the CORS filter any more in the resource server. We threw that one together pretty quickly anyway, and it should have been a red light that we had to do anything as technically focused by hand (especially where it concerns security). Fortunately it is now redundant, so we can just throw it away, and go back to sleeping at night!
Securing the Resource Server
You might remember in the intermediate state that we started from there is no security in place for the resource server.
Aside: Lack of software security might not even be a problem if your network architecture mirrors the application architecture (you can just make the resource server physically inaccessible to anyone but the UI server). As a simple demonstration of that we can make the resource server only accessible on localhost. Just add this to
application.properties
in the resource server:
server.address: 127.0.0.1
Wow, that was easy! Do that with a network address that’s only visible in your data center and you have a security solution that works for all resource servers and all user desktops.
Suppose that we decide we do need security at the software level (quite likely for a number of reasons). That’s not going to be a problem, because all we need to do is add Spring Security as a dependency (in the resource server POM):
org.springframework.boot
spring-boot-starter-security
That’s enough to get us a secure resource server, but it won’t get us a working application yet, for the same reason that it didn’t in Part III: there is no shared authentication state between the two servers.
Sharing Authentication State
We can use the same mechanism to share authentication (and CSRF) state as we did in the last, i.e. Spring Session. We add the dependency to both servers as before:
org.springframework.session
spring-session
org.springframework.boot
spring-boot-starter-redis
but this time the configuration is much simpler because we can just add the same Filter
declaration to both. First the UI server, declaring explicitly that we want all headers to be forwarded (i.e. none are "sensitive"):
zuul:
routes:
resource:
sensitive-headers:
Then we can move on to the resource server. There are two small changes to make: one is to explicitly disable HTTP Basic in the resource server (to prevent the browser from popping up authentication dialogs):
@SpringBootApplication
@RestController
class ResourceApplication extends WebSecurityConfigurerAdapter {
...
@Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic().disable();
http.authorizeRequests().anyRequest().authenticated();
}
}
Aside: an alternative, which would also prevent the authentication dialog, would be to keep HTTP Basic but change the 401 challenge to something other than "Basic". You can do that with a one-line implementation of
AuthenticationEntryPoint
in theHttpSecurity
configuration callback.
and the other is to explicitly ask for a non-stateless session creation policy in application.properties
:
security.sessions: NEVER
As long as redis is still running in the background (use the docker-compose.yml
if you like to start it) then the system will work. Load the homepage for the UI at http://localhost:8080 and login and you will see the message from the backend rendered on the homepage.
How Does it Work?
What is going on behind the scenes now? First we can look at the HTTP requests in the UI server (and API Gateway):
VERB | PATH | STATUS | RESPONSE |
---|---|---|---|
GET |
/ |
200 |
index.html |
GET |
/*.js |
200 |
Assets form angular |
GET |
/user |
401 |
Unauthorized (ignored) |
GET |
/resource |
401 |
Unauthenticated access to resource |
GET |
/user |
200 |
JSON authenticated user |
GET |
/resource |
200 |
(Proxied) JSON greeting |
That’s identical to the sequence at the end of Part II except for the fact that the cookie names are slightly different ("SESSION" instead of "JSESSIONID") because we are using Spring Session. But the architecture is different and that last request to "/resource" is special because it was proxied to the resource server.
We can see the reverse proxy in action by looking at the "/trace" endpoint in the UI server (from Spring Boot Actuator, which we added with the Spring Cloud dependencies). Go to http://localhost:8080/trace in a new browser (if you don’t have one already get a JSON plugin for your browser to make it nice and readable). You will need to authenticate with HTTP Basic (browser popup), but the same credentials are valid as for your login form. At or near the start you should see a pair of requests something like this:
Try to use a different browser so that there is no chance of authentication crossover (e.g. use Firefox if yoused Chrome for testing the UI) - it won’t stop the app from working, but it will make the traces harder to read if they contain a mixture of authentication from the same browser. |
{
"timestamp": 1420558194546,
"info": {
"method": "GET",
"path": "/",
"query": ""
"remote": true,
"proxy": "resource",
"headers": {
"request": {
"accept": "application/json, text/plain, */*",
"x-xsrf-token": "542c7005-309c-4f50-8a1d-d6c74afe8260",
"cookie": "SESSION=c18846b5-f805-4679-9820-cd13bd83be67; XSRF-TOKEN=542c7005-309c-4f50-8a1d-d6c74afe8260",
"x-forwarded-prefix": "/resource",
"x-forwarded-host": "localhost:8080"
},
"response": {
"Content-Type": "application/json;charset=UTF-8",
"status": "200"
}
},
}
},
{
"timestamp": 1420558200232,
"info": {
"method": "GET",
"path": "/resource/",
"headers": {
"request": {
"host": "localhost:8080",
"accept": "application/json, text/plain, */*",
"x-xsrf-token": "542c7005-309c-4f50-8a1d-d6c74afe8260",
"cookie": "SESSION=c18846b5-f805-4679-9820-cd13bd83be67; XSRF-TOKEN=542c7005-309c-4f50-8a1d-d6c74afe8260"
},
"response": {
"Content-Type": "application/json;charset=UTF-8",
"status": "200"
}
}
}
},
The second entry there is the request from the client to the gateway on "/resource" and you can see the cookies (added by the browser) and the CSRF header (added by Angular as discussed in Part II). The first entry has remote: true
and that means it’s tracing the call to the resource server. You can see it went out to a uri path "/" and you can see that (crucially) the cookies and CSRF headers have been sent too. Without Spring Session these headers would be meaningless to the resource server, but the way we have set it up it can now use those headers to re-constitute a session with authentication and CSRF token data. So the request is permitted and we are in business!
Conclusion
We covered quite a lot in this section but we got to a really nice place where there is a minimal amount of boilerplate code in our two servers, they are both nicely secure and the user experience isn’t compromised. That alone would be a reason to use the API Gateway pattern, but really we have only scratched the surface of what that might be used for (Netflix uses it for a lot of things). Read up on Spring Cloud to find out more on how to make it easy to add more features to the gateway. The next section in this series will extend the application architecture a bit by extracting the authentication responsibilities to a separate server (the Single Sign On pattern).
Single Sign On with OAuth2
In this section we continue our discussion of how to use Spring Security with Angular in a "single page application". Here we show how to use Spring Security OAuth together with Spring Cloud to extend our API Gateway to do Single Sign On and OAuth2 token authentication to backend resources. This is the fifth in a series of sections, and you can catch up on the basic building blocks of the application or build it from scratch by reading the first section, or you can just go straight to the source code in Github. In the last section we built a small distributed application that used Spring Session to authenticate the backend resources and Spring Cloud to implement an embedded API Gateway in the UI server. In this section we extract the authentication responsibilities to a separate server to make our UI server the first of potentially many Single Sign On applications to the authorization server. This is a common pattern in many applications these days, both in the enterprise and in social startups. We will use an OAuth2 server as the authenticator, so that we can also use it to grant tokens for the backend resource server. Spring Cloud will automatically relay the access token to our backend, and enable us to further simplify the implementation of both the UI and resource servers.
Reminder: if you are working through this section with the sample application, be sure to clear your browser cache of cookies and HTTP Basic credentials. In Chrome the best way to do that for a single server is to open a new incognito window.
Creating an OAuth2 Authorization Server
Our first step is to create a new server to handle authentication and token management. Following the steps in Part I we can begin with Spring Boot Initializr. E.g. using curl on a UN*X like system:
$ curl https://start.spring.io/starter.tgz -d style=web \
-d style=security -d name=authserver | tar -xzvf -
You can then import that project (it’s a normal Maven Java project by default) into your favourite IDE, or just work with the files and "mvn" on the command line.
Adding the OAuth2 Dependencies
We need to add the Spring OAuth dependencies, so in our POM we add:
org.springframework.security.oauth
spring-security-oauth2
The authorization server is pretty easy to implement. A minimal version looks like this:
@SpringBootApplication
@EnableAuthorizationServer
public class AuthserverApplication extends WebMvcConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(AuthserverApplication.class, args);
}
}
We only have to do 1 more thing (after adding @EnableAuthorizationServer
):
---
...
security.oauth2.client.clientId: acme
security.oauth2.client.clientSecret: acmesecret
security.oauth2.client.authorized-grant-types: authorization_code,refresh_token,password
security.oauth2.client.scope: openid
---
This registers a client "acme" with a secret and some authorized grant types including "authorization_code".
Now let’s get it running on port 9999, with a predictable password for testing:
server.port=9999
security.user.password=password
server.contextPath=/uaa
...
We also set the context path so that it doesn’t use the default ("/") because otherwise you can get cookies for other servers on localhost being sent to the wrong server. So get the server running and we can make sure it is working:
$ mvn spring-boot:run
or start the main()
method in your IDE.
Testing the Authorization Server
Our server is using the Spring Boot default security settings, so like the server in Part I it will be protected by HTTP Basic authentication. To initiate an authorization code token grant you visit the authorization endpoint, e.g. at http://localhost:9999/uaa/oauth/authorize?response_type=code&client_id=acme&redirect_uri=http://example.com once you have authenticated you will get a redirect to example.com with an authorization code attached, e.g. http://example.com/?code=jYWioI.
for the purposes of this sample application we have created a client "acme" with no registered redirect, which is what enables us to get a redirect the example.com. In a production application you should always register a redirect (and use HTTPS). |
The code can be exchanged for an access token using the "acme" client credentials on the token endpoint:
$ curl acme:acmesecret@localhost:9999/uaa/oauth/token \
-d grant_type=authorization_code -d client_id=acme \
-d redirect_uri=http://example.com -d code=jYWioI
{"access_token":"2219199c-966e-4466-8b7e-12bb9038c9bb","token_type":"bearer","refresh_token":"d193caf4-5643-4988-9a4a-1c03c9d657aa","expires_in":43199,"scope":"openid"}
The access token is a UUID ("2219199c…"), backed by an in-memory token store in the server. We also got a refresh token that we can use to get a new access token when the current one expires.
since we allowed "password" grants for the "acme" client we can also get a token directly from the token endpoint using curl and user credentials instead of an authorization code. This is not suitable for a browser based client, but it’s useful for testing. |
If you followed the link above you would have seen the whitelabel UI provided by Spring OAuth. To start with we will use this and we can come back later to beef it up like we did in Part II for the self-contained server.
Changing the Resource Server
If we follow on from Part IV, our resource server is using Spring Session for authentication, so we can take that out and replace it with Spring OAuth. We also need to remove the Spring Session and Redis dependencies, so replace this:
org.springframework.session
spring-session
org.springframework.boot
spring-boot-starter-redis
with this:
org.springframework.security.oauth
spring-security-oauth2
and then remove the session Filter
from the main application class, replacing it with the convenient @EnableResourceServer
annotation (from Spring Security OAuth2):
@SpringBootApplication
@RestController
@EnableResourceServer
class ResourceApplication {
@RequestMapping("/")
public Message home() {
return new Message("Hello World");
}
public static void main(String[] args) {
SpringApplication.run(ResourceApplication.class, args);
}
}
With that one change the app is ready to challenge for an access token instead of HTTP Basic, but we need a config change to actually finish the process. We are going to add a small amount of external configuration (in "application.properties") to allow the resource server to decode the tokens it is given and authenticate a user:
...
security.oauth2.resource.userInfoUri: http://localhost:9999/uaa/user
This tells the server that it can use the token to access a "/user" endpoint and use that to derive authentication information (it’s a bit like the "/me" endpoint in the Facebook API). Effectively it provides a way for the resource server to decode the token, as expressed by the ResourceServerTokenServices
interface in Spring OAuth2.
Run the application and hit the home page with a command line client:
$ curl -v localhost:9000
> GET / HTTP/1.1
> User-Agent: curl/7.35.0
> Host: localhost:9000
> Accept: */*
>
< HTTP/1.1 401 Unauthorized
...
< WWW-Authenticate: Bearer realm="null", error="unauthorized", error_description="An Authentication object was not found in the SecurityContext"
< Content-Type: application/json;charset=UTF-8
{"error":"unauthorized","error_description":"An Authentication object was not found in the SecurityContext"}
and you will see a 401 with a "WWW-Authenticate" header indicating that it wants a bearer token.
the userInfoUri is by far not the only way of hooking a resource server up with a way to decode tokens. In fact it’s sort of a lowest common denominator (and not part of the spec), but quite often available from OAuth2 providers (like Facebook, Cloud Foundry, Github), and other choices are available. For instance you can encode the user authentication in the token itself (e.g. with JWT), or use a shared backend store. There is also a /token_info endpoint in CloudFoundry, which provides more detailed information than the user info endpoint, but which requires more thorough authentication. Different options (naturally) provide different benefits and trade-offs, but a full discussion of those is outside the scope of this section. |
Implementing the User Endpoint
On the authorization server we can easily add that endpoint
@SpringBootApplication
@RestController
@EnableAuthorizationServer
@EnableResourceServer
public class AuthserverApplication {
@RequestMapping("/user")
public Principal user(Principal user) {
return user;
}
...
}
We added a @RequestMapping
the same as the UI server in Part II, and also the @EnableResourceServer
annotation from Spring OAuth, which by default secures everything in an authorization server except the "/oauth/*" endpoints.
With that endpoint in place we can test it and the greeting resource, since they both now accept bearer tokens that were created by the authorization server:
$ TOKEN=2219199c-966e-4466-8b7e-12bb9038c9bb
$ curl -H "Authorization: Bearer $TOKEN" localhost:9000
{"id":"03af8be3-2fc3-4d75-acf7-c484d9cf32b1","content":"Hello World"}
$ curl -H "Authorization: Bearer $TOKEN" localhost:9999/uaa/user
{"details":...,"principal":{"username":"user",...},"name":"user"}
(substitute the value of the access token that you obtain from your own authorization server to get that working yourself).
The UI Server
The final piece of this application we need to complete is the UI server, extracting the authentication part and delegating to the authorization server. So, as with the resource server, we first need to remove the Spring Session and Redis dependencies and replace them with Spring OAuth2. Because we are using Zuul in the UI layer we actually use spring-cloud-starter-oauth2
instead of spring-security-oauth2
directly (this sets up some autoconfiguration for relaying tokens through the proxy).
Once that is done we can remove the session filter and the "/user" endpoint as well, and set up the application to redirect to the authorization server (using the @EnableOAuth2Sso
annotation):
@SpringBootApplication
@EnableZuulProxy
@EnableOAuth2Sso
public class UiApplication {
public static void main(String[] args) {
SpringApplication.run(UiApplication.class, args);
}
...
}
Recall from Part IV that the UI server, by virtue of the @EnableZuulProxy
, acts an API Gateway and we can declare the route mappings in YAML. So the "/user" endpoint can be proxied to the authorization server:
zuul:
routes:
resource:
path: /resource/**
url: http://localhost:9000
user:
path: /user/**
url: http://localhost:9999/uaa/user
Lastly, we need to change the application to a WebSecurityConfigurerAdapter
since now it is going to be used to modify the defaults in the SSO filter chain set up by @EnableOAuth2Sso
:
@SpringBootApplication
@EnableZuulProxy
@EnableOAuth2Sso
public class UiApplication extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.logout().logoutSuccessUrl("/").and()
.authorizeRequests().antMatchers("/index.html", "/app.html", "/")
.permitAll().anyRequest().authenticated().and()
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
}
The main changes (apart from the base class name) are that the matchers go into their own method, and there is no need for formLogin()
any more. The explicit logout()
configuration explicitly adds a success url that is unprotected, so that an XHR request to /logout
will return successfully.
There are also some mandatory external configuration properties for the @EnableOAuth2Sso
annotation to be able to contact and authenticate with the right authorization server. So we need this in application.yml
:
security:
...
oauth2:
client:
accessTokenUri: http://localhost:9999/uaa/oauth/token
userAuthorizationUri: http://localhost:9999/uaa/oauth/authorize
clientId: acme
clientSecret: acmesecret
resource:
userInfoUri: http://localhost:9999/uaa/user
The bulk of that is about the OAuth2 client ("acme") and the authorization server locations. There is also a userInfoUri
(just like in the resource server) so that the user can be authenticated in the UI app itself.
If you want the UI application be able to refresh expired access tokens automatically, you have to get an OAuth2RestOperations injected into the Zuul filter that does the relay. You can do this by just creating a bean of that type (check the OAuth2TokenRelayFilter for details): |
@Bean
protected OAuth2RestTemplate OAuth2RestTemplate(
OAuth2ProtectedResourceDetails resource, OAuth2ClientContext context) {
return new OAuth2RestTemplate(resource, context);
}
In the Client
There are some tweaks to the UI application on the front end that we still need to make to trigger the redirect to the authorization server. In this simple demo we can strip the Angular app down to its bare essentials so you can see what is going on more clearly. So we forgo, for now, the use of forms or routes, and we go back to a single Angular component:
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import 'rxjs/add/operator/finally';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'Demo';
authenticated = false;
greeting = {};
constructor(private http: HttpClient) {
this.authenticate();
}
authenticate() {
this.http.get('user').subscribe(response => {
if (response['name']) {
this.authenticated = true;
this.http.get('resource').subscribe(data => this.greeting = data);
} else {
this.authenticated = false;
}
}, () => { this.authenticated = false; });
}
logout() {
this.http.post('logout', {}).finally(() => {
this.authenticated = false;
}).subscribe();
}
}
The AppComponent
handles everything, fetching the user details and, if successful, the greeting. It also provides the logout
function.
Now we need to create the template for this new component:
app.component.html
class="container">
Greeting
[hidden]="!authenticated">
The ID is {{greeting.id}}
The content is {{greeting.content}}
[hidden]="authenticated">
Login to see your greeting
and include it in the home page as
.
Note that the navigation link for "Login" is a regular link with an href
(not an Angular route). The "/login" endpoint that this goes to is handled by Spring Security and if the user is not authenticated it will result in a redirect to the authorization server.
How Does it Work?
Run all the servers together now, and visit the UI in a browser at http://localhost:8080. Click on the "login" link and you will be redirected to the authorization server to authenticate (HTTP Basic popup) and approve the token grant (whitelabel HTML), before being redirected to the home page in the UI with the greeting fetched from the OAuth2 resource server using the same token as we authenticated the UI with.
The interactions between the browser and the backend can be seen in your browser if you use some developer tools (usually F12 opens this up, works in Chrome by default, may require a plugin in Firefox). Here’s a summary:
VERB | PATH | STATUS | RESPONSE |
---|---|---|---|
GET |
/ |
200 |
index.html |
GET |
/*.js |
200 |
Assets from angular |
GET |
/user |
302 |
Redirect to login page |
GET |
/login |
302 |
Redirect to auth server |
GET |
(uaa)/oauth/authorize |
401 |
(ignored) |
GET |
/login |
302 |
Redirect to auth server |
GET |
(uaa)/oauth/authorize |
200 |
HTTP Basic auth happens here |
POST |
(uaa)/oauth/authorize |
302 |
User approves grant, redirect to /login |
GET |
/login |
302 |
Redirect to home page |
GET |
/user |
200 |
(Proxied) JSON authenticated user |
GET |
/app.html |
200 |
HTML partial for home page |
GET |
/resource |
200 |
(Proxied) JSON greeting |
The requests prefixed with (uaa) are to the authorization server. The responses that are marked "ignored" are responses received by Angular in an XHR call, and since we aren’t processing that data they are dropped on the floor. We do look for an authenticated user in the case of the "/user" resource, but since it isn’t there in the first call, that response is dropped.
In the "/trace" endpoint of the UI (scroll down to the bottom) you will see the proxied backend requests to "/user" and "/resource", with remote:true
and the bearer token instead of the cookie (as it would have been in Part IV) being used for authentication. Spring Cloud Security has taken care of this for us: by recognising that we has @EnableOAuth2Sso
and @EnableZuulProxy
it has figured out that (by default) we want to relay the token to the proxied backends.
As in previous sections, try to use a different browser for "/trace" so that there is no chance of authentication crossover (e.g. use Firefox if you used Chrome for testing the UI). |
The Logout Experience
If you click on the "logout" link you will see that the home page changes (the greeting is no longer displayed) so the user is no longer authenticated with the UI server. Click back on "login" though and you actually don’t need to go back through the authentication and approval cycle in the authorization server (because you haven’t logged out of that). Opinions will be divided as to whether that is a desirable user experience, and it’s a notoriously tricky problem (Single Sign Out: Science Direct article and Shibboleth docs). The ideal user experience might not be technically feasible, and you also have to be suspicious sometimes that users really want what they say they want. "I want 'logout' to log me out" sounds simple enough, but the obvious response is, "Logged out of what? Do you want to be logged out of all the systems controlled by this SSO server, or just the one that you clicked the 'logout' link in?" If you are interested then there is a later section of this tutorial where it is discussed in more depth.
Conclusion
This is almost the end of our shallow tour through the Spring Security and Angular stack. We have a nice architecture now with clear responsibilities in three separate components, UI/API Gateway, resource server and authorization server/token granter. The amount of non-business code in all layers is now minimal, and it’s easy to see where to extend and improve the implementation with more business logic. The next steps will be to tidy up the UI in our authorization server, and probably add some more tests, including tests on the JavaScript client. Another interesting task is to extract all the boiler plate code and put it in a library (e.g. "spring-security-angular") containing Spring Security and Spring Session autoconfiguration and some webjars resources for the navigation controller in the Angular piece. Having read the sections in thir series, anyone who was hoping to learn the inner workings of either Angular or Spring Security will probably be disappointed, but if you wanted to see how they can work well together and how a little bit of configuration can go a long way, then hopefully you will have had a good experience. Spring Cloud is new and these samples required snapshots when they were written, but there are release candidates available and a GA release coming soon, so check it out and send some feedback via Github or gitter.im.
The next section in the series is about access decisions (beyond authentication) and employs multiple UI applications behind the same proxy.
Addendum: Bootstrap UI and JWT Tokens for the Authorization Server
You will find another version of this application in the source code in Github which has a pretty login page and user approval page implemented similarly to the way we did the login page in Part II. It also uses JWT to encode the tokens, so instead of using the "/user" endpoint, the resource server can pull enough information out of the token itself to do a simple authentication. The browser client still uses it, proxied through the UI server, so that it can determine if a user is authenticated (it doesn’t need to do that very often, compared to the likely number of calls to a resource server in a real application).
Multiple UI Applications and a Gateway
In this section we continue our discussion of how to use Spring Security with Angular in a "single page application". Here we show how to use Spring Session together with Spring Cloudto combine the features of the systems we built in parts II and IV, and actually end up building 3 single page applications with quite different responsibilities. The aim is to build a Gateway (like in part IV) that is used not only for API resources but also to load the UI from a backend server. We simplify the token-wrangling bits of part II by using the Gateway to pass through the authentication to the backends. Then we extend the system to show how we can make local, granular access decisions in the backends, while still controlling identity and authentication at the Gateway. This is a very powerful model for building distributed systems in general, and has a number of benefits that we can explore as we introduce the features in the code we build.
Reminder: if you are working through this section with the sample application, be sure to clear your browser cache of cookies and HTTP Basic credentials. In Chrome the best way to do that is to open a new incognito window.
Target Architecture
Here’s a picture of the basic system we are going to build to start with:
Like the other sample applications in this series it has a UI (HTML and JavaScript) and a Resource server. Like the sample in Section IV it has a Gateway, but here it is separate, not part of the UI. The UI effectively becomes part of the backend, giving us even more choice to re-configure and re-implement features, and also bringing other benefits as we will see.
The browser goes to the Gateway for everything and it doesn’t have to know about the architecture of the backend (fundamentally, it has no idea that there is a back end). One of the things the browser does in this Gateway is authentication, e.g. it sends a username and password like in Section II, and it gets a cookie in return. On subsequent requests it presents the cookie automatically and the Gateway passes it through to the backends. No code needs to be written on the client to enable the cookie passing. The backends use the cookie to authenticate and because all components share a session they share the same information about the user. Contrast this with Section V where the cookie had to be converted to an access token in the Gateway, and the access token then had to be independently decoded by all the backend components.
As in Section IV the Gateway simplifies the interaction between clients and servers, and it presents a small, well-defined surface on which to deal with security. For example, we don’t need to worry about Cross Origin Resource Sharing, which is a welcome relief since it is easy to get wrong.
The source code for the complete project we are going to build is in Github here, so you can just clone the project and work directly from there if you want. There is an extra component in the end state of this system ("double-admin") so ignore that for now.
Building the Backend
In this architecture the backend is very similar to the "spring-session" sample we built in Section III, with the exception that it doesn’t actually need a login page. The easiest way to get to what we want here is probably to copy the "resource" server from Section III and take the UI from the "basic" sample in Section I. To get from the "basic" UI to the one we want here, we need only to add a couple of dependencies (like when we first used Spring Session in Section III):
org.springframework.session
spring-session
org.springframework.boot
spring-boot-starter-redis
Since this is now a UI there is no need for the "/resource" endpoint. When you have done that you will have a very simple Angular application (the same as in the "basic" sample), which simplifies testing and reasoning about its behaviour greatly.
Lastly, we want this server to run as a backend, so we’ll give it a non-default port to listen on (in application.properties
):
server.port: 8081
security.sessions: NEVER
If that’s the whole content application.properties
then the application will be secure and accessible to a user called "user" with a password that is random, but printed on the console (at log level INFO) on startup. The "security.sessions" setting means that Spring Security will accept cookies as authentication tokens but won’t create them unless they already exist.
The Resource Server
The Resource server is easy to generate from one of our existing samples. It is the same as the "spring-session" Resource server in Section III: just a "/resource" endpoint Spring Session to get the distributed session data. We want this server to have a non-default port to listen on, and we want to be able to look up authentication in the session so we need this (in application.properties
):
server.port: 9000
security.sessions: NEVER
We are going to be POSTing changes to our message resource, which is a new feature in this tutorial. It means that we are going to need CSRF protection in the backend, and we need to do the usual trick to make Spring Security play nicely with Angular:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
The completed sample is here in github if you want to take a peek.
The Gateway
For an initial implementation of a Gateway (the simplest thing that could possibly work) we can just take an empty Spring Boot web application and add the @EnableZuulProxy
annotation. As we saw in Section I there are several ways to do that, and one is to use the Spring Initializr to generate a skeleton project. Even easier, is to use the Spring Cloud Initializr which is the same thing, but for Spring Cloud applications. Using the same sequence of command line operations as in Section I:
$ mkdir gateway && cd gateway
$ curl https://cloud-start.spring.io/starter.tgz -d style=web \
-d style=security -d style=cloud-zuul -d name=gateway \
-d style=redis | tar -xzvf -
You can then import that project (it’s a normal Maven Java project by default) into your favourite IDE, or just work with the files and "mvn" on the command line. There is a version in github if you want to go from there, but it has a few extra features that we don’t need yet.
Starting from the blank Initializr application, we add the Spring Session dependency (like in the UI above). The Gateway is ready to run, but it doesn’t yet know about our backend services, so let’s just set that up in its application.yml
(renaming from application.properties
if you did the curl thing above):
zuul:
sensitive-headers:
routes:
ui:
url: http://localhost:8081
resource:
url: http://localhost:9000
security:
user:
password:
password
sessions: ALWAYS
There are 2 routes in the proxy, both of which pass cookies downstream using the sensitive-headers
property, one each for the UI and resource server, and we have set up a default password and a session persistence strategy (telling Spring Security to always create a session on authentication). This last bit is important because we want authentication and therefore sessions to be managed in the Gateway.
Up and Running
We now have three components, running on 3 ports. If you point the browser at http://localhost:8080/ui/ you should get an HTTP Basic challenge, and you can authenticate as "user/password" (your credentials in the Gateway), and once you do that you should see a greeting in the UI, via a backend call through the proxy to the Resource server.
The interactions between the browser and the backend can be seen in your browser if you use some developer tools (usually F12 opens this up, works in Chrome by default, may require a plugin in Firefox). Here’s a summary:
VERB | PATH | STATUS | RESPONSE |
---|---|---|---|
GET |
/ui/ |
401 |
Browser prompts for authentication |
GET |
/ui/ |
200 |
index.html |
GET |
/ui/*.js |
200 |
Angular assets |
GET |
/ui/js/hello.js |
200 |
Application logic |
GET |
/ui/user |
200 |
authentication |
GET |
/resource/ |
200 |
JSON greeting |
You might not see the 401 because the browser treats the home page load as a single interaction. All requests are proxied (there is no content in the Gateway yet, beyond the Actuator endpoints for management).
Hurrah, it works! You have two backend servers, one of which is a UI, each with independent capabilities and able to be tested in isolation, and they are connected together with a secure Gateway that you control and for which you have configured the authentication. If the backends are not accessible to the browser it doesn’t matter (in fact it’s probably an advantage because it gives you yet more control over physical security).
Adding a Login Form
Just as in the "basic" sample in Section I we can now add a login form to the Gateway, e.g. by copying the code from Section II. When we do that we can also add some basic navigation elements in the Gateway, so the user doesn’t have to know the path to the UI backend in the proxy. So let’s first copy the static assets from the "single" UI into the Gateway, delete the message rendering and insert a login form into our home page (in the
somewhere):
class="container" [hidden]="authenticated">
Instead of the message rendering we will have a nice big navigation button:
class="container" [hidden]="!authenticated">
class="btn btn-primary" href="/ui/">Go To User Interface
If you are looking at the sample in github, it also has a minimal navigation bar with a "Logout" button. Here’s the login form in a screenshot:
To support the login form we need some TypeScript with a component implementing the login()
function we declared in the , and we need to set the
authenticated
flag so that the home page will render differently depending on whether or not the user is authenticated. For example:
include::src/app/app.component.ts
where the implementation of the login()
function is similar to that in Section II.
We can use the self
to store the authenticated
flag because there is only one component in this simple application.
If we run this enhanced Gateway, instead of having to remember the URL for the UI we can just load the home page and follow links. Here’s the home page for an authenticated user:
Granular Access Decisions in the Backend
Up to now our application is functionally very similar to the one in Section III or Section IV, but with an additional dedicated Gateway. The advantage of the extra layer may not be yet apparent, but we can emphasise it by expanding the system a bit. Suppose we want to use that Gateway to expose another backend UI, for users to "administrate" the content in the main UI, and that we want to restrict access to this feature to users with special roles. So we will add an "Admin" application behind the proxy, and the system will look like this:
There is a new component (Admin) and a new route in the Gateway in application.yml
:
zuul:
sensitive-headers:
routes:
ui:
url: http://localhost:8081
admin:
url: http://localhost:8082
resource:
url: http://localhost:9000
The fact that the existing UI is available to users in the "USER" role is indicated on the block diagram above in the Gateway box (green lettering), as is the fact that the "ADMIN" role is needed to go to the Admin application. The access decision for the "ADMIN" role could be applied in the Gateway, in which case it would appear in a WebSecurityConfigurerAdapter
, or it could be applied in the Admin application itself (and we will see how to do that below).
So first, create a new Spring Boot application, or copy the UI and edit it. You won’t need to change much in the UI app except the name to start with. The finished app is in Github here.
Suppose that within the Admin application we want to distinguish between "READER" and "WRITER" roles, so that we can permit (let’s say) users who are auditors to view the changes made by the main admin users. This is a granular access decision, where the rule is only known, and should only be known, in the backend application. In the Gateway we only need to ensure that our user accounts have the roles needed, and this information is available, but the Gateway doesn’t need to know how to interpret it. In the Gateway we create user accounts to keep the sample application self-contained:
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user").password("password").roles("USER")
.and()
.withUser("admin").password("admin").roles("USER", "ADMIN", "READER", "WRITER")
.and()
.withUser("audit").password("audit").roles("USER", "ADMIN", "READER");
}
}
where the "admin" user has been enhanced with 3 new roles ("ADMIN", "READER" and "WRITER") and we have also added an "audit" user with "ADMIN" access, but not "WRITER".
In a production system the user account data would be managed in a backend database (most likely a directory service), not hard coded in the Spring Configuration. Sample applications connecting to such a database are easy to find on the internet, for example in the Spring Security Samples. |
The access decisions go in the Admin application. For the "ADMIN" role (which is required globally for this backend) we do it in Spring Security:
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
...
.authorizeRequests()
.antMatchers("/index.html", "/").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
...
}
}
For the "READER" and "WRITER" roles the application itself is split, and since the application is implemented in JavaScript, that is where we need to make the access decision. One way to do this is to have a home page with a computed view embedded in it via the router:
class="container">
Admin
The route is computed when the component loads:
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
user: {};
constructor(private app: AppService, private http: HttpClient, private router: Router) {
app.authenticate(response => {
this.user = response;
this.message();
});
}
logout() {
this.http.post('logout', {}).subscribe(function() {
this.app.authenticated = false;
this.router.navigateByUrl('/login');
});
}
message() {
if (!this.app.authenticated) {
this.router.navigate(['/unauthenticated']);
} else {
if (this.app.writer) {
this.router.navigate(['/write']);
} else {
this.router.navigate(['/read']);
}
}
}
...
}
the first thing the application does is look at check if the user is authenticated, and computes the route by looking at the user data. The routes are declared in the main module:
const routes: Routes = [
{ path: '', pathMatch: 'full', redirectTo: 'read'},
{ path: 'read', component: ReadComponent},
{ path: 'write', component: WriteComponent},
{ path: 'unauthenticated', component: UnauthenticatedComponent},
{ path: 'changes', component: ChangesComponent}
];
Each of those components (one for each route) has to be implemented separately. Here’s the ReadComponent
as an example:
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({
templateUrl: './read.component.html'
})
export class ReadComponent {
greeting = {};
constructor(private http: HttpClient) {
http.get('/resource').subscribe(data => this.greeting = data);
}
}
Greeting
The ID is {{greeting.id}}
The content is {{greeting.content}}
The WriteComponent
is similar, but has a form to change the message in the backend:
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({
templateUrl: './write.component.html'
})
export class WriteComponent {
greeting = {};
constructor(private http: HttpClient) {
this.http.get('http://localhost:9000').subscribe(data => this.greeting = data);
}
update() {
this.http.post('/resource', {content: this.greeting['content']}).subscribe(response => {
this.greeting = response;
});
}
}
The AppService
also needs to provide the data to compute the route, so in the authenticate()
function we see this:
http.get('/user').subscribe(function(response) {
var user = response.json();
if (user.name) {
self.authenticated = true;
self.writer = user.roles && user.roles.indexOf("ROLE_WRITER")>0;
} else {
self.authenticated = false;
self.writer = false;
}
callback && callback(response);
})
To support this function on the backend we need the /user
endpoint, e.g. in our main application class:
@SpringBootApplication
@RestController
public class AdminApplication {
@RequestMapping("/user")
public Map<String, Object> user(Principal user) {
Map<String, Object> map = new LinkedHashMap<String, Object>();
map.put("name", user.getName());
map.put("roles", AuthorityUtils.authorityListToSet(((Authentication) user)
.getAuthorities()));
return map;
}
public static void main(String[] args) {
SpringApplication.run(AdminApplication.class, args);
}
}
the role names come back from the "/user" endpoint with the "ROLE_" prefix so we can distinguish them from other kinds of authorities (it’s a Spring Security thing). Thus the "ROLE_" prefix is needed in the JavaScript, but not in the Spring Security configuration, where it is clear from the method names that "roles" are the focus of the operations. |
Changes in the Gateway to Support Admin UI
We are going to use the roles to make access decisions in the Gateway as well (so we can conditionally display a link to the admin UI), so we should add the "roles" to the "/user" endpoint in the Gateway as well. Once that is in place we can add some JavaScript to set up a flag to indicate that the current user is an "ADMIN". In the authenticated()
function:
this.http.get('user', {headers: headers}).subscribe(data => {
this.authenticated = data && data['name'];
this.user = this.authenticated ? data['name'] : '';
this.admin = this.authenticated && data['roles'] && data['roles'].indexOf('ROLE_ADMIN') > -1;
});
and we also need to reset the admin
flag to false
when a user logs out:
this.logout = function() {
http.post('logout', {}).subscribe(function() {
self.authenticated = false;
self.admin = false;
});
}
and then in the HTML we can conditionally show a new link:
class="container" [hidden]="!authenticated">
class="btn btn-primary" href="/ui/">Go To User Interface
/>
class="container" [hidden]="!authenticated || !admin">
class="btn btn-primary" href="/admin/">Go To Admin Interface
Run all the apps and go to http://localhost:8080 to see the result. Everything should be working fine, and the UI should change depending on the currently authenticated user.
Why are we Here?
Now we have a nice little system with 2 independent user interfaces and a backend Resource server, all protected by the same authentication in a Gateway. The fact that the Gateway acts as a micro-proxy makes the implementation of the backend security concerns extremely simple, and they are free to concentrate on their own business concerns. The use of Spring Session has (again) avoided a huge amount of hassle and potential errors.
A powerful feature is that the backends can independently have any kind of authentication they like (e.g. you can go directly to the UI if you know its physical address and a set of local credentials). The Gateway imposes a completely unrelated set of constraints, as long as it can authenticate users and assign metadata to them that satisfy the access rules in the backends. This is an excellent design for being able to independently develop and test the backend components. If we wanted to, we could go back to an external OAuth2 server (like in Section V, or even something completely different) for the authentication at the Gateway, and the backends would not need to be touched.
A bonus feature of this architecture (single Gateway controlling authentication, and shared session token across all components) is that "Single Logout", a feature we identified as difficult to implement in Section V, comes for free. To be more precise, one particular approach to the user experience of single logout is automatically available in our finished system: if a user logs out of any of the UIs (Gateway, UI backend or Admin backend), he is logged out of all the others, assuming that each individual UI implemented a "logout" feature the same way (invalidating the session).
Testing an Angular Application
In this section we continue our discussion of how to use Spring Security with Angular in a "single page application". Here we show how to write and run unit tests for the client-side code using the Angular test framework. You can catch up on the basic building blocks of the application or build it from scratch by reading the first section, or you can just go straight to the source code in Github (the same source code as Part I, but with tests now added). This section actually has very little code using Spring or Spring Security, but it covers the client-side testing in a way that might not be so easy to find in the usual Angular community resources, and one which we feel will be comfortable for the majority of Spring users.
Reminder: if you are working through this section with the sample application, be sure to clear your browser cache of cookies and HTTP Basic credentials. In Chrome the best way to do that for a single server is to open a new incognito window.
Writing a Specification
Our "app" component in the "basic" application is very simple, so it won’t take a lot to test it thoroughly. Here’s a reminder of the code:
include::basic/src/app/app.component.ts
The main challenge we face is to provide the http
object in the test, so we can make assertions about how they are used in the component. Actually, even before we face that challenge we need to be able to create a component instance, so we can test what happens when it loads. Here’s how you can do that.
The Angular build in an app created from ng new
already has a spec and some configuration to run it. The generated spec is in "src/app", and it starts like this:
import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [],
declarations: [
AppComponent
]
}).compileComponents();
}));
it('should create the app', async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
}));
...
}
In this very basic test suite we have these important elements:
We
describe()
the thing that is being tested (the "AppComponent" in this case) with a function.Inside that function we provide a
beforeEach()
callback, which loads the Angular component.Behaviour is expressed through a call to
it()
, where we state in words what the expectation is, and then provide a function that makes assertions.The test environment is initialized before anything else happens. This is boilerplate for most Angular apps.
The test function here is so trivial it actually only asserts that the component exists, so if that fails then the test will fail.
Improving the Unit Test: Mocking HTTP Backend
To improve the spec to production grade we need to actually assert something about what happens when the controller loads. Since it makes a call to http.get()
we need to mock that call to avoid having to run the whole application just for a unit test. To do that we use the Angular HttpClientTestingModule
:
Unresolved directive in testing.adoc - include::basic/src/app/app.component.spec[indent=0]
The new pieces here are:
The declaration of the
HttpClientTestingModule
as an imports in theTestBed
inbeforeEach()
.In the test function we set expectations for the backend before we create the component, telling it to expect a call to 'resource/',and what the response should be.
Running the Specs
To run our test" code we can do ./ng test
(or ./ng build
) using the convenience script created when the project was set up. It also runs as part of the Maven lifecycle, so ./mvnw install
is also a good way to run the tests, and this is what will happen in your CI build.
End-to-End Tests
Angular also has a standard build set up for "end-to-end tests" using a browser and your generated JavaScript. These are written as "specs" in the top-level e2e
directory. All the samples in this tutorial contain a really simple end-to-end test which run in the Maven lifecycle (hence you will see a browser window popup if you run mvn install
in any of the "ui" apps).
Conclusion
Being able to run unit tests for Javascript is important in a modern web application and it’s a topic that we’ve ignored (or dodged) up to now in this series. With this installment we have presented the basic ingredients of how to write the tests, how to run them at development time and also, importantly, in a continuous integration setting. The approach we have taken is not going to suit everyone, so please don’t feel bad about doing it in a different way, but make sure you have all those ingredients. The way we did it here will probably feel comfortable to traditional Java enterprise developers, and integrates well with their existing tools and processes, so if you are in that category I hope you will find it useful as a starting point. More examples of testing with Angular and Jasmine can be found in plenty of places on the internet, but the first point of call might be the "single" sample from this series, which now has some up to date test code which is a bit less trivial than the code we needed to write for the "basic" sample in this tutorial.
Logout from an OAuth2 Client Application
In this section we continue our discussion of how to use Spring Security with Angular in a "single page application". Here we show how to take the OAuth2 samples and add a different logout experience. Many people who implement OAuth2 single sign on find that they have a puzzle to solve of how to logout "cleanly"? The reason it’s a puzzle is that there isn’t a single correct way to do it, and the solution you choose will be determined by the user experience you are looking for, and also the amount of complexity you are willing to take on. The reasons for the complexity stem from the fact that there are potentially multiple browser sessions in the system, all with different backend servers, so when a user logs out from one of them, what should happen to the others? This is the ninth section of a tutorial, and you can catch up on the basic building blocks of the application or build it from scratch by reading the first section, or you can just go straight to the source code in Github.
Logout Patterns
The user experience with logout of the oauth2
sample in this tutorial is that you logout of the UI app, but not from the authserver, so when you log back into the UI app the autheserver does not challenge again for credentials. This is completely expected, normal, and desirable when the autheserver is external - Google and other external authserver providers neither want nor allow you to logout from their servers from an untrusted application - but it isn’t the best user experience if the authserver is really part of the same system as the UI.
There are, broadly speaking, three patterns for logout from a UI app that is authenticated as an OAuth2 client:
External Authserver (EA, the original sample). The user perceives the authserver as a 3rd party (e.g. using Facebook or Google to authenticate). You don’t want to log out of the authserver when the app session ends. You do want approval for all grants. The
oauth2
(andoauth2-vanilla
) sample from this tutorial implement this pattern.Gateway and Internal Authserver (GIA). You only need to log out of 2 apps, and they are part of the same system, as perceived by the user. Usually you want to autoapprove all grants.
Single Logout (SL). One authserver and multiple UI apps all with their own authentication, and when the user logs out of one, you want them all to follow suit. Likely to fail with a naive implementation because of network partitions and server failures - you basically need globally consistent storage.
Sometimes, even if you have an external authserver, you want to control the authentication and add an internal layer of access control (e.g. scopes or roles that the authserver doesn’t support). Then it’s a good idea to use the EA for authentication, but have an internal authserver that can add the additional details you need to the tokens. The auth-server
sample from this other OAuth2 Tutorial shows you how to do that in a very simple way. You can then apply the GIA or SL patterns to the system that includes the internal authserver.
Here are some options if you don’t want EA:
Log out from authserver as well as UI app in browser client. Simple approach and works with some careful CRSF and CORS configuration. No SL.
Logout from authserver as soon as a token is available. Hard to implement in the UI, where the token is acquired, because you don’t have the session cookie for the authserver there. There is a feature request in Spring OAuth which shows an interesting approach: invalidate the session in the authserver as soon as an auth code is generated. The Github issue contains an aspect that implements the session invalidation, but it’s easier to do as a
HandlerInterceptor
. No SL.Proxy authserver through the same gateway as UI and hope that one cookie is enough to manage the state for the whole system. Doesn’t work because unless there is a shared session, which defeats the object to some extent (otherwise there is no session storage for the authserver). SL only if the session is shared between all apps.
Cookie relay in gateway. You are using the gateway as the source of truth for authentication, and the authserver has all the state it needs because the gateway manages the cookie instead of the browser. The browser never has a cookie from more than one server. No SL.
Use the token as global authentication and invalidate it when user logs out of the UI app. Downside: requires tokens to be invalidated by client apps, which isn’t really what they were designed to do. SL possible, but usual constraints apply.
Create and manage a global session token (in addition to the user token) in the authserver. This is the approach taken by OpenId Connect, and it does provide some options for SL, at the cost of some extra machinery. None of the options is immune from the usual distributed system limitations: if networks and application nodes are not stable there are no guarantees that a logout signal is shared among all participants when needed. All of the logout specs are still in draft form, and here are some links to the specs: Session Management, Front Channel Logout, and Back Channel Logout.
Note that where SL is hard or impossible, it might be better to put all the UIs behind a single gateway anyway. Then you can use GIA, which is easier, to control logout from your whole estate.
The easiest two options, which apply nicely in the GIA pattern can be implemented in the tutorial sample as follows (take the oauth2
sample and work from there).
Logout of Both Servers from Browser
It’s quite easy to add a couple of lines of code to the browser client that logout from the authserver as soon as the UI app is logged out. E.g.
logout() {
this.http.post('logout', {}).finally(() => {
self.authenticated = false;
this.http.post('http://localhost:9999/uaa/logout', {}, {withCredentials:true})
.subscribe(() => {
console.log('Logged out');
});
}).subscribe();
};
In this sample we hardcoded the authserver logout endpoint URL into the JavaScript, but it would be easy to externalize that if you needed to. It has to be a POST directly to the authserver because we want the session cookie to go along too. The XHR request will only go out from the browser with a cookie attached if we specifically ask for withCredentials:true
.
Conversely, on the server we need some CORS configuration because the request is coming from a different domain. E.g. in the WebSecurityConfigurerAdapter
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.requestMatchers().antMatchers("/login", "/logout", "/oauth/authorize", "/oauth/confirm_access")
.and()
.cors().configurationSource(configurationSource())
...
}
private CorsConfigurationSource configurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("*");
config.setAllowCredentials(true);
config.addAllowedHeader("X-Requested-With");
config.addAllowedHeader("Content-Type");
config.addAllowedMethod(HttpMethod.POST);
source.registerCorsConfiguration("/logout", config);
return source;
}
The "/logout" endpoint has been given some special treatment. It is allowed to be called from any origin, and explicitly allows credentials (e.g. cookies) to be sent. The allowed headers are just the ones that Angular sends in teh sample app.
In addition to the CORS configuration we also need to disable CSRF for the logout endpoint, because Angular will not send the X-XSRF-TOKEN
header in a cross-domain request. The authserver didn’t require any CSRF configuration before now, but it’s easy to add an ignore for the logout endpoint:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.ignoringAntMatchers("/logout/**")
...
}
Dropping CSRF protection is not really advisable, but you might be prepared to tolerate it for this restricted use case. |
With those two simple changes, one in the UI app client, and one in the authserver, you will find that once you logout of the UI app, when you log back in, you will always be prompted for a password.
Another useful change is to set the OAuth2 client to autoapprove, so that the user doesn’t have to approve the token grant. This is common in a internal authserver, where the user doesn’t perceive it as a separate system. In the AuthorizationServerConfigurerAdapter
you just need a flag when the client is initialized:
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory().withClient("acme")
...
.autoApprove(true);
}
Invalidate Session in Authserver
If you don’t like to give up the CSRF protection on the logout endpoint, you can try the other easy approach, which is to invalidate the user session in the authserver as soon as a token is granted (actually as soon as an auth code is generated). This is also super easy to implement: starting from the oauth2
sample, just add a HandlerInterceptor
to the OAuth2 endpoints.
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints)
throws Exception {
...
endpoints.addInterceptor(new HandlerInterceptorAdapter() {
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
if (modelAndView != null
&& modelAndView.getView() instanceof RedirectView) {
RedirectView redirect = (RedirectView) modelAndView.getView();
String url = redirect.getUrl();
if (url.contains("code=") || url.contains("error=")) {
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}
}
}
}
});
}
This interceptor looks for a RedirectView
, which is a signal that the user is being redirected back to the client app, and checks if the location contains an auth code or an error. You could add "token=" if you were using implicit grants as well.
With this simple change, as soon as you authenticate, the session in the authserver is already dead, so there’s no need to try and manage it from the client. When you log out of the UI app, and then log back in, the authserver doesn’t recognize you and prompts for credentials. This pattern is the one implemented by the oauth2-logout
sample in the source code for this tutorial. The downside of this approach is that you don’t really have true single sign on any more - any other apps that are part of your system will find that the authserver session is dead and they have to prompt for authentication again - it isn’t a great user experience if there are multiple apps.
Conclusion
In this section we have seen how to implement a couple of different patterns for logout from an OAuth2 client application (taking as a starting point the application from section five of the tutorial), and some options for other patterns were discussed. These options are not exhaustive, but should give you a good idea of the trade offs involved, and some tools for thinking about the best solution for your use case. There were only couple of lines of JavaScript in this section, and that wasn’t really specific to Angular (it adds a flag to XHR requests), so all the lessons and patterns are applicable beyond the narrow scope of the sample apps in this guide. A recurring theme is that all approaches to single logout (SL) where there are multiple UI apps and a single authserver tend to be flawed in some way: the best you can do is choose the approach that makes your users the least uncomfortable. If you have an internal authserver and a system that is composed of many components, then possibly the only architecture that feels to the user like a single system is a gateway for all user interactions.
Want to write a new guide or contribute to an existing one? Check out our contribution guidelines.
All guides are released with an ASLv2 license for the code, and an Attribution, NoDerivatives creative commons license for the writing. |