Okta Authentication
What you'll learn
- How to test Okta authentication in Cypress
- How to set Okta credentials in Cypress
- How to adapt an Okta app for testing
The scope of this guide is to demonstrate authentication solely against the Okta Universal Directory.
Okta Developer Console Setup
If not already setup, you'll need to create an Okta application within the Okta Developer Console. Once the Okta application is created, the Okta Developer Console will provide a Client ID, which will be used alongside your Okta domain to configure Okta SDKs as shown in the subsequent sections of this guide.
Setting Okta app credentials in Cypress
To have access to test user credentials within our tests we need to configure
Cypress to use the Okta environment variables set in the
.env
file.
- 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: {
auth_username: process.env.AUTH_USERNAME,
auth_password: process.env.AUTH_PASSWORD,
okta_domain: process.env.REACT_APP_OKTA_DOMAIN,
okta_client_id: process.env.REACT_APP_OKTA_CLIENTID,
},
})
import { defineConfig } from 'cypress'
// Populate process.env with values from .env file
require('dotenv').config()
export default defineConfig({
env: {
auth_username: process.env.AUTH_USERNAME,
auth_password: process.env.AUTH_PASSWORD,
okta_domain: process.env.REACT_APP_OKTA_DOMAIN,
okta_client_id: process.env.REACT_APP_OKTA_CLIENTID,
},
})
Custom Command for Okta Authentication
There are two ways you can authenticate to Okta:
Login with cy.origin()
We'll write a custom command called loginByOkta
to perform a login to
Okta. This command will use
cy.origin()
to
- Navigate to the Okta origin
- Input user credentials
- Sign in and redirect back to the Cypress Real World App
- Cache the results with
cy.session()
// Okta
const loginToOkta = (username: string, password: string) => {
Cypress.log({
displayName: 'OKTA LOGIN',
message: [`🔐 Authenticating | ${username}`],
autoEnd: false,
})
cy.visit('/')
cy.origin(
Cypress.env('okta_domain'),
{ args: { username, password } },
({ username, password }) => {
cy.get('input[name="identifier"]').type(username)
cy.get('input[name="credentials.passcode"]').type(password, {
log: false,
})
cy.get('[type="submit"]').click()
}
)
cy.get('[data-test="sidenav-username"]').should('contain', username)
}
// right now our custom command is light. More on this later!
Cypress.Commands.add('loginByOkta', (username: string, password: string) => {
return loginToOkta(username, password)
})
Now, we can use our loginByOkta
command in the test. Below is our test to
login as a user via Okta and run a few basic sanity checks.
The runnable version of this test is in the Real World App (RWA).
describe('Okta', function () {
beforeEach(function () {
cy.task('db:seed')
cy.loginByOkta(Cypress.env('okta_username'), Cypress.env('okta_password'))
})
it('verifies signed in user does not have a bank account', function () {
cy.get('[data-test="sidenav-bankaccounts"]').click()
cy.get('[data-test="empty-list-header"]').should('be.visible')
})
})
Lastly, we can refactor our login command to take advantage of
cy.session()
to store our logged in user so we don't
have to reauthenticate with everything test.
Cypress.Commands.add('loginByOkta', (username: string, password: string) => {
cy.session(
`okta-${username}`,
() => {
return loginToOkta(username, password)
},
{
validate() {
cy.visit('/')
cy.get('[data-test="sidenav-username"]').should('contain', username)
},
}
)
})
Programmatic Login
Next, we'll write a command named loginByOktaApi
to perform a programmatic
login into Okta 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.
In order to make sure this is enabled inside the Real World App (RWA),
set the REACT_APP_OKTA_PROGRAMMATIC
environment variable to true
.
The loginByOktaApi
command will execute the following steps:
- Use the Okta Authentication API to perform the programmatic login.
- Use an instance of
OktaAuth
client from the Okta Auth SDK to gain theid_token
once a session token is obtained. - Finally the
oktaCypress
localStorage item is set with theaccess token
and user profile.
import { OktaAuth } from '@okta/okta-auth-js'
// Okta
Cypress.Commands.add('loginByOktaApi', (username, password) => {
cy.request({
method: 'POST',
url: `https://${Cypress.env('okta_domain')}/api/v1/authn`,
body: {
username,
password,
},
}).then(({ body }) => {
const user = body._embedded.user
const config = {
issuer: `https://${Cypress.env('okta_domain')}/oauth2/default`,
clientId: Cypress.env('okta_client_id'),
redirectUri: 'http://localhost:3000/implicit/callback',
scopes: ['openid', 'email', 'profile'],
}
const authClient = new OktaAuth(config)
return authClient.token
.getWithoutPrompt({ sessionToken: body.sessionToken })
.then(({ tokens }) => {
const userItem = {
token: tokens.accessToken.value,
user: {
sub: user.id,
email: user.profile.login,
given_name: user.profile.firstName,
family_name: user.profile.lastName,
preferred_username: user.profile.login,
},
}
window.localStorage.setItem('oktaCypress', JSON.stringify(userItem))
log.snapshot('after')
log.end()
})
})
})
With our Okta app setup properly in Okta Developer console, necessary
environment variables in place, and our loginByOktaApi
command implemented, we
will be able to authenticate with Okta while our app is under test. Below is a
test to login as a user via Okta, complete the onboarding
process and logout.
describe('Okta', function () {
beforeEach(function () {
cy.task('db:seed')
cy.loginByOktaApi(
Cypress.env('auth_username'),
Cypress.env('auth_password')
)
})
it('shows onboarding', function () {
cy.contains('Get Started').should('be.visible')
})
})
The runnable version of this test is in the Real World App (RWA).
Adapting an Okta App for Testing
The previous section focused on the programmatic Okta authentication practice within Cypress tests. To use this practice, it's assumed you are testing an app appropriately built or adapted to use Okta.
Unlike programmatic login, authenticating with
cy.origin()
doesn't require adapting the application
to work. This step is only needed if implementing programmatic login.
The following sections provides guidance on building or adapting an app to use Okta 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 Okta React SDK for React Single Page Applications (SPA), which uses the Okta Auth SDK underneath. The back end uses the Okta JWT Verifier for Node.js to validate JWTs from Okta.
Use the yarn dev:okta
command when starting the
Cypress Real World App.
Adapting the back end
In order to validate API requests from the frontend, we install Okta JWT Verifier for Node.js and configure it using the Okta Domain and Client ID provided after Creating an Okta application.
import OktaJwtVerifier from '@okta/jwt-verifier'
dotenv.config()
// Okta Validate the JWT Signature
const oktaJwtVerifier = new OktaJwtVerifier({
issuer: `https://${process.env.REACT_APP_OKTA_DOMAIN}/oauth2/default`,
clientId: process.env.REACT_APP_OKTA_CLIENTID,
assertClaims: {
aud: 'api://default',
cid: process.env.REACT_APP_OKTA_CLIENTID,
},
})
Next, we'll define an Express middleware function to be use in our routes to
verify the Okta JWT sent by the front end API requests as
the Bearer
token.
// ...
export const verifyOktaToken = (req, res, next) => {
const bearerHeader = req.headers['authorization']
if (bearerHeader) {
const bearer = bearerHeader.split(' ')
const bearerToken = bearer[1]
oktaJwtVerifier
.verifyAccessToken(bearerToken, 'api://default')
.then((jwt) => {
// the token is valid
req.user = {
// @ts-ignore
sub: jwt.sub,
}
return next()
})
.catch((err) => {
// a validation failed, inspect the error
console.log('error', err)
})
} else {
res.status(401).send({
error: 'Unauthorized',
})
}
}
Once this helper is defined, we can use it globally to apply to all routes:
// initial imports ...
import { verifyOktaToken } from './helpers'
// ...
if (process.env.REACT_APP_OKTA) {
app.use(verifyOktaToken)
}
// routes ...
Adapting the front end
We need to update our front end React app to allow for authentication with Okta using the Okta React SDK.
First, we create a AppOkta.tsx
container, based off of the App.tsx
component.
AppOkta.tsx
uses the useOktaAuth
React Hook, replaces the Sign Up and Sign
In routes with a SecureRoute
and LoginCallback
and wraps the component with
the withOktaAuth
higher order component (HOC).
A useEffect
hook is added to get the access token for the authenticated user
and send an OKTA
event with the user
and token
objects to work with the
existing authentication layer (authMachine.ts
). We define a route for
implicit/callback
to render the LoginCallback
component and a SecureRoute
for the root path.
// initial imports ...
import {
LoginCallback,
SecureRoute,
useOktaAuth,
withOktaAuth,
} from '@okta/okta-react'
// ...
const AppOkta: React.FC = () => {
const { authState, oktaAuth } = useOktaAuth()
// ...
useEffect(() => {
if (authState.isAuthenticated) {
oktaAuth.getUser().then((user) => {
authService.send('OKTA', { user, token: oktaAuthState.accessToken })
})
}
}, [authState, oktaAuth])
// ...
return (
<div className={classes.root}>
// ...
{authState.matches('unauthorized') && (
<>
<Route path="/implicit/callback" component={LoginCallback} />
<SecureRoute exact path="/" />
</>
)}
// ...
</div>
)
}
export default withOktaAuth(AppOkta)
The full AppOkta.tsx component is in the
Next, we update our entry point (index.tsx
) to wrap our application with the
<Security>
component from the
Okta React SDK providing issuer
,
clientId
from our Okta application, along with a redirectUri
as props using
the REACT_APP_OKTA
variables are defined in our .env
.
// initial imports ...
import { OktaAuth } from '@okta/okta-auth-js'
import { Security } from '@okta/okta-react'
import AppOkta from './containers/AppOkta'
// ...
const oktaAuth = new OktaAuth({
issuer: `https://${process.env.REACT_APP_OKTA_DOMAIN}/oauth2/default`,
clientId: process.env.REACT_APP_OKTA_CLIENTID,
redirectUri: window.location.origin + '/implicit/callback',
})
ReactDOM.render(
<Router history={history}>
<ThemeProvider theme={theme}>
{process.env.REACT_APP_OKTA ? (
<Security oktaAuth={oktaAuth}>
<AppOkta />
</Security>
) : (
<App />
)}
</ThemeProvider>
</Router>,
document.getElementById('root')
)
An update to our
AppOkta.tsx component
is needed to conditionally use the oktaCypress
localStorage item.
In the code below, we conditionally apply a useEffect
block based on being
under test with Cypress (using window.Cypress
).
In addition, we will update the export to be wrapped with the withOktaAuth
higher order component only if we are not under test in Cypress. This allows our
application to work with the Okta redirect login flow in
development/production but not when under test in Cypress.
// initial imports ...
import { LoginCallback, SecureRoute, useOktaAuth, withOktaAuth } from "@okta/okta-react";
// ...
const AppOkta: React.FC = () => {
const { authState, oktaAuth } = useOktaAuth();
// ...
// If under test in Cypress, get credentials from "oktaCypress" localstorage item and send event to our state management to log the user into the SPA
if (window.Cypress) {
useEffect(() => {
const okta = JSON.parse(localStorage.getItem("oktaCypress")!);
authService.send("OKTA", {
user: okta.user,
token: okta.token,
});
}, []);
} else {
useEffect(() => {
if (authState.isAuthenticated) {
oktaAuth.getUser().then((user) => {
authService.send("OKTA", { user, token: oktaAuthState.accessToken });
});
}
}, [authState, oktaAuth]);
}
// ...
return (
<div className={classes.root}>
// ...
{authState.matches("unauthorized") && (
<>
<Route path="/implicit/callback" component={LoginCallback} />
<SecureRoute exact path="/" />
</>
)}
// ...
</div>
);
};
// Conditional export wrapped with `withOktaAuth` if we are not under test in Cypress
let appOkta = window.Cypress ? AppOkta : withOktaAuth(AppOkta);
export default appOkta;