1. Introduction

In the previous example we accepted all defaults and inspected the filter chain and API responses to gain an understanding of the Spring Security framework. In this chapter we will begin customizing the authentication configuration to begin to show how and why this can be accomplished.

1.1. Goals

You will learn:

  • to create a customized security authentication configurations

  • to obtain the identity of the current, authenticated user for a request

  • to incorporate authentication into integration tests

1.2. Objectives

At the conclusion of this lecture and related exercises, you will be able to:

  1. create multiple, custom authentication filter chains

  2. enable open access to static resources

  3. enable anonymous access to certain URIs

  4. enforce authenticated access to certain URIs

  5. locate the current authenticated user identity

  6. enable Cross-Origin Resource Sharing (CORS) exchanges with browsers

  7. add an authenticated identity to RestTemplate client

  8. add authentication to integration tests

2. Configuring Security

To override security defaults and define a customized FilterChainProxy-- we must supply one or more classes that define our own SecurityFilterChain(s).

2.1. WebSecurityConfigurer and Component-based Approaches

Spring provides two ways to do this:

  • WebSecurityConfigurer/ WebSecurityConfigurerAdapter - is the legacy and recently deprecated (Spring Security 5.7.0-M2; 2022) definition class that acts as a modular factory for security aspects of the application. [1] There can be one-to-N WebSecurityConfigurers and each can define a SecurityFilterChain and supporting services.

  • Component-based configuration - is the modern approach to defining security aspects of the application. The same types of components are defined with the component-based approach, but they are done independent of one another.

You will likely encounter the WebSecurityConfigurer approach for a long while — so I will provide some coverage of that here — while focusing on the component-based approach.

To highlight that the FilterChainProxy is populated with a prioritized list of SecurityFilterChain — I am going to purposely create multiple chains.

  • one with the API rules (APIConfiguration) - highest priority

  • one with the former default rules (AltConfiguration) - lowest priority

  • one with access rules for Swagger (SwaggerSecurity) - medium priority

The priority indicates the order in which they will be processed and will also influence the order for the SecurityFilterChain s they produce. Normally I would not highlight Swagger in these examples — but it provides an additional example of how well we can customize Spring Security.

security custom filter chains
Figure 1. Multiple SecurityFilterChains

2.2. Core Application Security Configuration

The example will eventually contain several SecurityFilterChains, but lets start with focusing on just one of them — the "API Configuration". This initial configuration will define the configuration for access to static resources, dynamic resources, and how to authenticate our users.

2.2.1. WebSecurityConfigurerAdapter Approach

In the deprecated WebSecurityConfiguration approach, we would start by defining a @Configuration class that extends WebSecurityConfigurerAdapter and overrides one or more of its configuration methods.

