2
votes

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 :

  1. When i go to localhost:8080 (back) i'm redirect to Azure portal to authenticate. Then, I can access to my endpoints
  2. I can protect my endpoints with my own roles
  3. 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
1
where is authorization/authentication configuration? does Office 365 support OpenID? - user1115139
I just found some docs that explain how to authenticate spring-boot-office app and authorization with office groups... - Clément Drouin
Share the answer for others - user1115139
I mean that i didn't found how to authenticate with custom permission, just with Office groups (so it don't help me) - Clément Drouin
I'm asking to add any other configuration related to security here - user1115139

1 Answers

1
votes

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