Multiple Spring WebSecurityConfigurerAdapter - java

So I have multiple Controller classes exposing a number of endpoints in my service. One of these controllers is exposing endpoints used for webhooks. I want to expose all the endpoints in the webhooks Controller class and secure them with one WebSecurityConfigurerAdapter configuration and then all the other endpoints which stretch across a few other Controller classes be configured with a separate WebSecurityConfigurerAdapter configuration. The webhooks will be accessed by third party vendors and want them using a separate auth0 audience from the other endpoints which will be accessed internally. I have it setup now as follows but it doesn't seem to be working. In order for the two configurations to co-exist they need to have an #Order annotation assigned to their configure methods and these int values must be unique. But what is happening is all requests coming in seem to go to the Order(1) configuration first but are never making it to Order(2) if they are of the pattern described in my Order(2) config. Maybe that's not how it's supposed to work or maybe my implementation is incorrect. But when I pass in a request in a "/webhooks/" endpoint it always gives me a 401 error and then when I change that configuration to Order(1) it starts to work. And this behavior happens the other way as well. When I make the "/webhooks/" patter the first order I get a 401 for all the other requests because they are not making it to the next Order. Here is my code...
#EnableWebSecurity
public class SecurityConfig {
#Configuration
public static class ApiSecurityConfig extends WebSecurityConfigurerAdapter {
#Value("${auth0.api-audience}")
private String audience;
#Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
private String issuer;
#Override
#Order(1)
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// in dev if you want to bypass auth you can change /ping to /*
.mvcMatchers(HttpMethod.GET, "/actuator/**", "/test").permitAll()
.mvcMatchers("/api/**")
.authenticated()
.and()
.cors()
.configurationSource(corsConfigurationSource())
.and()
.oauth2ResourceServer()
.jwt()
.decoder(jwtDecoder(audience, issuer));
// disable cors and csrf when running locally
if (getApplicationContext().getEnvironment().acceptsProfiles(Profiles.of("local"))) {
http.cors().and().csrf().disable();
}
}
static JwtDecoder jwtDecoder(String audience, String issuer) {
OAuth2TokenValidator<Jwt> withAudience = new AudienceValidator(audience);
OAuth2TokenValidator<Jwt> withBrand = new BrandValidator(audience);
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuer);
OAuth2TokenValidator<Jwt> validator = new DelegatingOAuth2TokenValidator<>(withAudience, withBrand, withIssuer);
NimbusJwtDecoder jwtDecoder = JwtDecoders.fromOidcIssuerLocation(issuer);
jwtDecoder.setJwtValidator(validator);
return jwtDecoder;
}
}
#Configuration
#Order(2)
public static class WebhookSecurityConfig extends WebSecurityConfigurerAdapter {
#Value("${auth0.webhook-audience}")
private String audience;
#Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
private String issuer;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.mvcMatchers("/webhooks/**")
.authenticated()
.and()
.cors()
.configurationSource(corsConfigurationSource())
.and()
.oauth2ResourceServer()
.jwt()
.decoder(jwtDecoder(audience, issuer));
// disable cors and csrf when running locally
if (getApplicationContext().getEnvironment().acceptsProfiles(Profiles.of("local"))) {
http.cors().and().csrf().disable();
}
}
JwtDecoder jwtDecoder(String audience, String issuer) {
OAuth2TokenValidator<Jwt> withAudience = new AudienceValidator(audience);
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuer);
OAuth2TokenValidator<Jwt> validator = new DelegatingOAuth2TokenValidator<>(withAudience, withIssuer);
NimbusJwtDecoder jwtDecoder = JwtDecoders.fromOidcIssuerLocation(issuer);
jwtDecoder.setJwtValidator(validator);
return jwtDecoder;
}
}
static CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedMethods(List.of(
HttpMethod.GET.name(),
HttpMethod.PUT.name(),
HttpMethod.POST.name(),
HttpMethod.DELETE.name()
));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration.applyPermitDefaultValues());
return source;
}
}
In addition to the behavior I am seeing I am also seeing on startup of the Spring application...
2022-04-18 16:42:00,616 INFO org.springframework.security.web.DefaultSecurityFilterChain : Will not secure any request
2022-04-18 16:42:00,618 INFO org.springframework.security.web.DefaultSecurityFilterChain : Will not secure any request

Related

#PreAuthorize not working after upgrading to Spring 3.0

Upgraded our entire microservice to the latest Spring 3.0 release, updated our JWT and FilterChain implementations to the new syntax (e.g removing WebSecurityConfigurerAdapter). Most of our endpoints continued to work, but notably every endpoint containing a #PreAuthorize annotation are no longer working.
One endpoint we have contains the following expression, and it has failed:
#PreAuthorize(
"""
hasAuthority('COGNITO_ADMIN')
or ( hasAuthority('COGNITO_PMO')
and #authorizationService.notBlacklistedFromClientBySalesforceId(#baseQuestionnaireCreateRequestV1.salesforceClientId)
)
or ( hasAuthority('COGNITO_RESEARCHER')
and #authorizationService.notBlacklistedFromClientBySalesforceId(#baseQuestionnaireCreateRequestV1.salesforceClientId)
)
""")
However, I have reduced this down to just this, and it will fail as well.
#PreAuthorize("hasAuthority('COGNITO_ADMIN')")
Each time this will fail with a AuthenticationCredentialsNotFoundException, as it appears the default AuthorizationManagerBeforeMethodInterceptor pulls out a null context here:
private Supplier<Authentication> getAuthentication(SecurityContextHolderStrategy strategy) {
return () -> {
Authentication authentication = strategy.getContext().getAuthentication();
if (authentication == null) {
throw new AuthenticationCredentialsNotFoundException("An Authentication object was not found in the SecurityContext");
} else {
return authentication;
}
};
}
At a loss as to why this is failing only for #PreAuthorize annotated endpoints. All other endpoints authorize correctly and authentication is set correctly.
Current Implementations
AuthorizationService
#Aspect
#Component
#RequiredArgsConstructor
public class AuthorizationService {
public boolean notBlacklistedFromClientBySalesforceId(String salesForceId) {
var client = clientService.getClientBySalesforceId(salesForceId);
return isUserBlacklistedForClient(client.getId());
}
...
}
SecurityConfig
#EnableWebSecurity
#Configuration
#ConfigurationProperties(prefix = "security")
#Setter
#Validated
public class OAuth2ResourceServerSecurityConfiguration {
#NotNull private String jwkSetUri;
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
String cognitoAdmin = ADMIN.toCognitoGroup();
String cognitoPmo = PMO.toCognitoGroup();
String cognitoResearcher = RESEARCHER.toCognitoGroup();
String cognitoContractor = CONTRACTOR.toCognitoGroup();
http.cors()
.and()
.authorizeHttpRequests()
// Restrict client restriction endpoints to admins/pmo only
.requestMatchers("/clientBlacklist", "/clientBlacklist/*")
.hasAnyAuthority(cognitoAdmin, cognitoPmo)
// Restrict tag POST, PUT, DELETE endpoints to admins only
.requestMatchers(HttpMethod.GET, "/tags")
.hasAnyAuthority(cognitoAdmin, cognitoPmo, cognitoResearcher, cognitoContractor)
.requestMatchers(HttpMethod.POST, "/tags")
.hasAnyAuthority(cognitoAdmin)
.requestMatchers(HttpMethod.PUT, "/tags/*")
.hasAnyAuthority(cognitoAdmin)
.requestMatchers(HttpMethod.DELETE, "/tags/*")
.hasAnyAuthority(cognitoAdmin)
// Restrict questionnaireWhitelist endpoint to admin, pmo, and researcher only
.requestMatchers("/questionnaireWhitelist", "/questionnaireWhitelist/*")
.hasAnyAuthority(cognitoAdmin, cognitoPmo, cognitoResearcher)
// Restrict creating/deleting questionnaires to admin, pmo, and researcher only
.requestMatchers(HttpMethod.POST, "/questionnaires")
.hasAnyAuthority(cognitoAdmin, cognitoPmo, cognitoResearcher)
.requestMatchers(HttpMethod.DELETE, "/questionnaires/*")
.hasAnyAuthority(cognitoAdmin, cognitoPmo, cognitoResearcher)
// Restrict updating/deleting templates to admins/pmo only
.requestMatchers(HttpMethod.PUT, "/templates/*")
.hasAnyAuthority(cognitoAdmin)
.requestMatchers(HttpMethod.DELETE, "/templates/*")
.hasAnyAuthority(cognitoAdmin, cognitoPmo)
// Restrict clients to admin, pmo, and researcher only
.requestMatchers("/clients", "/clients/*")
.hasAnyAuthority(cognitoAdmin, cognitoPmo, cognitoResearcher)
// Allows only ADMIN, PMO, CONTRACTOR, AND RESEARCHER roles to hit endpoints (restricts
// UNASSIGNED)
.anyRequest()
.hasAnyAuthority(cognitoAdmin, cognitoPmo, cognitoResearcher, CONTRACTOR.toCognitoGroup())
.and()
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(new CognitoJwtAuthenticationConverter());
return http.build();
}
Application.java
#EnableCaching
#EnableMongoAuditing
#ConfigurationPropertiesScan
#EnableMethodSecurity
#SpringBootApplication
public class QnrBldrApplication {
public static void main(String[] args) {
SpringApplication.run(QnrBldrApplication.class, args);
}
}

