diff --git a/docs/examples/external_auth.md b/docs/examples/external_auth.md new file mode 100644 index 000000000..8df6d906f --- /dev/null +++ b/docs/examples/external_auth.md @@ -0,0 +1,193 @@ +## External Authentication + +API clients authenticate with [JSON Web Tokens](http://jwt.io). +PostgREST does not support any other authentication mechanism +directly, but they can be built on top. In this demo we will build +a system that works with an external authentication server +and integrates with a PostgREST server by sharing the same JWT secret. + +For a better understanding of JWT and PostgREST authentication system you should read +the [User Management](users/) example as well. + +I'll use a [Rails](http://rubyonrails.org) application using [Devise](https://github.com/plataformatec/devise) +just to make the example more concrete, but this could be replicated for +any other external authentication system using the same principles. +In case Rails is not your cup of tea you can continue reading and +just skip the Ruby code samples. I'll also assume +the use of JQuery for some client-side code samples for the sake of simplicity. + +I won't delve into Devise authentication details, for this would require a tutorial on its own, +so I'm assuming that the reader's authentication system is already working. + +### Sharing the JWT Secret + +Allowing a third party to generate valid JWTs for your PostgREST API +is just a matter of sharing a secret. So you need to give your authenticator +software the same secret that was used in your API server under the ```--jwt-secret``` +parameter. + +This could be done easly using environment variables. You set a ```JWT_SECRET``` variable +in the environment where you run your rails app and it will be accessible in the global +variable ```ENV['JWT_SECRET']```. + +### User Model + +We will map each user in this example to two database roles. +So our application users are either ```admin``` or ```customer```. +If they are just visitors (not logged in) to our website they will be ```anonymous```. +One way of mapping users is to add a field in our users table indicating their database role. +I'll add a text field called role to my users table: + +```sql +ALTER TABLE users ADD role text NOT NULL DEFAULT 'customer'; +``` + +Besides the main user that PostgREST uses to connect to PostgreSQL +and the anonymous user, we will need two aditional roles for our example: + +* admin - to be used by users that access all the system rows. +* customer - to be used when user has restricted access to database rows. + +Bellow we have the commands to create all roles that will be used: +```sql +CREATE USER authenticator NOINHERIT; +CREATE ROLE anonymous; +CREATE ROLE admin; +CREATE ROLE customer; + +GRANT customer, admin, anonymous TO authenticator; +``` + +### Generating a JWT + +Several libraries are available to generate JWT, you will find a very handy list in [their website](http://jwt.io) +under **Libraries**. +To continue our Rails example I'll use the ruby library [json_web_token](https://github.com/garyf/json_web_token). + +In order to make the gem available in my Rails project I add the following line to my Gemfile: + +``` +gem 'json_web_token' +``` + +Then we create a Rails controller to serve JWTs for my authenticated users. +For this I just open a file ```app/controllers/api_tokens_controller.rb``` with the content: + +```ruby +class ApiTokensController < ApplicationController + TOKEN_TTL = 1.hour + + def show + unless ENV['JWT_SECRET'].present? + return render json: {error: "you need to have JWT_SECRET configured to get an API token"}, status: 500 + end + + unless current_user.present? + return render json: {error: "only authenticated users can request the API token"}, status: 401 + end + + expires_in TOKEN_TTL, public: false + render json: {token: jwt}, status: 200 + end + + private + def jwt + JsonWebToken.sign(claims, key: ENV['JWT_SECRET']) + end + + def claims + # This token will expire 1 hour after being issued + { + role: current_user.role, + user_id: current_user.id.to_s, + exp: (Time.now + TOKEN_TTL).to_i + } + end +end +``` + +
Token Time to Live
+
+ In the code above we leverage the HTTP time based cache headers to expire the
+ endpoint cache at the same time as the token. In this example we have a token
+ that will be refresh one hour after its issuing time.
+ That's why both are based on the TOKEN_TTL constant.
+
DRY priviledge checking conditions
+
+ You can encapsulate conditions that will be commonly used to check for privileges while reading a database row.
+ We used a function current_user_id() but we could add more conditions to functions
+ as the system becomes more complex.
+ Remeber to mark your functions as STABLE so that PostgreSQL can inline then while planning the query.
+