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:
-
create multiple, custom authentication filter chains
-
enable open access to static resources
-
enable anonymous access to certain URIs
-
enforce authenticated access to certain URIs
-
locate the current authenticated user identity
-
enable Cross-Origin Resource Sharing (CORS) exchanges with browsers
-
add an authenticated identity to RestTemplate client
-
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-NWebSecurityConfigurers
and each can define aSecurityFilterChain
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.
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.
@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.
@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 theconfigure(WebSecurity)
method.Ignore Static Content Configuration - WebSecurityConfigurerAdapter approachimport 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 theWebSecurity
object.Ignore Static Content Configuration - Component-based approachimport org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; @Bean public WebSecurityCustomizer apiStaticResources() { return (web)->web.ignoring().antMatchers("/content/**"); }
WebSecurityCustomers Functional Interfacepublic 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.
$ 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.
$ 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 theconfigure(HttpSecurity)
method.SecurityFilterChain Matcher - WebSecurityConfigurerAdapter approachimport 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 theSecurityFilterChain
It has the sameHttpSecurity
object injected, but note thatbuild()
is called within this method to return aSecurityFilterChain
.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.
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.
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.
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.
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.
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. |
< 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).
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
.
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.
$ 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.
$ 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.
$ 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.
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.
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 |
Figure 3. Swagger with BASIC Auth Configured
|
When making a call — Swagger UI adds the Authorization header with the previously entered credentials. |
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.
$ 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.
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
.
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
.
http.cors(cfg->cfg.configurationSource(corsPermitAllConfigurationSource()));
private CorsConfigurationSource corsPermitAllConfigurationSource() {
return (request) -> {
CorsConfiguration config = new CorsConfiguration();
config.applyPermitDefaultValues();
return config;
};
}
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).
$ 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
6.4. Constrained CORS
We can define more limited rules for CORS acceptance by using additional commands of the CorsConfiguration
object.
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.
$ 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.
$ 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.
...
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.
@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 |
@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
@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.
@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.
@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