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. +

+
+ +We also need to create a route in the ```config/routes.rb``` file: + +```ruby +resource :api_token, only: [:show] +``` + +Now, any authenticated user in our rails application can request an api_token making a GET +request to ```/api_token```. This endpoint will return a json object with one property +whose value is the token the API requests should use. + +### Orders Endpoint + +Here is how to create a view to generate an endpoint ```/orders``` filtered by +the logged in user: + +```sql +ALTER DATABASE mydb SET postgrest.claims.user_id TO ''; + +CREATE OR REPLACE FUNCTION current_user_id() +RETURNS integer +STABLE +LANGUAGE SQL +AS $$ + SELECT nullif(current_setting('postgrest.claims.user_id'), '')::integer; +$$; + +CREATE SCHEMA private; + +CREATE TABLE private.orders ( + id serial primary key, + user_id int references users, + created_at timestamp not null default current_timestamp, + updated_at timestamp not null default current_timestamp +); + +CREATE VIEW orders AS +SELECT + id, user_id, created_at, updated_at +FROM + private.orders o +WHERE + current_user = 'admin' OR o.user_id = current_user_id(); +``` + +
+

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. +

+
+ +### Using the JWT + +Now whenever you are authenticated in your Rails application you can use some Javascript + code to get the token and use it: +```javascript +$.getJSON('/api_json').done(function(data){ + $.ajax('/orders', {'Authorization': 'Bearer ' + data.token}).done(function(data){ + console.log('Visible Orders: ', data); + }) +}).fail(function(){ + console.log('Error fetching API token'); +}) +``` +We could also store the token to avoid having to fetch it again in the same page. + +### Conclusion + +This section explained the implementation details for building an +external authentication system working with PostgREST. +With the previous [User Management](users/) example this should give a clearer +idea of how to set up authentication for your API. diff --git a/mkdocs.yml b/mkdocs.yml index 1957209d3..e012d5156 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -23,4 +23,5 @@ pages: - Examples: - Getting Started: examples/start.md - User Management: examples/users.md + - External Authentication: examples/external_auth.md - Multi-Tenant Blog: examples/blog.md