MomenSherif / react-oauth

Google OAuth2 using the new Google Identity Services SDK for React 🚀
https://www.npmjs.com/package/@react-oauth/google
MIT License
1.07k stars 136 forks source link

GoogleLogin and useGoogleLogin are not returning the same response #12

Closed eakl closed 2 years ago

eakl commented 2 years ago

How can I use a custom login button? GoogleLogin component and useGoogleLogin aren't returning the same response

With GoogleLogin the response is:

{
  clientId: 'XXXXXX,
  credential: 'credential_token',
  select_by: 'btn'
}

With useGoogleLogin, the response is:

{
  access_token: "xxxxxxxxx",
  authuser: "0",
  expires_in: 3599,
  hd: "domain.com"
  prompt: "none"
  scope: "email profile openid https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email"
  token_type: "Bearer"
}

The hook isn't supposed to return the credential key/value as the component does? I am sending the credential key/value to my server which verifies it to send me back the user profile.

import { OAuth2Client } from 'google-auth-library'
// ...
const userProfile = googleClient.verifyIdToken({
    idToken: req.body.credentials,
    audience: req.body.clientId,
  })
MomenSherif commented 2 years ago

Authenticating the user involves obtaining an ID token and validating it. ID tokens are a standardized feature of OpenID Connect designed for use in sharing identity assertions on the Internet.

You can get id_token (JWT) if you are using the personalized button for authentication.

and useGoogleLogin hook is wrapping the Authorization part in new Google SDK if you are using it in implicit flow like that, it will return access_token to be used for fetching data from google APIs for example

  const googleLogin = useGoogleLogin({
    onSuccess: async tokenResponse => {
      console.log(tokenResponse);
      // fetching userinfo can be done on the client or the server
      const userInfo = await axios
        .get('https://www.googleapis.com/oauth2/v3/userinfo', {
          headers: { Authorization: `Bearer ${tokenResponse.access_token}` },
        })
        .then(res => res.data);

      console.log(userInfo);
    },
    // flow: 'implicit', // implicit is the default
  });

But my recommendation for you as you have a backend, go with Authorization code flow, which will return code that you will exchange with your backend to obtain

Client

const googleLogin = useGoogleLogin({
  onSuccess: async ({ code }) => {
    const tokens = await axios.post('http://localhost:3001/auth/google', {  // http://localhost:3001/auth/google backend that will exchange the code
      code,
    });

    console.log(tokens);
  },
  flow: 'auth-code',
});

Backend using express

require('dotenv').config();
const express = require('express');
const {
  OAuth2Client,
} = require('google-auth-library');
const cors = require('cors');

const app = express();

app.use(cors());
app.use(express.json());

const oAuth2Client = new OAuth2Client(
  process.env.CLIENT_ID,
  process.env.CLIENT_SECRET,
  'postmessage',
);

app.post('/auth/google', async (req, res) => {
  const { tokens } = await oAuth2Client.getToken(req.body.code); // exchange code for tokens
  console.log(tokens);

  res.json(tokens);
});

app.post('/auth/google/refresh-token', async (req, res) => {
  const user = new UserRefreshClient(
    clientId,
    clientSecret,
    req.body.refreshToken,
  );
  const { credentials } = await user.refreshAccessToken(); // optain new tokens
  res.json(credentials);
})

app.listen(3001, () => console.log(`server is running`));
eakl commented 2 years ago

Thanks @MomenSherif for the details. I could make it work with your code snippet. Just curious, When should I call the refresh token endpoint for Google auth? At every new session?

Thanks for this package. it's very clean.

MomenSherif commented 2 years ago

@eakl Most welcome ❤

you can set a timer to refresh the token with a new one before expiration, the timer can be set after user login, or the app initialized with a refresh token available

another implementation is to setup an interceptor for your requests to validate token expiration time, and if it will expire soon it will fire a request to refresh the token.

Any approach of them makes sense and is easy to implement for you, Go ahead with it 🎉

gusaiani commented 2 years ago

@MomenSherif, thanks for the explanation above.

And thanks for this library. I'm certainly inclined to use it.

Having said what you said, would you still consider adding an option for the useGoogleLogin hook to have the request return credential?

Use case would be something like:

  1. we want to have a custom-looking button for the sign-in
  2. our back-end is not trivial and the back-end team is not able to prioritize work to implement what you describe above

Or maybe there are other ways to style the GoogleLogin component?

MomenSherif commented 2 years ago

@gusaiani unfortunately customizing the personalized button is very limited because Google renders it in an iframe and allows only certain options to be customizable through known props because they want the same look and feel across all applications.

