From 7f71538b0d79dbd46012aa593975f869c5bb8a28 Mon Sep 17 00:00:00 2001 From: thanhnh-miichisoft Date: Fri, 29 Mar 2024 15:11:32 +0700 Subject: [PATCH] fix id token --- README.md | 33 +++- packages/@react-oauth/google/README.md | 35 ++-- .../google/src/hooks/useGoogleLogin.ts | 164 +++++++++++++----- 3 files changed, 169 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index 3b62ac6..f061d84 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,7 @@ useGoogleOneTapLogin({ }); ``` -### Custom login button (implicit & authorization code flow) +### Custom login button (implicit, authorization code & credential flow) #### Implicit flow @@ -140,7 +140,9 @@ const login = useGoogleLogin({ onSuccess: tokenResponse => console.log(tokenResponse), }); - login()}>Sign in with Google 🚀; + login()}> + Sign in with Google 🚀{' '} +; ``` #### Authorization code flow @@ -155,7 +157,26 @@ const login = useGoogleLogin({ flow: 'auth-code', }); - login()}>Sign in with Google 🚀; + login()}> + Sign in with Google 🚀{' '} +; +``` + +#### Credential flow + +This will return a JWT token in `tokenResponse.credential`. + +```jsx +import { useGoogleLogin } from '@react-oauth/google'; + +const login = useGoogleLogin({ + onSuccess: credentialsResponse => console.log(tokenResponse), + flow: 'credential', +}); + + login()}> + Sign in with Google 🚀{' '} +; ``` #### Checks if the user granted all the specified scope or scopes @@ -182,8 +203,6 @@ const hasAccess = hasGrantedAnyScopeGoogle( ); ``` -#### [Content Security Policy (if needed)](https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid#content_security_policy) - ## API ### GoogleOAuthProvider @@ -191,7 +210,6 @@ const hasAccess = hasGrantedAnyScopeGoogle( | Required | Prop | Type | Description | | :------: | ------------------- | ---------- | --------------------------------------------------------------------------- | | ✓ | clientId | `string` | [**Google API client ID**](https://console.cloud.google.com/apis/dashboard) | -| | nonce | `string` | Nonce applied to GSI script tag. Propagates to GSI's inline style tag | | | onScriptLoadSuccess | `function` | Callback fires on load gsi script success | | | onScriptLoadError | `function` | Callback fires on load gsi script failure | @@ -225,7 +243,6 @@ const hasAccess = hasGrantedAnyScopeGoogle( | | intermediate_iframe_close_callback | `function` | Overrides the default intermediate iframe behavior when users manually close One Tap | | | itp_support | `boolean` | Enables upgraded One Tap UX on ITP browsers | | | hosted_domain | `string` | If your application knows the Workspace domain the user belongs to, use this to provide a hint to Google. For more information, see the [hd](https://developers.google.com/identity/protocols/oauth2/openid-connect#authenticationuriparameters) field in the OpenID Connect docs | -| | use_fedcm_for_prompt | `boolean` | Allow the browser to control user sign-in prompts and mediate the sign-in flow between your website and Google. | ### useGoogleLogin (Both implicit & authorization code flow) @@ -265,5 +282,3 @@ const hasAccess = hasGrantedAnyScopeGoogle( | | promptMomentNotification | `(notification: PromptMomentNotification) => void` | [PromptMomentNotification](https://developers.google.com/identity/gsi/web/reference/js-reference) methods and description | | | cancel_on_tap_outside | `boolean` | Controls whether to cancel the prompt if the user clicks outside of the prompt | | | hosted_domain | `string` | If your application knows the Workspace domain the user belongs to, use this to provide a hint to Google. For more information, see the [hd](https://developers.google.com/identity/protocols/oauth2/openid-connect#authenticationuriparameters) field in the OpenID Connect docs | -| | disabled | `boolean` | Controls whether to cancel the popup in cases such as when the user is already logged in | -| | use_fedcm_for_prompt | `boolean` | Allow the browser to control user sign-in prompts and mediate the sign-in flow between your website and Google. | diff --git a/packages/@react-oauth/google/README.md b/packages/@react-oauth/google/README.md index 3b62ac6..3f6db0f 100644 --- a/packages/@react-oauth/google/README.md +++ b/packages/@react-oauth/google/README.md @@ -12,7 +12,7 @@ $ npm install @react-oauth/google@latest $ yarn add @react-oauth/google@latest ``` -## Demo & How to use to fetch user details +## Demo https://react-oauth.vercel.app/ @@ -129,7 +129,7 @@ useGoogleOneTapLogin({ }); ``` -### Custom login button (implicit & authorization code flow) +### Custom login button (implicit, authorization code & credential flow) #### Implicit flow @@ -140,7 +140,9 @@ const login = useGoogleLogin({ onSuccess: tokenResponse => console.log(tokenResponse), }); - login()}>Sign in with Google 🚀; + login()}> + Sign in with Google 🚀{' '} +; ``` #### Authorization code flow @@ -155,7 +157,26 @@ const login = useGoogleLogin({ flow: 'auth-code', }); - login()}>Sign in with Google 🚀; + login()}> + Sign in with Google 🚀{' '} +; +``` + +#### Credential flow + +This will return a JWT token in `tokenResponse.credential`. + +```jsx +import { useGoogleLogin } from '@react-oauth/google'; + +const login = useGoogleLogin({ + onSuccess: credentialsResponse => console.log(tokenResponse), + flow: 'credential', +}); + + login()}> + Sign in with Google 🚀{' '} +; ``` #### Checks if the user granted all the specified scope or scopes @@ -182,8 +203,6 @@ const hasAccess = hasGrantedAnyScopeGoogle( ); ``` -#### [Content Security Policy (if needed)](https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid#content_security_policy) - ## API ### GoogleOAuthProvider @@ -191,7 +210,6 @@ const hasAccess = hasGrantedAnyScopeGoogle( | Required | Prop | Type | Description | | :------: | ------------------- | ---------- | --------------------------------------------------------------------------- | | ✓ | clientId | `string` | [**Google API client ID**](https://console.cloud.google.com/apis/dashboard) | -| | nonce | `string` | Nonce applied to GSI script tag. Propagates to GSI's inline style tag | | | onScriptLoadSuccess | `function` | Callback fires on load gsi script success | | | onScriptLoadError | `function` | Callback fires on load gsi script failure | @@ -225,7 +243,6 @@ const hasAccess = hasGrantedAnyScopeGoogle( | | intermediate_iframe_close_callback | `function` | Overrides the default intermediate iframe behavior when users manually close One Tap | | | itp_support | `boolean` | Enables upgraded One Tap UX on ITP browsers | | | hosted_domain | `string` | If your application knows the Workspace domain the user belongs to, use this to provide a hint to Google. For more information, see the [hd](https://developers.google.com/identity/protocols/oauth2/openid-connect#authenticationuriparameters) field in the OpenID Connect docs | -| | use_fedcm_for_prompt | `boolean` | Allow the browser to control user sign-in prompts and mediate the sign-in flow between your website and Google. | ### useGoogleLogin (Both implicit & authorization code flow) @@ -265,5 +282,3 @@ const hasAccess = hasGrantedAnyScopeGoogle( | | promptMomentNotification | `(notification: PromptMomentNotification) => void` | [PromptMomentNotification](https://developers.google.com/identity/gsi/web/reference/js-reference) methods and description | | | cancel_on_tap_outside | `boolean` | Controls whether to cancel the prompt if the user clicks outside of the prompt | | | hosted_domain | `string` | If your application knows the Workspace domain the user belongs to, use this to provide a hint to Google. For more information, see the [hd](https://developers.google.com/identity/protocols/oauth2/openid-connect#authenticationuriparameters) field in the OpenID Connect docs | -| | disabled | `boolean` | Controls whether to cancel the popup in cases such as when the user is already logged in | -| | use_fedcm_for_prompt | `boolean` | Allow the browser to control user sign-in prompts and mediate the sign-in flow between your website and Google. | diff --git a/packages/@react-oauth/google/src/hooks/useGoogleLogin.ts b/packages/@react-oauth/google/src/hooks/useGoogleLogin.ts index 0a6cdf1..61d7ced 100644 --- a/packages/@react-oauth/google/src/hooks/useGoogleLogin.ts +++ b/packages/@react-oauth/google/src/hooks/useGoogleLogin.ts @@ -1,16 +1,22 @@ -/* eslint-disable import/export */ import { useCallback, useEffect, useRef } from 'react'; - import { useGoogleOAuth } from '../GoogleOAuthProvider'; -import { - TokenClientConfig, - TokenResponse, +import type { CodeClientConfig, CodeResponse, - OverridableTokenClientConfig, + CredentialResponse, + GoogleCredentialResponse, + IdConfiguration, + MomentListener, NonOAuthError, + OverridableTokenClientConfig, + TokenClientConfig, + TokenResponse, } from '../types'; +import { extractClientId } from '../utils'; +type ImplicitOnError = ( + onError: Pick, +) => void; interface ImplicitFlowOptions extends Omit { onSuccess?: ( @@ -19,17 +25,15 @@ interface ImplicitFlowOptions 'error' | 'error_description' | 'error_uri' >, ) => void; - onError?: ( - errorResponse: Pick< - TokenResponse, - 'error' | 'error_description' | 'error_uri' - >, - ) => void; + onError?: ImplicitOnError; onNonOAuthError?: (nonOAuthError: NonOAuthError) => void; scope?: TokenClientConfig['scope']; overrideScope?: boolean; } +type AuthCodeOnError = ( + onError: Pick, +) => void; interface AuthCodeFlowOptions extends Omit { onSuccess?: ( @@ -38,17 +42,27 @@ interface AuthCodeFlowOptions 'error' | 'error_description' | 'error_uri' >, ) => void; - onError?: ( - errorResponse: Pick< - CodeResponse, - 'error' | 'error_description' | 'error_uri' - >, - ) => void; + onError?: AuthCodeOnError; onNonOAuthError?: (nonOAuthError: NonOAuthError) => void; scope?: CodeResponse['scope']; overrideScope?: boolean; } +type CredentialOnSuccess = ( + credentialResponse: Omit< + CredentialResponse, + 'error' | 'error_description' | 'error_uri' + >, +) => void; +type CredentialOnError = () => void; +interface CredentialFlowOptions + extends Omit { + onSuccess?: CredentialOnSuccess; + onError?: CredentialOnError; + state?: never; + promptMomentNotification?: MomentListener; +} + export type UseGoogleLoginOptionsImplicitFlow = { flow?: 'implicit'; } & ImplicitFlowOptions; @@ -57,9 +71,14 @@ export type UseGoogleLoginOptionsAuthCodeFlow = { flow?: 'auth-code'; } & AuthCodeFlowOptions; +export type UseGoogleLoginOptionsCredentialFlow = { + flow?: 'credential'; +} & CredentialFlowOptions; + export type UseGoogleLoginOptions = | UseGoogleLoginOptionsImplicitFlow - | UseGoogleLoginOptionsAuthCodeFlow; + | UseGoogleLoginOptionsAuthCodeFlow + | UseGoogleLoginOptionsCredentialFlow; export default function useGoogleLogin( options: UseGoogleLoginOptionsImplicitFlow, @@ -67,17 +86,29 @@ export default function useGoogleLogin( export default function useGoogleLogin( options: UseGoogleLoginOptionsAuthCodeFlow, ): () => void; +export default function useGoogleLogin( + options: UseGoogleLoginOptionsCredentialFlow, +): () => void; export default function useGoogleLogin({ flow = 'implicit', - scope = '', onSuccess, onError, - onNonOAuthError, - overrideScope, state, ...props }: UseGoogleLoginOptions): unknown { + const { + scope = '', + onNonOAuthError, + overrideScope, + ...implicitOrAuthProps + } = props as + | UseGoogleLoginOptionsImplicitFlow + | UseGoogleLoginOptionsAuthCodeFlow; + + const { promptMomentNotification, ...credentialsProps } = + props as UseGoogleLoginOptionsCredentialFlow; + const { clientId, scriptLoadedSuccessfully } = useGoogleOAuth(); const clientRef = useRef(); @@ -90,30 +121,63 @@ export default function useGoogleLogin({ const onNonOAuthErrorRef = useRef(onNonOAuthError); onNonOAuthErrorRef.current = onNonOAuthError; + const promptMomentNotificationRef = useRef(promptMomentNotification); + promptMomentNotificationRef.current = promptMomentNotification; + useEffect(() => { if (!scriptLoadedSuccessfully) return; - const clientMethod = - flow === 'implicit' ? 'initTokenClient' : 'initCodeClient'; - - const client = window?.google?.accounts?.oauth2[clientMethod]({ - client_id: clientId, - scope: overrideScope ? scope : `openid profile email ${scope}`, - callback: (response: TokenResponse | CodeResponse) => { - if (response.error) return onErrorRef.current?.(response); - - onSuccessRef.current?.(response as any); - }, - error_callback: (nonOAuthError: NonOAuthError) => { - onNonOAuthErrorRef.current?.(nonOAuthError); - }, - state, - ...props, - }); - - clientRef.current = client; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [clientId, scriptLoadedSuccessfully, flow, scope, state]); + if (flow !== 'credential') { + const clientMethod = + flow === 'implicit' ? 'initTokenClient' : 'initCodeClient'; + + clientRef.current = window?.google?.accounts.oauth2[clientMethod]({ + client_id: clientId, + scope: overrideScope ? scope : `openid profile email ${scope}`, + callback: (response: TokenResponse | CodeResponse) => { + if (response.error) + return (onErrorRef.current as ImplicitOnError | AuthCodeOnError)?.( + response, + ); + + onSuccessRef.current?.(response as any); + }, + error_callback: (nonOAuthError: NonOAuthError) => { + onNonOAuthErrorRef.current?.(nonOAuthError); + }, + state, + ...implicitOrAuthProps, + }); + } else { + window?.google?.accounts?.id?.initialize({ + client_id: clientId, + callback: (credentialResponse: GoogleCredentialResponse) => { + if (!credentialResponse?.credential) { + return (onErrorRef.current as CredentialOnError)?.(); + } + + const { credential, select_by } = credentialResponse; + (onSuccessRef.current as CredentialOnSuccess)?.({ + credential, + clientId: extractClientId(credentialResponse), + select_by, + }); + }, + ...credentialsProps, + }); + + clientRef.current = window?.google?.accounts?.id; + } + }, [ + clientId, + credentialsProps, + flow, + implicitOrAuthProps, + overrideScope, + scope, + scriptLoadedSuccessfully, + state, + ]); const loginImplicitFlow = useCallback( (overrideConfig?: OverridableTokenClientConfig) => @@ -126,5 +190,17 @@ export default function useGoogleLogin({ [], ); - return flow === 'implicit' ? loginImplicitFlow : loginAuthCodeFlow; + const loginCredentialFlow = useCallback( + () => clientRef.current?.prompt(promptMomentNotificationRef.current), + [], + ); + + switch (flow) { + case 'implicit': + return loginImplicitFlow; + case 'auth-code': + return loginAuthCodeFlow; + case 'credential': + return loginCredentialFlow; + } }