Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Token refresh/renewal #7

Open
richcorbs opened this issue Sep 11, 2019 · 6 comments
Open

Token refresh/renewal #7

richcorbs opened this issue Sep 11, 2019 · 6 comments

Comments

@richcorbs
Copy link

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!

@richcorbs
Copy link
Author

richcorbs commented Sep 11, 2019

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:

  • take a JWT as input
  • check it for validity (using existing verify function?)
  • use the contents of the existing JWT to generate a refreshed/renewed JWT
  • return the refreshed/renewed JWT

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.

@richcorbs
Copy link
Author

richcorbs commented Sep 12, 2019

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.

@richcorbs
Copy link
Author

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.

@richcorbs
Copy link
Author

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$

@corepay
Copy link

corepay commented Apr 15, 2020

@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)

@martin-hasura
Copy link

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
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants