Auth0认证
你将学习到
- 如何在Cypress测试中使用Auth0进行认证
- 如何为测试适配Auth0应用
- Auth0速率限制的注意事项
本指南针对使用经典通用登录体验的Auth0单页应用测试进行设置。此配置推荐用于自动化端到端测试的"测试租户"和/或"测试API"设置。
Auth0应用设置
要开始使用Auth0,需要通过以下步骤在Auth0仪表板中设置应用:
- 访问Auth0仪表板并点击"创建应用"按钮。
- 输入应用的名称。
- 选择"单页应用"
应用创建后,访问应用下的应用设置标签页,并在以下部分添加本地开发URL和端口(例如http://localhost:3000
):
- 允许的回调URL
- 允许的登出URL
- 允许的Web来源
- 允许的来源(CORS)
在应用设置底部,点击显示高级设置,选择"授权类型"标签页并勾选"密码"(默认未勾选)。
接下来,点击租户图标(右上角头像菜单)进入租户设置。在常 规标签页中,前往API授权设置
- 将"默认受众"设置为测试应用的受众URL(例如
https://your-api-id.auth0.com/api/v2/
) - 将"默认目录"设置为**"用户名-密码认证"**
参考Auth0租户设置文档获取更多详情。
最后,在Auth0用户存储中为Cypress测试创建一个用户。这个专用于测试的目标用户将在测试规范中登录你的应用。如果测试需要,你可以创建多个用户来测试特定应用。
在Cypress中设置Auth0应用凭证
为了在测试中访问测试用户凭证,我们需要配置Cypress使用.env
文件中设置的Auth0环境变量。
- cypress.config.js 文件
- cypress.config.ts 文件
const { defineConfig } = require('cypress')
// 从.env文件填充process.env
require('dotenv').config()
module.exports = defineConfig({
env: {
auth0_username: process.env.AUTH0_USERNAME,
auth0_password: process.env.AUTH0_PASSWORD,
auth0_domain: process.env.REACT_APP_AUTH0_DOMAIN,
auth0_audience: process.env.REACT_APP_AUTH0_AUDIENCE,
auth0_scope: process.env.REACT_APP_AUTH0_SCOPE,
auth0_client_id: process.env.REACT_APP_AUTH0_CLIENTID,
auth0_client_secret: process.env.AUTH0_CLIENT_SECRET,
},
})
import { defineConfig } from 'cypress'
// 从.env文件填充process.env
require('dotenv').config()
export default defineConfig({
env: {
auth0_username: process.env.AUTH0_USERNAME,
auth0_password: process.env.AUTH0_PASSWORD,
auth0_domain: process.env.REACT_APP_AUTH0_DOMAIN,
auth0_audience: process.env.REACT_APP_AUTH0_AUDIENCE,
auth0_scope: process.env.REACT_APP_AUTH0_SCOPE,
auth0_client_id: process.env.REACT_APP_AUTH0_CLIENTID,
auth0_client_secret: process.env.AUTH0_CLIENT_SECRET,
},
})
注意auth0_client_secret
仅用于编程式登录。
Auth0认证的自定义命令
有两种方式可以认证到Auth0:
使用cy.origin()
登录
接下来,我们将编写一个名为loginToAuth0
的自定义命令来执行Auth0登录。该命令将使用cy.origin()
来:
- 导航到Auth0登录页
- 输入用户凭证
- 登录并重定向回Cypress Real World App
- 使用
cy.session()
缓存结果
function loginViaAuth0Ui(username: string, password: string) {
// 应用登录页重定向到Auth0
cy.visit('/')
// 在Auth0上登录
cy.origin(
Cypress.env('auth0_domain'),
{ args: { username, password } },
({ username, password }) => {
cy.get('input#username').type(username)
cy.get('input#password').type(password, { log: false })
cy.contains('button[value=default]', 'Continue').click()
}
)
// 确保Auth0已将我们重定向回RWA
cy.url().should('equal', 'http://localhost:3000/')
}
Cypress.Commands.add('loginToAuth0', (username: string, password: string) => {
const log = Cypress.log({
displayName: 'AUTH0 LOGIN',
message: [`🔐 认证中 | ${username}`],
// @ts-ignore
autoEnd: false,
})
log.snapshot('before')
loginViaAuth0Ui(username, password)
log.snapshot('after')
log.end()
})
现在,我们可以在测试中使用loginToAuth0
命令。以下是通过Auth0登录用户并运行基本健全性检查的测试。
此测试的可运行版本在真实世界应用(RWA)中。
describe('Auth0', function () {
beforeEach(function () {
cy.task('db:seed')
cy.intercept('POST', '/graphql').as('createBankAccount')
cy.loginToAuth0(
Cypress.env('auth0_username'),
Cypress.env('auth0_password')
)
cy.visit('/')
})
it('显示引导页面', function () {
cy.contains('Get Started').should('be.visible')
})
})
最后,我们可以重构登录命令以利用cy.session()
存储已登录用户,这样我们就不必在每次测试前重新认证。
Cypress.Commands.add('loginToAuth0', (username: string, password: string) => {
const log = Cypress.log({
displayName: 'AUTH0 LOGIN',
message: [`🔐 认证中 | ${username}`],
// @ts-ignore
autoEnd: false,
})
log.snapshot('before')
cy.session(
`auth0-${username}`,
() => {
loginViaAuth0Ui(username, password)
},
{
validate: () => {
// 验证localStorage中是否存在访问令牌
cy.wrap(localStorage)
.invoke('getItem', 'authAccessToken')
.should('exist')
},
}
)
log.snapshot('after')
log.end()
})
编程式登录
以下是使用/oauth/token端点编程式登录Auth0的命令,并在localStorage
中设置一个包含 认证用户详情的项,我们将在应用代码中使用它来验证测试中的认证状态。
loginByAuth0Api
命令将执行以下步骤:
- 使用/oauth/token端点执行编程式登录。
- 最后将
access token
、id_token
和用户配置文件设置为localStorage
中的auth0Cypress
项。
Cypress.Commands.add(
'loginByAuth0Api',
(username: string, password: string) => {
cy.log(`以${username}身份登录`)
const client_id = Cypress.env('auth0_client_id')
const client_secret = Cypress.env('auth0_client_secret')
const audience = Cypress.env('auth0_audience')
const scope = Cypress.env('auth0_scope')
cy.request({
method: 'POST',
url: `https://${Cypress.env('auth0_domain')}/oauth/token`,
body: {
grant_type: 'password',
username,
password,
audience,
scope,
client_id,
client_secret,
},
}).then(({ body }) => {
const claims = jwt.decode(body.id_token)
const {
nickname,
name,
picture,
updated_at,
email,
email_verified,
sub,
exp,
} = claims
const item = {
body: {
...body,
decodedToken: {
claims,
user: {
nickname,
name,
picture,
updated_at,
email,
email_verified,
sub,
},
audience,
client_id,
},
},
expiresAt: exp,
}
window.localStorage.setItem('auth0Cypress', JSON.stringify(item))
cy.visit('/')
})
}
)
通过正确设置Auth0开发者控制台中的应用、必要的环境变量以及实现loginByAuth0Api
命令,我们能够在测试应用时使用Auth0进行认证。以下是通过Auth0登录用户、完成引导流程并登出的测试。
describe('Auth0', function () {
beforeEach(function () {
cy.task('db:seed')
cy.loginByAuth0Api(
Cypress.env('auth0_username'),
Cypress.env('auth0_password')
)
})
it('显示引导页面', function () {
cy.contains('Get Started').should('be.visible')
})
})
为测试适配Auth0应用
前几节重点介绍了Cypress测试中推荐的Auth0认证实践。要使用此实践,假设你正在测试一个已正确构建或适配为使用Auth0的应用。
以下部分提供了构建或适配应用以使用Auth0认证的指南。请注意,如果你正在使用cy.origin()
登录且你的应用已成功集成Auth0,则无需对应用进行任何更改,本指南的其余部分应仅作为信息参考。
真实世界应用(RWA)被使用,并为React SPA和Express后端提供了配置和可运行代码。
前端使用auth0-react SDK用于React单页应用(SPA),其底层使用auth0-spa-js SDK。后端使用express-jwt来验证针对Auth0的JWT。
启动Cypress Real World App时使用yarn dev:auth0
命令。
适配后端
为了验证来自前端的API请求,我们安装express-jwt和jwks-rsa并配置验证来自Auth0的JWT。
import jwt from 'express-jwt'
import jwksRsa from 'jwks-rsa'
dotenv.config()
const auth0JwtConfig = {
secret: jwksRsa.expressJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: `https://${process.env.REACT_APP_AUTH0_DOMAIN}/.well-known/jwks.json`,
}),
// 验证受众和发行者
audience: process.env.REACT_APP_AUTH0_AUDIENCE,
issuer: `https://${process.env.REACT_APP_AUTH0_DOMAIN}/`,
algorithms: ['RS256'],
}
接下来,我们将定义一个Express中间件函数,用于在我们的路由中验证前端API请求发送的Auth0 JWT作为Bearer
令牌。
// ...
export const checkJwt = jwt(auth0JwtConfig).unless({ path: ['/testData/*'] })
定义此辅助函数后,我们可以全局应用它到所有路由:
// 初始导入...
import { checkJwt } from './helpers'
// ...
if (process.env.REACT_APP_AUTH0) {
app.use(checkJwt)
}
// 路由...
适配前端
我们需要更新前端React应用以允许使用Auth0进行认证。如上所述,使用auth0-react SDK用于React单页应用(SPA)。
首先,我们创建一个AppAuth0.tsx
容器来渲染我们的应用,因为它已通过Auth0认证。该组件与App.tsx
组件相同,但使用了useAuth0
React Hook,移除了注册和登录路由的需求,并用withAuthenticationRequired
高阶函数(HOC)包装了组件。
添加了一个useEffect
钩子来获取认证用户的访问令牌,并发送一个带有user
和token
对象的AUTH0
事件,以与现有的认证层(authMachine.ts
)一起工作。
// 初始导入...
import { withAuthenticationRequired, useAuth0 } from '@auth0/auth0-react'
// ...
const AppAuth0 = () => {
const { isAuthenticated, user, getAccessTokenSilently } = useAuth0()
// ...
useEffect(() => {
;(async function waitForToken() {
const token = await getAccessTokenSilently()
authService.send('AUTH0', { user, token })
})()
}, [user, getAccessTokenSilently])
// ...
const isLoggedIn =
isAuthenticated &&
(authState.matches('authorized') ||
authState.matches('refreshing') ||
authState.matches('updating'))
return <div className={classes.root}>// ...</div>
}
export default withAuthenticationRequired(AppAuth0)
注意:完整的AppAuth0.tsx组件在真实世界应用(RWA)中。
接下来,我们更新入口点(index.tsx
),用auth0-react SDK的<Auth0Provider>
包装我们的应用,提供一个自定义的onRedirectCallback
。我们传递上面.env
中设置的Auth0环境变量的props,并渲染我们的<AppAuth0>
组件作为应用。
// 初始导入...
import AppAuth0 from "./containers/AppAuth0";
// ..
const onRedirectCallback = (appState: any) => {
history.replace((appState && appState.returnTo) || window.location.pathname);
};
if (process.env.REACT_APP_AUTH0) {
ReactDOM.render(
<Auth0Provider
domain={process.env.REACT_APP_AUTH0_DOMAIN!}
clientId={process.env.REACT_APP_AUTH0_CLIENTID!}
redirectUri={window.location.origin}
audience={process.env.REACT_APP_AUTH0_AUDIENCE}
scope={process.env.REACT_APP_AUTH0_SCOPE}
onRedirectCallback={onRedirectCallback}
>
<Router history={history}>
<ThemeProvider theme={theme}>
<AppAuth0 />
</ThemeProvider>
</Router>
</Auth0Provider>,
document.getElementById("root")
);
} else {
// 渲染passport-local App.tsx
}
需要更新我们的AppAuth0.tsx组件以有条件地使用auth0Cypress
localStorage
项。
在下面的代码中,我们基于在Cypress下测试(使用window.Cypress
)有条件地应用useEffect
块。
此外,我们将更新导出,如果 不在Cypress测试中,则用withAuthenticationRequired
包装。这允许我们的应用在开发/生产中使用Auth0重定向登录流程,但在Cypress测试中不使用。
// 初始导入...
import { withAuthenticationRequired, useAuth0 } from "@auth0/auth0-react";
// ...
const AppAuth0 = () => {
const { isAuthenticated, user, getAccessTokenSilently } = useAuth0();
// ...
useEffect(() => {
(async function waitForToken() {
const token = await getAccessTokenSilently();
authService.send("AUTH0", { user, token });
})();
}, [user, getAccessTokenSilently]);
// 如果在Cypress测试中,从"auth0Cypress" localstorage获取凭证
// 并向我们的状态管理发送事件以将用户登录到SPA
if (window.Cypress) {
useEffect(() => {
const auth0 = JSON.parse(localStorage.getItem("auth0Cypress")!);
authService.send("AUTH0", {
user: auth0.body.decodedToken.user,
token: auth0.body.access_token,
});
}, []);
} else {
useEffect(() => {
(async function waitForToken() {
const token = await getAccessTokenSilently();
authService.send("AUTH0", { user, token });
})();
}, [isAuthenticated, user, getAccessTokenSilently]);
}
// ...
const isLoggedIn =
isAuthenticated &&
(authState.matches("authorized") ||
authState.matches("refreshing") ||
authState.matches("updating"));
return (
<div className={classes.root}>
// ...
</div>
);
};
// 如果不在Cypress测试中,则有条件导出用`withAuthenticationRequired`包装
let appAuth0 = window.Cypress ? AppAuth0 : withAuthenticationRequired(AppAuth0);
export default appAuth0
Auth0登录速率限制
注意Auth0文档中的速率限制 -
随着测试套件规模的增大以及启用并行运行以加快测试运行时间,可能会达到此限制。
如果遇到此速率限制,可以在测试运行前向loginByAuth0
命令添加编程式方法来清除被阻止的IP。
你需要获取一个API令牌来与Auth0管理API交互。此令牌是一个JSON Web令牌(JWT),它包含API的特定授予权限。
将此令牌作为环境变量AUTH0_MGMT_API_TOKEN
添加到我们的真实世界应用(RWA) .env
文件中,使用你的API令牌。
// ... 其他键
AUTH0_MGMT_API_TOKEN = '你的管理API令牌'
有了这个令牌,我们可以向Auth0异常删除被阻止IP地址端点添加交互到我们的loginByAuth0Api
命令。这将向Auth0管理API异常端点发送删除请求,以解除可能在测试运行期间被阻止的IP。
icanhazip.com是一个免费的托管服务,用于查找系统的当前外部IP地址。
Cypress.Commands.add('loginByAuth0Api', (username, password) => {
// 当被Auth0速率限制时有用
cy.exec('curl -4 icanhazip.com')
.its('stdout')
.then((ip) => {
cy.request({
method: 'DELETE',
url: `https://${Cypress.env(
'auth0_domain'
)}/api/v2/anomaly/blocks/ips/${ip}`,
auth: {
bearer: Cypress.env('auth0_mgmt_api_token'),
},
})
})
// ... loginByAuth0Api命令的剩余部分
})