-
Notifications
You must be signed in to change notification settings - Fork 645
Allow users to create multiple API tokens #697
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
Conversation
This is my first time using Ember.js, so if I've completely misunderstood how some parts of this should be structured just give me a pointer to how it should be structured and I'll fix it up. The big thing that seems the most likely to be non-idiomatic to me is manually fiddling around with the |
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.
Just started a review but gtg. I'll look at more later!
CREATE TABLE api_tokens ( | ||
id SERIAL PRIMARY KEY, | ||
user_id integer NOT NULL REFERENCES users(id), | ||
token character varying DEFAULT random_string(32) NOT NULL 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.
😰 oh... oh my... there's a possibility in the current code that two users could have the same api_token.... probably pretty remote but it's still there......... good thing this code fixes it!
I suppose if random_string creates a duplicate that creating a new token would just fail? And the user would try again? Probably fine.
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 random_string function is using a 62 character dictionary, giving a 32 character string ~190 bits of randomness. That makes it more collision resistant than a UUID, and UUIDs are already collision resistant enough that you're recommended not to ever bother checking for a collision.
Using an approximation to the birthday problem the chance of having two colliding tokens in the first 10,000 users is ~2.2×10-50, if every person on Earth were to create two accounts the chance of any two colliding is ~5×10-38.
But yeah, if random_string
somehow does create a duplicate it would raise a constraint violation, which would presumably be returned as an error in token::new
, which I assume conduit
or some sort of middleware would transform to a 500 error response. Once I re-add the client-side error handling that should then be shown to user somehow, even if it's just a generic please try again message.
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.
This is super niche, but can we use text
instead of varchar
here? They're identical under the hood, but are considered different types and specifically have conversion issues if you were to use array_agg
or similar.
The screenshots look awesome!!! Some thoughts:
I don't think this is worth doing, no. Honestly, the only time I use down migrations are in development. To make production a bit less risky, though, could you remove the line in the up migration that drops the
I think empty string, that you have here, is fine. |
This sounds like a good idea.... and something that I don't think we're doing anywhere right now, oops.... Along these lines, wdyt about putting a limit on the number of tokens one user can have? Something high by reasonable standards (100? 500? 1000?) but not unlimited? |
What's the reason to require a token name? We're going to have tokens with empty string names in the database from the current tokens anyway, and if people don't want to name their tokens that's their choice. |
I think it is actually checked everywhere right now, Actually, looking at those
Sure, that should be easy to add.
Since the actual token value is only returned during creation the only distinguishing factor for unnamed tokens is the date they were created. I guess it's not strictly necessary, but it seems better UX to force the user to add something there so they can distinguish them later. GitHub even goes so far as to disallow having duplicate names for its tokens. |
I've been thinking about
what's your opinion on adding something really simple like enum AuthenticationMode {
SessionCookie,
ApiToken,
}
impl<'a> RequestUser for Request + 'a {
fn auth_mode(&self) -> CargoResult<AuthenticationMode> {
self.extensions().find::<AuthenticationMode>().chain_error(|| Unauthorized)
}
} and setting it during the It probably wouldn't scale for any other permissions, but if API tokens ever get limited permissions those could be reused for denying API tokens to create new tokens and this could be dropped. |
55c5582
to
b4c8190
Compare
I'm pretty happy with where this is now, I'll squash everything that's there now down to a single commit tonight (~10 hours from now); I don't think there's anything really useful in the history. Then I'll take another look at the two TODOs on the diesel code for the user lookup by api token, but the rest of it is done other than review feedback. |
f29e2a4
to
e8381df
Compare
Hi @Nemo157, sorry this has taken me so long to look at but I'll be reviewing it in the next few days hopefully! |
I just rebased this onto master, there was only one significant change which was that the new |
Thank you and I'm sorry this has taken me so long!!! I'm trying this out right now :) |
Fixes rust-lang#688 Summary of changes: * Database: * Add new `api_tokens` table that stores the named tokens, their creation date and last used date * API: * Remove singular `api_token` field from `User`, it is still in the database for easy rollback, but should be removed from there as well soon. * Remove existing `/me/reset_token` endpoint. * Update user middleware to search incoming tokens via the `api_tokens` table, updating the last used date as it does so, and to record whether the current request was authenticated via an API token or a session cookie * This also changes this search to function via `diesel` where it was previously using `pg` * Add new set of endpoints at `/me/tokens` allowing creating, listing and revoking API tokens * Listing tokens doesn't return the tokens value, that's only available once in the response from creating the token. * Tests * Frontend: * Remove the special support for an `api_token` parameter coming back from the `/me` endpoint * Add new `api-token` model * Has a special `adapter` as the endpoints are namespaced under `/me/tokens` instead of the default `/api/v1/api_tokens` * Has a special `serializer` to set the key used when creating a new API token * Update `/me/index` controller and template * Render a sorted list of all API tokens in the data store instead of the single token * Remove button to reset the token * Add button to start creating a new API token * Add new `api-token-row` component for rendering the API tokens in the list on `/me/index` * Has three major states: * New token that has yet to be saved to the API, shows a text box to enter a name and button to save the token * Newly saved token, shows the normal token data + the actual token value and details on how to save it with cargo * Normal token, shows the name, creation date and last used date along with a button to revoke * In any of those states can have an error message shown below the token data if creating or revoking the token failed * Some small general CSS changes: * A small variant for buttons that reduces the padding * Support for disabled state on buttons making them a little lighter than the specified color
20d85b0
to
e5e1b33
Compare
Ok 2 small things:
Everything seems to be working great with the permissons and revoking the tokens! |
Also we added rustfmt, so I rebased and ran it on your changes, which should get the build passing again :) |
Cool, made those changes and re-ran the migration to check it worked. |
Hmmmmm ok one more thing: I see you have |
autofocus on injected elements doesn't work in all browsers
That is odd, it autofocuses for me with Safari 9.1.2 (I know, I can't update) but doesn't with Chrome 51.0.2704.10 or 59.0.3071.104 on macOS. By the sounds of turbolinks/turbolinks-classic#365 (and its fix) Firefox (and now Chrome) don't autofocus on injected elements. Added manual focussing to support browsers that don't autofocus. |
@@ -132,7 +143,6 @@ table! { | |||
id -> Int4, | |||
email -> Nullable<Varchar>, |
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.
This made me realize that we're using varchar
all over the place. 😓
|
||
/// Converts this `ApiToken` model into an `EncodableApiToken` for JSON | ||
/// serialization. | ||
pub fn encodable(self) -> EncodableApiToken { |
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.
Is this method separate from encodable_with_token
for security reasons? What do you think about separating them into separate types to make future mistakes less likely, and further clarify intent?
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.
Sort of for security reasons. The HTTP API is following the common practice of only returning the token value once when creating the token. I'm pretty ambivalent about separating this into two types or not; if there were a way to specify one as a superset of the other + an extra field then it would definitely be the better way to go, but since it currently requires manual synchronisation if a field is added I see downsides to both approaches.
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.
Do you think fields are likely to be added to this type frequently enough that we need to be concerned about synchronization?
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.
No, I'll separate them out now so we can at least see what that looks like.
src/token.rs
Outdated
} | ||
} | ||
|
||
struct BadRequest<T: CargoError>(T); |
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.
Should this be in a file focused on HTTP responses?
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.
Also -- reading further down, this is only ever passed human
. Should it just take a String
instead until there's evidence that it needs to take something else?
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's a bit of a hack to support the Ember frontend. My preference would be to drop this and just have human
return errors with the status code BadRequest
, but cargo
does not handle that well. I could move this to util::errors
and add a comment about what it's intended use is.
src/token.rs
Outdated
|
||
/// Handles the `GET /me/tokens` route. | ||
pub fn list(req: &mut Request) -> CargoResult<Response> { | ||
let tokens = ApiToken::find_for_user(&*req.db_conn()?, req.user()?.id)? |
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.
What do you think about pulling these out into local variables? I personally find the condensed punctuation hard to read
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.
Sure.
src/token.rs
Outdated
/// The incoming serialization format for the `ApiToken` model. | ||
#[derive(RustcDecodable, RustcEncodable)] | ||
struct NewApiToken { | ||
pub name: 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.
Nit: Does pub
do anything 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.
Nope, not sure where that came from...
src/user/mod.rs
Outdated
.map(|r| Model::from_row(&r)) | ||
.chain_error(|| NotFound) | ||
pub fn find_by_api_token(conn: &PgConnection, token_: &str) -> CargoResult<User> { | ||
// TODO: This should be doable as a single query, but I can't figure |
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.
What's the SQL that you would write to do this as a single query? I don't see any way that you could do this in one query.
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.
UPDATE api_tokens
SET last_used_at = now()
FROM users
WHERE api_tokens.token = $1
AND users.id = api_tokens.user_id
RETURNING users.*
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.
Hm intersting. I didn't realize you could set FROM
on an update like that. I'll look into providing an API. I think we can ✂️ this comment though, the answer is "you can't"
@@ -419,54 +391,3 @@ pub fn updates(req: &mut Request) -> CargoResult<Response> { | |||
meta: Meta { more: more }, | |||
})) | |||
} | |||
|
|||
#[cfg(test)] |
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.
Does this PR make these tests invalid? It doesn't seem like the behavior they're testing has changed, and I'm not sure where these tests moved to
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.
Nope, I can readd and update them. They just seemed very low value tests with the tokens pulled out to be a separate resource.
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.
Looking at the commit which added them they seem to be testing something very specific which is still relevant 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.
Actually, I believe the intent of the first test is covered under create_token_multiple_have_different_values
in src/test/token
. The second one I can try and come up with an equivalent with the new data structure.
I'm really really really sorry this has taken me so long. I'm feeling ready to get this merged in-- just testing it out one last time ❤️ |
Merged and deployed to prod, it seems to be doing well!! I'm so excited about this change, thank you so much for your patience!!! |
Fixes #688
Summary of changes:
Database:
api_tokens
table that stores the named tokens, their creation date and last used dateAPI:
api_token
field fromUser
, it is still in the database for easy rollback, but should be removed from there as well soon./me/reset_token
endpoint.api_tokens
table, updating the last used date as it does so, and to record whether the current request was authenticated via an API token or a session cookiediesel
where it was previously usingpg
/me/tokens
allowing creating, listing and revoking API tokensFrontend:
api_token
parameter coming back from the/me
endpointapi-token
modeladapter
as the endpoints are namespaced under/me/tokens
instead of the default/api/v1/api_tokens
serializer
to set the key used when creating a new API token/me/index
controller and templateapi-token-row
component for rendering the API tokens inthe list on
/me/index
old description
WIP for #688, still has a bit of work to go, but it's currently working and stylish enough that feedback would be useful.I took inspiration from GitHub's "Personal Access Tokens" around showing the token value only once along with the last usage of the token.
TODOs
Put one of the existing api tokens back in when reverting the SQL migration (not really sure if this is worth doing)user.api_token
column(need something to set as the name of the original token)Warning before revocationlast_used_at
changes when using tokenScreenshots
The settings page

Creating a new token

Showing the newly created token
