redwoodjs / redwood

The App Framework for Startups
https://redwoodjs.com
MIT License
17.3k stars 992 forks source link

Support Google Login without using Firebase or Auth0 by implementing a dedicated AuthProvider #1592

Open dthyresson opened 3 years ago

dthyresson commented 3 years ago

A Discord chat pointed me to the following implementation:

https://github.com/bennettrogers/redwood-googleauth/blob/master/web/src/contexts/GoogleAuthContext/GoogleAuthContext.js

where using

https://www.npmjs.com/package/react-use-googlelogin

as the client one can implement a GoogleAuthProviders

import React, { useState, useEffect, useContext } from 'react';
import { useGoogleLogin } from 'react-use-googlelogin';
import { createGraphQLClient, gql } from '@redwoodjs/web';

import { AuthCache } from 'src/contexts/GoogleAuthContext/AuthCache';

const graphQLClient = createGraphQLClient();

const authCache = new AuthCache();

const GoogleAuthContext = React.createContext();

const STORE_GOOGLE_AUTH_MUTATION = gql`
  mutation StoreGoogleAuthMutation($code: String!) {
    storeGoogleAuth(code: $code) {
      id
      email
      jwt
    }
  }
`;

const useGoogleAuth = () => {
  const context = useContext(GoogleAuthContext);
  if (context === undefined) {
    throw new Error('useGoogleAuth must be used within a GoogleAuthProvider');
  }
  return context;
};

const GoogleAuthProvider = ({ children }) => {
  const [authenticated, setAuthenticated] = useState(false);
  const [currentUser, setCurrentUser] = useState();
  const [loading, setLoading] = useState(true);
  const googleAuth = useGoogleLogin({
    clientId: process.env.REDWOOD_ENV_GOOGLE_API_CLIENT_ID,
    redirectUri: process.env.GOOGLE_API_REDIRECT_URL,
    scope: process.env.REDWOOD_ENV_GOOGLE_API_SCOPES,
    cookiePolicy: 'single_host_origin',
    fetchBasicProfile: true,
    uxMode: 'popup',
    persist: false, // Handle persisting on our own so we can use localstorage instead of sessionstorage (to persist across tabs and window closing)
  });

  useEffect(() => {
    const restoreAuthState = () => {
      const user = authCache.getUser();
      const isAuthenticated = !!user;
      setAuthenticated(isAuthenticated);
      if (isAuthenticated) {
        setCurrentUser(user);
      }
      setLoading(false);
    };
    restoreAuthState();
  }, []);

  const login = async () => {
    try {
      const code = await googleAuth.grantOfflineAccess();

      // Send the auth code to the API which uses our client id/secret to get access/refresh/id tokens
      const {
        data: { storeGoogleAuth: googleUser },
      } = await graphQLClient.mutate({
        mutation: STORE_GOOGLE_AUTH_MUTATION,
        variables: { code },
      });

      authCache.saveUser(googleUser);
      setCurrentUser(googleUser);
      setAuthenticated(true);

      return googleUser;
    } catch (error) {
      console.error(error);
    }
  };

  const logout = async () => {
    try {
      setAuthenticated(false);
      await googleAuth.signOut();
      authCache.clearUser();
      setCurrentUser(null);
    } catch (error) {
      console.error(error);
    }
  };

  // Mirror the structure of the Redwood auth providers to give to RedwoodProvider
  const authValues = {
    type: 'google',
    client: undefined, // TODO: return the client from react-use-googlelogin if desired?
    loading,
    authenticated,
    currentUser,
    login,
    logout,
    getToken: async () => {
      return currentUser?.jwt || null;
    },
  };

  return (
    <GoogleAuthContext.Provider value={authValues}>
      {children}
    </GoogleAuthContext.Provider>
  );
};

export { GoogleAuthProvider, useGoogleAuth };

Could use this code to implement in Redwood auth package instead a one off component.

dthyresson commented 3 years ago

Note: Supabase also offers Google auth now.

jtoar commented 3 years ago

@dthyresson based on the discussion of offloading auth providers in this thread https://github.com/redwoodjs/redwood/issues/2260, we should probably hold off on this for now right? I know you didn't say it was urgent, just mean that we should wait till we have better auth infrastructure in place

Tobbe commented 1 year ago

@dthyresson @jtoar How do you guys feel about this one? What's the next step here?

dthyresson commented 1 year ago

This was an idea of mine from 2020 which I don’t think is fully formed given where Redwood auth is now in 2023.

If we want to implement social auth (Google, GitHub, etc) it would likely either piggyback off a cookie dbAuth or some other JWT backed auth.

I think we should close this issue and have an RFC for social auth support.

jtoar commented 1 year ago

@dthyresson sounds good; I don't have enough context to make the RFC for social auth. Does it involve more than just making dbAuth work with social logins? (Was that the idea?)

But we can close before an RFC is written, just didn't want to lose the todo

Tobbe commented 1 year ago

Some prior art (haven't tested either of them) https://github.com/realStandal/redwoodjs-dbauth-oauth https://github.com/usekeyp/oauth2-client-redwood

schybo commented 1 year ago

I've been building Google Auth out on the side for my personal app. I've had to duplicate some of the code for DbAuthHandler for now.

I'm pretty sure if you just expose more of the DbAuthHandler to be replaced/customized it may open the door to any such auth provider that the Redwood user wants and you don't have to go about supporting each one. Which I believe would be ideal.

Such as if you could pass in methods for the login check and the session key creation upon creation. If I could overwrite these it'd be extremely less hacky from my end.

I'm open to submitting an RFC to illustrate what I mean cc/ @Tobbe

Tobbe commented 1 year ago

@schybo Thanks for volunteering Brent! An RFC would be awesome! Sorry I didn't see your message sooner 🙁

schybo commented 1 year ago

@Tobbe Sure thing. I'll try to get to it this weekend. Do you have an example RFC format you want me to follow that you could share?

Tobbe commented 1 year ago

I don't have an RFC template. The format isn't too important. And don't spend too much time on it. Just write something and then I'll ask questions if anything is unclear.

schybo commented 1 year ago

Just finished integrating Google Auth with my Redwood site - so here's what's up.

I think the only beneficial changes you should make is to semantically expose the _loginResponse on the authHandler. So go from _loginResponse to loginResponse. Why? It allows creation of the session token that dbAuth understands.

I created a new function /googleAuth that handled verifying the JWT token I post from the FE after the google login response and then either creating user + attaching Google token + login OR just login. Once verified I do:

const loginResponse = authHandler._loginResponse(user)

return {
      body: loginResponse[0],
      headers: {
        'Content-Type': 'application/json',
        'access-control-allow-credentials': true,
        ...loginResponse[1],
      },
      statusCode: 200,
    }

This is the sole core change I recommend we make such that this method is noted as external; not internal, and that we should maintain it as such throughout upgrades to RedwoodJS. It allows anybody to integrate other oAuth providers via just creating a custom function, doing the verification there, and then returning the session token (of which dbAuth understands and created).

From an ease of integration perspective, I could see us making a generator for this:

All this would be Google Auth specific but would provide value to RedwoodJs - and to those who don't want to worry about the details, despite how minor they are. We could also make this an external project that people can plugin instead of being part of the core repo. Similar to redwoodjs-stripe

Let me know your thoughts! @Tobbe

If the generator makes sense I can take that on.

Tobbe commented 1 year ago

Thanks @schybo! That's a great start 👍

It would be great if this general design/architecture would work for other OAuth providers too, and it sounds like you think it will. But just to make sure, do you think you could try implementing Discord as a provider too? If we had both Google and Discord working I'd feel much more confident that this is a general solution 🙂 Also thinking about how to have both enabled at the same time. Someone might want to give their users a few different options when it comes to what OAuth provider the user prefers. So the solution we come up with here must support a gui where the end user can pick from a list of oauth providers, and then know how to authorize with the chosen provider, and know to keep talking to that specific provider. (Hope that made sense, let me know otherwise)

A generator/setup command definitely makes sense to me!

schybo commented 1 year ago

@Tobbe Makes total sense. I'll work on setting up Discord now and see how extensible it is. Will report back.

Tobbe commented 1 year ago

@schybo You can probably take a lot of inspiration from https://github.com/UseKeyp/oauth2-client-redwood/blob/main/api/src/lib/oAuth/providers/discord/discord.js

Tobbe commented 1 year ago

Ohh, and please do the Authorization Code flow, not the Implicit flow

schybo commented 1 year ago

Yep. I've got the authorization flow working locally for Discord. I'll need to clean everything up and play around with it in production though. I'll report back. Just an update it's progressing though.

schybo commented 1 year ago

@Tobbe Update + question.

Discord auth works fine also with just exposing loginResponse. You also don't need to store a refresh token for just login capabilities since simply redirecting to login again seems reasonable.

I can proceed with a draft PR to

support a gui where the end user can pick from a list of oauth providers, and then know how to authorize with the chosen provider, and know to keep talking to that specific provider if you'd like

Tobbe commented 1 year ago

@schybo Thanks for the update. A draft PR would be great 🙂

schybo commented 1 year ago

Haven't forgotten about this! Trying to iron out bugs/quality for a production release of my app before spending some cycles here. So if anybody wants to get to it earlier - please do reach out 🙂

Tobbe commented 1 year ago

All good! Take your time 🙂

ROZBEH commented 1 year ago

@schybo thanks for working on this - great work. And yes I truly appreciate it if you can get the PR out there :-))