Spring boot application with Office 365 authentication - spring

I'm developing an application (spring boot for backend and react for frontend) with authentication with Office 365. But I want to use my own group and permissions for Users. For instance, when a User access to /api/auth for the first time, I want to retrieve informations from microsoft graph and save it to my DB and then protect my endpoint with my own roles/permissions.
So far I managed to do this :
When i go to localhost:8080 (back) i'm redirect to Azure portal to authenticate. Then, I can access to my endpoints
I can protect my endpoints with my own roles
When i go to localhost:3000 (react app) I have a button that redirect me to the portal and give me an azure access token (thanks to MSAL.js)
So my problem is that I can't validate this azure token in my backend and send a new token from the back to the front to send request (like GET /api/users or POST /api/todos). I think my backend configuration and implementation is wrong but I didn't find a way to validate tokens and return a token for my backend...
I hope I was clear, english isn't my native language
Here is my WebSecurityConfig
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests().antMatchers("/**")
.authenticated()
.and()
.csrf().csrfTokenRepository(csrfTokenRepository()).and()
.addFilterAfter(csrfHeaderFilter(), CsrfFilter.class)
.oauth2Login();
}
private Filter csrfHeaderFilter() {
return new OncePerRequestFilter() {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
if (csrf != null) {
Cookie cookie = WebUtils.getCookie(request, "XSRF-TOKEN");
String token = csrf.getToken();
if (cookie == null || token != null && !token.equals(cookie.getValue())) {
cookie = new Cookie("XSRF-TOKEN", token);
cookie.setPath("/");
response.addCookie(cookie);
}
}
filterChain.doFilter(request, response);
}
};
}
#Bean
public HttpFirewall allowUrlEncodedSlashHttpFirewall() {
StrictHttpFirewall firewall = new StrictHttpFirewall();
firewall.setAllowUrlEncodedSlash(true);
return firewall;
}
#Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
web.httpFirewall(allowUrlEncodedSlashHttpFirewall());
}
private CsrfTokenRepository csrfTokenRepository() {
HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
repository.setHeaderName("X-XSRF-TOKEN");
return repository;
}
}
And here's how I protect my endpoints :
#PreAuthorize("hasPermission(#foo, 'write')")
The conf :
#Configuration
public class AclPermissionEvaluator implements PermissionEvaluator {
#Autowired
private RoleRepository roleRepository;
#Autowired
private SecurityService securityService;
#Override
public boolean hasPermission(final Authentication authentication, final Object privilegeName, final Object privilegeType) {
DefaultOidcUser principal = (DefaultOidcUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Map<String, Object> userAttributes = principal.getAttributes();
UserInfo userInfo = securityService.getUserInfoByLogin((String) userAttributes.get("unique_name"));
if (userInfo == null || StringUtils.isBlank((CharSequence) privilegeName) || StringUtils.isBlank((CharSequence) privilegeType)) {
return false;
}
for (Permission permission : roleRepository.getByName(userInfo.getRoleName()).getPermissions()) {
if (permission.getPermission().startsWith((String) privilegeName) || permission.getPermission().equals("*")) {
return true;
}
}
return false;
}
//We don't need an implementation of this function for now
#Override
public boolean hasPermission(final Authentication authentication, final Serializable serializable, final String s, final Object o) {
return false;
}
}
Security Service :
#Service
#Transactional
public class SecurityServiceImpl implements SecurityService {
#Autowired private UserRepository userRepo;
#Autowired
private RoleRepository roleRepository;
#Override
public UserInfo getUserInfoByLogin(String username) {
User user = userRepo.getUserByUsername(username);
ModelMapper modelMapper = new ModelMapper();
return modelMapper.map(user, UserInfo.class);
}
}
RoleRepository :
#Repository
public interface RoleRepository extends JpaRepository<Role, Long> {
public Role getByName(String name);
}
And on my application.yml :
spring:
security:
oauth2:
client:
registration:
azure:
clientId: <my-clientId>
clientSecret: <my-secret>
azure:
activedirectory:
tenant-id: <my-tenant-id>
user-group:
allowed-groups: all
active-directory-groups: all

you can use similar to the below code
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
#Import(SecurityProblemSupport.class)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final CorsFilter corsFilter;
private final SecurityProblemSupport problemSupport;
private final OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService;
public SecurityConfiguration(CorsFilter corsFilter, SecurityProblemSupport problemSupport, OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService) {
this.corsFilter = corsFilter;
this.problemSupport = problemSupport;
this.oidcUserService = oidcUserService;
}
#Override
public void configure(WebSecurity web) {
web.ignoring()
.antMatchers(HttpMethod.OPTIONS, "/**")
.antMatchers("/app/**/*.{js,html}")
.antMatchers("/i18n/**")
.antMatchers("/content/**")
.antMatchers("/swagger-ui/index.html")
.antMatchers("/test/**");
}
#Override
public void configure(HttpSecurity http) throws Exception {
// #formatter:off
http
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and()
.addFilterBefore(corsFilter, CsrfFilter.class)
.exceptionHandling()
.accessDeniedHandler(problemSupport)
.and()
.headers()
.contentSecurityPolicy("default-src 'self'; frame-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://storage.googleapis.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:")
.and()
.referrerPolicy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN)
.and()
.featurePolicy("geolocation 'none'; midi 'none'; sync-xhr 'none'; microphone 'none'; camera 'none'; magnetometer 'none'; gyroscope 'none'; speaker 'none'; fullscreen 'self'; payment 'none'")
.and()
.frameOptions()
.deny()
.and()
.authorizeRequests()
.antMatchers("/api/auth-info").permitAll()
.antMatchers("/api/**").authenticated()
.antMatchers("/management/health").permitAll()
.antMatchers("/management/info").permitAll()
.antMatchers("/management/prometheus").permitAll()
.antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN)
.and()
.oauth2Login()
.userInfoEndpoint()
.oidcUserService(oidcUserService);
// #formatter:on
}
/**
* Map authorities from "groups" or "roles" claim in ID Token.
*
* #return a {#link GrantedAuthoritiesMapper} that maps groups from
* the IdP to Spring Security Authorities.
*/
#Bean
public GrantedAuthoritiesMapper userAuthoritiesMapper() {
return (authorities) -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
authorities.forEach(authority -> {
mappedAuthorities.addAll(authorities);
});
return mappedAuthorities;
};
}
}
you need to change the userAuthoritiesMapper method to achieve what you looking for
extra configuration
spring security config
spring:
security:
oauth2:
client:
registration:
azure:
client-id: <<CLIENT_ID>>
client-secret: <<CLIENT_SECRET>>
Azur Active directory config
azure:
activedirectory:
tenant-id: <<YOUR_TENANT_ID>>
active-directory-groups: Users
b2c:
reply-url: http://localhost:9000 # should be absolute url.
logout-success-url: http://localhost:9000
I'll give a secret whenever you stuck in any of the spring boot configurations go and find how jhipster make it
you can find more information by visiting this article posted by jhipster creator
NOTE: SecurityProblemSupport is a library implemented by zalando
finally the code posted copied from this repo

Related

Unable to resolve CORS errors

Assumptions
We are developing a web application with the following library.
When a request is sent from the front end to the back end, a CORS error occurs.
Frontend: Vue.js (Version: 3)
Backend: SpringBoot (version: 2.7.6)
Authentication: SpringSecurity
What we want to achieve
We would like to resolve the following CORS errors that occur when a request is sent from the front-end side to the back-end side.
Access to XMLHttpRequest at 'http://localhost:8085/users/profile/1' from origin 'http://localhost:8888' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Source code
Send request to Spring in Vue.js (Edit.vue)
onClickDelete() {
const path = 'users/profile/'
axios.delete(
process.env.VUE_APP_ROOT_API + path + this.$store.state.user_id,{
headers: {
"Authorization": "Bearer " + this.$store.state.jwt_token,
},
})
.then(response => {
})
.catch(error => {
console.log(error)
})
},
Receiving process in Spring (UsersController.java)
#RestController
#RequestMapping("/users/profile")
public class UsersController {
#DeleteMapping("/{user_id}")
#ResponseStatus(code = HttpStatus.NO_CONTENT, value = HttpStatus.NO_CONTENT)
public void profiledelete(#PathVariable("user_id") Long id) throws Exception {
}
}
SpringSecurity configuration file (WebSecurityConfig.java)
#Profile("production")
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserRepository userRepository;
private final JsonRequestAuthenticationProvider jsonRequestAuthenticationProvider;
#Value("${security.secret-key:secret}")
private String secretKey = "secret";
public WebSecurityConfig(JsonRequestAuthenticationProvider jsonRequestAuthenticationProvider// ,
) {
this.jsonRequestAuthenticationProvider = jsonRequestAuthenticationProvider;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
JsonRequestAuthenticationFilter jsonAuthFilter =
new JsonRequestAuthenticationFilter(userRepository);
jsonAuthFilter.setAuthenticationManager(authenticationManagerBean());
http.cors().configurationSource(request -> new CorsConfiguration().applyPermitDefaultValues());
http.addFilter(jsonAuthFilter);
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.exceptionHandling().accessDeniedHandler(accessDeniedHandler())
.and()
.csrf().
disable()
.addFilterBefore(tokenFilter(), UsernamePasswordAuthenticationFilter.class)
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
;
}
What we tried
#CrossOrigin to the process (UsersController.java) that receives the process in Spring
What we did
Receive process in Spring (UsersController.java)
#RestController
#RequestMapping("/users/profile")
#CrossOrigin
public class UsersController {
#DeleteMapping("/{user_id}")
#ResponseStatus(code = HttpStatus.NO_CONTENT, value = HttpStatus.NO_CONTENT)
public void profiledelete(#PathVariable("user_id") Long id) throws Exception {
}
}
Result
The CORS error is still displayed.
Additional Information
Before SpringSecurity was installed, I think that granting #CrossOrigin on the Spring side solved the CORS error.
When the GET method is used in other requests, it succeeds without any CORS errors with the Spring side.
This seems to be an issue with your setup with spring security.
There are two primary ways to fix this error; however, I would also recommend upgrading to a newer version of spring security, because WebSecurityConfigurerAdapter has now been deprecated.
Primary method
CORS on Spring security (2.x)
#Profile("production")
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserRepository userRepository;
private final JsonRequestAuthenticationProvider jsonRequestAuthenticationProvider;
#Value("${security.secret-key:secret}")
private String secretKey = "secret";
public WebSecurityConfig(JsonRequestAuthenticationProvider jsonRequestAuthenticationProvider// ,
) {
this.jsonRequestAuthenticationProvider = jsonRequestAuthenticationProvider;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
JsonRequestAuthenticationFilter jsonAuthFilter =
new JsonRequestAuthenticationFilter(userRepository);
jsonAuthFilter.setAuthenticationManager(authenticationManagerBean());
http.cors().configurationSource(request -> {
var cors = new CorsConfiguration();
cors.setAllowedOrigins(List.of("*"));
cors.setAllowedMethods(List.of("GET","POST", "PUT", "DELETE", "OPTIONS"));
cors.setAllowedHeaders(List.of("*"));
return cors;
});
http.addFilter(jsonAuthFilter);
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.exceptionHandling().accessDeniedHandler(accessDeniedHandler())
.and()
.csrf().
disable()
.addFilterBefore(tokenFilter(), UsernamePasswordAuthenticationFilter.class)
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
;
}
CORS disable
#Profile("production")
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserRepository userRepository;
private final JsonRequestAuthenticationProvider jsonRequestAuthenticationProvider;
#Value("${security.secret-key:secret}")
private String secretKey = "secret";
public WebSecurityConfig(JsonRequestAuthenticationProvider jsonRequestAuthenticationProvider// ,
) {
this.jsonRequestAuthenticationProvider = jsonRequestAuthenticationProvider;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
JsonRequestAuthenticationFilter jsonAuthFilter =
new JsonRequestAuthenticationFilter(userRepository);
jsonAuthFilter.setAuthenticationManager(authenticationManagerBean());
http.cors().disable();
http.addFilter(jsonAuthFilter);
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.exceptionHandling().accessDeniedHandler(accessDeniedHandler())
.and()
.csrf().
disable()
.addFilterBefore(tokenFilter(), UsernamePasswordAuthenticationFilter.class)
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
;
}
CORS on Spring security (3.x)
#Configuration
public class WebConfiguration implements WebMvcConfigurer {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedMethods("*");
}
}
Always go for the second method.

403 after login with OAuth 2

I am using Spring Security 5 and I implemented the login but everytime I try to call other URL after login I get a 403 Unhautorized. My doFilterInternal is not even called (it is for the login though).
It gets on org.springframework.security.web.session.SessionManagementFilter#doFilter but it has no security context or authentication present neither a session.
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(
securedEnabled = true,
jsr250Enabled = true,
prePostEnabled = true
)
public class SecurityConfig {
#Autowired
private CustomUserDetailsService customUserDetailsService;
#Autowired
private CustomOAuth2UserService customOAuth2UserService;
#Autowired
private OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler;
#Autowired
private OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler;
#Autowired
private HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository;
#Bean
public TokenAuthenticationFilter tokenAuthenticationFilter() {
return new TokenAuthenticationFilter();
}
/*
By default, Spring OAuth2 uses HttpSessionOAuth2AuthorizationRequestRepository to save
the authorization request. But, since our service is stateless, we can't save it in
the session. We'll save the request in a Base64 encoded cookie instead.
*/
#Bean
public HttpCookieOAuth2AuthorizationRequestRepository cookieAuthorizationRequestRepository() {
return new HttpCookieOAuth2AuthorizationRequestRepository();
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeHttpRequests()
.requestMatchers( "/auth/login").permitAll()
.anyRequest().authenticated();
return http.build();
}
}
HttpCookieOAuth2AuthorizationRequestRepository
#Component
public class HttpCookieOAuth2AuthorizationRequestRepository implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> {
public static final String OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME = "oauth2_auth_request";
public static final String REDIRECT_URI_PARAM_COOKIE_NAME = "redirect_uri";
private static final int cookieExpireSeconds = 180;
#Override
public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) {
return CookieUtils.getCookie(request, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME)
.map(cookie -> CookieUtils.deserialize(cookie, OAuth2AuthorizationRequest.class))
.orElse(null);
}
#Override
public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, HttpServletResponse response) {
if (authorizationRequest == null) {
CookieUtils.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);
CookieUtils.deleteCookie(request, response, REDIRECT_URI_PARAM_COOKIE_NAME);
return;
}
CookieUtils.addCookie(response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME, CookieUtils.serialize(authorizationRequest), cookieExpireSeconds);
String redirectUriAfterLogin = request.getParameter(REDIRECT_URI_PARAM_COOKIE_NAME);
if (StringUtils.isNotBlank(redirectUriAfterLogin)) {
CookieUtils.addCookie(response, REDIRECT_URI_PARAM_COOKIE_NAME, redirectUriAfterLogin, cookieExpireSeconds);
}
}
#Override
public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request, HttpServletResponse response) {
return this.loadAuthorizationRequest(request);
}
// #Override
// public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request) {
// return this.loadAuthorizationRequest(request);
// }
public void removeAuthorizationRequestCookies(HttpServletRequest request, HttpServletResponse response) {
CookieUtils.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);
CookieUtils.deleteCookie(request, response, REDIRECT_URI_PARAM_COOKIE_NAME);
}
}
You are missing the resource-server configuration in your HTTP config with either JWT decoder or token introspection ("opaqueToken" in spring security configuration DSL). Sample configuration from this tutorials I wrote:
#Bean
SecurityFilterChain filterChain(HttpSecurity http,
Converter<Jwt, AbstractAuthenticationToken> authenticationConverter,
ServerProperties serverProperties)
throws Exception {
// Enable OAuth2 with custom authorities mapping
http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(authenticationConverter);
// Enable and configure CORS
http.cors().configurationSource(corsConfigurationSource());
// State-less session (state in access-token only)
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// Disable CSRF because of state-less session-management
http.csrf().disable();
// Return 401 (unauthorized) instead of 302 (redirect to login) when
// authorization is missing or invalid
http.exceptionHandling().authenticationEntryPoint((request, response, authException) -> {
response.addHeader(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"Restricted Content\"");
response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
});
// Route security: authenticated to all routes but actuator and Swagger-UI
// #formatter:off
http.authorizeHttpRequests()
.requestMatchers("/actuator/health/readiness", "/actuator/health/liveness", "/v3/api-docs", "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()
.anyRequest().authenticated();
// #formatter:on
return http.build();
}
OAuth2 login is for clients (server side rendered UI with template engine like Thymeleaf or JSF) and requires sessions (and CSRF protection), not for resource-servers (REST APIs) which should respond to unauthorized requests to secured resources with 401 (unauthorized) and not 302 (redirect to login). Use a certified OpenID client lib in your client to manage redirection to authorization server, token acquisition and refreshing, and requests authorization (setting of Authorization header with access-token).
You asked for SessionCreationPolicy.STATELESS and it's what you get.
If there is no "state", there is no recording of the fact that the user has logged in successfully. It's a setup for REST services not for UI interaction.

"addCorsMapping" blocking Swagger UI

I'm working on a Spring application and I have some troubles with Swagger and Spring Security.
I had to add a specific configuration to allow almost every access (CORS) and it worked well so far, but somehow it is blocking Swagger....
This is my SwaggerConfiguration.java
#Configuration
#EnableSwagger2
#SwaggerDefinition(
info = #Info(
description = "Web Service",
version = "V0.0.1",
title = "Web Service",
contact = #Contact(
name = "Me",
email = "dev#me.com",
url = "https://www.me.com/"
)
),
consumes = {"application/json"},
produces = {"application/json"},
schemes = {SwaggerDefinition.Scheme.HTTP, SwaggerDefinition.Scheme.HTTPS}
)
public class SwaggerConfiguration {
/** List of Swagger endpoints (used by {#code WebSecurityConfig}) */
static final String[] SWAGGER_ENDPOINTS = {
"/v2/api-docs",
"/swagger-resources",
"/swagger-resources/**",
"/configuration/ui",
"/configuration/security",
"/swagger-ui.html",
"/webjars/**"
};
#Bean
public Docket swaggerSpringMvcPlugin() {
return new Docket(DocumentationType.SWAGGER_2)
.groupName("admin-api")
.select()
.paths(paths()) // and by paths
.build();
}
private Predicate<String> paths() {
return or(
regex("/admin.*"),
regex("/issuer.*"),
regex("/validator.*"),
regex("/data.*"));
}
}
And this is my WebSecurityConfig.java :
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private JwtTokenDecoder jwtTokenDecoder;
#Bean
// Mandatory to be able to have % in URL
// FIXME Set it only for dev environment
public HttpFirewall allowUrlEncodedPercentHttpFirewall() {
StrictHttpFirewall firewall = new StrictHttpFirewall();
firewall.setAllowUrlEncodedPercent(true);
firewall.setAllowUrlEncodedSlash(true);
return firewall;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.httpBasic().disable()
.formLogin().disable()
.logout().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// Install the JWT authentication filter
http.addFilterBefore(new JwtAuthenticationFilter(jwtTokenDecoder), BasicAuthenticationFilter.class);
// Authorize only authenticated requests
http.authorizeRequests()
.anyRequest().authenticated();
http.cors();
}
#Override
public void configure(WebSecurity web) {
// Allow access to /admin/login without authentication
web.ignoring().mvcMatchers("/admin/login", "/admin/validate", "/campaigns", "/data/**", "/issuer/**", "/validator/**");
web.ignoring().antMatchers(SwaggerConfiguration.SWAGGER_ENDPOINTS);
web.httpFirewall(allowUrlEncodedPercentHttpFirewall());
}
}
Finally, I have a WebConfig.java used to set CORS authorizations.
Here it is :
#Configuration
#EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE");
}
}
Very simple. It should authorize almost any access.
When I remove it, Swagger is available from URL localhost:8080/swagger-ui.html (but not my webservices...)
When I put it back, it is blocked, with a 403 error (forbidden)
Any idea of what I am missing ?
So the solution was to add some configuration in WebConfig
I have added this implementation
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry
.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry
.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}

