Skip to content

Latest commit

 

History

History
364 lines (272 loc) · 21.8 KB

File metadata and controls

364 lines (272 loc) · 21.8 KB

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):

app.component.html
<div class="container">
  <ul class="nav nav-pills">
    <li><a routerLinkActive="active" routerLink="/home">Home</a></li>
    <li><a routerLinkActive="active" routerLink="/login">Login</a></li>
    <li><a (click)="logout()">Logout</a></li>
  </ul>
</div>
<div class="container">
  <router-outlet></router-outlet>
</div>

The main content is a <router-outlet/> and there is a navigation bar with login and logout links.

The <router-outlet/> 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:

app.module.ts
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:

app.component.ts
link:src/app/app.component.ts[role=include]

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 a finally() 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 the authenticate() 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:

app.service.ts
link:src/app/app.service.ts[role=include]

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":

home.component.html
link:src/app/home.component.html[role=include]

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:

home.component.ts
link:src/app/home.component.ts[role=include]

The Login Form

The login form also gets its own component:

login.component.html
link:src/app/login.component.html[role=include]

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 password.

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:

login.component.ts
link:src/app/login.component.ts[role=include]

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:

UiApplication.java
@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):

UiApplication.java
@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:

application.yml
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:

app.module.ts
@Injectable()
export class XhrInterceptor implements HttpInterceptor {

  intercept(req: HttpRequest<any>, 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:

app.module.ts
@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:

UiApplication.java
@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).

Note
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 is stateful. 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.

Tip
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.