So finally I implemented it :)
It uses 'fallBackToNext', a method we already have in our codebase that behave as fallBackTo but with an async lambda parameter, so the next future is executed only if the first one is already a failure (preventing big computations from happening when not needed, but reducing parallelism).
Here are most of the logic:
/**
 * This combination of action is the implementation class of the "orElse" operator,
 * allowing to have one and only one action to be executed within the given actions
 */
class EitherActions[A](actions: Seq[Action[A]]) extends Action[A] {
  require(actions.nonEmpty, "The actions to combine should not be empty")
  override def parser: BodyParser[A] = actions.head.parser
  override def executionContext: ExecutionContext = actions.head.executionContext
  /**
   * @param request
   * @return either the first result to be successful, or the first to be failure
   */
  override def apply(
      request: Request[A]
  ): Future[Result] = {
    // as we know actions is nonEmpty, we can start with actions.head and directly fold on actions.tail
    // this removes the need to manage an awkward "zero" value in the fold
    val firstResult = actions.head.apply(request)
    // we wrap all apply() calls into changeUnauthorizedIntoFailure to be able to use fallbackToNext on 403
    val finalResult = actions.tail.foldLeft( changeUnauthorizedIntoFailure(firstResult) ) {
      ( previousResult, nextAction ) =>
        RichFuture(previousResult).fallbackToNext{ () =>
          changeUnauthorizedIntoFailure(nextAction.apply(request))
        }(executionContext)
    }
    // restore the original message
    changeUnauthorizedIntoSuccess(finalResult)
  }
  /**
   * to use fallBackToNext, we need to have failed Future, thus we change the Success(403) into a Failure(403)
   * we keep the original result to be able to restore it at the end if none of the combined actions did success
   */
  private def changeUnauthorizedIntoFailure(
      before: Future[Result]
  ): Future[Result] = {
    val after = before.transform {
      case Success(originalResult) if originalResult.header.status == Unauthorized =>
        Failure(EitherActions.UnauthorizedWrappedException(originalResult = originalResult))
      case Success(originalResult) if originalResult.header.status == Forbidden =>
        Failure(EitherActions.UnauthorizedWrappedException(originalResult = originalResult))
      case keepResult@_ => keepResult
    }(executionContext)
    after
  }
  /**
   * after the last call, if we still have a UnauthorizedWrappedException, we change it back to a Success(403)
   * to restore the original message
   */
  private def changeUnauthorizedIntoSuccess(
      before: Future[Result]
  ): Future[Result] = {
    val after = before.transform {
      case Failure(EitherActions.UnauthorizedWrappedException(_, _, result)) => Success(result)
      case keepResult@_ => keepResult
    }(executionContext)
    after
  }
  def orElse( other: Action[A]): EitherActions[A] = {
    new EitherActions[A]( actions :+ other)
  }
}
object EitherActions {
  private case class UnauthorizedWrappedException(
      private val message: String = "",
      private val cause: Throwable = None.orNull,
      val originalResult: Result,
  ) extends Exception(message, cause)
}