Skip to content
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

Merged
merged 7 commits into from
Apr 1, 2021

Conversation

djrodgerspryor
Copy link
Contributor

@djrodgerspryor djrodgerspryor commented Feb 4, 2021

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 thus Authenticator.token() now require &mut self, so caller will need to store the Authenticator with in a mutable variable.

@ggriffiniii
Copy link
Collaborator

Regarding the change to &mut self, I would be interested to hear the use case where that's useful, but it seems overly limiting to me. Prior to this change Autheticator::token could be called from multiple threads concurrently, but with this change that will no longer be the case. I suspect that api should continue to accept &self, especially since TokenStorage requires Send+Sync (which I agree it should). This will presumably require most impl's of TokenStorage use some form of interior mutability, but I would suspect they will already need to do that in order to satisfy the Send+Sync bound.

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.
@djrodgerspryor
Copy link
Contributor Author

Yep, good call: interior mutability is a better fit. I've changed the API back to not requiring mutable references and used an RwLock in the example.

@dermesser
Copy link
Owner

This looks quite good to me, I asked @ggriffiniii to provide a review and then we can merge.

@@ -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 {
Copy link
Collaborator

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.

Copy link
Owner

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,
Copy link
Collaborator

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.

Copy link
Owner

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/error.rs Show resolved Hide resolved
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<()>;
Copy link
Collaborator

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.

Copy link
Owner

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)
Copy link
Collaborator

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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

@dermesser
Copy link
Owner

It seems that what's left here are relatively easily addressed comments, so I hope it will be ready for merge soon!

@dermesser
Copy link
Owner

@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!

@djrodgerspryor
Copy link
Contributor Author

The nitpicking is great 😄

I should have time to come back to this in a few days

@chrisabruce
Copy link

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.
@djrodgerspryor
Copy link
Contributor Author

@dermesser and @ggriffiniii all suggestions implemented: please take a look!

Copy link
Collaborator

@ggriffiniii ggriffiniii left a 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.

src/storage.rs Outdated Show resolved Hide resolved
.iter()
.map(|scope| scope.as_ref())
.sorted()
.unique()
Copy link
Collaborator

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.

Copy link
Contributor Author

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.

Copy link
Owner

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();
Copy link
Collaborator

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.

Copy link
Owner

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();
Copy link
Collaborator

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

@dermesser
Copy link
Owner

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?

@dermesser dermesser merged commit e63aa4b into dermesser:master Apr 1, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants