How do you "Sign In with Google" with NextJS and Firebase Auth on Safari - reactjs

I'm using:
firebase: ^8.6.8
next: ^11.0.1
react: ^17.0.2
When trying to sign in with Google on Safari, I get errors related to a Content Blocker with both signInWithPopUp and signInWithRedirect:
Content blocker prevented frame displaying https://my-site.com/page
from loading a resource from https://apis.google.com/js/api.js?onload=__iframefcb318667
Does anybody know how to handle this properly?
Thank you!

Okay, here is the solution I used:
Instead of using signInWithRedirect or signInWithPopup that seem to cause problem on Apple devices using Safari, I directly use [https://apis.google.com/js/platform.js] and then signInWithCredential using the googleUser.
I also used the package react-google-signin [https://www.npmjs.com/package/react-google-login] to simplify my code.
Step by step:
Enable Google Sign-in method on Firebase Console for Authentication
On the Credentials page of Google Cloud Platform [https://console.developers.google.com/apis/credentials], choose your project and under OAuth 2.0 Client IDs get the Client Id.
Save the Client ID as an environment variable NEXT_PUBLIC_GOOGLE_CLIENT_ID in your Project Settings on Vercel.
In your _document.tsx add the https://apis.google.com/js/platform.js script and needed meta data:
<meta name="google-signin-client_id" content={process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID}></meta>
<meta name="google-signin-cookiepolicy" content="single_host_origin"></meta>
<meta name="google-signin-scope" content="profile email"></meta>
<script src="https://apis.google.com/js/platform.js" async defer></script>
Define your Google Sign in button:
const GoogleButton: FC<GoogleButtonProps> = ({onSuccess, onFailure}) => {
const onSignInFailure = (failure: any) => {
console.error('FAILURE: ', failure);
onFailure(...);
};
const onSignIn = (googleUser: any) => {
signInWithGoogleUser(googleUser)
.then(onSuccess)
.catch(onFailure);
}
return (
<GoogleLogin
render={(renderProps) => (
<button
onClick={renderProps.onClick}
disabled={renderProps.disabled}>
This is my custom Google button
</button>
)}
clientId={process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!}
buttonText="Login"
onSuccess={onSignIn}
onFailure={onSignInFailed}
cookiePolicy={'single_host_origin'}
prompt="select_account"
/>
);
};
I specified prompt="select_account" to be sure it prompt the user for the account selection even if hse is already logged in.
Then, inside signInWithGoogleUser, is use signInWithCredential to use the googleUser to authentify on Firebase Auth:
export const signInWithGoogleUser = (googleUser: any) => {
const auth = firebase.auth();
const provider = firebase.auth.GoogleAuthProvider;
const credential = provider.credential(googleUser.getAuthResponse().id_token);
return auth.signInWithCredential(credential);
};
NOTES
a. My project is running on Vercel [https://vercel.com/] with the Database (Firestore) and Authentication on Firebase [https://firebase.com]. You will have to adapt my solution if you use another stack.
b. With this, it works on almost all iPhones and Mac computers. In case it still fails, it is due to an installed content/cookie blocker and I display an error message asking the visitor to reactivate cookies or register using email and password.
c. I simplified a bit the code to have a more condensed answer.

Related

Electron App with Azure AD - without Interactive browser

I am trying to integrate Azure AD authentication with my Electron App (with Angular). I took reference from this link and able to integrate: https://learn.microsoft.com/en-us/azure/active-directory/develop/tutorial-v2-nodejs-desktop
Issue: It's using getTokenInteractive() method and it's navigating to an external browser. As per my requirement we don't have to navigate to an external browser, it should open the UI inside my electron App where end users can provide their credentials.
Another option if possible we can open the Azure AD url part of my electron App.
I took reference from this link and able to integrate: https://learn.microsoft.com/en-us/azure/active-directory/develop/tutorial-v2-nodejs-desktop
async getTokenInteractive(tokenRequest) {
try {
const openBrowser = async (url) => {
await shell.openExternal(url);
};
const authResponse = await this.clientApplication.acquireTokenInteractive({
...tokenRequest,
openBrowser,
successTemplate: '<h1>Successfully signed in!</h1> <p>You can close this window now.</p>',
errorTemplate: '<h1>Oops! Something went wrong</h1> <p>Check the console for more information.</p>',
});
return authResponse;
} catch (error) {
throw error;
}
}

Prevent flash of wrong page in NextJS app after MSAL-React redirect to/from Azure AD B2C

Context & Reproducible Scenario
I'm using the combination of these libraries and tools:
NextJS 12+ (based on React 18+)
MSAL-Browser 2.25+ and MSAL-React 1.6+ (Microsoft's libs for OpenID login against Azure B2C)
I'm using the Auth Code + PKCE redirect flow so this is the flow for users:
They land on /, the home page
They click a /me router link
They go to Azure B2C to log in because said page has this logic:
<MsalAuthenticationTemplate
interactionType={InteractionType.Redirect}
authenticationRequest={loginRequest}>
where loginRequest.state is set to router.asPath (the "intended" page: /me)
Note that the page is also wrapped in a <NoSsr> component based off Stack Overflow.
User logs in on Azure B2C, gets redirected back to my app at / (the root)
⛔ Problem: the user now briefly sees the / (home) page
After a very brief moment, the user gets sent to /me where they are signed in
The MSAL docs don't seem to have much on the state property from OIDC or this redirect behavior, and I can't find much about this in the MSAL sample for NextJS either.
In short: the issue
How do I make sure MSAL-React in my NextJS application send users to the "intended" page immediately on startup, without briefly showing the root page where the Identity Server redirects to?
Relevant extra information
Here's my custom _app.js component, which seems relevant because it is a component that triggers handleRedirectPromise which causes the redirect to intended page:
export default function MyApp({ Component, pageProps }) {
return (
<MsalProvider instance={msalInstance}>
<PageHeader></PageHeader>
<Component {...pageProps} />
</MsalProvider>
);
}
PS. To help folks searching online find this question: the behavior is triggered by navigateToLoginRequestUrl: true (is the default) in the configuration. Setting it to false plainly disables sending the user to the intended page at all.
Attempted solutions with middleware
I figured based on how APP_INITIALIZERs work in Angular, to use middleware like this at some point:
// From another file:
// export const msalInstance = new PublicClientApplication(msalConfig);
export async function middleware(_request) {
const targetUrlAfterLoginRedirect = await msalInstance.handleRedirectPromise()
.then((result) => {
if (!!result && !!result.state) {
return result.state;
}
return null;
});
console.log('Found intended target before login flow: ', targetUrlAfterLoginRedirect);
// TODO: Send user to the intended page with router.
}
However, this logs on the server's console:
Found intended target before login flow: null
So it seems middleware is too early for msal-react to cope with? Shame, because middleware would've been perfect, to allow as much SSR for target pages as possible.
It's not an option to change the redirect URL on B2C's side, because I'll be constantly adding new routes to my app that need this behavior.
Note that I also tried to use middleware to just sniff out the state myself, but since the middleware runs on Node it won't have access to the hash fragment.
Animated GIF showing the flashing home page
Here's an animated gif that shows the /home page is briefly (200ms or so) shown before /me is properly opened. Warning, gif is a wee bit flashy so in a spoiler tag:
Attempted solution with custom NavigationClient
I've tried adding a custom NavigationClient to more closely mimic the nextjs sample from Microsoft's repository, like this:
import { NavigationClient } from "#azure/msal-browser";
// See: https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-react/docs/performance.md#how-to-configure-azuremsal-react-to-use-your-routers-navigate-function-for-client-side-navigation
export class CustomNavigationClient extends NavigationClient {
constructor(router) {
super();
this.router = router;
}
async navigateInternal(url, options) {
console.log('👍 Navigating Internal to', url);
const relativePath = url.replace(window.location.origin, "");
if (options.noHistory) {
this.router.replace(relativePath);
} else {
this.router.push(relativePath);
}
return false;
}
}
This did not solve the issue. The console.log is there allowing me to confirm this code is not run on the server, as the Node logs don't show it.
Attempted solution: go through MSAL's SSR docs
Another thing I've tried is going through the documentation claiming #azure/msal-react supports Server Side Rendering (SSR) but those docs nor the linked samples demonstrate how to solve my issue.
Attempted solution in _app.tsx
Another workaround I considered was to sniff out the hash fragment client side when the user returns to my app (and make sure the intended page is also in that state). I can successfully send the OpenID state to B2C like this...
const extendedAuthenticationRequest = {
...authenticationRequest,
state: `~path~${asPath}~path~`,
};
...and see it returned in the Network tab of the dev tools.
However, when I try to extract it in my _app.tsx still doesn't work. I tried this code from another Stack Overflow answer to get the .hash:
const [isMounted, setMounted] = useState(false);
useEffect(() => {
if (isMounted) {
console.log('====> saw the following hash', window.location.hash);
const matches = /~path~(.+)~path~/.exec(window.location.hash);
if (matches && matches.length > 0 && matches[1]) {
const targetUrlAfterOpenIdRedirect = decodeURIComponent(matches[1]);
console.log("Routing to", targetUrlAfterOpenIdRedirect);
router.replace(targetUrlAfterOpenIdRedirect);
}
} else {
setMounted(true);
}
}, [isMounted]);
if (!isMounted) return null;
// else: render <MsalProvider> and the intended page component
This does find the intended page from the state and executes routing, but still flashes the /home page before going to the intended page.
Footnote: related GitHub issue
Submitted an issue at MSAL's GitHub repository too.

Google OAuth2 Login redirect uri mismatch on Heroku but not localhost - stuck on a call to backend

I am using the MERN stack on a site where I use JWT for register/login. I've set up Google login using react-google-login on my site. On the localhost, everything works fine.
When I deployed to Heroku, I initially got zero response from pressing the button and an error of a redirect-URI-mismatch. So I went into my google developer account and added the Heroku page to the authorized javascript origins.
Currently, the Google login opens and everything appears successful....at first. The initial response (the first logged 'response' shown below) to Google shows up. So I'm getting a code returned.
function GoogleOAuth({ setSignedIn }) {
const responseGoogle = (response) => {
console.log('response', response);
const { code } = response;
if (code) {
axios
.post("/api/users/create-tokens", { code })
.then((res) => {
console.log(res.data);
toast.success("Success! You're connected to Google!")
})
.catch((err) => console.log(err.message));
}
};
In the associated google account, it shows that this app now has access. The problem arises at the second part - where it says if(code). Again, the call to the backend works fine locally but in Heroku it appears 'stuck' here and then the message in the console is redirect uri mismatch. I tried putting in the backend api address to the developer console's redirect uri section but that didn't work. Anyone have any idea what is going on at this point in Heroku? I really can't figure it out.
<GoogleContainer>
<Logo src={GoogleLogo} />
<GoogleLogin
clientId= {CLIENT_ID}
render={(renderProps) => (
<GoogleBtn
onClick={renderProps.onClick}
disabled={renderProps.disabled}
style={styleObj}
>
Connect to Google
</GoogleBtn>
)}
onSuccess={responseGoogle}
isSignedIn={true}
onFailure={responseError}
cookiePolicy={"single_host_origin"}
responseType='code'
accessType='offline'
scope='openid email profile https://www.googleapis.com/auth/calendar'
/>{" "}
</GoogleContainer>
);

Getting unauthorized access error when implementing SAML 2.0 based applications

I am getting this error when clicking on Login with Microsoft button
We're unable to complete your request
unauthorized_client: The client does not exist or is not enabled for consumers.
If you are the application developer, configure a new application through the App Registrations in the Azure Portal at https://go.microsoft.com/fwlink/?linkid=2083908.
I am working on Front-End and I am also passing the correct client-id but still getting this error
Here is the code --
const App = () => {
const loginHandler = (err, data, msal) => {
console.log(err, data);
// some actions
if (!err && data) {
// onMsalInstanceChange(msal);
console.log(msal);
}
};
return (
<div className="app">
<MicrosoftLogin clientId={config.clientId} authCallback={loginHandler} />
</div>
);
};
It looks like you are using OAuth2 / OpenID Connect to login with an application that uses SAML?
You need to create an enterprise application.

Next Js Firebase Recaptcha V3 verification

I'm very confused about the Recaptcha V3 implementation and it is not clear to me if actually need to implement it on my website or if initializing Appcheck with my Recaptcha V3 credentials is enough: Appcheck does successfully initialize and I have enforced firestore and the cloud storage to use it.
I don't want bots to create infinite accounts on my website and raise like crazy my costs so I looked into implementing Recaptcha on forms: the developer documentation is a joke (https://developers.google.com/recaptcha/docs/v3) as it is not explained how to verify the token which is returned
I saw an old article from 2017 telling you to use Cloud Functions (which may take up to 10-12 seconds to fire up in case of cold-start) but this sounds really far-fetched and 5 years later I hope we have a better solution: https://firebase.googleblog.com/2017/08/guard-your-web-content-from-abuse-with.html
Am I overthinking this? Would Appcheck protect my app from people abusing my contact form and sign up section? If this is not enough, how can I implement Recaptcha V3 with React and Firebase?
I am using Next JS and so far my code looks something like this (where I replaced my publishable key "mySyteKey"):
import Script from "next/script";
export default function TestRecaptcha() {
const handleSubmit = (e) => {
e.preventDefault();
grecaptcha.ready(function () {
grecaptcha.execute('mySiteKey', {action: 'submit'}).then(function (token) {
// How the hell do I verify this token!?
console.log(token)
}), error =>{
console.log(error.message)
}
});
}
return (
<div>
<Script src="https://www.google.com/recaptcha/api.js?render=mySiteKey"
strategy="beforeInteractive"/>
<form onSubmit={(e) => handleSubmit(e)}>
<button
type="submit">
Submit
</button>
</form>
</div>
)
}

Resources