Growing an active user base is a top priority for all developers. Your product pages need copy, design, and engineering that can efficiently and effectively convert visitors into users. A visitor is an anonymous person browsing your site or app. A user is a visitor who exchanges some information to access more layers or features of your app. Even with just an email address or username, you can build a profile or identity for a user.
As such, user authentication is the backbone of conversion. User registration and login must be secure and as frictionless as possible. Long and tedious forms or requirements during registration can turn prospects away. Errors or downtime during login can frustrate even your most loyal customers, leading to dissatisfaction or support tickets.
Why Use Google OAuth for User Authentication
A reliable way to implement user registration and authentication in an application and turn visitors into users quickly and securely is by using a registration and authentication system that your visitors already use and trust, such as Google.
As a developer, you can take advantage of Google's high security and trust Google as a source to safeguard user credentials so that you don't have to. Google social login leverages the OAuth 2.0 protocol for authorization and uses OpenID Connect (OIDC) as the standard for user authentication. You can delegate to Google's UI/UX and engineering the burden of password requirements, password protection, password error handling, email verification, profile data collection, and more.
How easy or difficult is it to implement Google Login on your own? It depends. On the surface, it may be like a simple endeavor: In the JavaScript ecosystem, use a library like google-auth-library
, write a few lines of code, and call it a day.
While google-auth-library
is a robust client library for using OAuth 2.0 authorization and authentication with Google APIs, it's essential to know how to use it effectively to avoid introducing security vulnerabilities in your application that could put your end-users and your organization at risk.
For instance, a recent post on X went viral: a developer named Klaas shared with the X developer community a code snippet on how to implement "google login with just the google-auth-library, no frameworks in 50 lines of code":
export const googleLoginAction = {
getAuthUrl: () => {
const query = {
client_id: oauth_google.client_id,
redirect_uri: oauth_google.redirect_uri,
response_type: "code",
scope: oauth_google.scopes,
};
const url = new URL(oauth_google.endpoint);
url.search = new URLSearchParams(query).toString();
return url.toString();
},
verifyGoogleCode: async ({ code }: { code: string }) => {
log.info({ code }, "verifying google auth code");
try {
const tokenResponse = await fetch("https://oauth2.googleapis.com/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
client_id: oauth_google.client_id,
client_secret: oauth_google.client_secret,
code,
grant_type: "authorization_code",
redirect_uri: oauth_google.redirect_uri,
}),
});
const tokenData = (await tokenResponse.json()) as { id_token: string };
const ticket = await client.verifyToken({
idToken: tokenData.id_token,
audience: oauth_google.client_id,
});
const userData = ticket.getPayload();
log.info({ userData }, "google auth successful");
} catch (error) {
log.error({ error }, "failed to verify google auth code");
throw new Error("Failed to verify Google authentication");
}
},
};
Klaas' X post ignited a passionate discussion around user authentication and general security. As of the time of writing, the post had gathered 449k impressions, 4.7k likes, 387 reposts, and 83 comments.
Google OAuth Implementation Can Go Wrong
Some of the commenters asked questions about the code's security posture, while others offered insights and advice on how to improve the security of that Google OAuth implementation.
Some commenters assumed the code was running on the client and were alarmed by the code handling a client secret, which could lead to a security compromise. Klaas clarified that the code is intended to run in the backend and further explained its purpose as follows:
"this [code] has two simple functions, create a URL where you redirect the user to and a verify code function, you have the create a /callback (on the server) to get that URL. You still need to make authentication/authorization"
Some other commenters pointed out that the code could be subject to a Cross-Site Request Forgery (CSRF) attack, which confused others: How can there be a CSRF attack if there's no cookie involved or when the code does not run in the client?
First and foremost, let's recap what a Cross-Site Request Forgery (CSRF) attack is. Andrea Chiarelli, Principal Developer Advocate at Okta, summarizes a CSRF attack as follows:
- A typical Cross-Site Request Forgery (CSRF or XSRF) attack aims to perform an operation in a web application on behalf of a user without their explicit consent.
- The attacker leads the user to perform an action, like clicking a link.
- This action sends an HTTP request to a website on behalf of the user.
- If the user has an active authenticated session on the trusted website, the request is processed as a legitimate request sent by the user.
Andrea warns us that "even though CSRF attacks are commonly associated with session cookies, be aware that Basic Authentication sessions are also vulnerable to CSRF attacks" and that, in fact, "a CSRF vulnerability relies on the authenticated session management".
Sandrino Di Mattia is a Senior Director on the Product Architecture team at Okta with robust and deep experience in cloud, identity, and security. In the thread, Sandrino commented that the code doesn’t seem to perform any state validation, making it open to CSRF attacks.
You may be wondering like others did: "CSRF attack without any cookies, must be a new thing". Let's explore how improperly implemented OAuth can lead to a CSRF attack.
Cross-Site Reference Forgery (CSRF) Are Not About Cookies
Sandrino shares that CSRF is not related to cookies. Missing state validation is the most basic vulnerability in OAuth flows. OAuth 2.0 specification recommends a state
parameter that allows you to verify the authenticated state of your browser. The state
parameter preserves the browser's authenticated state set by the client in an authorization request, and the authorization server makes it available to the client in the response.
The state
parameter helps you mitigate CSRF attacks by using a unique and non-guessable value associated with each authentication request about to be initiated. That value allows you to prevent the attack by confirming that the value from the response matches the one you sent.
But how does a CSRF attack work without cookies?
Sandrino explains that an attacker can go through the OAuth flow but stop right when the redirect with the authorization code to the callback URL happens. Now, the attacker can send the link to the callback URL, including the code, to the victim, which consumes the code on their machine.
Depending on the implementation of your application, a few security, privacy, and data exploits could happen. For example, the attacker's account is now forced on the victim’s browser. The victim now could be uploading docs or filling in and sending personal data to the attacker's account, thinking it’s the victim's account. Or a worse scenario: the victim is already signed in, and now this new identity (the attacker's identity) is automatically linked to the victim's existing account.
Prevent CSRF Attacks in Google OAuth
Sandrino advises us that preventing this type of attack takes a few lines of code. In practice, on redirect, you store a random value in a cookie. You then send that value to Google using the state
param. On callback, you validate the incoming state
parameter with the value in the cookie. If the state
value in the response does not match the state
value in the cookie, you reject the login operation and stop further access. The state
parameter and PKCE can help prevent this type of attack.
For a deep dive on this topic, you can consult the "More Guidelines Than Rules: CSRF Vulnerabilities from Noncompliant OAuth 2.0 Implementations" paper from the Georgia Institute of Technology and the University of Florida. Check out section 3.3 on page 8.
There's a saying that goes: "You don't know what you don't know". But then, how can you know? By doing precisely what Klaas did: share code with others and be open to feedback and constructive criticism. At the same time, you can leverage the knowledge and expertise of others who have been through similar and more complex situations, which can help you avoid falling into traps or dangerous holes.
You may be thinking, "I can rely on AI for that!" You can use AI tools to learn and understand, but remember that those tools are imperfect, as they are an aggregation of human knowledge and can also make mistakes. There are instances where an AI tool can present you with "security" code with high confidence. Still, that code could be anything but secure, missing the implementation of security best practices or defensive mechanisms.
Use Libraries to Implement Google OAuth
It's important to highlight that if you want to implement a standard protocol yourself, you need to understand its specifications well, be aware of its nuances, and follow its details diligently. Instruments like the state
parameter and all other security measures included in specifications and standards are the result of discussions and experience of industry experts throughout the years.
As developers, we often fall into the dilemma of reinventing the wheel (do it yourself) or relying on those who have already done it in the best way (use a library).
Sandrino suggested in the thread that it's better to use a library like Passport.js over the code shared on the tweet—or rather, X post. Passport.js implements OAuth 2.0 the way that the standard defines it. You can inspect the implementation of Google OAuth in Passport.js by looking at the lib/strategy.js
file of the passport-google-oauth20
npm package.
The OAuth2Strategy
from Passport is well-documented. The comments in the code can quickly guide you on what's happening through that flow:
- The Google authentication strategy authenticates requests by delegating to Google using the OAuth 2.0 protocol.
Applications must supply a
verify
callback function that accepts anaccessToken
,refreshToken
, and service-specificprofile
and then calls anothercb
callback function, supplying auser
. Theuser
value should be set tofalse
if the credentials are invalid. If an exception occurs,err
should be set.
Let's see an example of how you'd use Google OAuth login with Passport.js:
passport.use(new GoogleStrategy({
clientID: '123-456-789',
clientSecret: 'shhh-its-a-secret'
callbackURL: 'https://www.example.net/auth/google/callback'
},
function(accessToken, refreshToken, profile, cb) {
User.findOrCreate(..., function (err, user) {
cb(err, user);
});
}
));
As you inspect the implementation code, you'll notice that Passport.js does much more than handle the login. OAuth 2.0 provides consented access and restricts actions of what a client app can perform on resources on behalf of the user without users ever sharing their credentials with the application. Using Google OAuth 2.0, you can also request access to user contacts and other resources on the Google platform. As the needs of your application scale, you may see yourself needing a more complex data access policy that calls for more code to write and maintain and more scenarios and edge cases to be aware of.
Google OAuth Attacks Go Beyond CSRF Attacks
Security is an ever-evolving landscape. It's critical for you to stay on the vanguard regarding security in the industry, especially with the rise of artificial intelligence (AI). Implementing authentication on your own is an investment in money, risk, and time. Relying on robust, time-tested libraries or identity service providers can minimize some of those costs.
As mentioned in the post, implementing social login with Google OAuth for your app is just one piece of the authentication puzzle. Once you have users, you are responsible for protecting the data they entrust you. For example, depending on the region where you operate, you may be subject to regulations around user management and privacy, such as GDPR, which also extends to logs, not just the user data you have in your databases.
As you scale and grow in popularity, you may also gain unwanted attention from bad actors and attackers who may want what you are securing: rich user data. But, even on a small scale, you may become the target of bad actors looking to test their skills to break into apps.
Recommended read: The biggest underestimated security threat of today
In the X thread we have been discussing, Jared Hanson, the creator of Passport.js and a Principal Product Architect at Okta, validated that "checking a nonce or state is highly recommended." Jared added, "It’s less simple, but it protects against common attacks."
Aaron Parecki, Director of Identity Standards at Okta, reminded me that it's not just CSRF attacks that are the concern when it comes to OAuth 2.0 flows. Even if you fix the CSRF attacks we have described, you could still be open to an OAuth 2.0 authorization code injection attack, which you can prevent with PKCE. You can learn more about that type of attack by watching the video below:
Aaron also hosted an "OAuth Happy Hour", which includes a demo of the authorization code injection attack. Be sure to check it out!
Let us know if you'd like a detailed follow-up blog post covering the OAuth 2.0 authorization code injection attack.
Aside: Use Auth0 to Implement Google Social Login
Auth0 makes it even easier to get started with Google. Every new tenant has the Google social connection enabled by default. You don't even have to set up a Google Cloud project to see Google social login in action. Auth0 maintains a set of developer keys you can use to start testing Google social login in your applications in development. Follow the "Google Social Connection to Login" lab to learn how to use those Google dev keys and set up ones for production.