3

On insert, how should I uniquely set the value of access_token to a random string and return that value to the client?

I have:

CREATE TABLE user (
    id             serial PRIMARY KEY,
    access_token   char(32) NOT NULL UNIQUE,
    joined         timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP
);

Database Server: PostgreSQL
Application Server: Sinatra (Ruby)
Client: iOS

Community
  • 1
  • 1
ma11hew28
  • 121,420
  • 116
  • 450
  • 651
  • 1
    Why is this a Ruby question? – the Tin Man Feb 10 '14 at 14:54
  • Have you considered triggers? When an insert happens a trigger can be defined such that the `access_token` is set to a random string and that value be returned? See http://www.postgresql.org/docs/9.3/static/sql-createtrigger.html –  Feb 10 '14 at 14:58
  • @theTinMan because I'm not sure which part of the [solution stack](http://en.wikipedia.org/wiki/Solution_stack) should do this. – ma11hew28 Feb 10 '14 at 15:09
  • Related question except doesn't require uniqueness: http://dba.stackexchange.com/questions/19632/how-to-create-column-in-db-with-default-value-random-string – ma11hew28 Feb 10 '14 at 15:10
  • 3
    What about using a UUID and a default value on the column? http://www.postgresql.org/docs/current/static/datatype-uuid.html –  Feb 10 '14 at 15:29
  • @user102890 thanks for informing me about triggers. I think, however, that a default value on the column, as suggested by `a_horse_with_no_name`, seems like a simpler solution. – ma11hew28 Feb 10 '14 at 23:22

2 Answers2

11

After reading the Ruby on Rails Security Guide and studying how other successful apps do it, I figured out a better way to generate access tokens.

Old Thinking

Before asking this question, having played with the Facebook Graph API, I knew I wanted to be able to make a RESTful request like:

GET /me?access_token=4976517fd7814040b2083864973ff422

and from the access_token, the server would be able to return the information about me.

Therefore, I thought the access_token had to be UNIQUE so that the server could do something like:

SELECT * FROM users WHERE access_token = '4976517fd7814040b2083864973ff422'

New Thinking

Let's look at some examples of access tokens from other successful APIs.

Facebook: 50601675619|5Lfrygz7QmiNVSgkvryzixIVHuo (App Token)
Twitter: 191074378-1GWuHmFyyKQUKWV6sR6EEzSCdLGnhqyZFBqLagHp
Instagram: fb2e77d.47a0479900504cb3ab4a1f626d174d2d
GitHub: e72e16c7e42f292c6912e7710c838347ae178b4a

Facebook & Twitter both clearly prefix a user's access token with his ID. Instagram appears to do the same, just encoded. Such an access token essentially has two parts: user ID & session ID.

An access token with the user ID embedded allows the server to:

SELECT * FROM users WHERE id = '50601675619'

Then, it can simply check that the second part of the access token matches the session ID stored in the database for that user, similar to validating a username & password during log-in.

Final Solution

The second part of Instagram's access token appears to be a lowercased version 4 UUID without hyphens. So, I'll do the same by following the comment by @a_horse_with_no_name & answer by @ClodoaldoNeto. Except, I'll rename the column access_token to session_id and remove the UNIQUE constraint.

Community
  • 1
  • 1
ma11hew28
  • 121,420
  • 116
  • 450
  • 651
5

As commented above use the uuid type

create table user_table (
    id             serial primary key,
    access_token   uuid default uuid_generate_v4() not null unique,
    joined         timestamp with time zone not null default current_timestamp
);

Then insert the default values

insert into user_table default values
returning access_token;
             access_token             
--------------------------------------
 341ab75c-6b4e-4df0-a2ea-5148434fce5a

To be able to use the uuid functions it is necessary to install the uuid-ossp extension as superuser in the target database

create extension "uuid-ossp";

You can use the uuid functions from Ruby or if they don't exist from somewhere else.


Now reading your comments it looks like you are after a session ID.

A session ID could be generated using a UUID generator. But notice that a session ID is just an ID for an certain session. It will change every time the user logs in. The session ID should not be stored in the user's table.

