Open richcorbs opened 5 years ago
i was thinking I could have the client "ping" a refresh/renew function periodically as part of the user's activity on the site, not while they are idle.
This function would:
verify
function?)I've never created a postgresql stored procedure/function but it seems like the pieces are there. I may take a crack at it if I'm feeling brave.
Something like this maybe?
CREATE OR REPLACE FUNCTION public.refresh_token(token text)
RETURNS TABLE(payload json, jwt_token text, valid boolean)
LANGUAGE sql
AS $$
SELECT
payload::json,
sign(
json_build_object(
'sub', payload->'https://hasura.io/jwt/claims'->'x-hasura-user-id'::text,
'iss', 'Hasura-JWT-Auth',
'iat', round(extract(epoch from now())),
'exp', round(extract(epoch from now() + interval '24 hour')),
'https://hasura.io/jwt/claims', json_build_object(
'x-hasura-user-id', payload->'https://hasura.io/jwt/claims'->'x-hasura-user-id'::text,
'x-hasura-default-role', payload->'https://hasura.io/jwt/claims'->'x-hasura-default-role'::text,
'x-hasura-allowed-roles', payload->'https://hasura.io/jwt/claims'->'x-hasura-allowed-roles'::text
)
), current_setting('hasura.jwt_secret_key'))::text as jwt_token,
valid::boolean
FROM verify(token)
$$;
This seems to work. I just don't know how to handle it when the token isn't valid. Maybe the client does the right thing depending on the value returned for valid
? Seems weak.
This probably isn't the best approach. Should incorporate a refresh token and return a "user" object just like authenticate
does so that it can work with Hasura.
I think I got it:
CREATE OR REPLACE FUNCTION public.refresh_jwt(temp_token uuid, jwt text)
RETURNS SETOF users
LANGUAGE sql
STABLE
AS $function$
SELECT
(SELECT (payload->'https://hasura.io/jwt/claims'->>'x-hasura-user-id')::uuid FROM verify(refresh_jwt.jwt, current_setting('hasura.jwt_secret_key'))) as id,
name,
email,
password_hash,
organization_id,
temp_token,
role,
created_at,
updated_at,
enabled,
(SELECT
sign(
json_build_object(
'sub', payload->'https://hasura.io/jwt/claims'->'x-hasura-user-id'::text,
'iss', 'Hasura-JWT-Auth',
'iat', round(extract(epoch from now())),
'exp', round(extract(epoch from now() + interval '24 hour')),
'https://hasura.io/jwt/claims', json_build_object(
'x-hasura-user-id', payload->'https://hasura.io/jwt/claims'->'x-hasura-user-id'::text,
'x-hasura-organization-id', payload->'https://hasura.io/jwt/claims'->'x-hasura-organization-id'::text,
'x-hasura-default-role', payload->'https://hasura.io/jwt/claims'->'x-hasura-default-role'::text,
'x-hasura-allowed-roles', payload->'https://hasura.io/jwt/claims'->'x-hasura-allowed-roles'::text
)
), current_setting('hasura.jwt_secret_key'))::text as jwt_token
FROM verify(refresh_jwt.jwt, current_setting('hasura.jwt_secret_key'))) as jwt_token,
cleartext_password
FROM users
WHERE users.temp_token = refresh_jwt.temp_token
AND users.enabled = true
$function$
@richcorbs
How is this coming along for you? Ready to get started and want to lock in my auth strategy before anything else. Any adjustments or lessons learned?
I assume you have a /verify
endpoint that executes this function...?
(good work, thanks for sharing)
Another approach to take on this -
You could create a refresh_token field in your user table:
create table hasura_user(
id serial primary key,
email varchar unique,
crypt_password varchar,
cleartext_password varchar,
default_role varchar default 'user',
allowed_roles jsonb default '["user"]',
enabled boolean default true,
refresh_token text,
jwt_token text
);
Then sign a refresh_token
in your hasura_auth
function.
Characteristics of refresh vs access tokens (jwt_token in this example) are that they're long vs short lived, and they don't have credentials (they're just used for refreshing the access token).
For this example I created a 1 week refresh token and a 5 minute access token. I also hardcoded the roles as anonymous
(since Hasura is expecting some form of role, so we'll tell it the person has the most limited set of permissions).
create or replace function hasura_auth(email in varchar, cleartext_password in varchar) returns setof hasura_user as $$
select
id,
email,
crypt_password,
cleartext_password,
default_role,
allowed_roles,
enabled,
sign(
json_build_object(
'sub', id::text,
'iss', 'Hasura-JWT-Auth',
'iat', round(extract(epoch from now())),
'exp', round(extract(epoch from now() + interval '168 hour')),
'https://hasura.io/jwt/claims', json_build_object(
'x-hasura-user-id', id::text,
'x-hasura-default-role', 'anonymous',
'x-hasura-allowed-roles', ('["anonymous"]')::jsonb
)
), current_setting('hasura.jwt_secret_key')) as refresh_token,
sign(
json_build_object(
'sub', id::text,
'iss', 'Hasura-JWT-Auth',
'iat', round(extract(epoch from now())),
'exp', round(extract(epoch from now() + interval '5 minute')),
'https://hasura.io/jwt/claims', json_build_object(
'x-hasura-user-id', id::text,
'x-hasura-default-role', default_role,
'x-hasura-allowed-roles', allowed_roles
)
), current_setting('hasura.jwt_secret_key')) as jwt_token
from hasura_user h
where h.email = hasura_auth.email
and h.enabled
and h.crypt_password = hasura_encrypt_password(hasura_auth.cleartext_password, h.crypt_password);
$$ language 'sql' stable;
I can then created a hasura_refresh
function. This one will rely on Hasura's sessions (you can enable passing sessions through the expanded page of the function in the Console: https://hasura.io/docs/1.0/graphql/core/schema/custom-functions.html#accessing-hasura-session-variables-in-custom-functions).
Use case: your access token (jwt_token) has expired. You come through the front-door of Hasura using your refresh token in the header. Hasura will validate the token, and use your x-hasura-user-id
from the session to get you a new access token.
Note: This isn't technically a refresh_token, since generally a refresh_token would just re-sign the current token with a new exp. Here, we're actually re-generating the token - which actually has its upsides since in this model you could check every 5 minutes to make sure the user is still enabled, and their claims are still valid.
create or replace function hasura_refresh(hasura_session json) returns setof hasura_user as $$
select
id,
email,
crypt_password,
cleartext_password,
default_role,
allowed_roles,
enabled,
'' as refresh_token,
sign(
json_build_object(
'sub', id::text,
'iss', 'Hasura-JWT-Auth',
'iat', round(extract(epoch from now())),
'exp', round(extract(epoch from now() + interval '5 minute')),
'https://hasura.io/jwt/claims', json_build_object(
'x-hasura-user-id', id::text,
'x-hasura-default-role', default_role,
'x-hasura-allowed-roles', allowed_roles
)
), current_setting('hasura.jwt_secret_key')) as jwt_token
from hasura_user h
where h.id = (hasura_session ->> 'x-hasura-user-id')::int
and h.enabled
$$ language 'sql' stable;
After we've tracked everything - our request (anonymous header):
query authLogin {
hasura_auth(args: {email: "your_email", cleartext_password: "your_password"}) {
jwt_token
refresh_token
}
}
And our refresh request (refresh_token authorization header):
query refreshToken {
hasura_refresh {
jwt_token
}
}
Have you considered how token refresh/renewal might work using this setup?
As long as the user is actively using the system it would be ideal if they could continue to do so without having to re-auth.
Just looking for ideas if you've got them.
BTW, I have this setup running in a limited release production environment!