Skip to content

Instantly share code, notes, and snippets.

@colllin
Last active February 21, 2024 14:55

Revisions

  1. colllin revised this gist Jan 31, 2020. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion Readme.md
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,6 @@
    ## Auth0 + FaunaDB ABAC integration: How to expire Fauna user secrets.

    Fauna doesn't yet provide expiration/TTL for ABAC tokens, so we need to implement it ourselves.
    Fauna doesn't (yet?) provide guaranteed expiration/TTL for ABAC tokens, so we need to implement it ourselves if we care about it.

    ### What's in the box?

  2. colllin revised this gist Nov 5, 2019. 2 changed files with 7 additions and 28 deletions.
    4 changes: 1 addition & 3 deletions Readme.md
    Original file line number Diff line number Diff line change
    @@ -39,9 +39,7 @@ Fauna doesn't yet provide expiration/TTL for ABAC tokens, so we need to implemen
    - Verifies the JWT (via Auth0 for safety). Rejects the promise if invalid or expired.
    - Looks up user by Auth0 user ID (from index named by `fauna_user_index_auth0_id`)
    - you need to setup this index prior to using this function.
    - Override the user's password with a randomly generated string.
    - ATTENTION: this is bad if you actually store user passwords in Fauna.
    - Login the user with the randomly generated password to obtain a user token and secret.
    - Create a user token for the user (i.e. `Login()`, but via `Create(Tokens(), ...)`).
    - Create a document in the `auth0_token_exchanges` collection containing the `ref`
    of the user token (NOT the secret), the provided JWT, and the decoded JWT payload.
    - Return the user secret (by resolving the promise).
    31 changes: 6 additions & 25 deletions exchange-jwt-for-secret.js
    Original file line number Diff line number Diff line change
    @@ -1,7 +1,6 @@
    const util = require('util')
    const jwt = require('jsonwebtoken')
    const faunadb = require('faunadb')
    const uuidv4 = require('uuid/v4')
    const config = require('./config.js')
    const q = faunadb.query

    @@ -18,20 +17,10 @@ function findUserRef(index, matchValues) {
    )
    }

    function setUserPassword(userRef, newPassword) {
    return q.Update(
    userRef,
    {
    credentials: {password: newPassword}
    },
    );
    }

    function loginUser(userRef, password) {
    return q.Login(
    userRef,
    {password: password}
    );
    function loginUser(userRef) {
    // This is *supposed* to work according to Fauna docs, but currently has a bug that requires credentials.
    // return q.Login(userRef);
    return q.Create(q.Tokens(), {instance: userRef});
    }

    /* idempotent function to create the necessary collections in your Fauna database */
    @@ -53,19 +42,11 @@ function exchangeJwtForSecret(
    })
    const jwt_auth0_id = jwtPayload.sub;
    const jwt_exp_ms = jwtPayload.exp * 1000;
    const overrideUserPassword = uuidv4();
    return client.query(
    q.Let(
    {
    userToken: q.Let(
    {
    userRef: findUserRef(fauna_user_index_auth0_id, jwt_auth0_id)
    },
    q.Do(
    setUserPassword(q.Var('userRef'), overrideUserPassword),
    loginUser(q.Var('userRef'), overrideUserPassword),
    ),
    ),
    userRef: findUserRef(fauna_user_index_auth0_id, jwt_auth0_id),
    userToken: loginUser(q.Var('userRef'))
    },
    q.Do(
    q.Create(
  3. colllin revised this gist Oct 12, 2019. 1 changed file with 21 additions and 0 deletions.
    21 changes: 21 additions & 0 deletions Readme.md
    Original file line number Diff line number Diff line change
    @@ -45,6 +45,27 @@ Fauna doesn't yet provide expiration/TTL for ABAC tokens, so we need to implemen
    - Create a document in the `auth0_token_exchanges` collection containing the `ref`
    of the user token (NOT the secret), the provided JWT, and the decoded JWT payload.
    - Return the user secret (by resolving the promise).
    - In my app's integration, I added some logic at the beginning to create the User document for this Auth0 ID if there isn't one already.
    ```
    function findOrCreateUserRef(index_users_by_auth0_id, auth0_id) {
    return q.Select(
    ['ref'],
    q.Let(
    {userMatch: q.Match(q.Index(index_users_by_auth0_id), auth0_id)},
    q.If(
    q.Exists(q.Var('userMatch')),
    q.Get(q.Var('userMatch')),
    q.Create(q.Collection('users'), {
    data: {
    auth0_id: auth0_id
    }
    })
    )
    )
    )
    }
    ```
    1. `delete-expired-tokens.js`: deletes all expired tokens which were issued in exchange for Auth0 JWTs.
    - This is intended to be called in a cron, and can be called as often as desired. The lower limit
    is probably once/day and the upper limit is probably once every 5 minutes.
  4. colllin created this gist Oct 12, 2019.
    60 changes: 60 additions & 0 deletions Readme.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,60 @@
    ## Auth0 + FaunaDB ABAC integration: How to expire Fauna user secrets.

    Fauna doesn't yet provide expiration/TTL for ABAC tokens, so we need to implement it ourselves.

    ### What's in the box?

    3 javascript functions, each of which can be imported into your project or run from the command-line
    using `node path/to/script.js arg1 arg2 ... argN`:

    1. `deploy-schema.js`: a javascript function for creating supporting collections and indexes in your Fauna database.
    - Intended to be called once per Fauna database. Safe to call it multiple times (will not cause harm).
    - Needs to be called with a Fauna SERVER secret prior to using the other provided functions.
    - Logic
    - args: `fauna_server_secret`
    - returns: a `Promise`
    - Creates a collection named `auth0_token_exchanges`.
    - This collection will store one document for each user secret issued in exchange for an Auth0 token.
    - The documents will contain this data:
    - `token_ref`: the Ref of the Fauna user token which was issued
    - `expires_at`: the Time at which this token should expire
    - `meta`: any other data passed into the `custom_metadata` argument to `exchange-jwt-for-secret.js`
    - For example, you might want to log the decoded JWT payload or the entire JWT,
    which could be useful for your own indexing/querying/auditing/debugging purposes.
    - We exclude any identifying data by default to avoid unintentionally storing any
    sensitive user data which may be governed by HIPAA, etc.
    - Creates an index named `auth0_token_exchanges_by_expiration` which indexes
    the documents by `data.jwt_payload.exp`.
    1. `exchange-jwt-for-secret.js`: verifies Auth0 JWTs, looks-up the user in Fauna by auth0_id,
    creates an ABAC token for the user, records the token and JWT expiration time in Fauna,
    and returns the token secret.
    - This is intended to be served in an API endpoint that you create.
    - Clients should call this endpoint upon receiving a JWT to obtain a Fauna user secret.
    - Clients can then use this Fauna user secret to communicate directly with your Fauna database,
    e.g. for the native GraphQL endpoint.
    - Logic
    - args: `auth0_jwt, custom_metadata, auth0_client_id, auth0_client_cert_pubkey,
    fauna_server_secret, fauna_index_users_by_auth0_id`
    - returns: a `Promise`
    - Verifies the JWT (via Auth0 for safety). Rejects the promise if invalid or expired.
    - Looks up user by Auth0 user ID (from index named by `fauna_user_index_auth0_id`)
    - you need to setup this index prior to using this function.
    - Override the user's password with a randomly generated string.
    - ATTENTION: this is bad if you actually store user passwords in Fauna.
    - Login the user with the randomly generated password to obtain a user token and secret.
    - Create a document in the `auth0_token_exchanges` collection containing the `ref`
    of the user token (NOT the secret), the provided JWT, and the decoded JWT payload.
    - Return the user secret (by resolving the promise).
    1. `delete-expired-tokens.js`: deletes all expired tokens which were issued in exchange for Auth0 JWTs.
    - This is intended to be called in a cron, and can be called as often as desired. The lower limit
    is probably once/day and the upper limit is probably once every 5 minutes.
    - If you don't call this function in a cron, then the rest of this code is pointless, because your
    user tokens will never expire (which is the normal behavior without any of this code).
    - Logic
    - args: `fauna_server_secret`
    - returns: a `Promise`
    - Queries index `auth0_token_exchanges_by_expiration` for all documents which are
    past the expiration timestamp specified in `data.jwt_payload.exp`.
    - For each matching instance returned from `auth0_token_exchanges`, deletes the
    ABAC token referenced by `data.token_ref`.
    - Deletes each of the matching instances returned from `auth0_token_exchanges`.
    4 changes: 4 additions & 0 deletions config.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,4 @@
    module.exports = {
    COLLECTION_NAME: 'auth0_token_exchanges',
    INDEX_NAME: 'auth0_token_exchanges_by_expiration',
    }
    78 changes: 78 additions & 0 deletions delete-expired-tokens.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,78 @@
    const util = require('util')
    const jwt = require('jsonwebtoken')
    const faunadb = require('faunadb')
    const uuidv4 = require('uuid/v4')
    const config = require('./config.js')
    const q = faunadb.query


    function findExpiredTokens() {
    return q.Map(
    q.Paginate(
    q.Match(q.Index(config.INDEX_NAME)),
    {before: q.Time("now"), size: 200},
    ),
    q.Lambda(
    ['expires_at', 'ref'],
    q.Get(q.Var('ref')),
    ),
    )
    }

    function deleteOnePageOfExpiredTokens() {
    return q.Foreach(
    findExpiredTokens(),
    q.Lambda(
    ['auth0TokenExchange'],
    q.Do(
    q.Delete(
    q.Select(
    ['data', 'token_ref'],
    q.Var('auth0TokenExchange')
    )
    ),
    q.Delete(
    q.Select(
    ['ref'],
    q.Var('auth0TokenExchange')
    )
    )
    )
    )
    )
    }

    /* idempotent function to create the necessary collections in your Fauna database */
    async function deleteExpiredTokens(fauna_server_secret) {
    const client = new faunadb.Client({
    secret: fauna_server_secret
    })

    let page = null;
    let count = 0;
    try {
    do {
    page = await client.query(
    deleteOnePageOfExpiredTokens()
    )
    count += page.data.length
    } while (page.before)
    } catch (e) {
    if (e.message === 'unauthorized') {
    e.message = 'unauthorized: missing or invalid fauna_server_secret, or not enough permissions';
    throw e
    } else {
    throw e
    }
    }

    return `Deleted ${count} expired tokens.`
    }

    if (require.main === module) {
    deleteExpiredTokens.apply(this, process.argv.slice(2))
    .then((result) => console.log(result))
    .catch((error) => console.error(error))
    }

    module.exports = deleteExpiredTokens;
    58 changes: 58 additions & 0 deletions deploy-schema.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,58 @@
    // Originally from:
    // https://github.com/fauna/netlify-faunadb-todomvc/blob/master/scripts/bootstrap-fauna-database.js

    const faunadb = require('faunadb')
    const config = require('./config.js')
    const q = faunadb.query

    /* idempotent function to create the necessary collections in your Fauna database */
    function ensureFaunaCollections(fauna_server_secret) {
    if (!fauna_server_secret) {
    console.log('Missing first argument: fauna_server_secret. Try with:')
    console.log()
    console.log('node ensure-fauna-collections.js fnYourFaunaSecretHere')
    console.log()
    console.log('You can create fauna DB keys here: https://dashboard.fauna.com/db/keys')
    return false
    }

    const client = new faunadb.Client({
    secret: fauna_server_secret
    })

    console.log('Creating the collections...')
    return client.query(
    q.CreateCollection({
    name: config.COLLECTION_NAME,
    })
    )
    .then(() => client.query(
    q.CreateIndex({
    name: config.INDEX_NAME,
    source: q.Collection(config.COLLECTION_NAME),
    values: [
    {field: ['data', 'expires_at']},
    {field: ['ref']},
    ],
    })
    ))
    .then(console.log.bind(console))
    .catch((e) => {
    if (e.message === 'instance already exists') {
    console.log("collection already created... skipping. You're good to go!");
    } else if (e.message === 'unauthorized') {
    e.message = 'unauthorized: missing or invalid fauna_server_secret, or not enough permissions';
    throw e
    } else {
    throw e
    }
    })
    }

    if (require.main === module) {
    ensureFaunaCollections.apply(this, process.argv.slice(2))
    .then((result) => console.log(result))
    .catch((error) => console.error(error))
    }

    module.exports = ensureFaunaCollections;
    101 changes: 101 additions & 0 deletions exchange-jwt-for-secret.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,101 @@
    const util = require('util')
    const jwt = require('jsonwebtoken')
    const faunadb = require('faunadb')
    const uuidv4 = require('uuid/v4')
    const config = require('./config.js')
    const q = faunadb.query


    function findUserRef(index, matchValues) {
    return q.Select(
    ['ref'],
    q.Get(
    q.Match(
    q.Index(index),
    matchValues,
    ),
    ),
    )
    }

    function setUserPassword(userRef, newPassword) {
    return q.Update(
    userRef,
    {
    credentials: {password: newPassword}
    },
    );
    }

    function loginUser(userRef, password) {
    return q.Login(
    userRef,
    {password: password}
    );
    }

    /* idempotent function to create the necessary collections in your Fauna database */
    function exchangeJwtForSecret(
    auth0_jwt, // A JWT returned from a successful Auth0 login (or silentAuth or refresh)
    custom_metadata, // Any custom metadata you wish to store in Fauna in the newly created `auth0_token_exchanges` record of this exchange.
    auth0_client_id, // From your Auth0 Client Application > Settings > Client ID
    auth0_client_cert_pubkey, // From your Auth0 Client Application > Settings > Advanced Settings > Certificates > Signing Certificate
    fauna_server_secret, // From the associated Fauna database. Must be SERVER secret (or higher).
    fauna_user_index_auth0_id, // The name of the index we can use to look up a user by Auth0 ID, e.g. `Match(Index('<your_index_name>'), '<an auth0 user ID>')`
    ) {
    return util.promisify(jwt.verify)(auth0_jwt, auth0_client_cert_pubkey, {
    algorithms: ['RS256'], // You could also use HS256 and your Client Secret rather than the public key.
    audience: auth0_client_id,
    clockTolerance: 15, // liberal — our goal here isn't to detect expired JWTs down to the ms.
    }).then((jwtPayload) => {
    const client = new faunadb.Client({
    secret: fauna_server_secret
    })
    const jwt_auth0_id = jwtPayload.sub;
    const jwt_exp_ms = jwtPayload.exp * 1000;
    const overrideUserPassword = uuidv4();
    return client.query(
    q.Let(
    {
    userToken: q.Let(
    {
    userRef: findUserRef(fauna_user_index_auth0_id, jwt_auth0_id)
    },
    q.Do(
    setUserPassword(q.Var('userRef'), overrideUserPassword),
    loginUser(q.Var('userRef'), overrideUserPassword),
    ),
    ),
    },
    q.Do(
    q.Create(
    q.Collection(config.COLLECTION_NAME),
    {
    data: {
    token_ref: q.Select(['ref'], q.Var('userToken')),
    expires_at: q.ToTime(jwt_exp_ms),
    meta: custom_metadata,
    }
    }
    ),
    q.Select(['secret'], q.Var('userToken')),
    ),
    ),
    ).catch((e) => { // Catch here so we don't accidentally swallow a failed JWT verification.
    if (e.message === 'unauthorized') {
    e.message = 'unauthorized: missing or invalid fauna_server_secret, or not enough permissions';
    throw e
    } else {
    throw e
    }
    })
    });
    }

    if (require.main === module) {
    exchangeJwtForSecret.apply(this, process.argv.slice(2))
    .then((result) => console.log(result))
    .catch((error) => console.error(error))
    }

    module.exports = exchangeJwtForSecret;