OAuth 2 with spring security and setting the State parameter in the redirect

I am using Spring boot with Spring security, with custom "Filter" Class calling to CIAM server with OAuth 2 authentication. I want to set explicitly or override the default setting so that I could set custom dynamic STATE parameter in the redirect URL that Spring Security prepares under the hood and sends the user to the CIAM server login page. This seamed trivial to me but it turned out to be far from that.
The goal is to add the custom STATE parameter of the OAuth2 redirect link so that after the authentication is finished and the CIAM server redirects me back to my page I take back the STATE parameter which is automatically included in the successful redirect link from the CIAM server.
The Security configuration
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true,
proxyTargetClass = true)
#EnableOAuth2Client
#Order(3)
public class OAuth2LoginWebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
CiamOAuth2ClientFilter oAuth2CiamClientFilter;
#Bean
public InMemoryUserDetailsManager inMemoryUserDetailsManager() {
return new InMemoryUserDetailsManager();
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/**/*.css", "/**/*.png", "/**/*.gif", "/**/*.jpg", "/h2-console/**", "/css/**",
"/img/**", "/font-awesome/**", "/fonts/**", "/js/**", "/signout","/signout/**", "/health");
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.antMatcher("/**")
.authorizeRequests()
.antMatchers("/user/**").hasRole("USER")
.antMatchers("/backoffice/**").hasRole("ADMIN")
.antMatchers("/api/**").hasRole("API")
.antMatchers(/*"/", */"/login**", "/webjars/**", "/favicon.*", "/resources/**",
"/auth/**", "/signin/**","css/**","js/**", "/signup/**", "/signout/", "/health", "/awsTest/login")
.permitAll()
.anyRequest()
.authenticated()
.and()
.exceptionHandling()
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login/callback"))
.and()
.addFilterBefore(oAuth2CiamClientFilter.ciamFilter(), BasicAuthenticationFilter.class)
.logout()
.logoutUrl("/signout")
.logoutSuccessUrl("/logout");
}
}
The custom filter class
#Configuration
public class CiamOAuth2ClientFilter {
#Autowired
AuthorizationCodeResourceDetails oauth2CiamResourceDetails;
#Autowired
CiamOAuth2ClientProperties oauth2CiamClientProperties;
#Autowired
OAuth2ClientContext oauth2ClientContext;
#Autowired
CiamPrincipalExtractor ciamPrincipalExtractor;
#Bean
public FilterRegistrationBean oauth2ClientFilterRegistration(OAuth2ClientContextFilter filter) {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(filter);
registration.setOrder(-100);
registration.addInitParameter("test", "trrrrrrr");
System.out.println("333333333333333333333333");
System.out.println(registration);
return registration;
}
public Filter ciamFilter() {
System.out.println("postaeget");
System.out.println(oauth2CiamClientProperties);
System.out.println(" _-------------------------------: " + oauth2CiamClientProperties.getResource().getUserInfoUri());
UserInfoTokenServices tokenService = new UserInfoTokenServices(oauth2CiamClientProperties.getResource().getUserInfoUri(), oauth2CiamResourceDetails.getClientId());
OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(oauth2CiamResourceDetails, oauth2ClientContext);
OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter("/login/callback");
tokenService.setRestTemplate(restTemplate);
tokenService.setPrincipalExtractor(ciamPrincipalExtractor);
filter.setRestTemplate(restTemplate);
filter.setTokenServices(tokenService);
return filter;
}
}
Application yml settings file connected with the issue
security:
oauth2:
client:
clientId: ...
clientSecret: ....
accessTokenUri: ...
userAuthorizationUri: ...
useCurrentUri: false
preEstablishedRedirectUri: https://localhost/login/callback
clientAuthenticationScheme: query
authenticationScheme: header
serverLogoutUrl: ..
postLogoutRedirectUri: https://localhost/signout
scope:
- openid
- profile
- email
- offline_access
state: TEST
resource:
userInfoUri: ...
preferTokenInfo: ...
In my case
I configure OAuth2ClientAuthenticationProcessingFilter somewhere in #Configuration:
private Filter ssoFilter() {
OAuth2ClientAuthenticationProcessingFilter facebookFilter = new OAuth2ClientAuthenticationProcessingFilter(API_LOGIN_FACEBOOK);
OAuth2RestTemplate facebookTemplate = new OAuth2RestTemplate(facebook(), oauth2ClientContext);
AuthorizationCodeAccessTokenProvider authorizationCodeAccessTokenProviderWithUrl = new AuthorizationCodeAccessTokenProvider();
authorizationCodeAccessTokenProviderWithUrl.setStateKeyGenerator(new StateKeyGeneratorWithRedirectUrl());
facebookTemplate.setAccessTokenProvider(authorizationCodeAccessTokenProviderWithUrl);
facebookFilter.setRestTemplate(facebookTemplate);
UserInfoTokenServices tokenServices = new CheckedUserInfoTokenServices(
facebookResource().getUserInfoUri(), facebook().getClientId(),
facebookPrincipalExtractor, blogPreAuthenticationChecks(), blogPostAuthenticationChecks());
tokenServices.setAuthoritiesExtractor(new FacebookAuthoritiesExtractor());
tokenServices.setRestTemplate(facebookTemplate);
facebookFilter.setTokenServices(tokenServices);
facebookFilter.setAuthenticationSuccessHandler(new OAuth2AuthenticationSuccessHandler());
return facebookFilter;
}
And you can access to current request in StateKeyGeneratorWithRedirectUrl with:
RequestContextHolder.getRequestAttributes()
so you can extract Referer header for example:
public class StateKeyGeneratorWithRedirectUrl extends DefaultStateKeyGenerator {
private RandomValueStringGenerator generator = new RandomValueStringGenerator();
#Override
public String generateKey(OAuth2ProtectedResourceDetails resource) {
HttpServletRequest currentHttpRequest = getCurrentHttpRequest();
if (currentHttpRequest!=null){
String referer = currentHttpRequest.getHeader("Referer");
if (!StringUtils.isEmpty(referer)){
return generator.generate()+","+referer;
}
}
return generator.generate();
}
private static HttpServletRequest getCurrentHttpRequest(){
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes instanceof ServletRequestAttributes) {
return ((ServletRequestAttributes)requestAttributes).getRequest();
}
return null;
}
}
Next - read state from callback:
public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
public static final String DEFAULT = "/";
#Override
protected String determineTargetUrl(HttpServletRequest request,
HttpServletResponse response) {
UriComponents uriComponents = UriComponentsBuilder.newInstance()
.query(request.getQueryString())
.build();
MultiValueMap<String, String> queryParams = uriComponents.getQueryParams();
String stateEncoded = queryParams.getFirst("state");
if (stateEncoded == null) {
return DEFAULT;
}
String stateDecoded = URLDecoder.decode(stateEncoded, StandardCharsets.UTF_8);
String[] split = stateDecoded.split(",");
String redirect;
if (split.length != 2){
return DEFAULT;
} else {
return split[1];
}
}
}

Spring security OAuth2 authentication and form login in one app

Is it possible to combine authoryzation and authentication by login basic and by oauth2 in one application?
My project is based on jhipster project with simple spring security session login, now i need add oauth2 security for mobile app and it's look like it is not possible.
Now i have situation when work one of them, oauth2 ok if WebSecurityConfigurerAdapter had bigger order number than ResourceServerConfiguration. That's mean if oauth security filter is first.
I read a lot in stackoverflow and try many solution like:
Spring security oauth2 and form login configuration for me thats one don't work.
Now i know that is related with some security filter conflict but i dont know how to fix it.
if someone had a similar problem and he managed to do it, or know how to get around or make it better I will be grateful for the information. Thanks in advance for your help :)
#Configuration
#EnableWebSecurity
public class SecurityOauth2Configuration extends WebSecurityConfigurerAdapter {
#Inject
private UserDetailsService userDetailsService;
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers("/scripts/**/*.{js,html}")
.antMatchers("/bower_components/**")
.antMatchers("/i18n/**")
.antMatchers("/assets/**")
.antMatchers("/swagger-ui/index.html")
.antMatchers("/api/register")
.antMatchers("/api/activate")
.antMatchers("/api/account/reset_password/init")
.antMatchers("/api/account/reset_password/finish")
.antMatchers("/test/**");
}
#Configuration
#EnableAuthorizationServer
public static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
private static final String OAUTH_SECURITY = "jhipster.security.authentication.oauth.";
private static final String CLIENTID = "clientid";
private static final String SECRET = "secret";
private static final String TOKEN_VALIDATION_TIME = "tokenValidityInSeconds";
#Autowired
private AuthenticationManager authenticationManager;
#Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.tokenKeyAccess("isAnonymous() || hasAuthority('"+AuthoritiesConstants.USER+"')").checkTokenAccess("hasAuthority('"+AuthoritiesConstants.USER+"')");
}
#Inject
private Environment env;
#Inject
private DataSource dataSource;
#Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)
.tokenStore(tokenStore())
;
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.inMemory()
.withClient(env.getProperty(OAUTH_SECURITY + CLIENTID))
.scopes("read", "write")
.authorities(AuthoritiesConstants.ADMIN, AuthoritiesConstants.USER)
.authorizedGrantTypes("password", "refresh_token")
.secret(env.getProperty(OAUTH_SECURITY + SECRET))
.accessTokenValiditySeconds(env.getProperty(OAUTH_SECURITY + TOKEN_VALIDATION_TIME, Integer.class, 18000));
}
}
#Configuration
#Order(1)
public static class SecurityWebConfiguration extends WebSecurityConfigurerAdapter {
#Inject
private Environment env;
#Inject
private AjaxAuthenticationSuccessHandler ajaxAuthenticationSuccessHandler;
#Inject
private AjaxAuthenticationFailureHandler ajaxAuthenticationFailureHandler;
#Inject
private AjaxLogoutOauthSuccessHandler ajaxLogoutSuccessHandler;
#Inject
private RememberMeServices rememberMeServices;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable().authorizeRequests()
.and()
.formLogin()
.loginProcessingUrl("/api/authentication")
.successHandler(ajaxAuthenticationSuccessHandler)
.failureHandler(ajaxAuthenticationFailureHandler)
.usernameParameter("j_username")
.passwordParameter("j_password")
.permitAll()
.and()
.rememberMe()
.rememberMeServices(rememberMeServices)
.key(env.getProperty("jhipster.security.rememberme.key"))
.and()
.logout()
.logoutUrl("/api/logout")
.logoutSuccessHandler(ajaxLogoutSuccessHandler)
.deleteCookies("JSESSIONID")
.permitAll()
.and()
.exceptionHandling()
;
}
}
#Order(2)
#Configuration
#EnableResourceServer
public static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
#Inject
private Http401UnauthorizedEntryPoint authenticationEntryPoint;
#Inject
private AjaxLogoutOauthSuccessHandler ajaxLogoutSuccessHandler;
#Override
public void configure(HttpSecurity http) throws Exception {
ContentNegotiationStrategy contentNegotiationStrategy = http.getSharedObject(ContentNegotiationStrategy.class);
if (contentNegotiationStrategy == null) {
contentNegotiationStrategy = new HeaderContentNegotiationStrategy();
}
MediaTypeRequestMatcher preferredMatcher = new MediaTypeRequestMatcher(contentNegotiationStrategy,
MediaType.APPLICATION_FORM_URLENCODED,
MediaType.APPLICATION_JSON,
MediaType.MULTIPART_FORM_DATA);
http
.authorizeRequests()
.and()
.anonymous()
.disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.httpBasic()
.and()
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.defaultAuthenticationEntryPointFor(authenticationEntryPoint, preferredMatcher)
.and()
.authorizeRequests()
.antMatchers("/api/**").fullyAuthenticated();
}
}
}
For this settings WebSecurityConfigurerAdapter session work correctly. For OAuth after correctly authorizatization i get valid acces token, but for request with this token from session i get this result:
public static String getCurrentLogin() {
SecurityContext securityContext = SecurityContextHolder.getContext();
Authentication authentication = securityContext.getAuthentication();
UserDetails springSecurityUser = null;
String userName = null;
if(authentication != null) {
if (authentication.getPrincipal() instanceof UserDetails) {
springSecurityUser = (UserDetails) authentication.getPrincipal();
userName = springSecurityUser.getUsername();
} else if (authentication.getPrincipal() instanceof String) {
userName = (String) authentication.getPrincipal();
}
}
System.out.println(userName); // show anonymousUser
System.out.println(authentication.isAuthenticated()); //show true
System.out.println(authentication.getAuthorities()); //show [ROLE_ANONYMOUS]
System.out.println(userName); //show anonymousUser
return userName;
}
function write in console:
anonymousUser
true
[ROLE_ANONYMOUS]
anonymousUser
and should be user1
true
[ROLE_USER]
user1
The apps git urls:
https://github.com/rynkowsw/oauth2 it is oauth2 app
https://github.com/rynkowsw/web-and-oauth2-security this is web and oauth2 security app
This app are adapted from jhipster.github.io
to run app you need have postgres db in localhost, like in db resource file:
driver-class-name: org.postgresql.ds.PGSimpleDataSource
url: jdbc:postgresql://localhost:5432/gymapp
name: gymapp
serverName: localhost:5432
username: postgres
password: jaja
To test app the fastest way is:
http://localhost:8080/oauth/token
headers: Authorization: Basic amhpcHN0ZXJhcHA6bXlTZWNyZXRPQXV0aFNlY3JldA==
this string after basic is combination default jhispter oauth secret and clientid base64 encrypt result
then
http://localhost:8080/api/account
headers: Authorization: bearer [token from response in first request]
For this same db, result for oauth are:
for oauth2 app
{
login: "user"
password: null
firstName: "User"
lastName: "User"
email: "user#localhost"
activated: true
langKey: "en"
authorities: [1]
0: "ROLE_USER"
-
}
for web + oauth2 security:
{
login: "anonymousUser"
password: null
firstName: "Anonymous"
lastName: "User"
email: "anonymous#localhost"
activated: true
langKey: "en"
authorities: [0]
}

Resources