Last active
February 21, 2024 14:55
Revisions
-
colllin revised this gist
Jan 31, 2020 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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 guaranteed expiration/TTL for ABAC tokens, so we need to implement it ourselves if we care about it. ### What's in the box? -
colllin revised this gist
Nov 5, 2019 . 2 changed files with 7 additions and 28 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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. - 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). This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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 config = require('./config.js') const q = faunadb.query @@ -18,20 +17,10 @@ function findUserRef(index, matchValues) { ) } 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; return client.query( q.Let( { userRef: findUserRef(fauna_user_index_auth0_id, jwt_auth0_id), userToken: loginUser(q.Var('userRef')) }, q.Do( q.Create( -
colllin revised this gist
Oct 12, 2019 . 1 changed file with 21 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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. -
colllin created this gist
Oct 12, 2019 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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`. This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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', } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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; This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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; This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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;