Skip to content

Instantly share code, notes, and snippets.

@Bulletninja
Forked from colllin/Readme.md
Created May 26, 2020 22:26
Show Gist options
  • Save Bulletninja/2b55cf19401574161b786d8ac16b5670 to your computer and use it in GitHub Desktop.
Save Bulletninja/2b55cf19401574161b786d8ac16b5670 to your computer and use it in GitHub Desktop.
Auth0 + FaunaDB integration strategy

Goal

Solutions

At the very least, we need two pieces of functionality:

  1. Create a user document in Fauna to represent each Auth0 user.
  2. Exchange an Auth0 JWT for a FaunaDB user secret.

Creating the user document is straight-forward — you could either generate it the first time you need it, or you could create a signup flow which is triggered upon first login in the case of a missing user record.

The biggest question is the security around the FaunaDB secret. The security around the secret should be equal to the security around the JWT. Anything less would become the new weakest link in our security, and anything more would be wasted effort. I have a few ideas for implementation:

  1. Create a /fauna-login endpoint which verifies an Auth0 JWT and returns a Fauna secret for that user. The UI would call this endpoint anytime it receives a JWT, and store the Fauna secret in memory. It would send this secret in an Authorization header to communicate directly with the FaunaDB native GraphQL endpoint. This is slightly less secure than a JWT, since the secret never expires, and has similar storage and transmission practices.

  2. Create a /fauna-login endpoint which verifies an Auth0 JWT and sets the user secret in a cookie (httponly, secure, samesite) instead of returning it in the response. This is more secure since the secret isn't exposed on the client, but...

    New considerations:

    • We would need to proxy Fauna's GraphQL endpoint. This endpoint would read the user secret from the cookie and pass it in an Authorization header when forwarding the request to Fauna.
    • If we're using a cookie for authentication, then I believe we're opening ourselves to CSRF. We should use one of the patterns in Owasp's CSRF cheat sheet to mitigate. I need to think more about it before doing it, but the JWT itself might actually serve as a reasonable stateless CSRF token since it is signed and has an expiration. In any case, the CSRF token would need to be sent along with any requests to our proxy graphql endpoint.
  3. Represent Auth0 JWTs in a new collection "auth0_jwt" in our Fauna database, generate Fauna secrets for auth0_jwt instances instead of user instances, and specify all ABAC rules in terms of permissions granted to auth0_jwt instances (which act on behalf of the users they reference). Since we can set a TTL on the auth0_jwt collection, we now have expiring secrets for each user, and the collection (and number of secrets) scales with the number of active JWTs. Now, the client pattern is the same as #1, except that we return a secret representing an instance from the auth0_jwt collection instead of a user collection, and the client can again store this secret in-memory and use it in an Authorization header to communicate directly with Fauna's native GraphQL endpoint.

    Considerations:

    • I haven't tested the behavior of instance secrets and TTL settings. I know that TTL is not guaranteed to cleanup documents immediately on expiration, in which case you may need to also include some kind of expiration check in your ABAC rules.
    • This makes your ABAC rules more complex. Ignoring any performance penalties for now, it makes the rules harder to write and less readable, but ideally this would amount to a bit of boilerplate in your ABAC rules, and you would simply copy and paste this boilerplate when creating a new rule.
    • Could Delegates be used here to delegate the user's permissions to the user's "auth0_jwt" instance, rather than re-writing all of our ABAC rules?

Deeper Auth0 Integration — "Rules" and "Custom Database Connections"

Auth0 Rules could be used as an integration point instead of our token exchange endpoint (/fauna-login) for Options 1 or 3 above. Our custom Rule would place the Fauna secret into the JWT which is then returned to the application. This would make our UI code a little simpler but would require more complex Auth0 integration and deployment strategies. Otherwise it has pretty much the same security implications as creating a token exchange endpoint, except in Option 1 (when Fauna secrets don't expire), where it would increase the risks associated with JWT exposure.

Fauna could also be integrated as an Auth0 Custom Database Connection. In this scenario, Fauna would serve as your "custom user database", used to store and verify user credentials. The integration would require writing a couple webtask scripts/handlers to perform login, signup, etc. In this approach, we would need to obtain a Fauna secret during user login and return it within the JWT, since you wouldn't have access to the user's password later for the call to Fauna's Login() function. Similarly, this approach requires an expiring Fauna secret in order to at least maintain the security level provided by the JWT itself.

Note: As of writing, Auth0's Custom Database feature is only available to Enterprise customers. I've contacted support to ask about pricing as an add-on to the Developer or Developer Pro plans, but probably won't be able to post pricing here.

Fauna Feature Requests

  1. TTL (or expiration times) on secrets would make implementing option #1 above as secure as implementing option #3, and much easier to implement and maintain. It would also make it the clear choice over #2, since the JWT is the weakest link in scenario #2, and we are doing extra work to secure the database secret due to the lack of expiration.
  2. [much lower priority] Being able to Login() a user without a password (only a SERVER or ADMIN secret) might be interesting to avoid the runaround with generating a random password.

FQL Reference

How to generate a random user password and immediately log that user in, returning the instance secret:

Login(
    Select(
        ['ref'],
        Update(
            Select(
                ['ref'], 
                Get(Match(Index('unique_User_auth0_id'), 'auth0|abc123'))
            ),
            {credentials: {password: '<random string>'}}
        )
    ),
    {password: '<the same random string>'}
)

...which returns:

{ ref: Ref(Tokens(), "2449923875922837502387086"),
  ts: 1569885516220000,
  instance: Ref(Collection("users"), "2449832759625837042385929"),
  secret: 'fnEDZabcabcabcabcabcabcabcabcabcabcabcabcabcabcabc' }

Notes:

  • I believe this requires a SERVER secret in order to update the user and then login as them.
  • A Fauna CLIENT secret can Login() a user, provided they have the user ref and password, e.g. Login(Ref(Collection("users"), "244749204758312929"), {password: '123'}), or access to lookup the user ref in an index. Since a CLIENT secret is intended for public/anonymous access and is therefore intended to be exposed, it's important to generate a secure password for the users, and not just set them all as something simple like '123'.
@charlie17
Copy link

I came upon your write-up and found it quite helpful as a thought process. I notice Fauna does now seem to offer ttl on its login function. Does this tip the scales now in favor of your option #1 above? Other thoughts?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment