Google Authentication
What you'll learn​
- How to set up Cypress to test authentication with Google
- How to set Google app credentials in Cypress
- How to create a custom command for Google authentication
- How to adapt a Google app for testing
The technique we'll use for testing is to use the Google OAuth 2.0 Playground to create a refresh token that can be exchanged for an access token and id token during the testing phase.
Google Project and Application Setup​
First, a Google project is required. If you don't already have a project, you can create one using the Google Cloud Console. More information is available in the Google Cloud APIs Getting Started.
Next, use the Google API Console
to create credentials
for your web application. In the top navigation, click Create Credentials
and
choose OAuth client ID
.
On the Create OAuth client ID
page, enter the following:
- Application Type: Web Application
- Name: Your Web Application Name
- Authorized JavaScript origins:
http://localhost:3000
- Authorized redirect URIs:
http://localhost:3000/callback
andhttps://developers.google.com/oauthplayground
Once saved, note the client ID and client secret. You can find these under the "OAuth 2.0 Client IDs" on the Google API Credentials page.
Using the Google OAuth 2.0 Playground to Create Testing Credentials​
The refresh token from this process is unique to the authenticated Google user. This process must be repeated for each user intended for testing.
Visit the
Google OAuth 2.0 Playground.
Click the icon in the upper right corner to reveal a
OAuth 2.0 configuration
panel. In this panel set the following:
- OAuth flow: Server-side
- Access type: Offline
- Check
Use your own OAuth credentials
. - OAuth Client ID: Your Google Application Client ID
- OAuth Client secret: Your Google Application Client Secret
Select the Google APIs needed for your application under
Step 1 (Select & authorize APIs), including the
https://www.googleapis.com/auth/userinfo.profile
endpoint under
Google OAuth2 API v2 at a minimum. Click Authorize APIs.
Next, sign in with Google credentials to your test Google user account. You'll be redirected back to the Google OAuth 2.0 Playground under Step 2 (Exchange authorization code for tokens). Click the Exchange authorization code for token button.
You'll be taken to Step 3 (Configure request to API). Note the returned refresh token to be used with testing.
Setting Google app credentials in Cypress​
To have access to test user credentials within our tests we need to configure
Cypress to use the Google environment variables set in the
.env
file.
REACT_APP_GOOGLE_CLIENTID = 'your-client-id'
REACT_APP_GOOGLE_CLIENT_SECRET = 'your-client-secret'
GOOGLE_REFRESH_TOKEN = 'your-refresh-token'
- cypress.config.js
- cypress.config.ts
const { defineConfig } = require('cypress')
// Populate process.env with values from .env file
require('dotenv').config()
module.exports = defineConfig({
env: {
googleRefreshToken: process.env.GOOGLE_REFRESH_TOKEN,
googleClientId: process.env.REACT_APP_GOOGLE_CLIENTID,
googleClientSecret: process.env.REACT_APP_GOOGLE_CLIENT_SECRET,
},
})
import { defineConfig } from 'cypress'
// Populate process.env with values from .env file
require('dotenv').config()
export default defineConfig({
env: {
googleRefreshToken: process.env.GOOGLE_REFRESH_TOKEN,
googleClientId: process.env.REACT_APP_GOOGLE_CLIENTID,
googleClientSecret: process.env.REACT_APP_GOOGLE_CLIENT_SECRET,
},
})
Custom Command for Google Authentication​
Next, we'll write a command named loginByGoogleApi
to perform a programmatic
login into Google and set an item in localStorage
with the
authenticated users details, which we'll use in our application code to verify
we are authenticated under test.
The loginByGoogleApi
command will execute the following steps:
- Use the refresh token from the
Google OAuth 2.0 Playground
to perform the programmatic login, exchanging the refresh token for an
access_token
. - Use the
access_token
returned to get the Google User profile. - Finally the
googleCypress
localStorage item is set with theaccess token
and user profile.
Cypress.Commands.add('loginByGoogleApi', () => {
cy.log('Logging in to Google')
cy.request({
method: 'POST',
url: 'https://www.googleapis.com/oauth2/v4/token',
body: {
grant_type: 'refresh_token',
client_id: Cypress.env('googleClientId'),
client_secret: Cypress.env('googleClientSecret'),
refresh_token: Cypress.env('googleRefreshToken'),
},
}).then(({ body }) => {
const { access_token, id_token } = body
cy.request({
method: 'GET',
url: 'https://www.googleapis.com/oauth2/v3/userinfo',
headers: { Authorization: `Bearer ${access_token}` },
}).then(({ body }) => {
cy.log(body)
const userItem = {
token: id_token,
user: {
googleId: body.sub,
email: body.email,
givenName: body.given_name,
familyName: body.family_name,
imageUrl: body.picture,
},
}
window.localStorage.setItem('googleCypress', JSON.stringify(userItem))
cy.visit('/')
})
})
})
With our Google app setup properly, necessary environment variables in place,
and our loginByGoogleApi
command implemented, we'll be able to authenticate
with Google while our app is under test. Below is a test to login as a user via
Google, complete the onboarding process and logout.
The runnable version of this test is in the Real World App (RWA).
describe('Google', function () {
beforeEach(function () {
cy.task('db:seed')
cy.loginByGoogleApi()
})
it('shows onboarding', function () {
cy.contains('Get Started').should('be.visible')
})
})
Adapting a Google App for Testing​
The previous sections focused on the recommended Google authentication practice within Cypress tests. To use this practice it's assumed you're testing an app appropriately built or adapted to use Google.
The following sections provides guidance on building or adapting an app to use Google authentication.
The Real World App (RWA) is used and provides configuration and runnable code for both the React SPA and the Express back end.
The front end uses the react-google-login component and the back end uses express-jwt to validate the JWT provided by Google.
Use the yarn dev:google
command when starting the
Cypress Real World App.
Adapting the back end​
In order to validate API requests from the frontend, we install express-jwt and jwks-rsa and configure validation for JWT's from Google.
import jwt from 'express-jwt'
import jwksRsa from 'jwks-rsa'
dotenv.config()
const googleJwtConfig = {
secret: jwksRsa.expressJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: 'https://www.googleapis.com/oauth2/v3/certs',
}),
// Validate the audience and the issuer.
audience: process.env.REACT_APP_GOOGLE_CLIENTID,
issuer: 'accounts.google.com',
algorithms: ['RS256'],
}
Next, we'll define an Express middleware function to be use in our routes to
verify the Google JWT sent by the front end API requests
as the Bearer
token.
// ...
export const checkJwt = jwt(googleJwtConfig).unless({ path: ['/testData/*'] })
Once this helper is defined, we can use globally to apply to all routes:
// initial imports ...
import { checkJwt } from './helpers'
// ...
if (process.env.REACT_APP_GOOGLE) {
app.use(checkJwt)
}
// routes ...
Adapting the front end​
We need to update our front end React app to allow for authentication with Google. As mentioned above, the front end uses the react-google-login component to perform the login.
First, we create an AppGoogle.tsx
container to render our application as it is
authenticated with Google. The component is identical to
the App.tsx
component, but has the addition of a GoogleLogin
component in
place of the original Sign Up and Sign In components.
A useGoogleLogin
hook is added to send a GOOGLE
event with the user
and
token
objects to work with the existing authentication layer
(authMachine.ts
).
The full AppGoogle.tsx component is in the Real World App (RWA).
// initial imports ...
import { GoogleLogin, useGoogleLogin } from 'react-google-login'
// ...
const AppGoogle = () => {
// ...
useGoogleLogin({
clientId: process.env.REACT_APP_GOOGLE_CLIENTID!,
onSuccess: (res) => {
authService.send('GOOGLE', { user: res.profileObj, token: res.tokenId })
},
cookiePolicy: 'single_host_origin',
isSignedIn: true,
})
// ...
const isLoggedIn =
isAuthenticated &&
(authState.matches('authorized') ||
authState.matches('refreshing') ||
authState.matches('updating'))
return (
<div className={classes.root}>
// ...
{authState.matches('unauthorized') && (
<Container component="main" maxWidth="xs">
<CssBaseline />
<div className={classes.paper}>
<GoogleLogin
clientId={process.env.REACT_APP_GOOGLE_CLIENTID!}
buttonText="Login"
cookiePolicy={'single_host_origin'}
/>
</div>
</Container>
)}
</div>
)
}
export default AppGoogle
Next, we update our entry point (index.tsx
) to conditionally load the
AppGoogle
component if we start the application with the REACT_APP_GOOGLE
environment variable set to true
.
import React from 'react'
import ReactDOM from 'react-dom'
import { Router } from 'react-router-dom'
import { history } from './utils/historyUtils'
import App from './containers/App'
import AppGoogle from './containers/AppGoogle'
import { createMuiTheme, ThemeProvider } from '@material-ui/core'
const theme = createMuiTheme({
palette: {
secondary: {
main: '#fff',
},
},
})
ReactDOM.render(
<Router history={history}>
<ThemeProvider theme={theme}>
{process.env.REACT_APP_GOOGLE ? <AppGoogle /> : <App />}
</ThemeProvider>
</Router>,
document.getElementById('root')
)