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

Decouple auth #5985

Merged
merged 227 commits into from Oct 13, 2022
Merged

Decouple auth #5985

merged 227 commits into from Oct 13, 2022

Conversation

Tobbe
Copy link
Member

@Tobbe Tobbe commented Jul 18, 2022

An attempt at some release notes:

For this release Redwood has totally revamped its authentication subsystem. The biggest change is that all auth providers are totally decoupled from Redwood's internals. We're doing this for a couple of reasons. One reason is we want to make maintaining the auth providers more sustainable for Redwood as a project. Auth providers can now be their own packages on NPM, so we're hoping devoted community members and auth companies will take over maintenance and ownership of auth providers, so we can focus on adding other features to Redwood. Another big reason is we wanted to make it easier for anyone to write their own custom auth provider. And finally we wanted to make it possible to have multiple auth providers configured at the same time. This is great if you for example want to switch from one provider to another and need to run both for a short time while moving all your users over to the new provider. Or if you want to have a different auth system for API access to your app.

To pull this off we had to make some majorly breaking changes. To make auth less tied to RW internals we've a little bit more code into user apps. You'll see this in a new auth.{js,tsx?} file in /web/src. For most project it should be enough to run our auth setup command again, passing in your current auth provider and the --force flag. But please make sure you commit all your currently modified files to git before running the setup command so you easily can review what changes it does to your files before committing them.


I started out just wanting to provide better types for the auth client methods, like logIn in the snippet below

const { logIn } = useAuth()

const onSubmit = (data: FormData) => {
  logIn({ username: data.email, password: data.password  })
    .then((data) => {
      // I want proper types for `data` here
    })
}

<Form onSubmit={onSubmit}>
  // ...
</Form>

When I started working on my solution I soon realized that this new implementation would also allow us to fully decouple the vendor specific auth logic. Plus finally allow users to truly implement their own custom auth solutions and integrations.

Looking at

export type AuthFactory<
ClientType,
ConfigType,
AuthClientType extends AuthClient
> = (
client: ClientType,
config: ConfigType
) => AuthClientType | Promise<AuthClientType>

we can see that we already had a bit of a factory pattern going. I took that idea and ran with it 🙂

So now everything starts with using a factory to create the <AuthProvier> component and useAuth hook that we need for RW's auth.
The key is we can use a vendor-specific "create" method that injects the types from that vendor into the generic auth methods provided by us, the RW framework.

I haven't started updating our auth generator yet, but what's going to be different is I'm going to make it generate a new auth.{js,ts} file next to App.{js,tsx}. It will look something like this

// auth.ts
import netlifyIdentity from 'netlify-identity-widget'
import { createNetlifyAuth } from '@redwoodjs/auth'

// In the future the `createNetlifyAuth` import could come from @netlify/redwoodjs
// And when we do that, we could also have it import `netlifyIdentity` itself

const { AuthProvider, useAuth } = createNetlifyAuth(netlifyIdentity);

export { AuthProvider, useAuth };

And with that file App.tsx would look something like this

// App.tsx
import { AuthProvider } from './auth'
import { FatalErrorBoundary } from '@redwoodjs/web'
import { RedwoodApolloProvider } from '@redwoodjs/web/apollo'

import FatalErrorPage from 'src/pages/FatalErrorPage'
import Routes from 'src/Routes'

import './index.css'

const App = () => (
  <FatalErrorBoundary page={FatalErrorPage}>
    <AuthProvider>
      <RedwoodApolloProvider>
        <Routes />
      </RedwoodApolloProvider>
    </AuthProvider>
  </FatalErrorBoundary>
)

export default App

So App.tsx would look the same no matter what auth provider you use. Only auth.ts would change

Here's a slightly trimmed down version of the Netlify factory

import type * as NetlifyIdentityNS from 'netlify-identity-widget'

import { isBrowser } from '@redwoodjs/prerender/browserUtils'

import { createAuthentication } from 'src/authFactory'

