-
Notifications
You must be signed in to change notification settings - Fork 110
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Custom token storage #146
Custom token storage #146
Conversation
Regarding the change to |
Allow users to build their own token storage system by implementing the `TokenStorage` trait. This allows use of more secure storage mechanisms like OS keychains, encrypted files, or secret-management tools. Custom storage providers are Box-ed to avoid adding more generics to the API — the indirection cost will only apply if using a custom store. I've added `anyhow` to allow easy handling of a wide range of errors from custom storage providers.
Instead, suggest using interior mutability (and RwLock in the example) to manage storage of token states. This makes it easier to share authenticators between threads.
1a76032
to
384963e
Compare
Yep, good call: interior mutability is a better fit. I've changed the API back to not requiring mutable references and used an |
This looks quite good to me, I asked @ggriffiniii to provide a review and then we can merge. |
src/authenticator.rs
Outdated
@@ -236,6 +237,14 @@ impl<C, F> AuthenticatorBuilder<C, F> { | |||
} | |||
} | |||
|
|||
/// Use the provided token storage mechanism | |||
pub fn with_storage(self, storage_type: StorageType) -> Self { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would consider if we would prefer to accept a Box<dyn TokenStorage>
here rather than StorageType. There is already a way to choose a Disk storage with persist_tokens_to_disk
and Memory is the default behavior so there isn't much to be gained by exposing StorageType at this time unless we feel like there's a potential for more variants in the future and we want to provide this more general purpose method now to eliminate the need for continuing to expand the number of methods for each variant.
I personally think it's probably unlikely that we'll need many more variants especially since the one being added here allows the utmost flexibility.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 - take the Box and wrap it into StorageType::Custom()
src/types.rs
Outdated
@@ -56,7 +56,7 @@ impl From<TokenInfo> for AccessToken { | |||
/// It authenticates certain operations, and must be refreshed once | |||
/// it reached it's expiry date. | |||
#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] | |||
pub(crate) struct TokenInfo { | |||
pub struct TokenInfo { | |||
/// used when authenticating calls to oauth2 enabled services. | |||
pub(crate) access_token: String, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It probably doesn't make a lot of sense to keep these attributes private to the crate if this struct is now exposed. That just unnecessarily limits users to rely on the serde serialization.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, public TokenInfo
fields helped me debug things before, if the struct is public, let's make the fields public too :)
src/storage.rs
Outdated
/// Store a token for the given set of scopes so that it can be retrieved later by get() | ||
/// ScopeSet implements Hash so that you can easily serialize and store it. | ||
/// TokenInfo can be serialized with serde. | ||
async fn set(&self, scopes: ScopeSet<'_, &str>, token: TokenInfo) -> anyhow::Result<()>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think I would make ScopeSet public, instead opting to just provide scopes: &[&str]
for both get and set. ScopeSet is really designed to make the current implementation of Disk and Memory efficient and doesn't seem very widely applicable outside those contexts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
src/storage.rs
Outdated
Storage::Custom(custom_storage) => { | ||
let str_scopes: Vec<_> = scopes.scopes.iter().map(|scope| scope.as_ref()).collect(); | ||
|
||
(*custom_storage) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the deref of custom_storage is unnecessary.
src/storage.rs
Outdated
Storage::Custom(custom_storage) => { | ||
let str_scopes: Vec<_> = scopes.scopes.iter().map(|scope| scope.as_ref()).collect(); | ||
|
||
(*custom_storage) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same here
It seems that what's left here are relatively easily addressed comments, so I hope it will be ready for merge soon! |
@djrodgerspryor Hey Daniel, are you tired of all the nitpicking, or do you still have this PR in mind? :) Like I said, it's a really important feature, so I'd be inconsolable to see us having driven you away! |
The nitpicking is great 😄 I should have time to come back to this in a few days |
What's left to get this ready to merge? |
Just pass `&[&str]` into custom storage providers. The scopeset struct has a range of unnecessary internal features. It's now also part of the interface for custom storage providers that the given scopes will be both unique and sorted. The only slightly awkward thing is that there's no conventient way to expose a `scopes_covered_by` helper method (which almost all custom storage engines will need), but it's still included in the example code.
For easier debugging, and for implementing custom storage solutions which might not use serde.
By only allowing a custom storage. To use one of the built-in storage mechanism, there is already a special-purpose `persist_tokens_to_disk` method available.
@dermesser and @ggriffiniii all suggestions implemented: please take a look! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall, this looks good to me. Just a single nitpick about sort/uniq within get/set and a larger question for @dermesser that would require a public api change to avoid the unfortunate allocations/copies of the scopes slice when using custom token storage.
.iter() | ||
.map(|scope| scope.as_ref()) | ||
.sorted() | ||
.unique() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wouldn't bother with sorting or unique here. Many implementations will not benefit from sort/uniq and those that do can do it within their TokenStorage::{get,set} without it being much more expensive than what's being done here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I disagree: without this, the interface isn't well defined and it'll be easy for implementers to mess-up.
Eg. can get(['read', 'write'])
return a different result from get(['write', 'read'])
? What about get(['read', 'write', 'read'])
?
Actually, my preference would probably be to give a Scopes
struct which takes this invariance into account in its Eq
and Hash
implementations and could implement a helpful .covers
method like the scopes_covered_by
method in the example.
That won't help the allocation problem though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
giving unique keys from yup-oauth2 to custom storage providres seems nice towards implementers, at a small cost. So I'm ok with how it is at the moment :)
.map(|scope| scope.as_ref()) | ||
.sorted() | ||
.unique() | ||
.collect(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The allocation here is unfortunate. Comment for @dermesser: It may be worth considering changing the public API so that scope lists are passed as &[&str]
rather than generic &[T] where T: AsRef<str>
. I liked the latter when it was possible to do it without unnecessary overhead, but if it's causing the implementation to copy the scope list on every call to get a new token (as it now will when using a custom storage token) it's probably not worth the usability benefit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
well, let's be realistic: We are dealing with -- sometimes intercontinental -- network calls here, over a HTTP2 connection with JSON. And then possibly seconds for an API response that is tens to hundreds of times bigger than the scope strings.
A couple allocations for short strings are acceptable here, IMHO, given the nice properties of &[T] where T: AsRef<str>
.
.map(|scope| scope.as_ref()) | ||
.sorted() | ||
.unique() | ||
.collect(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same comments from get() apply here
I'm good with this and will merge it as beta -- if there are more improvements, we can deal with it in other PRs. What do you think, is this ok as a sub-version bump (5.2), as it only extends enums as far as I can see -- or should it be v6.0? |
Related to: #145
Allow users to build their own token storage system by implementing the
TokenStorage
trait. This allows use of more secure storage mechanisms like OS keychains, encrypted files, or secret-management tools.Custom storage providers are Box-ed to avoid adding more generics to the API — the indirection cost will only apply if using a custom store.
I've added
anyhow
to allow easy handling of a wide range of errors from custom storage providers.As discussed in the linked issue, this approach uses boxing to extend the existing storage enum and minimise API changes.
The one significant API change is that
Storage.set
and thusAuthenticator.token()
now require&mut self
, so caller will need to store theAuthenticator
with in a mutable variable.