Tiven Wang
Wang Tiven June 02, 2017
425 favorite favorites
bookmark bookmark
share share

Try CloudFoundry Series

Setup UAA service

Setup the UAA service on CloudFoundry platform by following the previous article Try Cloud Foundry 3 - Components UAA

Single Sign On With UAA

You can download the complete project source code from Github

git clone https://github.com/anypossiblew/try-cloud-foundry.git
cd try-cloud-foundry
git checkout 5-uaa-oauth2

Creating a New Project

mkdir try-cf-app && cd try-cf-app

curl https://start.spring.io/starter.tgz -d style=web -d name=try-cf-app | tar -xzvf -

or use the Spring Initializr to bootstrap your application.

Run spring boot application with:

./mvnw spring-boot:run

When the info display, the embedded tomcat server is started:

2017-06-01 10:17:48.028  INFO 11592 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2017-06-01 10:17:48.433  INFO 11592 --- [           main] wang.tiven.trycfapp.tryCfAppApplication  : Started tryCfAppApplication in 7.802 seconds (JVM running for 12.929)

Add a Home Page

In your new project create an index.html in the “src/main/resources/static” folder.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8"/>
  <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
  <title>Try CF App</title>
  <meta name="description" content=""/>
  <meta name="viewport" content="width=device-width"/>
  <base href="/"/>
  <link rel="stylesheet" type="text/css" href="/webjars/bootstrap/css/bootstrap.min.css"/>
  <script type="text/javascript" src="/webjars/jquery/jquery.min.js"></script>
  <script type="text/javascript" src="/webjars/bootstrap/js/bootstrap.min.js"></script>
</head>
<body>
  <h1>Try CF App</h1>
  <div class="container"></div>
</body>
</html>

adding some dependencies in ‘pom.xml’:

<dependency>
  <groupId>org.webjars</groupId>
  <artifactId>angularjs</artifactId>
  <version>1.4.3</version>
</dependency>
<dependency>
  <groupId>org.webjars</groupId>
  <artifactId>jquery</artifactId>
  <version>2.1.1</version>
</dependency>
<dependency>
  <groupId>org.webjars</groupId>
  <artifactId>bootstrap</artifactId>
  <version>3.2.0</version>
</dependency>
<dependency>
  <groupId>org.webjars</groupId>
  <artifactId>webjars-locator</artifactId>
</dependency>

Securing the Application

add Spring Security as a dependency

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.security.oauth</groupId>
  <artifactId>spring-security-oauth2</artifactId>
</dependency>

To make the link to Facebook we need an @EnableOAuth2Sso annotation on our main class:

@SpringBootApplication
@EnableOAuth2Sso
public class tryCfAppApplication {

  public static void main(String[] args) {
		SpringApplication.run(tryCfAppApplication.class, args);
	}

}

and some configuration (converting application.properties to YAML for better readability):

security:
  oauth2:
    client:
      clientId: app
      clientSecret: appclientsecret
      accessTokenUri: https://try-cf-uaa.cfapps.io/oauth/token
      userAuthorizationUri: https://try-cf-uaa.cfapps.io/oauth/authorize
      tokenName: access_token
      authenticationScheme: query
      clientAuthenticationScheme: form
    resource:
      userInfoUri: https://try-cf-uaa.cfapps.io/userinfo

The configuration refers to a client app registered with your initial configurations in your UAA service, in which you have to supply a registered redirect (home page) for the app. This one is registered to “localhost:8080” so it only works in an app running on that address.

With that change you can run the app again and visit the home page at http://localhost:8080. Instead of the home page you should be redirected to login with Facebook. If you do that, and accept any authorizations you are asked to make, you will be redirected back to the local app and the home page will be visible. If you stay logged into UAA, you won’t have to re-authenticate with this local app, even if you open it in a fresh browser with no cookies and no cached data. (That’s what Single Sign On means.)

Now you can restart your local server, and open the http://localhost:8080/index.html, you will be redirected to the authorization uri of the UAA service. Input your user authentication information and click the sign in button, and you will be redirected to authorization page if you are the first login, click allow button to be redirected to your localhost login page or deny button to reject.

https://try-cf-uaa.cfapps.io/oauth/authorize?grant_type=authorization_code&client_id=app&client_secret=appclientsecret&redirect_uri=http://localhost:8080/index.html

https://try-cf-uaa.cfapps.io/login

Application Authorization

select the requested permission

and click the **AUTHORIZE** button:

http://localhost:8080/login?code=SVYvp1nSbd&state=NunIm4

Add User Info

Add the annotation @RestController on the application class to enable the restful api, the controller needs an endpoint at “/user” that describes the currently authenticated user. That’s quite easy to do, e.g. in our main class:

@SpringBootApplication
@EnableOAuth2Sso
@RestController
public class tryCfAppApplication {

  ...

  @RequestMapping("/user")
  public Principal user(Principal principal) {
    return principal;
  }

}

Restart server and access http://localhost:8080/user and login if you haven’t, you will get the user authentication details:

