Двухфакторная аутентификация с spring security oauth2
Я ищу идеи, как реализовать двухфакторную аутентификацию (2FA) с spring security OAuth2. Требование состоит в том, что пользователю требуется двухфакторная аутентификация только для определенных приложений с конфиденциальной информацией. Эти webapps имеют свои собственные идентификаторы клиентов.
одна идея, которая выскочила в моем уме, заключалась бы в" неправильном использовании " страницы утверждения области, чтобы заставить пользователя ввести код/PIN-код 2FA (или что-то еще).
потоки образцов будут выглядеть так это:
доступ к приложениям без и с 2FA
- пользователя
- пользователь получает доступ к приложению A, которое не требует 2FA
- перенаправление в приложение OAuth, пользователь входит в систему с именем пользователя и паролем
- перенаправлено обратно в приложение A, и пользователь вошел в систему
- пользователь получает доступ к приложению B, которое также не требует 2FA
- перенаправление в приложение OAuth, перенаправление обратно в приложение B, и пользователь напрямую регистрируется в
- пользователь получает доступ к приложению s, которое тут требует 2-факторную авторизацию
- перенаправление в приложение OAuth, пользователь должен дополнительно предоставить токен 2FA
- перенаправлено обратно в приложение S, и пользователь вошел в систему
прямой доступ к приложению с 2FA
- пользователя
- пользователь получает доступ к приложению s, которое тут требует 2-факторную авторизацию
- перенаправление в приложение OAuth, пользователь входит в систему с имя пользователя и пароль, пользователь должен дополнительно предоставить токен 2FA
- перенаправлено обратно в приложение S, и пользователь вошел в систему
У вас есть другие идеи, как это сделать?
2 ответов
Итак, вот как была реализована двухфакторная аутентификация:
фильтр зарегистрирован для пути/oauth / authorize после фильтра безопасности spring:
@Order(200)
public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
@Override
protected void afterSpringSecurityFilterChain(ServletContext servletContext) {
FilterRegistration.Dynamic twoFactorAuthenticationFilter = servletContext.addFilter("twoFactorAuthenticationFilter", new DelegatingFilterProxy(AppConfig.TWO_FACTOR_AUTHENTICATION_BEAN));
twoFactorAuthenticationFilter.addMappingForUrlPatterns(null, false, "/oauth/authorize");
super.afterSpringSecurityFilterChain(servletContext);
}
}
этот фильтр проверяет, не прошел ли пользователь проверку подлинности со 2-м фактором (проверив, если ROLE_TWO_FACTOR_AUTHENTICATED
авторитет недоступен) и создает OAuth AuthorizationRequest
в сессии. Затем пользователь перенаправляется на страницу, где он должен ввести 2FA код:
/**
* Stores the oauth authorizationRequest in the session so that it can
* later be picked by the {@link com.example.CustomOAuth2RequestFactory}
* to continue with the authoriztion flow.
*/
public class TwoFactorAuthenticationFilter extends OncePerRequestFilter {
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
private OAuth2RequestFactory oAuth2RequestFactory;
@Autowired
public void setClientDetailsService(ClientDetailsService clientDetailsService) {
oAuth2RequestFactory = new DefaultOAuth2RequestFactory(clientDetailsService);
}
private boolean twoFactorAuthenticationEnabled(Collection<? extends GrantedAuthority> authorities) {
return authorities.stream().anyMatch(
authority -> ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED.equals(authority.getAuthority())
);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// Check if the user hasn't done the two factor authentication.
if (AuthenticationUtil.isAuthenticated() && !AuthenticationUtil.hasAuthority(ROLE_TWO_FACTOR_AUTHENTICATED)) {
AuthorizationRequest authorizationRequest = oAuth2RequestFactory.createAuthorizationRequest(paramsFromRequest(request));
/* Check if the client's authorities (authorizationRequest.getAuthorities()) or the user's ones
require two factor authenticatoin. */
if (twoFactorAuthenticationEnabled(authorizationRequest.getAuthorities()) ||
twoFactorAuthenticationEnabled(SecurityContextHolder.getContext().getAuthentication().getAuthorities())) {
// Save the authorizationRequest in the session. This allows the CustomOAuth2RequestFactory
// to return this saved request to the AuthenticationEndpoint after the user successfully
// did the two factor authentication.
request.getSession().setAttribute(CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME, authorizationRequest);
// redirect the the page where the user needs to enter the two factor authentiation code
redirectStrategy.sendRedirect(request, response,
ServletUriComponentsBuilder.fromCurrentContextPath()
.path(TwoFactorAuthenticationController.PATH)
.toUriString());
return;
} else {
request.getSession().removeAttribute(CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
}
}
filterChain.doFilter(request, response);
}
private Map<String, String> paramsFromRequest(HttpServletRequest request) {
Map<String, String> params = new HashMap<>();
for (Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
params.put(entry.getKey(), entry.getValue()[0]);
}
return params;
}
}
на TwoFactorAuthenticationController
который обрабатывает ввод 2FA-кода добавляет полномочия ROLE_TWO_FACTOR_AUTHENTICATED
если код был правильным и перенаправляет пользователя обратно в конечную точку /oauth/authorize.
@Controller
@RequestMapping(TwoFactorAuthenticationController.PATH)
public class TwoFactorAuthenticationController {
private static final Logger LOG = LoggerFactory.getLogger(TwoFactorAuthenticationController.class);
public static final String PATH = "/secure/two_factor_authentication";
@RequestMapping(method = RequestMethod.GET)
public String auth(HttpServletRequest request, HttpSession session, ....) {
if (AuthenticationUtil.isAuthenticatedWithAuthority(ROLE_TWO_FACTOR_AUTHENTICATED)) {
LOG.info("User {} already has {} authority - no need to enter code again", ROLE_TWO_FACTOR_AUTHENTICATED);
throw ....;
}
else if (session.getAttribute(CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME) == null) {
LOG.warn("Error while entering 2FA code - attribute {} not found in session.", CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
throw ....;
}
return ....; // Show the form to enter the 2FA secret
}
@RequestMapping(method = RequestMethod.POST)
public String auth(....) {
if (userEnteredCorrect2FASecret()) {
AuthenticationUtil.addAuthority(ROLE_TWO_FACTOR_AUTHENTICATED);
return "forward:/oauth/authorize"; // Continue with the OAuth flow
}
return ....; // Show the form to enter the 2FA secret again
}
}
таможни OAuth2RequestFactory
загружает ранее сохраненный AuthorizationRequest
из сеанса, если он доступен, и возвращает его или создает новый, если в сеансе его нет.
/**
* If the session contains an {@link AuthorizationRequest}, this one is used and returned.
* The {@link com.example.TwoFactorAuthenticationFilter} saved the original AuthorizationRequest. This allows
* to redirect the user away from the /oauth/authorize endpoint during oauth authorization
* and show him e.g. a the page where he has to enter a code for two factor authentication.
* Redirecting him back to /oauth/authorize will use the original authorizationRequest from the session
* and continue with the oauth authorization.
*/
public class CustomOAuth2RequestFactory extends DefaultOAuth2RequestFactory {
public static final String SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME = "savedAuthorizationRequest";
public CustomOAuth2RequestFactory(ClientDetailsService clientDetailsService) {
super(clientDetailsService);
}
@Override
public AuthorizationRequest createAuthorizationRequest(Map<String, String> authorizationParameters) {
ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
HttpSession session = attr.getRequest().getSession(false);
if (session != null) {
AuthorizationRequest authorizationRequest = (AuthorizationRequest) session.getAttribute(SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
if (authorizationRequest != null) {
session.removeAttribute(SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
return authorizationRequest;
}
}
return super.createAuthorizationRequest(authorizationParameters);
}
}
этот пользовательский OAuth2RequestFactory установлен на сервер авторизации например:
<bean id="customOAuth2RequestFactory" class="com.example.CustomOAuth2RequestFactory">
<constructor-arg index="0" ref="clientDetailsService" />
</bean>
<!-- Configures the authorization-server and provides the /oauth/authorize endpoint -->
<oauth:authorization-server client-details-service-ref="clientDetailsService" token-services-ref="tokenServices"
user-approval-handler-ref="approvalStoreUserApprovalHandler" redirect-resolver-ref="redirectResolver"
authorization-request-manager-ref="customOAuth2RequestFactory">
<oauth:authorization-code authorization-code-services-ref="authorizationCodeServices"/>
<oauth:implicit />
<oauth:refresh-token />
<oauth:client-credentials />
<oauth:password />
</oauth:authorization-server>
при использовании Java config вы можете создать TwoFactorAuthenticationInterceptor
вместо TwoFactorAuthenticationFilter
и зарегистрировать его в AuthorizationServerConfigurer
С
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig implements AuthorizationServerConfigurer {
...
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.addInterceptor(twoFactorAuthenticationInterceptor())
...
.requestFactory(customOAuth2RequestFactory());
}
@Bean
public HandlerInterceptor twoFactorAuthenticationInterceptor() {
return new TwoFactorAuthenticationInterceptor();
}
}
на TwoFactorAuthenticationInterceptor
содержит ту же логику, что TwoFactorAuthenticationFilter
в своем preHandle
метод.
Я не мог заставить принятое решение работать. Я работал над этим некоторое время, и, наконец, я написал свое решение, используя идеи, объясненные здесь и в этой теме "нулевой клиент в многофакторной аутентификации OAuth2"
вот местоположение GitHub для рабочего решения для меня: https://github.com/turgos/oauth2-2FA
Я ценю, если вы поделитесь своими отзывами, если вы видите какие-либо проблемы или лучше подход.
ниже вы можете найти ключевые файлы конфигурации для этого решения.
AuthorizationServerConfig
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private ClientDetailsService clientDetailsService;
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.inMemory()
.withClient("ClientId")
.secret("secret")
.authorizedGrantTypes("authorization_code")
.scopes("user_info")
.authorities(TwoFactorAuthenticationFilter.ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED)
.autoApprove(true);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)
.requestFactory(customOAuth2RequestFactory());
}
@Bean
public DefaultOAuth2RequestFactory customOAuth2RequestFactory(){
return new CustomOAuth2RequestFactory(clientDetailsService);
}
@Bean
public FilterRegistrationBean twoFactorAuthenticationFilterRegistration(){
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(twoFactorAuthenticationFilter());
registration.addUrlPatterns("/oauth/authorize");
registration.setName("twoFactorAuthenticationFilter");
return registration;
}
@Bean
public TwoFactorAuthenticationFilter twoFactorAuthenticationFilter(){
return new TwoFactorAuthenticationFilter();
}
}
CustomOAuth2RequestFactory
public class CustomOAuth2RequestFactory extends DefaultOAuth2RequestFactory {
private static final Logger LOG = LoggerFactory.getLogger(CustomOAuth2RequestFactory.class);
public static final String SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME = "savedAuthorizationRequest";
public CustomOAuth2RequestFactory(ClientDetailsService clientDetailsService) {
super(clientDetailsService);
}
@Override
public AuthorizationRequest createAuthorizationRequest(Map<String, String> authorizationParameters) {
ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
HttpSession session = attr.getRequest().getSession(false);
if (session != null) {
AuthorizationRequest authorizationRequest = (AuthorizationRequest) session.getAttribute(SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
if (authorizationRequest != null) {
session.removeAttribute(SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
LOG.debug("createAuthorizationRequest(): return saved copy.");
return authorizationRequest;
}
}
LOG.debug("createAuthorizationRequest(): create");
return super.createAuthorizationRequest(authorizationParameters);
}
}
WebSecurityConfig
@EnableResourceServer
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends WebSecurityConfigurerAdapter {
@Autowired
CustomDetailsService customDetailsService;
@Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}
@Bean(name = "authenticationManager")
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/webjars/**");
web.ignoring().antMatchers("/css/**","/fonts/**","/libs/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception { // @formatter:off
http.requestMatchers()
.antMatchers("/login", "/oauth/authorize", "/secure/two_factor_authentication","/exit", "/resources/**")
.and()
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.formLogin().loginPage("/login")
.permitAll();
} // @formatter:on
@Override
@Autowired // <-- This is crucial otherwise Spring Boot creates its own
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// auth//.parentAuthenticationManager(authenticationManager)
// .inMemoryAuthentication()
// .withUser("demo")
// .password("demo")
// .roles("USER");
auth.userDetailsService(customDetailsService).passwordEncoder(encoder());
}
}
TwoFactorAuthenticationFilter
public class TwoFactorAuthenticationFilter extends OncePerRequestFilter {
private static final Logger LOG = LoggerFactory.getLogger(TwoFactorAuthenticationFilter.class);
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
private OAuth2RequestFactory oAuth2RequestFactory;
//These next two are added as a test to avoid the compilation errors that happened when they were not defined.
public static final String ROLE_TWO_FACTOR_AUTHENTICATED = "ROLE_TWO_FACTOR_AUTHENTICATED";
public static final String ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED = "ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED";
@Autowired
public void setClientDetailsService(ClientDetailsService clientDetailsService) {
oAuth2RequestFactory = new DefaultOAuth2RequestFactory(clientDetailsService);
}
private boolean twoFactorAuthenticationEnabled(Collection<? extends GrantedAuthority> authorities) {
return authorities.stream().anyMatch(
authority -> ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED.equals(authority.getAuthority())
);
}
private Map<String, String> paramsFromRequest(HttpServletRequest request) {
Map<String, String> params = new HashMap<>();
for (Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
params.put(entry.getKey(), entry.getValue()[0]);
}
return params;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// Check if the user hasn't done the two factor authentication.
if (isAuthenticated() && !hasAuthority(ROLE_TWO_FACTOR_AUTHENTICATED)) {
AuthorizationRequest authorizationRequest = oAuth2RequestFactory.createAuthorizationRequest(paramsFromRequest(request));
/* Check if the client's authorities (authorizationRequest.getAuthorities()) or the user's ones
require two factor authentication. */
if (twoFactorAuthenticationEnabled(authorizationRequest.getAuthorities()) ||
twoFactorAuthenticationEnabled(SecurityContextHolder.getContext().getAuthentication().getAuthorities())) {
// Save the authorizationRequest in the session. This allows the CustomOAuth2RequestFactory
// to return this saved request to the AuthenticationEndpoint after the user successfully
// did the two factor authentication.
request.getSession().setAttribute(CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME, authorizationRequest);
LOG.debug("doFilterInternal(): redirecting to {}", TwoFactorAuthenticationController.PATH);
// redirect the the page where the user needs to enter the two factor authentication code
redirectStrategy.sendRedirect(request, response,
TwoFactorAuthenticationController.PATH
);
return;
}
}
LOG.debug("doFilterInternal(): without redirect.");
filterChain.doFilter(request, response);
}
public boolean isAuthenticated(){
return SecurityContextHolder.getContext().getAuthentication().isAuthenticated();
}
private boolean hasAuthority(String checkedAuthority){
return SecurityContextHolder.getContext().getAuthentication().getAuthorities().stream().anyMatch(
authority -> checkedAuthority.equals(authority.getAuthority())
);
}
}
TwoFactorAuthenticationController
@Controller
@RequestMapping(TwoFactorAuthenticationController.PATH)
public class TwoFactorAuthenticationController {
private static final Logger LOG = LoggerFactory.getLogger(TwoFactorAuthenticationController.class);
public static final String PATH = "/secure/two_factor_authentication";
@RequestMapping(method = RequestMethod.GET)
public String auth(HttpServletRequest request, HttpSession session) {
if (isAuthenticatedWithAuthority(TwoFactorAuthenticationFilter.ROLE_TWO_FACTOR_AUTHENTICATED)) {
LOG.debug("User {} already has {} authority - no need to enter code again", TwoFactorAuthenticationFilter.ROLE_TWO_FACTOR_AUTHENTICATED);
//throw ....;
}
else if (session.getAttribute(CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME) == null) {
LOG.debug("Error while entering 2FA code - attribute {} not found in session.", CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
//throw ....;
}
LOG.debug("auth() HTML.Get");
return "loginSecret"; // Show the form to enter the 2FA secret
}
@RequestMapping(method = RequestMethod.POST)
public String auth(@ModelAttribute(value="secret") String secret, BindingResult result, Model model) {
LOG.debug("auth() HTML.Post");
if (userEnteredCorrect2FASecret(secret)) {
addAuthority(TwoFactorAuthenticationFilter.ROLE_TWO_FACTOR_AUTHENTICATED);
return "forward:/oauth/authorize"; // Continue with the OAuth flow
}
model.addAttribute("isIncorrectSecret", true);
return "loginSecret"; // Show the form to enter the 2FA secret again
}
private boolean isAuthenticatedWithAuthority(String checkedAuthority){
return SecurityContextHolder.getContext().getAuthentication().getAuthorities().stream().anyMatch(
authority -> checkedAuthority.equals(authority.getAuthority())
);
}
private boolean addAuthority(String authority){
Collection<SimpleGrantedAuthority> oldAuthorities = (Collection<SimpleGrantedAuthority>)SecurityContextHolder.getContext().getAuthentication().getAuthorities();
SimpleGrantedAuthority newAuthority = new SimpleGrantedAuthority(authority);
List<SimpleGrantedAuthority> updatedAuthorities = new ArrayList<SimpleGrantedAuthority>();
updatedAuthorities.add(newAuthority);
updatedAuthorities.addAll(oldAuthorities);
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(
SecurityContextHolder.getContext().getAuthentication().getPrincipal(),
SecurityContextHolder.getContext().getAuthentication().getCredentials(),
updatedAuthorities)
);
return true;
}
private boolean userEnteredCorrect2FASecret(String secret){
/* later on, we need to pass a temporary secret for each user and control it here */
/* this is just a temporary way to check things are working */
if(secret.equals("123"))
return true;
else;
return false;
}
}