Merge pull request #432 from diogob/external_auth_example
External auth example
This commit is contained in:
@@ -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
|
||||||
|
```
|
||||||
|
|
||||||
|
<div class="admonition note">
|
||||||
|
<p class="admonition-title">Token Time to Live</p>
|
||||||
|
<p>
|
||||||
|
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 <code>TOKEN_TTL</code> constant.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
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();
|
||||||
|
```
|
||||||
|
|
||||||
|
<div class="admonition note">
|
||||||
|
<p class="admonition-title">DRY priviledge checking conditions</p>
|
||||||
|
<p>
|
||||||
|
You can encapsulate conditions that will be commonly used to check for privileges while reading a database row.
|
||||||
|
We used a function <code>current_user_id()</code> but we could add more conditions to functions
|
||||||
|
as the system becomes more complex.<br/>
|
||||||
|
Remeber to mark your functions as <code>STABLE</code> so that PostgreSQL can inline then while planning the query.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### 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.
|
||||||
@@ -23,4 +23,5 @@ pages:
|
|||||||
- Examples:
|
- Examples:
|
||||||
- Getting Started: examples/start.md
|
- Getting Started: examples/start.md
|
||||||
- User Management: examples/users.md
|
- User Management: examples/users.md
|
||||||
|
- External Authentication: examples/external_auth.md
|
||||||
- Multi-Tenant Blog: examples/blog.md
|
- Multi-Tenant Blog: examples/blog.md
|
||||||
|
|||||||
Reference in New Issue
Block a user