{
  "authorities": [
    {
    "authority": "ROLE_USER"
    }
  ],
  "details": {
    "remoteAddress": "0:0:0:0:0:0:0:1",
    "sessionId": "5E5D2F3797CCEE8E6B10AF13C478BC9A",
    ...
  },
  "authenticated": true,
  "userAuthentication": {
    "authorities": [
      {
      "authority": "ROLE_USER"
      }
    ],
    "details": {
      "user_id": "0da8195f-3027-406d-a899-1c742b2eba9e",
      "sub": "0da8195f-3027-406d-a899-1c742b2eba9e",
      "user_name": "marissa",
      ...
    },
    "authenticated": true,
    "principal": "0da8195f-3027-406d-a899-1c742b2eba9e",
    "credentials": "N/A",
    "name": "0da8195f-3027-406d-a899-1c742b2eba9e"
  },
  "principal": "0da8195f-3027-406d-a899-1c742b2eba9e",
  "clientOnly": false,
  "credentials": "",
  "oauth2Request": {
    "clientId": "app",
    "scope": [ ],
    ...
  },
  "name": "0da8195f-3027-406d-a899-1c742b2eba9e"
}

Add User in Welcome page

To render some content conditional on whether the user is authenticated or not we could use server side rendering (e.g. with Freemarker or Tymeleaf), or we can just ask the browser to to it, using some JavaScript. To do that we are going to use AngularJS, but if you prefer to use a different framework, it shouldn’t be very hard to translate the client code.

<script type="text/javascript" src="/webjars/angularjs/angular.min.js"></script>

<script type="text/javascript">
angular.module("app", []).controller("home", function($http) {
  var self = this;
  $http.get("/user").success(function(data) {
    self.user = data.userAuthentication.details.name;
    self.authenticated = true;
  }).error(function() {
    self.user = "N/A";
    self.authenticated = false;
  });
});
</script>

...

<body ng-app="app" ng-controller="home as home">
  <h1>Try CF App</h1>
  <div class="container" ng-show="home.authenticated">
    Logged in as: <span ng-bind="home.user"></span>
  </div>
</body>

Now you get the user info in the index page like this:

Try CF App
  Logged in as: Marissa Bloggs

Add Spring Security Configurations

We can’t access the welcome page as anonymous yet, the application let us login whenever. So we need add Spring Security configurations to enable anonymous access.

@SpringBootApplication
@EnableOAuth2Sso
@RestController
public class tryCfAppApplication extends WebSecurityConfigurerAdapter {

  ...

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http
      .antMatcher("/**")
      .authorizeRequests()
        .antMatchers("/", "/login**", "/webjars/**")
        .permitAll()
      .anyRequest()
        .authenticated();
  }

}

Add the login and logout section in welcome page:


<script type="text/javascript">
angular.module("app", []).controller("home", function($http) {
  var self = this;
  ...
  self.logout = function() {
    $http.post('/logout', {}).success(function() {
      self.authenticated = false;
      $location.path("/");
    }).error(function(data) {
      console.log("Logout failed")
      self.authenticated = false;
    });
  };
});
</script>
...
<body ng-app="app" ng-controller="home as home">
  <h1>Try CF App</h1>
  <div class="container" ng-show="!home.authenticated">
    Login with: <a href="/login">try-cf-uaa@CloudFoundry</a>
  </div>
  <div class="container" ng-show="home.authenticated">
    Logged in as: <span ng-bind="home.user"></span>
    <div>
      <button ng-click="home.logout()" class="btn btn-primary">Logout</button>
    </div>
  </div>
</body>

Adding a Logout Endpoint

Spring Security has built in support for a /logout endpoint which will do the right thing for us (clear the session and invalidate the cookie). To configure the endpoint we simply extend the existing configure() method in our WebSecurityConfigurer:

@Override
protected void configure(HttpSecurity http) throws Exception {
  http.antMatcher("/**")
    ... // existing code here
    .and().logout().logoutSuccessUrl("/").permitAll();
}

The /logout endpoint requires us to POST to it, and to protect the user from Cross Site Request Forgery (CSRF, pronounced “sea surf”), it requires a token to be included in the request. The value of the token is linked to the current session, which is what provides the protection, so we need a way to get that data into our JavaScript app.

AngularJS also has built in support for CSRF (they call it XSRF), but it is implemented in a slightly different way than the out-of-the box behaviour of Spring Security. What Angular would like is for the server to send it a cookie called “XSRF-TOKEN” and if it sees that, it will send the value back as a header named “X-XSRF-TOKEN”. To teach Spring Security about this we need to add a filter that creates the cookie and also we need to tell the existing CRSF filter about the header name. In the WebSecurityConfigurer:

@Override
protected void configure(HttpSecurity http) throws Exception {
  http.antMatcher("/**")
    ... // existing code here
    .and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}

With those changes in place we are ready to run the app and try out the new logout button. Start the app and load the home page in a new browser window. Click on the “login” link to take you to Facebook (if you are already logged in there you might not notice the redirect). Click on the “Logout” button to cancel the current session and return the app to the unauthenticated state. If you are curious you should be able to see the new cookies and headers in the requests that the browser exchanges with the local server.

Remember that now the logout endpoint is working with the browser client, then all other HTTP requests (POST, PUT, DELETE, etc.) will also work just as well. So this should be a good platform for an application with some more realistic features.

References

Similar Posts

Comments

Back to Top