WebSecurityConfigurer Approach
@Configuration(proxyBeanMethods = false)
@Order(0) (2)
public class APIConfiguration extends WebSecurityConfigurerAdapter { (1)
    @Override
    public void configure(WebSecurity web) throws Exception { ... } (3)
    @Override
    protected void configure(HttpSecurity http) throws Exception { ... } (4)
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception { ... } (5)
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception { ... } (6)
1 Create @Configuration class that extends WebSecurityConfigurerAdapter to customize SecurityFilterChain
2 APIConfiguration has a high priority resulting SecurityFilterChain for dynamic resources
3 configure a SecurityFilterChain for static web resources
4 configure a SecurityFilterChain for dynamic web resources
5 optionally configure an AuthenticationManager for multiple authentication sources
6 optionally expose AuthenticationManager as an injectable bean for re-use in other SecurityFilterChains

Each SecurityFilterChain will have a reference to its AuthenticationManager. The WebSecurityConfigurerAdapter provides the chance to custom configure the AuthenticationManager using a builder.

The adapter also provides an accessor method that can be used to expose the built AuthenticationManager as a pre-built component for other SecurityFilterChains to reference.

2.2.2. Component-based Approach

In the modern Component-based approach, we define each aspect of our security infrastructure as a separate component. These @Bean factory methods are within a normal @Configuration class that requires no inheritance.

Component-based Approach
@Bean
public WebSecurityCustomizer apiStaticResources() { ... } (1)
@Bean
@Order(0) (3)
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception { ...} (2)
@Bean
public AuthenticationManager authnManager(HttpSecurity http, ...) throws Exception { (5)
    AuthenticationManagerBuilder builder = http (4)
                             .getSharedObject(AuthenticationManagerBuilder.class);
    ... }
1 define a bean to configure a SecurityFilterChain for static web resources
2 define a bean to configure a SecurityFilterChain for dynamic web resources
3 high priority assigned to SecurityFilterChain
4 optionally configure an AuthenticationManager for multiple authentication sources
5 expose AuthenticationManager as an injectable bean for use in SecurityFilterChains

The SecurityFilterChain for static resources gets defined within a lambda function implementing the WebSecurityCustomizer interface. The SecurityFilterChain for dynamic resources gets directly defined by within the @Bean factory method.

There is no longer any direct linkage between the configuration of the AuthenticationManager and the SecurityFilterChains being built. The linkage is provided through a getSharedObject call of the HttpSecurity object that can be injected into the bean methods.

2.3. Ignoring Static Resources

One of the easiest rules to put into place is to provide open access to static content. This is normally image files, web CSS files, etc. Spring recommends not including dynamic content in this list. Keep it limited to static files.

Access is defined by configuring the WebSecurity object.

  • In the WebSecurityConfigurerAdapter approach, the modification is performed within the method overriding the configure(WebSecurity) method.

    Ignore Static Content Configuration - WebSecurityConfigurerAdapter approach
    import org.springframework.security.config.annotation.web.builders.WebSecurity;
    
    @Configuration
    @Order(0)
    public class APIConfiguration extends WebSecurityConfigurerAdapter {
        @Override
        public void configure(WebSecurity web) throws Exception {
            web.ignoring().antMatchers("/content/**");
        }
  • In the Component-based approach, a lambda function implementing the WebSecurityCustomizer functional interface is returned. That lambda will be called to customize the WebSecurity object.

    Ignore Static Content Configuration - Component-based approach
    import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
    