Spring Security: extract oidc role claims to spring authorities

I am trying to get role claims from an OAuth2AuthenticationToken to be detected as Spring Security authorities. There is a custom role defined on OIDC provider side (Azure AD in my case) that is nested inside the DefaultOidcUser, but not added automatically to the authorities:
I tried to extract them from the Jwt Token like this
However, when I do that, neither of the following methods is called (neither during login, nor later, even in the default configuration):
JwtGrantedAuthoritiesConverter.convert(Jwt)
JwtAuthenticationConverter.convert(Jwt)
JwtAuthenticationConverter.extractAuthorities(Jwt)
My current configuration is:
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
#Import(SecurityProblemSupport.class)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
// #formatter:off
http
.csrf()
<some more config that has nothing to do with oauth/oidc>
.and()
.oauth2Login()
.and()
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(jwtAuthenticationConverter())
.and()
.and()
.oauth2Client()
;
}
private JwtAuthenticationConverter jwtAuthenticationConverter() {
// create a custom JWT converter to map the roles from the token as granted authorities
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
}
I also tried with a
CustomJwtAuthConverter implements Converter<Jwt, AbstractAuthenticationToken>
but to no avail.
Any help would be appreciated.
Managed to achieve it using an authorities mapper that also extracts claims from the oidcToken:
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
[...]
#Bean
public GrantedAuthoritiesMapper userAuthoritiesMapper() {
return authorities -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
authorities.forEach(
authority -> {
// Check for OidcUserAuthority because Spring Security 5.2 returns
// each scope as a GrantedAuthority, which we don't care about.
if (authority instanceof OidcUserAuthority) {
OidcUserAuthority oidcUserAuthority = (OidcUserAuthority) authority;
mappedAuthorities.addAll(SecurityUtils.extractAuthorityFromClaims(oidcUserAuthority.getUserInfo().getClaims()));
mappedAuthorities.addAll(SecurityUtils.extractAuthorityFromClaims(oidcUserAuthority.getIdToken().getClaims()));
}
}
);
return mappedAuthorities;
};
}
}
and inside SecurityUtils:
public static List<GrantedAuthority> extractAuthorityFromClaims(Map<String, Object> claims) {
return mapRolesToGrantedAuthorities(getRolesFromClaims(claims));
}
private static List<GrantedAuthority> mapRolesToGrantedAuthorities(Collection<String> roles) {
return roles.stream().filter(role -> role.startsWith("ROLE_")).map(SimpleGrantedAuthority::new).collect(Collectors.toList());
}
Afterwards the custom role should be present in mappedAuthorities and with it in the authorities of the token. Thus making the annotations "hasRole" and "hasAuthority" possible to use.
This is the solution I found when using keycloak.
#EnableGlobalMethodSecurity(securedEnabled = true)
#EnableWebSecurity(debug = true)
public class SecurityConfiguration {
private static final String REALM = "realm_access";
private static final String ROLES = "roles";
/**
* Configuration For oauth
*/
#Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(source -> new CustomJwtConfigure().convert(source));
return http.build();
}
public static class CustomJwtConfigure implements Converter<Jwt, JwtAuthenticationToken> {
#Override
public JwtAuthenticationToken convert(Jwt jwt) {
var tokenAttributes = jwt.getClaims();
var jsonObject = (JSONObject) tokenAttributes.get(REALM);
var roles = (JSONArray) jsonObject.get(ROLES);
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
roles.forEach(role -> grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + role)));
return new JwtAuthenticationToken(jwt, grantedAuthorities);
}
}
}
Link to example https://github.com/kesaven8/resourceServer-spring-boot
2022 update
I maintain a set of tutorials and samples to configure resource-servers security for:
both servlet and reactive applications
decoding JWTs and introspecting access-tokens
default or custom Authentication implementations
any OIDC authorization-server(s), including Keycloak of course (most samples support multiple realms / identity-providers)
The repo also contains a set of libs published on maven-central to:
mock OAuth2 identities during unit and integration tests (with authorities and any OpenID claim, including private ones)
configure resource-servers from properties file (including source claims for roles, roles prefix and case processing, CORS configuration, session-management, public routes and more)
Sample for a servlet with JWT decoder
#EnableMethodSecurity(prePostEnabled = true)
#Configuration
public class SecurityConfig {}
com.c4-soft.springaddons.security.issuers[0].location=https://localhost:8443/realms/master
com.c4-soft.springaddons.security.issuers[0].authorities.claims=realm_access.roles,resource_access.spring-addons-public.roles,resource_access.spring-addons-confidential.roles
com.c4-soft.springaddons.security.cors[0].path=/sample/**
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-webmvc-jwt-resource-server</artifactId>
<version>6.0.3</version>
</dependency>
No, nothing more requried.
Unit-tests with mocked authentication
Secured #Component without http request (#Service, #Repository, etc.)
#Import({ SecurityConfig.class, SecretRepo.class })
#AutoConfigureAddonsSecurity
class SecretRepoTest {
// auto-wire tested component
#Autowired
SecretRepo secretRepo;
#Test
void whenNotAuthenticatedThenThrows() {
// call tested components methods directly (do not use MockMvc nor WebTestClient)
assertThrows(Exception.class, () -> secretRepo.findSecretByUsername("ch4mpy"));
}
#Test
#WithMockJwtAuth(claims = #OpenIdClaims(preferredUsername = "Tonton Pirate"))
void whenAuthenticatedAsSomeoneElseThenThrows() {
assertThrows(Exception.class, () -> secretRepo.findSecretByUsername("ch4mpy"));
}
#Test
#WithMockJwtAuth(claims = #OpenIdClaims(preferredUsername = "ch4mpy"))
void whenAuthenticatedWithSameUsernameThenReturns() {
assertEquals("Don't ever tell it", secretRepo.findSecretByUsername("ch4mpy"));
}
}
Secured #Controller (sample for #WebMvcTest but works for #WebfluxTest too)
#WebMvcTest(GreetingController.class) // Use WebFluxTest or WebMvcTest
#AutoConfigureAddonsWebSecurity // If your web-security depends on it, setup spring-addons security
#Import({ SecurityConfig.class }) // Import your web-security configuration
class GreetingControllerAnnotatedTest {
// Mock controller injected dependencies
#MockBean
private MessageService messageService;
#Autowired
MockMvcSupport api;
#BeforeEach
public void setUp() {
when(messageService.greet(any())).thenAnswer(invocation -> {
final JwtAuthenticationToken auth = invocation.getArgument(0, JwtAuthenticationToken.class);
return String.format("Hello %s! You are granted with %s.", auth.getName(), auth.getAuthorities());
});
when(messageService.getSecret()).thenReturn("Secret message");
}
#Test
void greetWitoutAuthentication() throws Exception {
api.get("/greet").andExpect(status().isUnauthorized());
}
#Test
#WithMockAuthentication(authType = JwtAuthenticationToken.class, principalType = Jwt.class, authorities = "ROLE_AUTHORIZED_PERSONNEL")
void greetWithDefaultMockAuthentication() throws Exception {
api.get("/greet").andExpect(content().string("Hello user! You are granted with [ROLE_AUTHORIZED_PERSONNEL]."));
}
}
Advanced use-cases
The most advanced tutorial demoes how to define a custom Authentication implementation to parse (and expose to java code) any private claim into things that are security related but not roles (in the sample it's grant delegation between users).
It also shows how to extend spring-security SpEL to build a DSL like:
#GetMapping("greet/on-behalf-of/{username}")
#PreAuthorize("is(#username) or isNice() or onBehalfOf(#username).can('greet')")
public String getGreetingFor(#PathVariable("username") String username) {
return ...;
}

No 'Access-Control-Allow-Origin' header is present on the requested resource in angular 9 and spring boot 2

I added spring security to the spring boot application and I have some api end points that needs to be called no matter user login or not.(I mean these are the rest end points where I need to retrieve data in my front side angular).
So,I config it as:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true, proxyTargetClass = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsService customUserDetailsService;
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(customUserDetailsService)
.passwordEncoder(passwordEncoder());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().
disable()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/**")
.permitAll()
.antMatchers("/books").permitAll()
.antMatchers("/api/v1/search/**").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest()
.authenticated()
.and()
.httpBasic();
}
}
I have all the api exposed from : http://localhost:8080/api/v1/ like:
http://localhost:8080/api/v1/books
http://localhost:8080/api/v1/bookcategory
I have configured using .antMatchers("/api/v1/search/**"),and my config for restendpoint is:
#RequestMapping("/api/v1")
#RestController
#CrossOrigin(origins ="http://localhost:4200")
public class BasicAuthController {
#GetMapping(path = "/basicauth")
public AuthenticationBean basicauth() {
System.out.println("hitted here");
return new AuthenticationBean("You are authenticated");
}
}
I allowed the csfr policy using:
#Configuration
public class RepositoryConfig implements RepositoryRestConfigurer{
#Autowired
private EntityManager entityManager;
#Override
public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
config.exposeIdsFor(entityManager.getMetamodel().getEntities().stream()
.map(Type::getJavaType).toArray(Class[]::new));
//to handle cross origin
config.getCorsRegistry().addMapping("/**").allowedOrigins("http://localhost:4200");
}
}
BookRepository.java
public interface BookRepository extends JpaRepository<Book,Long> {
#RestResource(path = "categoryid")
Page<Book> findByCategoryId(#Param("id") Long id,Pageable pageable);
//to get book by searching
#RestResource(path = "searchbykeyword")
Page<Book> findByNameContaining(#Param("xyz") String keyword,Pageable pageable);
}
front side I have angular 9 as:
auth.service.ts
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { map } from 'rxjs/operators';
#Injectable({
providedIn: 'root'
})
export class AuthService {
// BASE_PATH: 'http://localhost:8080'
USER_NAME_SESSION_ATTRIBUTE_NAME = 'authenticatedUser';
public username: String;
public password: String;
constructor(private http: HttpClient) {
}
authenticationService(username: String, password: String) {
return this.http.get(`http://localhost:8080/api/v1/basicauth`,
{ headers: { authorization: this.createBasicAuthToken(username, password) } }).pipe(map((res) => {
this.username = username;
this.password = password;
this.registerSuccessfulLogin(username, password);
}));
}
createBasicAuthToken(username: String, password: String) {
return 'Basic ' + window.btoa(username + ":" + password)
}
}
//i didnot pasted all the codes.
So,I get error as when I goto link http://localhost:4200/books:
I have some projects using Angular+SpringBoot with security and I create a specific Bean to handle with CORS and I never have problem. If you can try, add this method bellow in your WebSecurityConfig class:
#Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:4200", "http://localhost:8080"));
configuration.setAllowedMethods(Arrays.asList("GET", "PUT", "POST","OPTIONS", "DELETE"));
configuration.setAllowedHeaders(Arrays.asList("authorization","content-type"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
The problem is CORS which is a security feature of your browser. It ensures that only resources form the same domain (and port!) can be accessed. Your Angular development server and the Tomcat run on a different port which causes the request to be declined. You have to configure CORS. However, you should know what you are doing because you are basically disabling a security feature. Usually it is not a problem tho. You can do this by adding the annotation #CrossOrigin to your controller methods or by using the Java configuration. For the second cause, I'm sure you'll easily find it on Google :)
CORS (Cross-Origin Resource Sharing) is a security feature of your browser that prevent authorized sites from using Resources from another origin
in nutshell it happens if your site on x:y origin and requesting resources from a:y, x:b or a:b origins (different port and/or domain)
what exactly happens in nutshell when this is the case
if you made a get (or post...) request from another origin, the browser will first make an option request to the same endpoint, if it's succeeded and has all the allowing headers it will make the get request, if not it will throw the error specifying why it was denied and don't make the original request
so we have two cases now, it's either the headers is returned only on the get request, but not the options one, or it's never returned

CrossOrigin with http security using jwt token in Spring boot

I have some issue regarding crossorigin and http security in my spring boot application.
I want to use http security when I annotate a method with #crossorigin in my controller class.
But it doesnt seams to work, the security is always triggered even if the method is not using #crosorigin.
Is it possible to fix this?
Jwtautoconfig class:
#ManagementContextConfiguration
#ConditionalOnProperty(name = {"af.security.active"}, havingValue = "true")
#Import({EnvironmentConfig.class, JwkRepository.class, JwtTokenUtil.class,
JwtAuthenticationProvider.class})
#EnableWebSecurity
#EnableConfigurationProperties(JwtSecurityProperties.class)
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
#Order(Ordered.HIGHEST_PRECEDENCE)
public class JwtAutoConfig extends WebSecurityConfigurerAdapter {
#Value("${af.security.jwt.white-list}")
private String[] ignoredPaths;
#Value("${af.security.job-seeker-role:arbetssökande}")
private String jobSeekerRole;
#Value("${af.security.officer-role:handläggare}")
private String officer;
#Bean(name = "jwtauthenticationentrypoint")
public JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint() {
return new JwtAuthenticationEntryPoint();
}
#Bean
public JwtSecurityHelper securityHelper(){
return new JwtSecurityHelper(jobSeekerRole, officer);
}
#Bean
public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {
JwtAuthenticationTokenFilter authenticationTokenFilter = new JwtAuthenticationTokenFilter();
authenticationTokenFilter.setAuthenticationManager(authenticationManager());
authenticationTokenFilter.setAuthenticationSuccessHandler(new JwtAuthenticationSuccessHandler());
return authenticationTokenFilter;
}
#Override
public void configure(HttpSecurity http) throws Exception {
http.requestMatchers()
.and()
.authorizeRequests()
.antMatchers("/**")
.authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf()
.disable();
// Custom JWT based security filter
http.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
// disable page caching
http.headers().cacheControl();
}
#Override
public void configure(WebSecurity web) {
final String[] trimmedIgnoredPaths = Stream.of(ignoredPaths)
.map(String::trim)
.toArray(String[]::new);
web.ignoring()
.antMatchers(HttpMethod.OPTIONS,"/**")
.and()
.ignoring().antMatchers(trimmedIgnoredPaths);
}
private Config hazelCastConfig(){
Config config = new Config();
config.setInstanceName("app-cache")
.setNetworkConfig(new NetworkConfig()
.setJoin(new JoinConfig()
.setMulticastConfig(new MulticastConfig()
.setEnabled(false)
)
)
)
.addMapConfig(
new MapConfig()
.setName("object-cache")
.setMaxSizeConfig(new MaxSizeConfig(10, MaxSizeConfig.MaxSizePolicy.FREE_HEAP_SIZE))
.setEvictionPolicy(EvictionPolicy.LRU)
.setStatisticsEnabled(true)
.setTimeToLiveSeconds(14400));
return config;
}
#Bean(name="hazelcast")
public HazelcastInstance hazelcastInstance() {
HazelcastInstance hazelcastInstance = new HazelcastInstanceFactory(hazelCastConfig()).getHazelcastInstance();
return hazelcastInstance;
}
}
CorsConfig class:
#Configuration
public class CorsConfig {
#Bean
public WebMvcConfigurer corsConfigurer()
{
return new WebMvcConfigurer() {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "DELETE", "GET" )
.allowCredentials(true);
}
};
}
}
And this is the method in my controller class:
#ApiOperation(value = "Hämtar alla frånvaron för en lista med användare")
#PostMapping(path= "/hamta-alla-franvaron", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<List<ExternalFranvaroDTO>> hamtaAllaFranvaron(
#ApiParam(value = "Identitet objekt som innehåller en lista av PISA_ID", required = true)
#Valid #RequestBody IdentitetForm identitet){
logger.info("MOTTAGET Rest-anrop (/hamta-alla-franvaron) Hamtar alla franvaron");
List<ExternalFranvaroDTO> externalFranvaroDTOLista = new ArrayList<>();
List<Franvaro> franvaron = franvaroService.hamtaAllaPagaendeOchNyaFriskskrivnaFranvaron(identitet.getPisaIds());
if(franvaron.isEmpty()) {
logger.debug("Inga pågende sjuk/vab anmälan");
return ResponseEntity.noContent().build();
}
franvaron.forEach( franvaro -> {
ExternalFranvaroDTO externalFranvaroDTO = transformeraTillExternalFranvaroDTO(franvaro);
externalFranvaroDTOLista.add(externalFranvaroDTO);
});
return ResponseEntity.ok().body(externalFranvaroDTOLista);
}
Now I want to only use http security when I use #crossorigin
I don't see any reason as to why you would like to combine it like this.
You should instead apply security to specific endpoints and configure a cors filter in spring security instead of setting it globally as you have done.
If you read the spring security documentation under HttpSecurity you can use the antMatcher and and match endpoints using ant syntax
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorize -> authorize
.antMatcher( // Here you can define endpoints using ant matching
"**/foo/**",
"**/bar/**"
)
.authenticated()
)
... // rest of configuration
}
You can also define a CORS filter using spring security
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
// by default uses a Bean by the name of corsConfigurationSource
.cors(withDefaults())
...
}
#Bean
CorsConfigurationSource corsConfigurationSource() {
final CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("https://example.com"));
configuration.setAllowedMethods(Arrays.asList("GET","POST"));
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
You can even activate and use the built in jwt filter and customize that filter too with your own converter etc. etc.
protected void configure(HttpSecurity http) {
http
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
}
// or add a custom converter
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
// adding a custom converter here
.jwtAuthenticationConverter(myConverter())
)
);
The spring security documentation is really, really, relly good and you should always use it first as a source of information.

Spring Data Rest with JWT

I'm trying to secure a simple Spring-Data-Rest app using jwt.
Taking the seed from https://github.com/spring-projects/spring-data-examples/tree/master/rest/security
The SecurityConfig is below (using normal username, password authentication)
How can I change this to JWT Authentication?
(Authorization is already done using #PreAuthorize("hasRole('ROLE_USER')") in repositories)
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
/**
* This section defines the user accounts which can be used for
* authentication as well as the roles each user has.
*/
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("greg").password("turnquist").roles("USER").and()
.withUser("ollie").password("gierke").roles("USER", "ADMIN");
}
/**
* This section defines the security policy for the app.
* - BASIC authentication is supported (enough for this REST-based demo)
* - /employees is secured using URL security shown below
* - CSRF headers are disabled since we are only testing the REST interface,
* not a web one.
*
* NOTE: GET is not shown which defaults to permitted.
*/
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic().and()
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/employees").hasRole("ADMIN")
.antMatchers(HttpMethod.PUT, "/employees/**").hasRole("ADMIN")
.antMatchers(HttpMethod.PATCH, "/employees/**").hasRole("ADMIN").and()
.csrf().disable();
}
}
here is a good tutorial for JWT Authentication in spring boot, but in can applied for spring applications as well: https://auth0.com/blog/implementing-jwt-authentication-on-spring-boot/
According to the tutorial in your SecurityConfiguration.configure you need
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
// this disables session creation on Spring Security
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter, is applied to /login URL and generates JWT token based on your login/password if such a user exists in the system.
JWTAuthorizationFilter verifies JWT token coming in http header
Of course you need to add more moving parts in order to enable JWT auth by this tutorial.
I followed the tutorial for Spring Security OAuth:
https://projects.spring.io/spring-security-oauth/docs/oauth2.html
In particular you have to enable the resource server. This is my (modified) configuration):
#EnableResourceServer
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenServices(tokenServices());
}
#Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
try {
// Load the public key of the authorization server.
String key = IOUtils.toString(getClass().getResource("/reng0-public.key"), Charset.forName("US-ASCII"));
converter.setVerifierKey(key);
} catch (IOException e) {
throw new RuntimeException(e);
}
return converter;
}
#Bean
#Primary
public ResourceServerTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
return defaultTokenServices;
}
}
The client has to add the Authorization: Bearer header to make it work.

Categories