Solved: the issue was not with the token replacement or config reading, but rather with the class the json was bound to.
public string storageUri = "#{storageUri}#";
is correctly replaced during startup, but not after injection.
public string storageUri { get; set; } = "#{storageUri}#"
is correctly replaced every time. Thanks to Miao Tian-MFST for helping me find this.