    @Bean
    public WebSecurityCustomizer apiStaticResources() {
        return (web)->web.ignoring().antMatchers("/content/**");
    }
    WebSecurityCustomers Functional Interface
    public interface WebSecurityCustomizer {
            void customize(WebSecurity web);
    }

Remember — our static content is packaged within the application by placing it under the src/main/resources/static directory of the source tree.

Static Content
$ tree src/main/resources/
src/main/resources/
|-- application.properties
`-- static
    `-- content
        |-- hello.js
        |-- hello_static.txt
        `-- index.html
$ cat src/main/resources/static/content/hello_static.txt
Hello, static file

With that rule in place, we can now access our static file without any credentials.

Anonymous Access to Static Content
$ curl -v -X GET http://localhost:8080/content/hello_static.txt
> GET /content/hello_static.txt HTTP/1.1
>
< HTTP/1.1 200
< Vary: Origin
< Vary: Access-Control-Request-Method
< Vary: Access-Control-Request-Headers
< Last-Modified: Fri, 03 Jul 2020 19:36:25 GMT
< Cache-Control: no-store
< Accept-Ranges: bytes
< Content-Type: text/plain
< Content-Length: 19
< Date: Fri, 03 Jul 2020 20:55:58 GMT
<
Hello, static file

2.4. SecurityFilterChain Matcher

The meat of the SecurityFilterChain definition is within the configuration of the HttpSecurity object.

The resulting SecurityFilterChain will have a requestMatcher that identifies which URIs the identified rules apply to. The default is "all" URIs. In the example below I am limiting the configuration to two URIs (/api/anonymous and /api/authn) using an Ant Matcher. A regular expression matcher is also available. The matchers also allow a specific method to be declared in the definition.

  • In the WebSecurityConfigurerAdapter approach, configuration is performed in the method overriding the configure(HttpSecurity) method.

    SecurityFilterChain Matcher - WebSecurityConfigurerAdapter approach
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    
    @Configuration
    @Order(0)
    public class APIConfiguration extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.requestMatchers(m->m.antMatchers("/api/anonymous/**","/api/authn/**"));(1)
            //... (2)
        }
        ...
    1 rules within this configuration will apply to URIs below /api/anonymous and /api/authn
    2 http.build() is not called
This method returns void and the build() method of HttpSecurity should not be called.
  • In the Component-based approach, the configuration is performed in a @Bean method that will directly return the SecurityFilterChain It has the same HttpSecurity object injected, but note that build() is called within this method to return a SecurityFilterChain.

    Non-deprated HttpSecurity Configuration Alternative
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.requestMatchers(m->m.antMatchers("/api/anonymous/**","/api/authn/**"));(1)
        //...
        return http.build(); (2)
    }
    1 rules within this configuration will apply to URIs below /api/anonymous and /api/authn
    2 http.build() is required for this @Bean factory
This method returns the SecurityFilterChain result of calling the build() method of HttpSecurity. This is different from the deprecated approach.

2.5. HttpSecurity Builder Methods

The HttpSecurity object is "builder-based" and has several options on how it can be called.

  • http.methodReturningBuilder().configureBuilder()

  • http.methodPassingBuilderToLambda(builder→builder.configureBuilder())

The builders are also designed to be chained. It is quite common to see the following syntax used.

Chained Builder Calls
http.authorizeRequests()
        .anyRequest()
        .authenticated()
    .and().formLogin()
    .and().httpBasic();

We can simply make separate calls. As much as I like chained builders — I am not a fan of that specific syntax when starting out. Especially if we are experimenting and commenting/uncommenting configuration statements. You will see me using separate calls with the pass-the-builder and configure with a lambda style. Either style functionally works the same.

Separate Builder Calls with Lambdas
http.authorizeRequests(cfg->cfg.anyRequest().authenticated());
http.formLogin();
http.httpBasic();

2.6. Match Requests

We first want to scope our HttpSecurity configuration commands using requestMatchers() (or one of its other variants). The configurations specified here will only be applied to URIs matching the supplied matchers. There are Ant and Regular Expression matchers available. The default is to match all URIs.

http.requestMatchers(m->m.antMatchers("/api/anonymous/**","/api/authn/**"));

Notice the requestMatchers are a primary item in the individual chains and the rest of the configuration is impacting the filters within that chain.

security authn reqmatcher
Figure 2. Request Matchers
Notice also that our initial SecurityFilterChain is within the other chains in the example and is high in priority because of our @Order value assignment:

2.7. Authorize Requests

Next I am showing the authentication requirements of the SecurityFilterChain. Calls to the /api/anonymous URIs do not require authentication. Calls to the /api/authn URIs do require authentication.

Defining Authentication Requirements
http.authorizeRequests(cfg->cfg.antMatchers("/api/anonymous/**").permitAll());
http.authorizeRequests(cfg->cfg.anyRequest().authenticated());

The permissions off the matcher include:

  • permitAll() - no constraints

  • denyAll() - nothing will be allowed

  • authenticated() - only authenticated callers may invoke these URIs

  • role restrictions that we won’t be covering just yet

You can also make your matcher criteria method-specific by adding in a HttpMethod specification.

Matchers Also Supports HttpMethod Criteria
import org.springframework.http.HttpMethod;
...
...(cfg->cfg.antMatchers(HttpMethod.GET, "/api/anonymous/**")

2.8. Authentication

In this part of the example, I am enabling BASIC Auth and eliminating FORM-based authentication. For demonstration only — I am providing a custom name for the realm name returned to browsers.

http.httpBasic(cfg->cfg.realmName("AuthConfigExample")); (1)
http.formLogin(cfg->cfg.disable());
Realm name is not a requirement to activate Basic Authentication. It is shown here solely as an example of something easily configured.
Example Realm Header used in Authentication Required Response
< HTTP/1.1 401
< WWW-Authenticate: Basic realm="AuthConfigExample" (1)
1 Realm Name returned in HTTP responses requiring authentication

2.9. Header Configuration

In this portion of the example, I am turning off two of the headers that were part of the default set: XSS protection and frame options. There seemed to be some debate on the value of the XSS header [2] and we have no concern about frame restrictions. By disabling them — I am providing an example of what can be changed.

CSRF protections have also been disabled to make non-safe methods more sane to execute at this time. Otherwise we would be required to supply a value in a POST that came from a previous GET (all maintained and enforced by optional filters).

Header Configuration
http.headers(cfg->{
    cfg.xssProtection().disable();
    cfg.frameOptions().disable();
});
http.csrf(cfg->cfg.disable());

2.10. Stateless Session Configuration

I have no interest in using the Http Session to maintain identity between calls — so this should eliminate the SET-COOKIE commands for the JSESSIONID.

Stateless Session Configuration
http.sessionManagement(cfg->
    cfg.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

3. Configuration Results

With the above configurations in place — we can demonstrate the desired functionality and trace the calls through the filter chain if there is an issue.

3.1. Successful Anonymous Call

The following shows a successful anonymous call and the returned headers. Remember that we have gotten rid of several unwanted features with their headers. The controller method has been modified to return the identity of the authenticated caller. We will take a look at that later — but know the source of the additional :caller= string was added for this wave of examples.

Successful Anonymous Call
$ curl -v -X GET http://localhost:8080/api/anonymous/hello?name=jim
> GET /api/anonymous/hello?name=jim HTTP/1.1
< HTTP/1.1 200
< X-Content-Type-Options: nosniff
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 25
< Date: Fri, 03 Jul 2020 22:11:11 GMT
<
hello, jim :caller=(null) (1)
1 we have no authenticated user

3.2. Successful Authenticated Call

The following shows a successful authenticated call and the returned headers.

Successful Authenticated Call
$ curl -v -X GET http://localhost:8080/api/authn/hello?name=jim -u user:password (1)
> GET /api/authn/hello?name=jim HTTP/1.1
> Authorization: BASIC dXNlcjpwYXNzd29yZA==
< HTTP/1.1 200
< X-Content-Type-Options: nosniff
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 23
< Date: Fri, 03 Jul 2020 22:12:34 GMT
<
hello, jim :caller=user (2)
1 example application configured with username/password of user/password
2 we have an authenticated user

3.3. Rejected Unauthenticated Call Attempt

The following shows a rejection of an anonymous caller attempting to invoke a URI requiring an authenticated user.

Rejected Unauthenticated Call Attempt
$ curl -v -X GET http://localhost:8080/api/authn/hello?name=jim (1)
> GET /api/authn/hello?name=jim HTTP/1.1
< HTTP/1.1 401
< WWW-Authenticate: Basic realm="AuthConfigExample"
< X-Content-Type-Options: nosniff
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Fri, 03 Jul 2020 22:14:20 GMT
<
{"timestamp":"2020-07-03T22:14:20.816+00:00","status":401,
"error":"Unauthorized","message":"Unauthorized","path":"/api/authn/hello"}
1 attempt to make anonymous call to authentication-required URI

4. Authenticated User

Authenticating the identity of the caller is a big win. We likely will want their identity at some point during the call.

4.1. Inject UserDetails into Call

One option is to inject the UserDetails containing the username (and authorities) for the caller. Methods that can be called without authentication will receive the UserDetails if the caller provides credentials but must protect itself against a null value if actually called anonymously.

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
...
public String getHello(@RequestParam(name = "name", defaultValue = "you") String name,
                       @AuthenticationPrincipal UserDetails user) {
    return "hello, " + name + " :caller=" + (user==null ? "(null)" : user.getUsername());
}

4.2. Obtain SecurityContext from Holder

The other option is to lookup the UserDetails through the SecurityContext stored within the SecurityContextHolder class. This allows any caller in the call flow to obtain the identity of the caller at any time.

import org.springframework.security.core.context.SecurityContextHolder;

public String getHelloAlt(@RequestParam(name = "name", defaultValue = "you") String name) {
    UserDetails user = (UserDetails) SecurityContextHolder
                            .getContext().getAuthentication().getPrincipal();
    return "hello, " + name + " :caller=" + user.getUsername();
}

5. Swagger BASIC Auth Configuration

Once we enabled default security on our application — we lost the ability to access the Swagger page without logging in. We did not have to create a separate SecurityFilterChain for just the Swagger endpoints — but doing so provides some nice modularity and excuse to further demonstrate Spring security configurability.

I have added a separate security configuration for the OpenAPI and Swagger endpoints.

5.1. Swagger Authentication Configuration

The following configuration allows the OpenAPI and Swagger endpoints to be accessed anonymously and handle authentication within OpenAPI/Swagger.

  • Swagger SecurityFilterChain using the WebSecurityConfigurerAdapter approach

    @Configuration(proxyBeanMethods = false)
    @Order(100) (1)
    public class SwaggerSecurity extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.requestMatchers(cfg->cfg
                    .antMatchers("/swagger-ui*", "/swagger-ui/**", "/v3/api-docs/**"));
            http.authorizeRequests(cfg->cfg.anyRequest().permitAll());
            http.csrf().disable();
        }
    }
    1 Priority (100) is after core application (0) and prior to default rules (1000)
  • Swagger SecurityFilterChain using the Component-based approach

    @Bean
    @Order(100) (1)
    public SecurityFilterChain swaggerSecurityFilterChain(HttpSecurity http) throws Exception {
        http.requestMatchers(cfg->cfg
                .antMatchers("/swagger-ui*", "/swagger-ui/**", "/v3/api-docs/**"));
        http.authorizeRequests(cfg->cfg.anyRequest().permitAll());
        http.csrf().disable();
        return http.build();
    }
    1 Priority (100) is after core application (0) and prior to default rules (1000)

5.2. Swagger Security Scheme

In order for Swagger to supply a username:password using BASIC Auth, we need to define a SecurityScheme for Swagger to use. The following bean defines the core object the methods will be referencing.

Swagger BASIC Auth Security Scheme
package info.ejava.examples.svc.authn;

import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springframework.context.annotation.Bean;
...

@Bean
public OpenAPI customOpenAPI() {
    return new OpenAPI()
            .components(new Components()
                    .addSecuritySchemes("basicAuth",
                            new SecurityScheme()
                                    .type(SecurityScheme.Type.HTTP)
                                    .scheme("basic")));
}

The @Operation annotations can now reference the SecuritySchema to inform the SwaggerUI that BASIC Auth can be used against that specific operation. Notice too that we needed to make the injected UserDetails optional — or even better — hidden from OpenAPI/Swagger since it is not part of the HTTP request.

Swagger Operation BASIC Auth Definition
package info.ejava.examples.svc.authn.authcfg.controllers;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;

@RestController
public class HelloController {
...
    @Operation(description = "sample authenticated GET",
                  security = @SecurityRequirement(name="basicAuth")) (1)
    @RequestMapping(path="/api/authn/hello",
            method= RequestMethod.GET)
    public String getHelloAuthn(@RequestParam(name = "name", defaultValue = "you") String name,
                                @Parameter(hidden = true) (2)
                                @AuthenticationPrincipal UserDetails user) {
        return "hello, " + name + " :caller=" + user.getUsername();
    }
1 added @SecurityRequirement to operation to express within OpenAPI that this call accepts Basic Auth
2 Identified parameter as not applicable to HTTP callers

With the @SecurityRequirement in place, the Swagger UI provides a means to supply username/password for subsequent calls.

security swagger authn
Figure 3. Swagger with BASIC Auth Configured

When making a call — Swagger UI adds the Authorization header with the previously entered credentials.

security swagger authn get
Figure 4. Swagger BASIC Auth Call

6. CORS

There is one more important security filter to add to our list before we end and it is complex enough to deserve its own section - Cross Origin Resource Sharing (CORS). Without support for CORS, javascript loaded by browsers will not be able to call the API unless it was loaded from the same base URL as the API. That even includes local development (i.e., javascript loaded from file system cannot invoke http://localhost:8080). In today’s modern web environments — it is common to deploy services independent of Javascript-based UI applications or to have the UI applications calling multiple services with different base URLs.

6.1. Default CORS Support

The following example shows the default CORS configuration for Spring Boot/Web MVC. The server is ignoring the Origin header supplied by the client and does not return any CORS-related authorization for the browser to use the response payload.

CORS Inactive, Origin Header Ignored
$ curl -v http://localhost:8080/api/anonymous/hello?name=jim
> GET /api/anonymous/hello?name=jim HTTP/1.1
> Host: localhost:8080
>
< HTTP/1.1 200
hello, jim :caller=(null)

$ curl -v http://localhost:8080/api/anonymous/hello?name=jim -H "Origin: http://127.0.0.1:8080"
> GET /api/anonymous/hello?name=jim HTTP/1.1
> Host: localhost:8080
> Origin: http://127.0.0.1:8080
>
< HTTP/1.1 200
hello, jim :caller=(null)

The lack of headers does not matter for curl, but the CORS response does get evaluated when executed within a browser.

6.2. Browser and CORS Response

6.2.1. Same Origin/Target Host

The following is an example of Javascript loaded from http://localhost:8080 and calling http://localhost:8080. No Origin header is passed by the browser because it knows the Javascript was loaded from the same source it is calling.

CORS Inactive, Origin Header Not Supplied for Same Source

security cors browser approved

6.2.2. Different Origin/Target Host

However, if we load the Javascript from an alternate source, the browser will fail to process the results. The following is an example of some Javascript loaded from http://127.0.0.1:8080 and calling http://localhost:8080.

CORS Inactive, Origin Header Supplied for Different Source

security cors inactive rejected

6.3. Enabling CORS

To globally enable CORS support, we can invoke http.cors(config-lambda) with a lamda function that will provide a configuration based on a given HttpServletRequest. This is being supplied when configuring the SecurityFilterChain.

Enabling CORS and Permit All
http.cors(cfg->cfg.configurationSource(corsPermitAllConfigurationSource()));
Example CORS Permit All Lambda Method Response
private CorsConfigurationSource corsPermitAllConfigurationSource() {
    return (request) -> {
        CorsConfiguration config = new CorsConfiguration();
        config.applyPermitDefaultValues();
        return config;
    };
}
CORS Evaluation Interface Implemented
public interface CorsConfigurationSource {
    CorsConfiguration getCorsConfiguration(HttpServletRequest request);
}

6.3.1. CORS Headers

With CORS enabled and permitting all, we see some new VARY headers, but that won’t be enough. The browser will be looking for the Access-Control-Allow-Origin header being returned with a value matching the Origin header passed in (* being a wildcard match).

CORS Approval Headers Returned in call cases
$ curl -v http://localhost:8080/api/anonymous/hello?name=jim
> GET /api/anonymous/hello?name=jim HTTP/1.1
> Host: localhost:8080
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200
< Vary: Origin
< Vary: Access-Control-Request-Method
< Vary: Access-Control-Request-Headers
hello, jim :caller=(null)

$ curl -v http://localhost:8080/api/anonymous/hello?name=jim -H "Origin: http://127.0.0.1:8080"
> GET /api/anonymous/hello?name=jim HTTP/1.1
> Host: localhost:8080
> Origin: http://127.0.0.1:8080
>
< HTTP/1.1 200
< Vary: Origin
< Vary: Access-Control-Request-Method
< Vary: Access-Control-Request-Headers
< Access-Control-Allow-Origin: * (1)
hello, jim :caller=(null)
1 Access-Control-Allow-Origin denotes approval for the given (* = wildcard) Origin

6.3.2. Browser Accepts Access-Control-Allow-Origin Header

Browser Accepts CORS Access-Control-Allow-Origin Response

security cors allactive accepted

6.4. Constrained CORS

We can define more limited rules for CORS acceptance by using additional commands of the CorsConfiguration object.

Limiting CORS Acceptance
private CorsConfigurationSource corsLimitedConfigurationSource() {
    return (request) -> {
            CorsConfiguration config = new CorsConfiguration();
            config.addAllowedOrigin("http://localhost:8080");
            config.setAllowedMethods(List.of("GET","POST"));
            return config;
    };
}

6.5. CORS Server Acceptance

In this example, I have loaded the Javascript from http://127.0.0.1:8080 and making a call to http://localhost:8080 in order to match the configured Origin matching rules. The server is return a 200/OK along with a Access-Control-Allow-Origin value that matches the specific Origin provided.

CORS Acceptance Response
$ curl -v http://127.0.0.1:8080/api/anonymous/hello?name=jim -H "Origin: http://localhost:8080"
*   Trying 127.0.0.1:8080...
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> GET /api/anonymous/hello?name=jim HTTP/1.1
> Host: 127.0.0.1:8080 (1)
> Origin: http://localhost:8080 (2)
>
< HTTP/1.1 200
< Vary: Origin
< Vary: Access-Control-Request-Method
< Vary: Access-Control-Request-Headers
< Access-Control-Allow-Origin: http://localhost:8080 (2)
hello, jim :caller=(null)
1 Example Host and Origin have been flipped to match approved localhost:8080 Origin
2 Access-Control-Allow-Origin denotes approval for the given Origin

6.6. CORS Server Rejection

This additional definition is enough to produce a 403/FORBIDDEN from the server versus a rejection from the browser.

CORS Rejection Response
$ curl -v http://localhost:8080/api/anonymous/hello?name=jim -H "Origin: http://127.0.0.1:8080"
> GET /api/anonymous/hello?name=jim HTTP/1.1
> Host: localhost:8080
> Origin: http://127.0.0.1:8080
>
< HTTP/1.1 403
< Vary: Origin
< Vary: Access-Control-Request-Method
< Vary: Access-Control-Request-Headers
Invalid CORS request

6.7. Spring MVC @CrossOrigin Annotation

Spring also offers an annotation-based way to enable the CORS protocol. In the example below, @CrossOrigin annotation has been added to the controller class or individual operations indicating CORS constraints.

This technique is static.

Spring MVC @CrossOrigin Annotation
...
import org.springframework.web.bind.annotation.CrossOrigin;
...
@CrossOrigin (1)
@RestController
public class HelloController {
1 defaults to all origins, etc.

7. RestTemplate Authentication

Now that we have locked down our endpoints — requiring authentication — I want to briefly show how we can authenticate with RestTemplate using an existing BASIC Authentication filter. I am going to delay demonstrating WebClient to limit the dependencies on the current example application — but we will do so in a similar way that does not change the interface to the caller.

RestTemplate Anonymous Client
@Bean
public RestTemplate anonymousUser(RestTemplateBuilder builder) {
    RestTemplate restTemplate = builder.requestFactory(
        //used to read the streams twice -- so we can use the logging filter below
        ()->new BufferingClientHttpRequestFactory(
                            new SimpleClientHttpRequestFactory()))
        .interceptors(new RestTemplateLoggingFilter())
        .build(); (1)
    return restTemplate;
}
1 vanilla RestTemplate with our debug log interceptor
RestTemplate Authenticating Client
@Bean
public RestTemplate authnUser(RestTemplateBuilder builder) {
    RestTemplate restTemplate = builder.requestFactory(
        //used to read the streams twice -- so we can use the logging filter below
        ()->new BufferingClientHttpRequestFactory(
                            new SimpleClientHttpRequestFactory()))
        .interceptors(
                new BasicAuthenticationInterceptor("user", "password"),(1)
                new RestTemplateLoggingFilter())
        .build();
    return restTemplate;
}
1 added BASIC Auth filter to add Authorization Header

7.1. Authentication Integration Tests with RestTemplate

The following shows the different RestTemplate instances being injected that have different credentials assigned. The different attribute names, matching the @Bean factory names act as a qualifier to supply the right instance of RestTemplate.

@SpringBootTest(classes= ClientTestConfiguration.class,
        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
        properties = "test=true") (1)
public class AuthnRestTemplateNTest {
    @Autowired
    private RestTemplate anonymousUser;
    @Autowired
    private RestTemplate authnUser;
1 test property triggers Swagger @Configuration and anything else not suitable during testing to disable

8. Mock MVC Authentication

There are many test frameworks within Spring and Spring Boot that I did not cover them all earlier. I limited them because covering them all early on added limited value with a lot of volume. However, I do want to show you a small example of MockMvc and how it too can be configured for authentication. The following example shows a

  • normal injection of the mock that will be an anonymous user

  • how to associate a mock to the security context

MockMvc Authentication Setup
@SpringBootTest(
        properties = "test=true")
@AutoConfigureMockMvc
public class AuthConfigMockMvcNTest {
    @Autowired
    private WebApplicationContext context;
    @Autowired
    private MockMvc anonymous;
    //example manual instantiation (1)
    private MockMvc user;
    private final String uri = "/api/anonymous/hello";

    @BeforeEach
    public void init() {
        user = MockMvcBuilders
            .webAppContextSetup(context)
            .apply(SecurityMockMvcConfigurers.springSecurity())
            .build();
    }
1 there is no functional difference between the injected or manually instantiated MockMvc the way it is performed here

8.1. MockMvc Anonymous Call

The first test is a baseline example showing a call thru the mock to a service that allows all callers and no required authentication.

MockMvc Anonymous Call
@Test
public void anonymous_can_call_get() throws Exception {
    anonymous.perform(MockMvcRequestBuilders.get(uri).queryParam("name","jim"))
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(content().string("hello, jim :caller=(null)"));
}

8.2. MockMvc Authenticated Call

The next example shows how we can inject an identity into the mock for use during the test method.

MockMvc Authenticated Call
@WithMockUser("user")
@Test
public void user_can_call_get() throws Exception {
    user.perform(MockMvcRequestBuilders.get(uri)
            .queryParam("name","jim"))
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(content().string("hello, jim :caller=user"));
}

Although I believe RestTemplate tests are pretty good at testing client access — the WebMvc framework was a very convenient to quickly verify and identify issues with the SecurityFilterChain definitions.

9. Summary

In this module we learned:

  • how to configure a SecurityFilterChain

  • how to define no security filters for static resources

  • how to customize the SecurityFilterChain for API endpoints

  • how to expose endpoints that can be called from anonymous users

  • how to require authenticated users for certain endpoints

  • how to CORS-enable the API

  • how to define BASIC Auth for OpenAPI and for use by Swagger


1. "Spring Security without the WebSecurityConfigurerAdapter", Spring.io, Feb 21, 2022
2. "X-XSS-Protection",MDN Web Docs