/spring-login

Application to demo login flows in Spring from Basic Authentication to OpenID

Primary LanguageCSS

Spring Boot / Security

Introduction

Scenario: It is a beautiful Sunday morning and you decide to pick up some coding and want create an admin panel for a web page. On the web you find a beautiful Bootstrap starting template but how get this template working together with Spring Boot / Thymeleaf. You start building and the page looks great, now the only you need to do is to integrate it with your companies authentication mechanism.

This tutorial will take you from a form based authentication towards an integration with Microsoft Active Directory as authentication

Setup

Go to https://start.spring.io and include the following dependencies: Web, Thymeleaf and Security. Extract the zip file and open the project with for example IntelliJ.

Create a the following controller:

@Controller
public class HomeController {

    @GetMapping("/")
    public String homePage(Model model) {
        return "index";
    }
}

in static/templates create a file called index.html

<html>
<body>
<h1>Welcome to my page</h1>
</body>


</html>

Now start the application and point your browser to: http://localhost:8080 A login page will be displayed this is default behavior added by Spring Security, the username is user and the password is generated during the start of the application and can be found in the console:

INFO 10712 --- [  restartedMain] .s.s.UserDetailsServiceAutoConfiguration :

Using generated security password: 27a47ee1-8506-48a4-b3dc-bf486eec1eb0

Try to login and you will see the index.html page.

Tip
Use the initial commit from this repository to get the same baseline

Step 1: Add the new Bootstrap template

For this code base we use https://startbootstrap.com/templates/sb-admin/ download the template unzip it and copy the folders css, js, scss, vendor folders to resources/static Place the html files in the templates folder, after a restart you still see the standard login page and after login the new template will be used

Replacing the login page

In order to replace the standard login page with the new login.html page the following steps are necessary, extend the HomeController with:

 @GetMapping("/login")
 public String login() {
   return "login";
 }
Note
There are other ways to map static files using a view resolver but for now we add an extra endpoint.

The next step is to add a security configuration:

@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                  .anyRequest().authenticated()
                  .and()
                .formLogin()
                  .loginPage("/login")
                  .permitAll();
    }
}

Now if we reload the page the new login page will be displayed but the styling will look awful. That’s because we now provided our own security configuration all the standard provided configuration is no longer applied. In order to fix the styling add:

        http
                .authorizeRequests()
                  .antMatchers("/css/**", "/js/**", "/scss/**", "/vendor/**").permitAll()
                  .anyRequest().authenticated()
                ...

This will tell the security configuration all those resources are available without authentication. Let’s reload the app and try to login. You will see the correct styling being applied however logging in does not work yet.

Fix the login procedure

Because we are using Thymeleaf as stated above the login.html page needs some tweaks:

<html  xmlns:th="http://www.thymeleaf.org">

...

<form th:action="@{/login}" method="post">

and replace:

<a class="btn btn-primary btn-block" href="index.html">Login</a>

with

<button class="btn btn-primary btn-block" type="submit">Login</button>

Reload the application and since the app uses name and not an e-mail address in the login page we have to change some of the configuration:

 @Bean
 public UserDetailsService userDetailsService() throws Exception {
    InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
    manager.createUser(User.withDefaultPasswordEncoder().username("test@test.com").password("password").roles("USER").build());
    return manager;
 }
Important
in a real live application the above piece of code should not be used!

After restarting the app again and providing the correct credentials the page will be redirected again to /login. Let’s put a breakpoint in UsernamePasswordAuthenticationFilter and follow the flow:

    private String usernameParameter = "username";
    private String passwordParameter = "password";
    private boolean postOnly = true;

    public UsernamePasswordAuthenticationFilter() {
        super(new AntPathRequestMatcher("/login", "POST"));
    }

    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            String username = this.obtainUsername(request);
            String password = this.obtainPassword(request);
            if (username == null) {
                username = "";
            }

            if (password == null) {
                password = "";
            }

            username = username.trim();
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            this.setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

    protected String obtainPassword(HttpServletRequest request) {
        return request.getParameter(this.passwordParameter);
    }

    protected String obtainUsername(HttpServletRequest request) {
        return request.getParameter(this.usernameParameter);
    }

The username and password cannot be obtained from the request, let’s fix this by adding:

<input type="email" id="inputEmail" name="username" ...>
<input type="password" id="inputPassword" name="password" ...>

to the html elements. And now finally the login page works!!

Note
you can change the names of the parameters by setting usernameParameter() on the form login in the SecurityConfiguration.

Showing errors

In order to show errors the following snippet can be added to login.html:

 <div th:if="${param.error}" class="alert alert-error">
   Invalid username and password.
 </div>
 <div th:if="${param.logout}" class="alert alert-success">
   You have been logged out.
 </div>

Moving toward

Now we have a basic understanding on how Spring Security works within an application and we added Basic Authentication we now want to integrate with OpenID Connect with Active Directory, OpenID Connect is an authentication protocol built on OAuth 2.0 that you can use to securely sign in a user to a web application. For more information see https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc

To be able to test this we need to setup Active Directory to be able to use in our application. There are a couple of steps to follow which are best described here: https://azure.microsoft.com/blog/spring-security-azure-ad/ Important is to correctly set the reply URL: http://localhost:8080/login/oauth2/code/azure

In our Spring application we need to enable the OAuth flow, extend the application.properties as follows:

spring.security.oauth2.client.registration.azure.client-id=<<applicationId>>
spring.security.oauth2.client.registration.azure.client-secret=<<generated_secret>>
azure.activedirectory.tenant-id=<<applicationId>>
azure.activedirectory.activeDirectoryGroups=unknown
server.use-forward-headers=true

The setting server.use-forward-headers is necessary to do the redirect properly with absolute urls. The group does not really matter at the moment it is necessary to define otherwise the app will not start, however for authentication it is not necessary.

And add the following dependency to the pom.xml

<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-oauth2-client</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-oauth2-jose</artifactId>
</dependency>

Remove the complete SecurityConfiguration class from the project as we are now only using application.properties to configure the OAuth flow.

Restart the application and now will be prompted with the Microsoft login page same as you are used to when logging in to your Outlook inbox.