import { AuthImplementation } from './AuthImplementation'

type NetlifyIdentity = typeof NetlifyIdentityNS

export function createNetlifyAuth(netlifyIdentity: NetlifyIdentity) {
  const authImplementation = createNetlifyAuthImplementation(netlifyIdentity)

  isBrowser && netlifyIdentity.init()

  return createAuthentication(authImplementation)
}

function createNetlifyAuthImplementation(netlifyIdentity: NetlifyIdentity) {
  return {
    type: 'netlify',
    signup: () => {
      return new Promise((resolve, reject) => {
        netlifyIdentity.open('signup')
        netlifyIdentity.on('close', () => {
          resolve(null)
        })
        netlifyIdentity.on('error', reject)
      })
    },
    login: () => {
      // ...
    },
    logout: () => {
      // ...
    },
    getToken: async () => {
      // ...
    },
    getUserMetadata: async () => {
      return netlifyIdentity.currentUser()
    },
    restoreAuthState: async () => {
      return netlifyIdentity.currentUser()
    },
  }
}

To implement this in user-land as a custom auth provider all that would have to change is to import createAuthentication and AuthImplementation from @redwoodjs/auth instead. And then in auth.ts import createNetlifyAuth from wherever the user placed it instead of importing it from the RW framework.

Testing

// Ideas for testing:
// 1. Have a custom `render` method in /src/auth.ts
//   * This requires the user to switch to that custom render method for any
//     test that involves auth
//   * The custom render method can be generated by the setup command
//   * All basic tests we generate with our generators continues to work
//     without showing any scary warnings in the console
//   * Had to use an ugly /dist/-import to get this to work
// 2. Have `MockProviders` import the user's /src/auth.ts file and grab
//    <AuthProvider> from there
//   * Invisible to the user. Auth "just works".
//   * If the user has some other file, not /src/auth.ts, we won't find the
//     <AuthProvider> component
//   * Probably need to add an option to RW's `render()` method to not wrap
//     with the auth provider, so that basic generated tests can keep working
//     without warnings printed to the console
// 3. Try to override `useAuth` in tests to just use some dummy implementation
//    that doesn't need a context provider like <AuthProvider>.
//   * All basic tests that we generate will keep working
//   * A lot more tests can be synchronous, giving us access to more of rtl's
//     testing methods. Overall synchronous tests are also simpler to
//     write and understand
//   * I couldn't find a way to reliably have the dummy implementation
//     override the user's implementation because there are _so_ many ways
//     it can be imported (unless we force our users to never rename auth.ts
//     and to never use relative import paths)
//   * Not testing as much of the real auth code as other solutions. On the
//     other hand most uses will most likely mainly use framework code here
//     so shouldn't be necessary to test all that much.
//   * No existing test needs to be updated
// 4. Educate our users to manually wrap components that (somewhere in the
//    component tree) uses auth in their own <AuthProvider>
//   * Can provide an option to RW's `render()` to make this easier (same
//     as used for solution 1.)
//   * More explicit what's going on
//   * No test-only code generated in /src/auth.ts that people who don't
//     write unit tests will never user
//   * Lots of repeated boilerplate code in tests that use auth
//   * Generated basic test continue to work

Breaking changes and codemods

useAuth is no longer exported from @redwoodjs/auth

No verifyOTP for supabase anymore. Have to use it from client instead
Don't think there actually was any way to access this method anyway. So not breaking to remove
But do double-check this

getUserMetadata no longer injects roles on the root object. Only returns the Clerk user object

Should we replace all @redwoodjs/auth imports with src/auth?
Should we try to detect what auth prover a project is using and then run the rw setup auth command for that auth service provider? Or should we just tell the user to do that on their own?
Need to clean up from old auth in App.js,tsx

Decision log

2022-08-16. On today's core-team meeting we decided to keep the client name that you get by destructuring the return value from useAuth()