All pops are listed here


Returning credential in custom hook, will be difficult as google doesn't expose it implicitly,


just wanted the package to be a small wrapper around the new SDK, with the same behaviors google gives us, and it's upon the consumer to use it as his application wants.

if you need refresh token, or for some reason you need to get `id_token` (google's JWT) from custom button, you will need the authorization code flow as mentioned above in the example.

you can tweak authorization flow to exchange code on the client side and ignore backend, but you will expose your client secret for any hackers.
gusaiani commented 2 years ago

Thank you very much, @MomenSherif.

MomenSherif commented 2 years ago

@gusaiani Most welcome 😃

kharithomas commented 2 years ago
app.post('/auth/google/refresh-token', async (req, res) => {
 const user = new UserRefreshClient(
    clientId,
    clientSecret,
    req.body.refreshToken,
  );
  const { credentials } = await user.refreshAccessToken(); // optain new tokens
  res.json(credentials);
})

Thank you so much for this example. I just recently moved from react-google-login and the setup has been a breeze. I noticed you're using UserRefreshClient in the code snippet - where does this come from?

MomenSherif commented 2 years ago

@kharithomas It's on backend side (express server in this example) if you want to use refresh token

If you just need user's token you can use <GoogleLogin /> it's straight forward

Or if you need custom button you can use useGoogleLogin, and check demo website in the Readme, will show you how to get user info step by step

Toxocious commented 2 years ago
app.post('/auth/google/refresh-token', async (req, res) => {
 const user = new UserRefreshClient(
    clientId,
    clientSecret,
    req.body.refreshToken,
  );
  const { credentials } = await user.refreshAccessToken(); // optain new tokens
  res.json(credentials);
})

Thank you so much for this example. I just recently moved from react-google-login and the setup has been a breeze. I noticed you're using UserRefreshClient in the code snippet - where does this come from?

A bit late to replying to this, sorry.

UserRefreshClient is included in the 'google-auth-library' package, so just import it via import { UserRefreshClient } from 'google-auth-library';.

kharithomas commented 2 years ago

@MomenSherif Thanks for your reply - realized I didn't see this. I was able to implement a solution as you mentioned.

@Toxocious Thanks for providing this alternate example.

mistryrn commented 2 years ago

Thank you so much for the authorization code flow example, @MomenSherif ! 🙇 It has been invaluable for a migration from the old Google Sign-in Library to Google Identity Services.

Just in case this saves anyone some of the headaches I have suffered debugging this: follow the example exactly as it is written! I thought I was being smart by inserting my app's redirect URI in the backend snippet, but instead I spent an entire night searching online to try and understand why my app "doesn't comply with Google's OAuth 2.0 policy" or why I was getting a redirect_uri_mismatch error 🙃

🚨 In the backend OAuth2Client, the 3rd param (redirectUri) must be 'postmessage'! 🚨

Do not, I repeat, DO NOT try to be smart and add your app's redirect URI there. It will cause a very unhelpful invalid_request error You can't sign in to this app because it doesn't comply with Google's OAuth 2.0 policy for keeping apps secure. You can let the app developer know that this app doesn't comply with one or more Google validation rules. or a slightly more helpful but still unclear redirect_uri_mismatch error.

Here's the snippet in question from the example:

const oAuth2Client = new OAuth2Client(
  process.env.CLIENT_ID,
  process.env.CLIENT_SECRET,
  'postmessage', // <- LEAVE THIS AS-IS! Do NOT insert your actual redirect URI
);

As far as an actual explanation for why it must be 'postmessage' (or why Google's docs neglect to mention it), all I've really found have been StackOverflow answers referencing an old Google+ platform sign-in doc:

liqwid commented 2 years ago

@mistryrn there's a special place in hell for developers of this api, right next to recaptcha creators

this absolutely HAS to be in the doc, spent like 5 hours figuring out, messaging support etc

SpurgeonPrakash commented 2 years ago

useGoogleLogin was giving us accessToken(Bearer type) with flow: 'auth-code' and it is giving us code without flow: auth-code, can somebody tell me how will i get credential which was send to onsuccess function with GoogleLogin

BadrBelhiti commented 2 years ago

Needed this for my Django backend. Here's the code I got working for the views:

from django.http import JsonResponse
import requests
import core.settings as settings

def login(request):
    auth_code = request.GET['code']

    data = {
        'code': auth_code,
        'client_id': settings.SOCIAL_AUTH_GOOGLE_OAUTH2_KEY, # client ID
        'client_secret': settings.SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET, # client secret
        'redirect_uri': 'postmessage',
        'grant_type': 'authorization_code'
    }

    response = requests.post('https://oauth2.googleapis.com/token', data=data)

    return JsonResponse(response.json(), status=200)

def refresh(request):
    refresh_token = request.GET['refresh_token']

    data = {
        'refresh_token': refresh_token,
        'client_id': settings.SOCIAL_AUTH_GOOGLE_OAUTH2_KEY, # client ID
        'client_secret': settings.SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET, # client secret
        'grant_type': 'refresh_token'
    }

    response = requests.post('https://oauth2.googleapis.com/token', data=data)

    return JsonResponse(response.json(), status=200)

Of course this is not production ready (needs validation, remove hardcoded values, etc.), but it demonstrates the API calls to Google's OAuth2 service.

ShreyJ1729 commented 1 year ago

I followed all the steps in @MomenSherif's post exactly but I am still ending up with an error related to an unauthorized client.

Basically I'm taking the code from the user from the frontend and sending to the backend, from where it's supposed to exchange the code for an accesstoken using google's api, but I keep getting an error 401: unauthorized_client, but I have enabled the calendar API on my project. I'm not sure what is going wrong.

Here's the frontend code:

const googleLogin = useGoogleLogin({
        flow: "auth-code",
        onSuccess: async codeResponse => {
            console.log(codeResponse);

            const tokens = await axios.post("http://localhost:3001/auth/google/", {
                code: codeResponse.code
            });

            console.log(tokens);
        }
    })

and here's the backend:

app.post('/auth/google', async (req, res) => {
    console.log("got request!")
    console.log(req.body.code)
    const tokens = await axios.post("https://oauth2.googleapis.com/token", {
        'code': req.body.code,
        'client_id': CLIENT_ID,
        'client_secret': CLIENT_SECRET,
        'redirect_uri': 'postmessage',
        'grant_type': 'authorization_code'
    });
    console.log(tokens);
    res.json(tokens);
});

Stack overflow link: https://stackoverflow.com/questions/74132586/accessing-google-calendar-api-using-service-account

AHh never mind I solved it - I was using a different oauth client ID for frontend/backend. After using the same IDs it worked.

ArthurMiroslavsky commented 1 year ago

Thank you so much for the authorization code flow example, @MomenSherif ! 🙇 It has been invaluable for a migration from the old Google Sign-in Library to Google Identity Services.

Just in case this saves anyone some of the headaches I have suffered debugging this: follow the example exactly as it is written! I thought I was being smart by inserting my app's redirect URI in the backend snippet, but instead I spent an entire night searching online to try and understand why my app "doesn't comply with Google's OAuth 2.0 policy" or why I was getting a redirect_uri_mismatch error 🙃

🚨 In the backend OAuth2Client, the 3rd param (redirectUri) must be 'postmessage'! 🚨

Do not, I repeat, DO NOT try to be smart and add your app's redirect URI there. It will cause a very unhelpful invalid_request error You can't sign in to this app because it doesn't comply with Google's OAuth 2.0 policy for keeping apps secure. You can let the app developer know that this app doesn't comply with one or more Google validation rules. or a slightly more helpful but still unclear redirect_uri_mismatch error.

Here's the snippet in question from the example:

const oAuth2Client = new OAuth2Client(
  process.env.CLIENT_ID,
  process.env.CLIENT_SECRET,
  'postmessage', // <- LEAVE THIS AS-IS! Do NOT insert your actual redirect URI
);

As far as an actual explanation for why it must be 'postmessage' (or why Google's docs neglect to mention it), all I've really found have been StackOverflow answers referencing an old Google+ platform sign-in doc:

Thanks man!

rau commented 1 year ago

Needed this for my Django backend. Here's the code I got working for the views:

from django.http import JsonResponse
import requests
import core.settings as settings

def login(request):
    auth_code = request.GET['code']

    data = {
        'code': auth_code,
        'client_id': settings.SOCIAL_AUTH_GOOGLE_OAUTH2_KEY, # client ID
        'client_secret': settings.SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET, # client secret
        'redirect_uri': 'postmessage',
        'grant_type': 'authorization_code'
    }

    response = requests.post('https://oauth2.googleapis.com/token', data=data)

    return JsonResponse(response.json(), status=200)

def refresh(request):
    refresh_token = request.GET['refresh_token']

    data = {
        'refresh_token': refresh_token,
        'client_id': settings.SOCIAL_AUTH_GOOGLE_OAUTH2_KEY, # client ID
        'client_secret': settings.SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET, # client secret
        'grant_type': 'refresh_token'
    }

    response = requests.post('https://oauth2.googleapis.com/token', data=data)

    return JsonResponse(response.json(), status=200)

Of course this is not production ready (needs validation, remove hardcoded values, etc.), but it demonstrates the API calls to Google's OAuth2 service.

I am continuously running into an error when doing it this way.

{'error': 'redirect_uri_mismatch', 'error_description': 'Bad Request'}

However, on the frontend, this is what my code looks like.

const Login = () => {
    const login = useGoogleLogin({
        onSuccess: (codeResponse) => {
            console.log(
                axios.post("http://localhost:8005/api/users/login/", codeResponse)
            )
        },
        flow: "auth-code",
        redirect_uri: "http://localhost:3000/",
    })

    return <Button onClick={() => login()}>Sign in with Google 🚀 </Button>
}

And on the backend

@action(detail=False, methods=["POST"])
    def login(self, request):
        env = environ.Env()
        env.read_env()

        auth_code = request.data["code"]

        data = {
            "code": auth_code,
            "client_id": env("GOOGLE_CLIENT_ID"),  # client ID
            "client_secret": env("GOOGLE_CLIENT_SECRET"),  # client secret
            "redirect_uri": "http://localhost:3000/",
            "grant_type": "authorization_code",
        }

        response = requests.post("https://oauth2.googleapis.com/token", data=data)

        return Response(response.json(), status=status.HTTP_200_OK)

As you can see, it's exactly the same as yours. How did you get past the redirect URI error?

EDIT: wtf, scroll up guys, I don't understand why either, but it really is just replacing your redirect_uri on backend with 'postmessage'. wtf google seriously??

Shankarwal commented 1 year ago

app.post('/auth/google/refresh-token', async (req, res) => { const user = new UserRefreshClient( clientId, clientSecret, req.body.refreshToken, ); const { credentials } = await user.refreshAccessToken(); // optain new tokens res.json(credentials); })

Hi @MomenSherif

Thank you so much for posting the solution, I have been scratching my head for this implicit-flow and auth-flow for quite a few hours and finally got the required solution. I just wanted to know where this UserRefreshClient came from.

Thanks Again!!!

Okeyemi commented 1 year ago

Please could any one help with

I get a error every time the popup window appears (Cross-Origin-Opener-Policy policy would block the window.closed call)

doutv commented 1 year ago

If someone use dj-rest-auth and django-allauth, and encounter error redirect uri mismatch

you can see this comment https://github.com/iMerica/dj-rest-auth/issues/525#issuecomment-1675885190 which set callback_url = "postmessage" manually

class GoogleLogin(
    SocialLoginView
):  # if you want to use Authorization Code Grant, use this
    adapter_class = GoogleOAuth2Adapter
    callback_url = "postmessage"
    client_class = OAuth2Client
sophie-pan commented 1 year ago

Thank you so much for the authorization code flow example, @MomenSherif ! 🙇 It has been invaluable for a migration from the old Google Sign-in Library to Google Identity Services.

Just in case this saves anyone some of the headaches I have suffered debugging this: follow the example exactly as it is written! I thought I was being smart by inserting my app's redirect URI in the backend snippet, but instead I spent an entire night searching online to try and understand why my app "doesn't comply with Google's OAuth 2.0 policy" or why I was getting a redirect_uri_mismatch error 🙃

🚨 In the backend OAuth2Client, the 3rd param (redirectUri) must be 'postmessage'! 🚨 Do not, I repeat, DO NOT try to be smart and add your app's redirect URI there. It will cause a very unhelpful invalid_request error You can't sign in to this app because it doesn't comply with Google's OAuth 2.0 policy for keeping apps secure. You can let the app developer know that this app doesn't comply with one or more Google validation rules. or a slightly more helpful but still unclear redirect_uri_mismatch error.

Here's the snippet in question from the example:

const oAuth2Client = new OAuth2Client(
  process.env.CLIENT_ID,
  process.env.CLIENT_SECRET,
  'postmessage', // <- LEAVE THIS AS-IS! Do NOT insert your actual redirect URI
);

As far as an actual explanation for why it must be 'postmessage' (or why Google's docs neglect to mention it), all I've really found have been StackOverflow answers referencing an old Google+ platform sign-in doc:

https://stackoverflow.com/a/18990247 https://stackoverflow.com/a/48121098 https://stackoverflow.com/a/55222567

"postmessage" is only for auth code retrieved from popup methods. If using redirect, pass the redirect_uri used for retrieving auth code.

akash-d-dev commented 1 year ago
const handleGoogleLogin = useGoogleLogin({
    flow: 'auth-code',
    ux_mode: 'redirect',
    redirect_uri: 'http://localhost:3000',
    // select_account: true,
    onSuccess: async ({ code }) => {
      try {
        console.log(code);
        const { data } = await axios.post(
          'http://localhost:8000/auth/google',
          null,
          {
            headers: {
              Authorization: `Bearer ${code}`, // Sending the code as a Bearer token
            },
          }
        );
        localStorage.setItem('auth', JSON.stringify(data));
        onClose();
      } catch (error) {
        console.log(error);
        alert('Failed to login');
      }
    },
    onError: (error) => {
      console.log(error);
      onClose();
    },
  });

So here I am trying to make the google login window open in full screen. The full screen window is opening but after I select any of my id to sign in there is no response, no error message no success message, and no console log too.

Any fix? uri and the redirect url in google client is same.

-These are some error message I get after the sign in page is opens image

MomenSherif commented 1 year ago

After successful login from redirect mode, Google redirects back to your URL and attaches authorization code in as query string the URL as I can remember

akash-d-dev commented 1 year ago

Can you please elaborate. How to get that url?

TypeErrorEngine2022 commented 1 year ago

For anyone who is using NestJS,

PUT CLIENT IN THE CONTROLLER

otherwise, you will get an invalid request

Correct code:

import { Body, Controller, Post } from "@nestjs/common";
import { AuthService } from "./auth.service";
import { OAuth2Client } from "google-auth-library";

@Controller("auth")
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  client = new OAuth2Client(
    process.env.GOOGLE_CLIENT_ID,
    process.env.GOOGLE_CLIENT_SECRET,
    "postmessage"
  );

  @Post("google")
  public async login(@Body() body: { code: string }) {
    console.log(body.code);
    const { tokens } = await this.client.getToken(body);
    return tokens;
  }
}
deflexable commented 10 months ago

I followed all the steps in @MomenSherif's post exactly but I am still ending up with an error related to an unauthorized client.

Basically I'm taking the code from the user from the frontend and sending to the backend, from where it's supposed to exchange the code for an accesstoken using google's api, but I keep getting an error 401: unauthorized_client, but I have enabled the calendar API on my project. I'm not sure what is going wrong.

Here's the frontend code:

const googleLogin = useGoogleLogin({
        flow: "auth-code",
        onSuccess: async codeResponse => {
            console.log(codeResponse);

            const tokens = await axios.post("http://localhost:3001/auth/google/", {
                code: codeResponse.code
            });

            console.log(tokens);
        }
    })

and here's the backend:

app.post('/auth/google', async (req, res) => {
    console.log("got request!")
    console.log(req.body.code)
    const tokens = await axios.post("https://oauth2.googleapis.com/token", {
        'code': req.body.code,
        'client_id': CLIENT_ID,
        'client_secret': CLIENT_SECRET,
        'redirect_uri': 'postmessage',
        'grant_type': 'authorization_code'
    });
    console.log(tokens);
    res.json(tokens);
});

Stack overflow link: https://stackoverflow.com/questions/74132586/accessing-google-calendar-api-using-service-account

AHh never mind I solved it - I was using a different oauth client ID for frontend/backend. After using the same IDs it worked.

i want to convert the access_token to id_token in the frontend but i don't know if there's any security issue that can arise by exposing client_secret to the frontend

deflexable commented 10 months ago

ok, got my answer here https://stackoverflow.com/questions/53622075/

Saibaba161 commented 4 months ago

This works perfectly if you follow as it is. But, I want to make some tweaks to this. Like, decoding the id_token JWT string, extracting some things and storing them in the database. For some reason, even when the slightest changes are made, it throws an 404 error.

SaveMeASpark20 commented 4 months ago

image

is this ok to have this kind of error

and when i try this in typescript react there's an error in const googleLogin = useGoogleLogin({ onSuccess: async ({code}) => {

  const tokens = await fetch('http://localhost:8000/auth/google', {
    method : "POST",
    headers : {
      Accept : "application/json",
    },
    body : code,
  }
)
  console.log(tokens);
},
flow: 'auth-code',

});

do i have need anything like in header authorization something

Saibaba161 commented 4 months ago

I have the same issue among other things.

SaveMeASpark20 commented 4 months ago

Can anyone help me to have resources to continue this google oauth. I already get the access_token: refresh_token: ' token_type: 'Bearer', id_token: ' expiry_date: }

what is next after this thanks

trunguyenspk commented 2 months ago

For who that using .net to build back-end, follow this guide to get access_token as JWT from access_token which return from Google https://github.com/googleapis/google-api-dotnet-client/issues/1486