diff --git a/docs/auth.rst b/docs/auth.rst index eeb55c0b0..90df4c086 100644 --- a/docs/auth.rst +++ b/docs/auth.rst @@ -1,22 +1,47 @@ -.. _roles: +.. raw:: html -Overview of Role System -======================= +

Authentication

PostgREST is designed to keep the database at the center of API security. All authorization happens through database roles and permissions. It is PostgREST's job to **authenticate** requests -- i.e. verify that a client is who they say they are -- and then let the database **authorize** client actions. -Authentication Sequence +.. _roles: + +Overview of role system ----------------------- There are three types of roles used by PostgREST, the **authenticator**, **anonymous** and **user** roles. The database administrator creates these roles and configures PostgREST to use them. .. image:: _static/security-roles.png -The authenticator should be created :code:`NOINHERIT` and configured in the database to have very limited access. It is a chameleon whose job is to "become" other users to service authenticated HTTP requests. The picture below shows how the server handles authentication. If auth succeeds, it switches into the user role specified by the request, otherwise it switches into the anonymous role (if it's set in :ref:`db-anon-role`). +The authenticator role is used for connecting to the database and should be configured to have very limited access. It is a chameleon whose job is to "become" other users to service authenticated HTTP requests. + + +.. code:: sql + + + CREATE ROLE authenticator LOGIN NOINHERIT NOCREATEDB NOCREATEROLE NOSUPERUSER; + +.. note:: + + The names "authenticator" and "anon" names are configurable and not sacred, we simply choose them for clarity. See :ref:`db-uri` and :ref:`db-anon-role`. + +.. _user_impersonation: + +User Impersonation +------------------ + +The picture below shows how the server handles authentication. If auth succeeds, it switches into the user role specified by the request, otherwise it switches into the anonymous role (if it's set in :ref:`db-anon-role`). .. image:: _static/security-anon-choice.png -Here are the technical details. We use `JSON Web Tokens `_ to authenticate API requests. As you'll recall a JWT contains a list of cryptographically signed claims. All claims are allowed but PostgREST cares specifically about a claim called role. +This role switching mechanism is called **user impersonation**. In PostgreSQL it's done with the ``SET ROLE`` statement. + +.. _jwt_impersonation: + +JWT-Based User Impersonation +---------------------------- + +We use `JSON Web Tokens `_ to authenticate API requests. As you'll recall a JWT contains a list of cryptographically signed claims. All claims are allowed but PostgREST cares specifically about a claim called role. .. code:: json @@ -35,127 +60,22 @@ Note that the database administrator must allow the authenticator role to switch .. code:: sql GRANT user123 TO authenticator; + -- similarly for the anonymous role + -- GRANT anonymous TO authenticator; -If the client included no JWT (or one without a role claim) then PostgREST switches into the anonymous role whose actual database-specific name, like that of with the authenticator role, is specified in the PostgREST server configuration file. The database administrator must set anonymous role permissions correctly to prevent anonymous users from seeing or changing things they shouldn't. +If the client included no JWT (or one without a role claim) then PostgREST switches into the anonymous role. The database administrator must set the anonymous role permissions correctly to prevent anonymous users from seeing or changing things they shouldn't. -Users and Groups ----------------- +.. _jwt_generation: -PostgreSQL manages database access permissions using the concept of roles. A role can be thought of as either a database user, or a group of database users, depending on how the role is set up. +JWT Generation +~~~~~~~~~~~~~~ -Roles for Each Web User -~~~~~~~~~~~~~~~~~~~~~~~ - -PostgREST can accommodate either viewpoint. If you treat a role as a single user then the JWT-based role switching described above does most of what you need. When an authenticated user makes a request PostgREST will switch into the role for that user, which in addition to restricting queries, is available to SQL through the :code:`current_user` variable. - -You can use row-level security to flexibly restrict visibility and access for the current user. Here is an `example `_ from Tomas Vondra, a chat table storing messages sent between users. Users can insert rows into it to send messages to other users, and query it to see messages sent to them by other users. - -.. code-block:: postgres - - CREATE TABLE chat ( - message_uuid UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - message_time TIMESTAMP NOT NULL DEFAULT now(), - message_from NAME NOT NULL DEFAULT current_user, - message_to NAME NOT NULL, - message_subject VARCHAR(64) NOT NULL, - message_body TEXT - ); - - ALTER TABLE chat ENABLE ROW LEVEL SECURITY; - -We want to enforce a policy that ensures a user can see only those messages sent by them or intended for them. Also we want to prevent a user from forging the message_from column with another person's name. - -PostgreSQL allows us to set this policy with row-level security: - -.. code-block:: postgres - - CREATE POLICY chat_policy ON chat - USING ((message_to = current_user) OR (message_from = current_user)) - WITH CHECK (message_from = current_user) - -Anyone accessing the generated API endpoint for the chat table will see exactly the rows they should, without our needing custom imperative server-side coding. - -.. warning:: - - Roles are namespaced per-cluster rather than per-database so they may be prone to collision. - -Web Users Sharing Role -~~~~~~~~~~~~~~~~~~~~~~ - -Alternately database roles can represent groups instead of (or in addition to) individual users. You may choose that all signed-in users for a web app share the role webuser. You can distinguish individual users by including extra claims in the JWT such as email. - -.. code:: json - - { - "role": "webuser", - "email": "john@doe.com" - } - -SQL code can access claims through GUC variables set by PostgREST per request. For instance to get the email claim, call this function: - -For PostgreSQL server version >= 14 - -.. code:: sql - - current_setting('request.jwt.claims', true)::json->>'email'; - - -For PostgreSQL server version < 14 - -.. code:: sql - - current_setting('request.jwt.claim.email', true); - -This allows JWT generation services to include extra information and your database code to react to it. For instance the RLS example could be modified to use this current_setting rather than current_user. The second 'true' argument tells current_setting to return NULL if the setting is missing from the current configuration. - -Hybrid User-Group Roles -~~~~~~~~~~~~~~~~~~~~~~~ - -You can mix the group and individual role policies. For instance we could still have a webuser role and individual users which inherit from it: - -.. code-block:: postgres - - CREATE ROLE webuser NOLOGIN; - -- grant this role access to certain tables etc - - CREATE ROLE user000 NOLOGIN; - GRANT webuser TO user000; - -- now user000 can do whatever webuser can - - GRANT user000 TO authenticator; - -- allow authenticator to switch into user000 role - -- (the role itself has nologin) - -.. _custom_validation: - -Custom Validation ------------------ - -PostgREST honors the :code:`exp` claim for token expiration, rejecting expired tokens. However it does not enforce any extra constraints. An example of an extra constraint would be to immediately revoke access for a certain user. The configuration file parameter :code:`db-pre-request` specifies a stored procedure to call immediately after the authenticator switches into a new role and before the main query itself runs. - -Here's an example. In the config file specify a stored procedure: - -.. code:: ini - - db-pre-request = "public.check_user" - -In the function you can run arbitrary code to check the request and raise an exception to block it if desired. - -.. code-block:: postgres - - CREATE OR REPLACE FUNCTION check_user() RETURNS void AS $$ - BEGIN - IF current_user = 'evil_user' THEN - RAISE EXCEPTION 'No, you are evil' - USING HINT = 'Stop being so evil and maybe you can log in'; - END IF; - END - $$ LANGUAGE plpgsql; +You can create a valid JWT either from inside your database(see :ref:`sql_user_management`) or via an external service(see :ref:`external_jwt`). .. _client_auth: Client Auth -=========== +~~~~~~~~~~~ To make an authenticated request the client must include an :code:`Authorization` HTTP header with the value :code:`Bearer `. For instance: @@ -173,84 +93,20 @@ To make an authenticated request the client must include an :code:`Authorization The ``Bearer`` header value can be used with or without capitalization(``bearer``). -JWT Generation --------------- - -You can create a valid JWT either from inside your database or via an external service. Each token is cryptographically signed with a secret key. In the case of symmetric cryptography the signer and verifier share the same secret passphrase. In asymmetric cryptography the signer uses the private key and the verifier the public key. PostgREST supports both symmetric and asymmetric cryptography. - -JWT from SQL -~~~~~~~~~~~~ - -You can create JWT tokens in SQL using the `pgjwt extension `_. It's simple and requires only pgcrypto. If you're on an environment like Amazon RDS which doesn't support installing new extensions, you can still manually run the `SQL inside pgjwt `_ (you'll need to replace ``@extschema@`` with another schema or just delete it) which creates the functions you will need. - -Next write a stored procedure that returns the token. The one below returns a token with a hard-coded role, which expires five minutes after it was issued. Note this function has a hard-coded secret as well. - -.. code-block:: postgres - - CREATE TYPE jwt_token AS ( - token text - ); - - CREATE FUNCTION jwt_test() RETURNS public.jwt_token AS $$ - SELECT public.sign( - row_to_json(r), 'reallyreallyreallyreallyverysafe' - ) AS token - FROM ( - SELECT - 'my_role'::text as role, - extract(epoch from now())::integer + 300 AS exp - ) r; - $$ LANGUAGE sql; - -PostgREST exposes this function to clients via a POST request to ``/rpc/jwt_test``. - -.. note:: - - To avoid hard-coding the secret in stored procedures, save it as a property of the database. - - .. code-block:: postgres - - -- run this once - ALTER DATABASE mydb SET "app.jwt_secret" TO 'reallyreallyreallyreallyverysafe'; - - -- then all functions can refer to app.jwt_secret - SELECT sign( - row_to_json(r), current_setting('app.jwt_secret') - ) AS token - FROM ... - -JWT from Auth0 +Symmetric Keys ~~~~~~~~~~~~~~ -An external service like `Auth0 `_ can do the hard work transforming OAuth from Github, Twitter, Google etc into a JWT suitable for PostgREST. Auth0 can also handle email signup and password reset flows. - -To use Auth0, create `an application `_ for your app and `an API `_ for your PostgREST server. Auth0 supports both HS256 and RS256 scheme for the issued tokens for APIs. For simplicity, you may first try HS256 scheme while creating your API on Auth0. Your application should use your PostgREST API's `API identifier `_ by setting it with the `audience parameter `_ during the authorization request. This will ensure that Auth0 will issue an access token for your PostgREST API. For PostgREST to verify the access token, you will need to set ``jwt-secret`` on PostgREST config file with your API's signing secret. - -.. note:: - - Our code requires a database role in the JWT. To add it you need to save the database role in Auth0 `app metadata `_. Then, you will need to write `a rule `_ that will extract the role from the user's app_metadata and set it as a `custom claim `_ in the access token. Note that, you may use Auth0's `core authorization feature `_ for more complex use cases. Metadata solution is mentioned here for simplicity. - - .. code:: javascript - - function (user, context, callback) { - - // Follow the documentations at - // https://postgrest.org/en/latest/configuration.html#db-role-claim-key - // to set a custom role claim on PostgREST - // and use it as custom claim attribute in this rule - const myRoleClaim = 'https://myapp.com/role'; - - user.app_metadata = user.app_metadata || {}; - context.accessToken[myRoleClaim] = user.app_metadata.role; - callback(null, user, context); - } +Each token is cryptographically signed with a secret key. In the case of symmetric cryptography the signer and verifier share the same secret passphrase, which can be configured with :ref:`jwt-secret`. +If it is set to a simple string value like “reallyreallyreallyreallyverysafe” then PostgREST interprets it as an HMAC-SHA256 passphrase. .. _asym_keys: Asymmetric Keys ~~~~~~~~~~~~~~~ -As described in the :ref:`configuration` section, PostgREST accepts a ``jwt-secret`` config file parameter. If it is set to a simple string value like "reallyreallyreallyreallyverysafe" then PostgREST interprets it as an HMAC-SHA256 passphrase. However you can also specify a literal JSON Web Key (JWK) or set. For example, you can use an RSA-256 public key encoded as a JWK: +In asymmetric cryptography the signer uses the private key and the verifier the public key. + +As described in the :ref:`configuration` section, PostgREST accepts a ``jwt-secret`` config file parameter. However you can also specify a literal JSON Web Key (JWK) or set. For example, you can use an RSA-256 public key encoded as a JWK: .. code-block:: json @@ -287,7 +143,14 @@ You can specify the literal value as we saw earlier, or reference a filename to jwt-secret = "@rsa.jwk.pub" -JWT security +.. _jwt_validation: + +JWT Validation +~~~~~~~~~~~~~~ + +PostgREST honors the :code:`exp` claim for token expiration, rejecting expired tokens. + +JWT Security ~~~~~~~~~~~~ There are at least three types of common critiques against using JWT: 1) against the standard itself, 2) against using libraries with known security vulnerabilities, and 3) against using JWT for web sessions. We'll briefly explain each critique, how PostgREST deals with it, and give recommendations for appropriate user action. @@ -300,197 +163,26 @@ The last type of critique focuses on the misuse of JWT for maintaining web sessi PostgREST uses JWT mainly for authentication and authorization purposes and encourages users to do the same. For web sessions, using cookies over HTTPS is good enough and well catered for by standard web frameworks. -Schema Isolation -================ +.. _custom_validation: -You can isolate your api schema from internal implementation details, as explained in :ref:`schema_isolation`. For an example of wrapping a private table with a public view see the :ref:`public_ui` section below. +Custom Validation +----------------- -.. _sql_user_management: - -SQL User Management -=================== - -Storing Users and Passwords ---------------------------- - -As mentioned, an external service can provide user management and coordinate with the PostgREST server using JWT. It's also possible to support logins entirely through SQL. It's a fair bit of work, so get ready. - -The following table, functions, and triggers will live in a :code:`basic_auth` schema that you shouldn't expose publicly in the API. The public views and functions will live in a different schema which internally references this internal information. - -First we'll need a table to keep track of our users: - -.. code:: sql - - -- We put things inside the basic_auth schema to hide - -- them from public view. Certain public procs/views will - -- refer to helpers and tables inside. - create schema if not exists basic_auth; - - create table if not exists - basic_auth.users ( - email text primary key check ( email ~* '^.+@.+\..+$' ), - pass text not null check (length(pass) < 512), - role name not null check (length(role) < 512) - ); - -We would like the role to be a foreign key to actual database roles, however PostgreSQL does not support these constraints against the :code:`pg_roles` table. We'll use a trigger to manually enforce it. - -.. code-block:: plpgsql - - create or replace function - basic_auth.check_role_exists() returns trigger as $$ - begin - if not exists (select 1 from pg_roles as r where r.rolname = new.role) then - raise foreign_key_violation using message = - 'unknown database role: ' || new.role; - return null; - end if; - return new; - end - $$ language plpgsql; - - drop trigger if exists ensure_user_role_exists on basic_auth.users; - create constraint trigger ensure_user_role_exists - after insert or update on basic_auth.users - for each row - execute procedure basic_auth.check_role_exists(); - -Next we'll use the pgcrypto extension and a trigger to keep passwords safe in the :code:`users` table. - -.. code-block:: plpgsql - - create extension if not exists pgcrypto; - - create or replace function - basic_auth.encrypt_pass() returns trigger as $$ - begin - if tg_op = 'INSERT' or new.pass <> old.pass then - new.pass = crypt(new.pass, gen_salt('bf')); - end if; - return new; - end - $$ language plpgsql; - - drop trigger if exists encrypt_pass on basic_auth.users; - create trigger encrypt_pass - before insert or update on basic_auth.users - for each row - execute procedure basic_auth.encrypt_pass(); - -With the table in place we can make a helper to check a password against the encrypted column. It returns the database role for a user if the email and password are correct. - -.. code-block:: plpgsql - - create or replace function - basic_auth.user_role(email text, pass text) returns name - language plpgsql - as $$ - begin - return ( - select role from basic_auth.users - where users.email = user_role.email - and users.pass = crypt(user_role.pass, users.pass) - ); - end; - $$; - -.. _public_ui: - -Public User Interface ---------------------- - -In the previous section we created an internal table to store user information. Here we create a login function which takes an email address and password and returns JWT if the credentials match a user in the internal table. - -Permissions -~~~~~~~~~~~ - -Your database roles need access to the schema, tables, views and functions in order to service HTTP requests. -Recall from the `Overview of Role System`_ that PostgREST uses special roles to process requests, namely the authenticator and -anonymous roles. Below is an example of permissions that allow anonymous users to create accounts and attempt to log in. - -.. code-block:: postgres - - -- the names "anon" and "authenticator" are configurable and not - -- sacred, we simply choose them for clarity - create role anon noinherit; - create role authenticator noinherit; - grant anon to authenticator; - -Then, add ``db-anon-role`` to the configuration file to allow anonymous requests. +PostgREST does not enforce any extra constraints besides :ref:`jwt_validation`. An example of an extra constraint would be to immediately revoke access for a certain user. Using :ref:`db-pre-request` you can specify a stored procedure to call immediately after :ref:`user_impersonation` and before the main query itself runs. .. code:: ini - db-anon-role = "anon" + db-pre-request = "public.check_user" -Logins -~~~~~~ - -As described in `JWT from SQL`_, we'll create a JWT inside our login function. Note that you'll need to adjust the secret key which is hard-coded in this example to a secure (at least thirty-two character) secret of your choosing. +In the function you can run arbitrary code to check the request and raise an exception to block it if desired. .. code-block:: postgres - -- add type - CREATE TYPE basic_auth.jwt_token AS ( - token text - ); - - -- login should be on your exposed schema - create or replace function - login(email text, pass text) returns basic_auth.jwt_token as $$ - declare - _role name; - result basic_auth.jwt_token; - begin - -- check email and password - select basic_auth.user_role(email, pass) into _role; - if _role is null then - raise invalid_password using message = 'invalid user or password'; - end if; - - select sign( - row_to_json(r), 'reallyreallyreallyreallyverysafe' - ) as token - from ( - select _role as role, login.email as email, - extract(epoch from now())::integer + 60*60 as exp - ) r - into result; - return result; - end; - $$ language plpgsql security definer; - - grant execute on function login(text,text) to anon; - -Since the above :code:`login` function is defined as `security definer `_, -the anonymous user :code:`anon` doesn't need permission to read the :code:`basic_auth.users` table. It doesn't even need permission to access the :code:`basic_auth` schema. -:code:`grant execute on function` is included for clarity but it might not be needed, see :ref:`func_privs` for more details. - -An API request to call this function would look like: - -.. tabs:: - - .. code-tab:: http - - POST /rpc/login HTTP/1.1 - - { "email": "foo@bar.com", "pass": "foobar" } - - .. code-tab:: bash Curl - - curl "http://localhost:3000/rpc/login" \ - -X POST -H "Content-Type: application/json" \ - -d '{ "email": "foo@bar.com", "pass": "foobar" }' - -The response would look like the snippet below. Try decoding the token at `jwt.io `_. (It was encoded with a secret of :code:`reallyreallyreallyreallyverysafe` as specified in the SQL code above. You'll want to change this secret in your app!) - -.. code:: json - - { - "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImZvb0BiYXIuY29tIiwicGFzcyI6ImZvb2JhciJ9.37066TTRlh-1hXhnA9oO9Pj6lgL6zFuJU0iCHhuCFno" - } - - -Alternatives -~~~~~~~~~~~~ - -See the how-to :ref:`sql-user-management-using-postgres-users-and-passwords` for a similar way that completely avoids the table :code:`basic_auth.users`. + CREATE OR REPLACE FUNCTION check_user() RETURNS void AS $$ + BEGIN + IF current_user = 'evil_user' THEN + RAISE EXCEPTION 'No, you are evil' + USING HINT = 'Stop being so evil and maybe you can log in'; + END IF; + END + $$ LANGUAGE plpgsql; diff --git a/docs/configuration.rst b/docs/configuration.rst index a2b41a303..cf4e6c8b2 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -7,8 +7,6 @@ Without configuration, PostgREST won't be able to serve requests. At the minimum To connect to a database it uses a `libpq connection string `_. The connection string can be set in the configuration file or via environment variable or can be read from an external file. See :ref:`db-uri` for details. Any parameter that is not set in the connection string is read from `libpq environment variables `_. The default connection string is ``postgresql://``, which reads **all** parameters from the environment. -The user with whom PostgREST connects to the database is also known as the authenticator role. For more information about the anonymous vs authenticator roles see :ref:`roles`. - Config parameters are read in the following order: 1. From the config file. @@ -463,6 +461,8 @@ db-uri The standard connection PostgreSQL `URI format `_. Symbols and unusual characters in the password or other fields should be percent encoded to avoid a parse error. If enforcing an SSL connection to the database is required you can use `sslmode `_ in the URI, for example ``postgres://user:pass@host:5432/dbname?sslmode=require``. + The user with whom PostgREST connects to the database is also known as the ``authenticator`` role. For more information see :ref:`roles`. + When running PostgREST on the same machine as PostgreSQL, it is also possible to connect to the database using a `Unix socket `_ and the `Peer Authentication method `_ as an alternative to TCP/IP communication and authentication with a password, this also grants higher performance. To do this you can omit the host and the password, e.g. ``postgres://user@/dbname``, see the `libpq connection string `_ documentation for more details. Choosing a value for this parameter beginning with the at sign such as ``@filename`` (e.g. ``@./configs/my-config``) loads the connection string out of an external file. diff --git a/docs/db_authz.rst b/docs/db_authz.rst new file mode 100644 index 000000000..8225a9a4c --- /dev/null +++ b/docs/db_authz.rst @@ -0,0 +1,89 @@ +.. raw:: html + +

Database Authorization

+ +Database authorization is the process of granting and verifying database access permissions. PostgreSQL manages permissions using the concept of roles. A role can be thought of as either a database user, or a group of database users, depending on how the role is set up. + +Roles for Each Web User +----------------------- + +PostgREST can accommodate either viewpoint. If you treat a role as a single user then the :ref:`jwt_impersonation` does most of what you need. When an authenticated user makes a request PostgREST will switch into the database role for that user, which in addition to restricting queries, is available to SQL through the :code:`current_user` variable. + +You can use row-level security to flexibly restrict visibility and access for the current user. Here is an `example `_ from Tomas Vondra, a chat table storing messages sent between users. Users can insert rows into it to send messages to other users, and query it to see messages sent to them by other users. + +.. code-block:: postgres + + CREATE TABLE chat ( + message_uuid UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + message_time TIMESTAMP NOT NULL DEFAULT now(), + message_from NAME NOT NULL DEFAULT current_user, + message_to NAME NOT NULL, + message_subject VARCHAR(64) NOT NULL, + message_body TEXT + ); + + ALTER TABLE chat ENABLE ROW LEVEL SECURITY; + +We want to enforce a policy that ensures a user can see only those messages sent by them or intended for them. Also we want to prevent a user from forging the ``message_from`` column with another person's name. + +PostgreSQL allows us to set this policy with row-level security: + +.. code-block:: postgres + + CREATE POLICY chat_policy ON chat + USING ((message_to = current_user) OR (message_from = current_user)) + WITH CHECK (message_from = current_user) + +Anyone accessing the generated API endpoint for the chat table will see exactly the rows they should, without our needing custom imperative server-side coding. + +.. warning:: + + Roles are namespaced per-cluster rather than per-database so they may be prone to collision. + +Web Users Sharing Role +---------------------- + +Alternately database roles can represent groups instead of (or in addition to) individual users. You may choose that all signed-in users for a web app share the role ``webuser``. You can distinguish individual users by including extra claims in the JWT such as email. + +.. code:: json + + { + "role": "webuser", + "email": "john@doe.com" + } + +SQL code can access claims through GUC variables set by PostgREST per request. For instance to get the email claim, call this function: + +For PostgreSQL server version >= 14 + +.. code:: sql + + current_setting('request.jwt.claims', true)::json->>'email'; + + +For PostgreSQL server version < 14 + +.. code:: sql + + current_setting('request.jwt.claim.email', true); + +This allows JWT generation services to include extra information and your database code to react to it. For instance the RLS example could be modified to use this ``current_setting`` rather than ``current_user``. The second ``'true'`` argument tells ``current_setting`` to return NULL if the setting is missing from the current configuration. + +Hybrid User-Group Roles +----------------------- + +You can mix the group and individual role policies. For instance we could still have a webuser role and individual users which inherit from it: + +.. code-block:: postgres + + CREATE ROLE webuser NOLOGIN; + -- grant this role access to certain tables etc + + CREATE ROLE user000 NOLOGIN; + GRANT webuser TO user000; + -- now user000 can do whatever webuser can + + GRANT user000 TO authenticator; + -- allow authenticator to switch into user000 role + -- (the role itself has nologin) + diff --git a/docs/ecosystem.rst b/docs/ecosystem.rst index f9fadfb57..ba6292bd6 100644 --- a/docs/ecosystem.rst +++ b/docs/ecosystem.rst @@ -1,3 +1,34 @@ +.. _external_jwt: + +External JWT Generation +----------------------- + +JWT from Auth0 +~~~~~~~~~~~~~~ + +An external service like `Auth0 `_ can do the hard work transforming OAuth from Github, Twitter, Google etc into a JWT suitable for PostgREST. Auth0 can also handle email signup and password reset flows. + +To use Auth0, create `an application `_ for your app and `an API `_ for your PostgREST server. Auth0 supports both HS256 and RS256 scheme for the issued tokens for APIs. For simplicity, you may first try HS256 scheme while creating your API on Auth0. Your application should use your PostgREST API's `API identifier `_ by setting it with the `audience parameter `_ during the authorization request. This will ensure that Auth0 will issue an access token for your PostgREST API. For PostgREST to verify the access token, you will need to set ``jwt-secret`` on PostgREST config file with your API's signing secret. + +.. note:: + + Our code requires a database role in the JWT. To add it you need to save the database role in Auth0 `app metadata `_. Then, you will need to write `a rule `_ that will extract the role from the user's app_metadata and set it as a `custom claim `_ in the access token. Note that, you may use Auth0's `core authorization feature `_ for more complex use cases. Metadata solution is mentioned here for simplicity. + + .. code:: javascript + + function (user, context, callback) { + + // Follow the documentations at + // https://postgrest.org/en/latest/configuration.html#db-role-claim-key + // to set a custom role claim on PostgREST + // and use it as custom claim attribute in this rule + const myRoleClaim = 'https://myapp.com/role'; + + user.app_metadata = user.app_metadata || {}; + context.accessToken[myRoleClaim] = user.app_metadata.role; + callback(null, user, context); + } + .. _community_tutorials: Community Tutorials diff --git a/docs/how-tos/sql-user-management-using-postgres-users-and-passwords.rst b/docs/how-tos/sql-user-management-using-postgres-users-and-passwords.rst index 4dec8ed1f..108959ab7 100644 --- a/docs/how-tos/sql-user-management-using-postgres-users-and-passwords.rst +++ b/docs/how-tos/sql-user-management-using-postgres-users-and-passwords.rst @@ -59,7 +59,7 @@ In order to be able to work with postgres' SCRAM-SHA-256 password hashes, we als CREATE FUNCTION basic_auth.pbkdf2(salt bytea, pw text, count integer, desired_length integer, algorithm text) RETURNS bytea LANGUAGE plpgsql IMMUTABLE AS $$ - DECLARE + DECLARE hash_length integer; block_count integer; output bytea; @@ -97,7 +97,7 @@ In order to be able to work with postgres' SCRAM-SHA-256 password hashes, we als -- FOR j IN 2 .. count LOOP the_last := ext_pgcrypto.HMAC(the_last, pw::bytea, algorithm); - + -- xor the two FOR k IN 1 .. length(xorsum) LOOP xorsum := set_byte(xorsum, k - 1, get_byte(xorsum, k - 1) # get_byte(the_last, k - 1)); @@ -210,8 +210,6 @@ anonymous roles. Below is an example of permissions that allow anonymous users t .. code-block:: postgres - -- the names "anon" and "authenticator" are configurable and not - -- sacred, we simply choose them for clarity CREATE ROLE anon NOINHERIT; CREATE role authenticator NOINHERIT LOGIN PASSWORD 'secret'; GRANT anon TO authenticator; @@ -299,7 +297,7 @@ Let's add a table, intended for the :code:`foo` user: Now try to get the table's contents with: .. tabs:: - + .. code-tab:: http GET /foobar HTTP/1.1 diff --git a/docs/how-tos/sql-user-management.rst b/docs/how-tos/sql-user-management.rst new file mode 100644 index 000000000..1f8716e86 --- /dev/null +++ b/docs/how-tos/sql-user-management.rst @@ -0,0 +1,228 @@ +.. _sql_user_management: + +SQL User Management +=================== + +As mentioned on :ref:`jwt_generation`, an external service can provide user management and coordinate with the PostgREST server using JWT. It’s also possible to support logins entirely through SQL. It’s a fair bit of work, so get ready. + +Storing Users and Passwords +--------------------------- + +The following table, functions, and triggers will live in a :code:`basic_auth` schema that you shouldn't expose publicly in the API. The public views and functions will live in a different schema which internally references this internal information. + +First we'll need a table to keep track of our users: + +.. code:: sql + + -- We put things inside the basic_auth schema to hide + -- them from public view. Certain public procs/views will + -- refer to helpers and tables inside. + create schema if not exists basic_auth; + + create table if not exists + basic_auth.users ( + email text primary key check ( email ~* '^.+@.+\..+$' ), + pass text not null check (length(pass) < 512), + role name not null check (length(role) < 512) + ); + +We would like the role to be a foreign key to actual database roles, however PostgreSQL does not support these constraints against the :code:`pg_roles` table. We'll use a trigger to manually enforce it. + +.. code-block:: plpgsql + + create or replace function + basic_auth.check_role_exists() returns trigger as $$ + begin + if not exists (select 1 from pg_roles as r where r.rolname = new.role) then + raise foreign_key_violation using message = + 'unknown database role: ' || new.role; + return null; + end if; + return new; + end + $$ language plpgsql; + + drop trigger if exists ensure_user_role_exists on basic_auth.users; + create constraint trigger ensure_user_role_exists + after insert or update on basic_auth.users + for each row + execute procedure basic_auth.check_role_exists(); + +Next we'll use the pgcrypto extension and a trigger to keep passwords safe in the :code:`users` table. + +.. code-block:: plpgsql + + create extension if not exists pgcrypto; + + create or replace function + basic_auth.encrypt_pass() returns trigger as $$ + begin + if tg_op = 'INSERT' or new.pass <> old.pass then + new.pass = crypt(new.pass, gen_salt('bf')); + end if; + return new; + end + $$ language plpgsql; + + drop trigger if exists encrypt_pass on basic_auth.users; + create trigger encrypt_pass + before insert or update on basic_auth.users + for each row + execute procedure basic_auth.encrypt_pass(); + +With the table in place we can make a helper to check a password against the encrypted column. It returns the database role for a user if the email and password are correct. + +.. code-block:: plpgsql + + create or replace function + basic_auth.user_role(email text, pass text) returns name + language plpgsql + as $$ + begin + return ( + select role from basic_auth.users + where users.email = user_role.email + and users.pass = crypt(user_role.pass, users.pass) + ); + end; + $$; + +.. _public_ui: + +Public User Interface +--------------------- + +In the previous section we created an internal table to store user information. Here we create a login function which takes an email address and password and returns JWT if the credentials match a user in the internal table. + +Permissions +~~~~~~~~~~~ + +Your database roles need access to the schema, tables, views and functions in order to service HTTP requests. +Recall from the :ref:`roles` that PostgREST uses special roles to process requests, namely the authenticator and +anonymous roles. Below is an example of permissions that allow anonymous users to create accounts and attempt to log in. + +.. code-block:: postgres + + create role anon noinherit; + create role authenticator noinherit; + grant anon to authenticator; + +Then, add ``db-anon-role`` to the configuration file to allow anonymous requests. + +.. code:: ini + + db-anon-role = "anon" + +JWT from SQL +~~~~~~~~~~~~ + +You can create JWT tokens in SQL using the `pgjwt extension `_. It's simple and requires only pgcrypto. If you're on an environment like Amazon RDS which doesn't support installing new extensions, you can still manually run the `SQL inside pgjwt `_ (you'll need to replace ``@extschema@`` with another schema or just delete it) which creates the functions you will need. + +Next write a stored procedure that returns the token. The one below returns a token with a hard-coded role, which expires five minutes after it was issued. Note this function has a hard-coded secret as well. + +.. code-block:: postgres + + CREATE TYPE jwt_token AS ( + token text + ); + + CREATE FUNCTION jwt_test() RETURNS public.jwt_token AS $$ + SELECT public.sign( + row_to_json(r), 'reallyreallyreallyreallyverysafe' + ) AS token + FROM ( + SELECT + 'my_role'::text as role, + extract(epoch from now())::integer + 300 AS exp + ) r; + $$ LANGUAGE sql; + +PostgREST exposes this function to clients via a POST request to ``/rpc/jwt_test``. + +.. note:: + + To avoid hard-coding the secret in stored procedures, save it as a property of the database. + + .. code-block:: postgres + + -- run this once + ALTER DATABASE mydb SET "app.jwt_secret" TO 'reallyreallyreallyreallyverysafe'; + + -- then all functions can refer to app.jwt_secret + SELECT sign( + row_to_json(r), current_setting('app.jwt_secret') + ) AS token + FROM ... + +Logins +~~~~~~ + +As described in `JWT from SQL`_, we'll create a JWT inside our login function. Note that you'll need to adjust the secret key which is hard-coded in this example to a secure (at least thirty-two character) secret of your choosing. + +.. code-block:: postgres + + -- add type + CREATE TYPE basic_auth.jwt_token AS ( + token text + ); + + -- login should be on your exposed schema + create or replace function + login(email text, pass text) returns basic_auth.jwt_token as $$ + declare + _role name; + result basic_auth.jwt_token; + begin + -- check email and password + select basic_auth.user_role(email, pass) into _role; + if _role is null then + raise invalid_password using message = 'invalid user or password'; + end if; + + select sign( + row_to_json(r), 'reallyreallyreallyreallyverysafe' + ) as token + from ( + select _role as role, login.email as email, + extract(epoch from now())::integer + 60*60 as exp + ) r + into result; + return result; + end; + $$ language plpgsql security definer; + + grant execute on function login(text,text) to anon; + +Since the above :code:`login` function is defined as `security definer `_, +the anonymous user :code:`anon` doesn't need permission to read the :code:`basic_auth.users` table. It doesn't even need permission to access the :code:`basic_auth` schema. +:code:`grant execute on function` is included for clarity but it might not be needed, see :ref:`func_privs` for more details. + +An API request to call this function would look like: + +.. tabs:: + + .. code-tab:: http + + POST /rpc/login HTTP/1.1 + + { "email": "foo@bar.com", "pass": "foobar" } + + .. code-tab:: bash Curl + + curl "http://localhost:3000/rpc/login" \ + -X POST -H "Content-Type: application/json" \ + -d '{ "email": "foo@bar.com", "pass": "foobar" }' + +The response would look like the snippet below. Try decoding the token at `jwt.io `_. (It was encoded with a secret of :code:`reallyreallyreallyreallyverysafe` as specified in the SQL code above. You'll want to change this secret in your app!) + +.. code:: json + + { + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImZvb0BiYXIuY29tIiwicGFzcyI6ImZvb2JhciJ9.37066TTRlh-1hXhnA9oO9Pj6lgL6zFuJU0iCHhuCFno" + } + + +Alternatives +~~~~~~~~~~~~ + +See the how-to :ref:`sql-user-management-using-postgres-users-and-passwords` for a similar way that completely avoids the table :code:`basic_auth.users`. diff --git a/docs/index.rst b/docs/index.rst index f138876d8..e5d90de2f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -130,6 +130,12 @@ Reference guides Technical references for PostgREST's functionality. +.. toctree:: + :caption: Authentication + :hidden: + + auth.rst + .. toctree:: :caption: API :hidden: @@ -154,6 +160,7 @@ Technical references for PostgREST's functionality. errors.rst +- :doc:`Authentication ` - :doc:`API ` - :doc:`configuration` - :doc:`Schema Cache ` @@ -165,10 +172,10 @@ Topic guides Explanations of some key concepts in PostgREST. .. toctree:: - :caption: Authentication + :caption: Database Authorization :hidden: - auth.rst + db_authz.rst .. toctree:: :caption: Schema Structure @@ -188,7 +195,7 @@ Explanations of some key concepts in PostgREST. install.rst -- :doc:`Authentication ` +- :doc:`Database Authorization ` - :doc:`Schema Structure ` - :doc:`Administration ` - :doc:`Installation ` @@ -205,15 +212,17 @@ These are recipes that'll help you address specific use-cases. :caption: How-to guides :hidden: + how-tos/sql-user-management how-tos/working-with-postgresql-data-types + how-tos/sql-user-management-using-postgres-users-and-passwords how-tos/providing-images-for-img how-tos/create-soap-endpoint - how-tos/sql-user-management-using-postgres-users-and-passwords -- :doc:`how-tos/providing-images-for-img` +- :doc:`how-tos/sql-user-management` - :doc:`how-tos/working-with-postgresql-data-types` -- :doc:`how-tos/create-soap-endpoint` - :doc:`how-tos/sql-user-management-using-postgres-users-and-passwords` +- :doc:`how-tos/providing-images-for-img` +- :doc:`how-tos/create-soap-endpoint` Ecosystem ---------