Left to do

  • Move the rest of the auth provider clients over to this new implementation
  • Clerk is (or at least was) a bit different than the other clients. Need to double check that implementation
  • See if we can give access to the "raw" auth provider client lib/sdk. Perhaps by augmenting useAuth inside auth.ts in user-land, unless I can come up with something better. Another option is to pass it to createAuthentication -> createAuthProvider and store it in AuthContext like we already do. Just have to make sure we can pass the types along.
  • Should we keep the client name, or should we perhaps go with something like providerClient? Keeping client.
  • Get rid of global.__REDWOOD__USE_AUTH
  • Update packages/auth/README.md
  • Update auth setup command
  • Write a test in an RW App that tests a component that uses useAuth. Test both logged in and logged out state
  • Manually test the RedwoodApolloProvider warning message shown during setup
  • Write test for custom hasRole
  • Write test for custom getCurrentUser
  • Update auth provider etc for mocks
  • Update test-project
  • Try to narrow the decoded type supported for parseJWT
  • Update auth docs
  • Write docs on writing custom auth
  • Test this with our auth playground to make sure all providers still work
  • Codemods. Might have to do one per existing auth provider.
  • Do Cognito as the first test of a custom and then external provider
  • Flightcontrol does checks for DbAuth in their deploy scripts. Make sure that stuff still works.
  • We have a updateApiImports codemod. Do we need a new one now? (Touches on DbAuthHandler)

Things moved away from the list above for handling in future PRs

  • Get rid of the any types in DbAuth
  • See if I can also improve the options types. Currently they're all unknown in AuthContext.ts, e.g. logIn(options?: unknown): Promise<TLogIn>. Might have to do a TLogInOptions generic for it
  • Don't force async on all the AuthProvider methods
  • Enforce roles[] (vs role | roles[]) for auth providers (not for users)
  • Move the provider implementations to separate packages
  • Print auth.ts for TS projects and auth.js for JS projects as part of notes in setup script
  • Step2 after the above has been implemented: Print the actual filename, like firebaseAuth2.ts if multiple auth providers have been set up.

Closes #3617 #1585

@netlify
Copy link

netlify bot commented Jul 18, 2022

Deploy Preview for redwoodjs-docs ready!

Name Link
🔨 Latest commit 5cfe770
🔍 Latest deploy log https://app.netlify.com/sites/redwoodjs-docs/deploys/6307db7880b4e90009e470c0
😎 Deploy Preview https://deploy-preview-5985--redwoodjs-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site settings.

@peterp
Copy link
Contributor

peterp commented Jul 22, 2022

@Tobbe One of the things that I would like to have for tRPC is the ability to implement my over version of getCurrentUser. Since I'm not using GraphQL I would like to point to my own endpoint.

const getCurrentUser = useCallback(async (): Promise<
Record<string, unknown>
> => {
const client = await rwClientPromise
// Always get a fresh token, rather than use the one in state
const token = await getToken()
const response = await global.fetch(global.RWJS_API_GRAPHQL_URL, {
method: 'POST',
// TODO: how can user configure this? inherit same `config` options given to auth client?
credentials: 'include',
headers: {
'content-type': 'application/json',
'auth-provider': client.type,
authorization: `Bearer ${token}`,
},
body: JSON.stringify({
query:
'query __REDWOOD__AUTH_GET_CURRENT_USER { redwood { currentUser } }',
}),
})
if (response.ok) {
const { data } = await response.json()
return data?.redwood?.currentUser
} else {
throw new Error(
`Could not fetch current user: ${response.statusText} (${response.status})`
)
}
}, [rwClientPromise, getToken])