In general, sessions are handled by the application using some framework provided module. That module will have its own means to store the session ID and the session data. In general the session ID goes in a cookie, not in the URL. The application will pass a salt to the session manager and it will take care of generating the session ID and keeping its state, be it in a cookie, in the URL, in the page, whatever. The module will store the session data where it is configured to do, memory, disk file, database.

So don't do session management yourself. Let it to the framework's solution. Much simpler and very important, much safer.

Clodoaldo Neto
  • 118,695
  • 26
  • 233
  • 260
  • Thanks! How can I guarantee that the generated UUID will be unique among all users' access tokens? I do not want to have to deal with the error caused by potentially inserting a duplicate `access_token`. One reason I don't like this error is that it increments the `serial` even though nothing is inserted, causing gaps in the `id`. – ma11hew28 Feb 10 '14 at 21:02
  • @MattDiPasquale: ids generated by a sequence a **completely** meaningless and therefor gaps in a sequence are meaningless as well. UUIDs should be unique enough for your use case - especially if only created on a single system. –  Feb 10 '14 at 23:10
  • @a_horse_with_no_name I believe [nothing is meaningless](https://www.facebook.com/mattdipasquale/posts/10100218289048796?stream_ref=5) and that anything is possible. But anyway, I figured out a better implementation that doesn't require the UUIDs to be unique. Thank you for your help! :-) – ma11hew28 Feb 10 '14 at 23:34
  • @MattDiPasquale: well gaps in a sequence **are** meaningless. If you are concerned about them, then you misunderstood what they do: *generate unique numbers* - that, and only that. –  Feb 10 '14 at 23:39
  • @a_horse_with_no_name well, one thing I don't like about gaps is that they can significantly reduce the number of rows you can insert in your table if you're not careful. For example, if you use `smallserial` (32767 max), but there are 10000 insert errors because a few idiots keep trying to sign up with already-taken usernames, then you will only be able to insert 22767 users into your table instead of 32767. – ma11hew28 Feb 10 '14 at 23:57
  • @a_horse_with_no_name Also, I want the user IDs to mean, like they kind of do on Facebook, that that user was (at least about) the nth user to join. I remember when Facebook got popular, my friends and I would compare user IDs to brag about who joined first. :-) I think that's cool. – ma11hew28 Feb 10 '14 at 23:59
  • @MattDiPasquale: if you are concerned about the number of inserts then why use a `smallserial` in the first place? Use a `bigserial` and you'll [never run out of numbers](http://stackoverflow.com/a/13133035/330315). Limiting yourself to 32767 rows is questionable to begin with. –  Feb 11 '14 at 00:04
  • @a_horse_with_no_name I figured `serial` is more efficient (smaller & faster) than `big serial` and that I could always `ALTER` `integer` to `bigint` if the app ever got more popular than Facebook is today. – ma11hew28 Feb 11 '14 at 00:06
  • 1
    @MattDiPasquale: if you are expecting only 32768 users that it won't make any difference if you use smallint, int, or bigint. –  Feb 11 '14 at 00:07
  • @a_horse_with_no_name maybe my wanting user ID to sort of also mean what number user you were to join the app means I should be using something other than `serial`. I'm new to PostgreSQL and a novice at SQL in general. So, I might not be doing everything correctly. – ma11hew28 Feb 11 '14 at 00:08
  • @a_horse_with_no_name "I expect nothing. I fear no one. I am free." — Nikos Kazantzakis – ma11hew28 Feb 11 '14 at 00:10
  • Still, you make a good point. Perhaps you've convinced me to use `big serial` instead of `serial`. – ma11hew28 Feb 11 '14 at 00:14
  • @MattDiPasquale A UUID is a 16 bytes number. Large enough to be considered unique. Even with millions of users your chance of a collision is very small. Check my update answer – Clodoaldo Neto Feb 11 '14 at 11:24
  • @ClodoaldoNeto thank you for the updated answer. That helped a lot. But, I also need to connect the user to the Node.js WebSocket chat server, which is separate from the RESTful Sinatra server. So, I think I'll need an OAuth2 access token. Maybe I'll make a table called `clients` and store the `client_id` & `client_secret` in there. The iPhone app can be one client and the web app can be another. I guess I could store users' access tokens in Redis. Or, maybe I should just use an existing [OAuth2 authorization server](https://github.com/jaredhanson/oauth2orize). – ma11hew28 Feb 12 '14 at 03:50