spring-security icon indicating copy to clipboard operation
spring-security copied to clipboard

Consider a `OneTimeToken` integration with Spring MVC

Open marcusdacoregio opened this issue 1 year ago • 3 comments

This would simplify the resolution of an OneTimeToken. Currently, a OneTimeTokenService should be injected and a OneTimeTokenAuthenticationRequest must be created manually.

@GetMapping("/ott/generate")
public String generateOtt(Authentication authentication, Model model) {
	OneTimeTokenAuthenticationRequest request = new OneTimeTokenAuthenticationRequest(authentication.getName());
	OneTimeToken oneTimeToken = this.oneTimeTokenService.generate(request);
	model.addAttribute("oneTimeToken", oneTimeToken);
	return "ott-generate";
}

For example, authenticated users might want to generate a One-Time Token to log in from another device, let's say a TV, where it is very unlikely that they want to type their long passwords. This is also great to avoid typing your password into a device that might be public, like a hotel TV or a cybercafe computer.

One idea is to provide a HandlerMethodArgumentResolver that resolves the OneTimeToken parameter.

class OneTimeTokenRequestArgumentResolver implements HandlerMethodArgumentResolver {

	private final SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy();

	private final AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();

	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		return OneTimeToken.class.equals(parameter.getParameterType());
	}

	@Override
	public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
		OneTimeTokenService oneTimeTokenService = this.applicationContext.getBean(OneTimeTokenService.class);
		}
		Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
		if (this.authenticationTrustResolver.isAnonymous(authentication)) {
			return null;
		}
		OneTimeTokenAuthenticationRequest request = new OneTimeTokenAuthenticationRequest(authentication.getName());
		return oneTimeTokenService.generate(request);
	}

}
@GetMapping("/ott/generate")
public String generateOtt(OneTimeToken oneTimeToken, Model model) {
	model.addAttribute("oneTimeToken", oneTimeToken);
	return "ott-generate";
}

We could also provide an additional annotation to allow customizing which OneTimeTokenService to use if the application has more than one.

@GetMapping("/ott/generate")
public String generateOtt(@GenerateOneTimeToken(oneTimeTokenServiceBeanName = "deviceOneTimeTokenService") OneTimeToken oneTimeToken, Model model) {
	model.addAttribute("oneTimeToken", oneTimeToken);
	return "ott-generate";
}

marcusdacoregio avatar Aug 16 '24 15:08 marcusdacoregio

An additional idea is to provide a HandlerMethodArgumentResolver that could detect OneTimeToken parameters annotated with @ConsumedOneTimeToken, it would:

  1. Use an AuthenticationConverter to create a OneTimeTokenAuthenticationToken
  2. Pass the OneTimeTokenAuthenticationToken to the OneTimeTokenService#consume
  3. Resolve the consumed OneTimeToken as the method argument

Then users would have total control over what to do with the consumed OneTimeToken without the whole burden of consuming it manually.

This idea came from a scenario where I don't want to authenticate the current session, instead, I'd like the session that originated the OTT authentication request. Currently, one has to configure several components to make sure that the OTT contains the session id and the session gets authenticated. While it's too much configuration it does not feel optimal as well.

marcusdacoregio avatar Aug 16 '24 18:08 marcusdacoregio

Related https://github.com/spring-projects/spring-security/pull/15492#discussion_r1729166058

marcusdacoregio avatar Aug 27 '24 13:08 marcusdacoregio

Hi @marcusdacoregio. Can I take this to work?

franticticktick avatar Sep 16 '24 14:09 franticticktick