@Tobbe Tobbe added the release:breaking This PR is a breaking change label Jul 22, 2022
…e-decouple-auth-web

 Conflicts:
	packages/api/src/index.ts
	packages/auth-providers-api/package.json
	packages/auth-providers-setup/package.json
	packages/auth-providers-setup/src/custom/templates/web/auth.ts.template
	packages/auth-providers-setup/src/dbAuth/templates/api/functions/auth.ts.template
	packages/auth-providers-setup/src/dbAuth/templates/api/functions/auth.webAuthn.ts.template
	packages/auth-providers-web/package.json
	packages/auth2/package.json
	packages/cli-helpers/README.md
	packages/cli-helpers/package.json
	packages/cli-helpers/src/auth/__tests__/fixtures/dbAuthSetup/templates/api/functions/auth.webAuthn.ts.template
	packages/cli/package.json
	packages/cli/src/commands/generate/sdl/sdl.js
	packages/cli/src/commands/setup/auth/__tests__/authHandler.test.js
	packages/cli/src/commands/setup/auth/auth.js
	packages/graphql-server/package.json
	packages/graphql-server/src/functions/useRequireAuth.ts
	packages/graphql-server/src/plugins/__tests__/useRedwoodAuthContext.test.ts
	packages/router/package.json
	packages/router/src/__tests__/router.test.tsx
	packages/router/src/router-context.tsx
	packages/telemetry/src/sendTelemetry.ts
	packages/web/package.json
	packages/web/src/apollo/index.tsx
	yarn.lock
@Tobbe Tobbe marked this pull request as ready for review October 13, 2022 19:47
@Tobbe Tobbe enabled auto-merge (squash) October 13, 2022 19:48
@Tobbe Tobbe merged commit 0942fba into redwoodjs:main Oct 13, 2022
@redwoodjs-bot redwoodjs-bot bot added this to the next-release milestone Oct 13, 2022
@Tobbe Tobbe deleted the tobbe-decouple-auth-web branch October 13, 2022 20:08
@Alonski
Copy link
Contributor

Alonski commented Oct 25, 2022

I think this broke Auth with Supabase...
Following the Supabase guide: https://supabase.com/docs/guides/with-redwoodjs
I eventually get an error with: client.auth.signIn is not a function
From this file: [supabase.js](myRedwoodApp/node_modules/@redwoodjs/auth/dist/authClients/supabase.js)

Edit:
Doh nevermind this isn't even released in 3.2 yet.

Hmm is the thought here that Redwood won't provide any providers OOTB anymore?

@jtoar jtoar modified the milestones: next-release, v4.0.0 Oct 25, 2022
dac09 added a commit that referenced this pull request Oct 26, 2022
…aching

* 'main' of github.com:redwoodjs/redwood: (244 commits)
  chore(deps): update dependency @replayio/playwright to v0.3.0 (#6735)
  chore: update all contributors
  Update Clerk docs (#6712)
  Update firebase auth docs (#6717)
  Clerk: Simplify web implementation (#6713)
  Add auth decoder to clerk auth setup (#6718)
  Auth: Update firebase setup script (#6716)
  chore: Remove redundant space " " (#6714)
  Update the Clerk setup script and templates (#6710)
  Fix decouple auth related type errors (#6709)
  fix(deps): update dependency css-minimizer-webpack-plugin to v4.2.2 (#6688)
  fix(deps): update dependency @graphql-codegen/cli to v2.13.7 (#6687)
  feat: publish 2nd canary (@next) from release branch (#6505)
  fix: don't pr if can't cherry pick cleanly (#6703)
  fix(dbAuth): add required packages to setup command (#6698)
  Netlify: Enable auth-providers-api and auth-providers-web installation (#6697)
  chore: make misc change to trigger canary publishing (#6695)
  chore: remove private on new packages (#6692)
  chore: run lint fix (#6691)
  Decouple auth (#5985)
  ...
@Tobbe
Copy link
Member Author

Tobbe commented Oct 26, 2022

Hmm is the thought here that Redwood won't provide any providers OOTB anymore?

Kind of. The end goal is to provide them all as plugins. Redwood doesn't have plugins yet though, so until it does, they'll still be part of Redwood like they are now, just in a different package.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
release:breaking This PR is a breaking change
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

Update custom auth generator to generate auth client
6 participants