如何构建身份和访问管理(IAM)服务 part1
导语:在本文中,我们探讨了开发身份和访问管理服务的几种方法之间的差异。
保护敏感数据是所有企业的关键安全任务之一。为了确保这种保护,企业需要仔细控制谁可以访问什么,因此实施身份和访问管理(IAM)机制至关重要。
然而,由于 IAM 选项多种多样,决定使用哪种服务、如何配置它以及是否需要自定义解决方案来实现安全目的可能会很困难。
在本文中,我们探讨了开发身份和访问管理服务的几种方法之间的差异。我们展示了如何构建自定义本地系统和基于云的系统的详细示例,并将这些系统与 IAM 系统的关键标准进行比较。
本指南对于想要探索构建 IAM 系统的选项并了解技术细节和细微差别的开发领导者将很有帮助。
什么是 IAM 服务以及如何构建一项服务?
根据Gartner 的说法,身份和访问管理使正确的个人能够在正确的时间以正确的理由访问正确的资源。不同类型和规模的企业都努力实施 IAM 实践,通过控制用户对关键信息的访问来保护敏感信息并防止数据泄露。
为了实施这些实践,企业使用专门的系统来提供必要的功能,例如双因素或多因素身份验证、用户身份管理、用户授权功能、权限控制等。
高效的 IAM 服务应遵循零信任原则,这意味着它必须提供以下内容:
基于身份的安全性,确保系统知道登录用户并确认用户的身份
基于角色的访问权限,确保每个帐户拥有尽可能少的权限:例如,仅具有日常工作所需的访问权限
保护用户的其他安全措施,例如多重身份验证 (MFA)、登录尝试计数、针对暴力和机器人攻击的策略、审核日志和备份可能性
您可以在本地部署 IAM 系统、订阅第三方供应商提供的基于云的模型或使用混合模型。
出于本文的目的,我们决定比较构建 IAM 解决方案的几种方法:
基于作为开放集成中心 (OIH) 分支的自托管服务器开发本地 IAM 解决方案。
使用 Auth0 配置由云提供商管理的 IAM 解决方案。
使用 AWS Cognito 配置由云提供商管理的 IAM 解决方案。
但在我们开始比较开发身份和访问管理服务的方法之前,让我们先讨论一下我们的解决方案的功能并探索其流程。
IAM 系统的关键功能
定义未来解决方案的需求至关重要,这样我们就可以在不同的实现准备就绪后对其进行公平的比较。
我们的目标是提供一个覆盖 1000 到 5000 个用户的 IAM 系统,这是中小型企业中的实际用户数量。
此类解决方案必须涵盖的常见任务包括:
提供对最终用户来说简单的用户注册流程
基于密码的身份验证,确认用户的真实身份
基于令牌的授权,确保用户被授予其应有的确切级别和类型的访问权限
创建独特的角色和权限以及将它们分配给实体或从实体中删除它们的能力
防止任何窃取用户身份的企图
访问审查和事件响应
IAM 解决方案可能包含的其他功能包括:
安全身份验证,例如支持 MFA
通过社交网络、Google 和 Microsoft 等第三方身份提供商进行身份验证
多租户支持提供管理多个企业的能力
现成的 UI 表单可减少从头开始创建自定义 UI 表单的时间和成本
在本文中,我们探讨了创建 IAM 解决方案的三种不同方法,确保必备列表中的功能,并在我们使用的平台允许的情况下尝试实现上面列出的其他功能。
典型的IAM管理流程
下图从用户、管理员和租户的角度直观地展示了基本理论 IAM 管理流程:
以下是用户与 IAM 解决方案交互的方式:
用户要么已经拥有访问令牌,要么访问令牌被重定向到 IAM 服务进行身份验证。
为了进行身份验证,用户指定其用户名和密码,或者选择其他身份验证选项,例如通过社交网络进行身份验证或使用授权代码流方法。
当用户通过身份验证后,系统向资源服务器发出请求。
资源服务器包含一个库,用于验证用户是否有权访问资源服务器内的业务逻辑部分或向 IAM 服务发出请求,后者根据其数据库检查用户权限。
管理员具有以下权限:
分配给他们的一组预定义角色
使他们能够创建新租户空间的系统权限
租户具有以下条件:
一组预定义的角色,允许他们仅执行与租户相关的操作
在租户空间内管理用户和角色/权限的机会
定义了需求和一般程序流程后,让我们开始实际实施。我们首先开发基于开放集成中心的自定义 IAM 服务。
1. 使用开放集成中心开发定制的 IAM 解决方案
开放集成中心(OIH) 是一个框架,可帮助开发人员确保业务应用程序之间轻松进行数据交换。它由多种服务组成,包括身份和访问管理服务。
如果您需要完全控制解决方案,使用 OIH 开发 IAM 服务是一个不错的选择。但是,这个框架不提供任何精美的 UI 进行自定义,您必须手动完成所有工作。
在本例中,我们将使用本地 IAM 服务器并花时间:
必要时分叉、审核和修改源代码
修复问题并维护部署管道、备份/恢复过程等。
OIH 提供的 IAM 服务的主要特点是:
支持最少的任务集,包括企业和用户管理、身份验证、授权和基于角色的访问控制(RBAC)
包含最小的依赖集,依赖很少的外部服务
使用JSON Web Tokens (JWT)、OAuth 2.0和OpenId Connect等基本且众所周知的技术
要开始开发自定义解决方案,您需要执行以下步骤:
分叉现有 IAM 解决方案的源代码
必要时修改逻辑
创建 Mongo 数据库
配置RabbitMQ代理
准备托管环境
现在,我们来讨论如何使用 OIH 实现我们的解决方案的基本流程。
1.1. 验证
对于身份验证,我们将描述一种不涉及外部社交联系的方法。要通过身份验证,用户应添加到至少一个企业(绑定到租户)。否则,身份验证流程将会失败。
身份验证过程分为三个主要阶段:
第 1 阶段:用户提供登录名和密码,并从api/v1/session端点检索会话令牌。在此阶段,我们不需要任何有关用户会员资格的信息。唯一的目标是验证该用户并获取可以与 JWT 交换的会话令牌。
以下代码片段演示了如何获取 api.js 文件中的令牌:
router.post('/session', authMiddleware.authenticate, authMiddleware.accountIsEnabled, async (req, res, next) => { if (!req.user) { // req.user will be set after authMiddleware.authenticate return next({ status: 401, message: CONSTANTS.ERROR_CODES.NOT_LOGGED_IN }); } const t = await TokenUtils.create(req.user); // id token will be created in a local database req.headers.authorization = 'Bearer ${t.token}'; res.status(200).send({ token: t.token, id: t._id }); });
出于可读性目的,会话端点被分解为多个预处理程序。我们的系统将在创建 ID 令牌之前调用每个预处理程序。
此时,我们最感兴趣的是中间件函数,也称为预处理程序或钩子函数 -authMiddleware.authenticate它通过与 Passport 库集成来工作,而 Passport 库又使用 Mongo 数据库进行会话存储:
authenticate: (req, res, next) => { passport.authenticate('local', async (err, user, errorMsg) => { if (err) { return next(err); } if (errorMsg) { if (errorMsg.name === 'IncorrectPasswordError') { /* todo: increase login timeout for the user*/ await Account.updateOne({ username: req.body.username, }, { $inc: { 'safeguard.failedLoginAttempts': 1, }, }, { timestamps: false, }); return next({ status: 401, message: CONSTANTS.ERROR_CODES.PASSWORD_INCORRECT }); } if (errorMsg.name === 'IncorrectUsernameError') { return next({ status: 401, message: CONSTANTS.ERROR_CODES.USER_NOT_FOUND }); } } if (!user) { return next({ status: 401, message: CONSTANTS.ERROR_CODES.DEFAULT }); } req.logIn(user, async (err) => { if (err) { log.error('Failed to log in user', err); return next({ status: 500, message: CONSTANTS.ERROR_CODES.DEFAULT }); } if (req.body['remember-me']) { req.session.cookie.maxAge = 30 * 24 * 60 * 60 * 1000; // 30 days } else { req.session.cookie.expires = false; // expires at end of session } await Account.updateOne({ username: req.body.username, }, { $set: { 'safeguard.lastLogin': new Date(), 'safeguard.failedLoginAttempts': 0, }, }, { timestamps: false, }); req.session.save((err) => { if (err) { log.error('Error saving session', err); return next(err); } return next(); }); }); })(req, res, next); },
Passport 库依赖于基于身份验证的策略,您可以使用以下代码初始化该策略:
const passport = require('passport'); const MongoStore = require('connect-mongo')(session); const LocalStrategy = require('passport-local').Strategyconst session = require('express-session'); /* ... */ const mongoSession = session({ secret: process.env.IAM_SESSION_COOKIE_SECRET, name: process.env.IAM_SESSION_COOKIE_NAME, store: new MongoStore({ mongooseConnection: this.mongoose.connection, touchAfter: 4 * 3600, autoRemove: 'native', autoRemoveInterval: 60 * 4, ttl: 3 * 24 * 60 * 60, }), saveUninitialized: false, resave: false, }); this.app.use(mongoSession); this.app.use(passport.initialize()); this.app.use(passport.session()); // authenticationStrategy reads a user from the database and checks a password passport.use(new LocalStrategy(authenticationStrategy())); /* ... */
当身份验证中间件被触发时,Passport 库会调用我们的身份验证策略。
如果一切顺利,身份验证策略将返回用户数据,并且会话将保存在存储中。登录尝试次数将重置为 0。
如果出现错误,我们需要首先检查密码是否错误,并增加尝试登录失败的次数。这是我们可以实施强力安全的地方。我们需要跟踪失败的登录尝试,并提出一种策略来阻止登录尝试一段时间。
第 2 阶段:检索到 ID 令牌后,我们可以使用它来获取用户所属的企业列表:
以下是检索企业列表的代码:
router.get('/organizations', authMiddleware.validateSession, async (req, res, next) => { try { const account = await AccountDAO.findOne({ _id: req.user.userid }); res.status(200).send({ account, organizations: req.user.organizations, // organizations is a part of user representation and defines user membership in organizations }); } catch (err) { logger.error(err); return next({ status: 500, message: CONSTANTS.ERROR_CODES.DEFAULT }); } });
以下是 validateSession 中间件如何检查第一步中检索到的 ID 令牌:
validateAuthentication: async (req, res, next) => { let payload = null; let client = null; let token = null; /** User has a valid cookie */ if (req.user) { req.user = req.user.toJSON(); req.user.userid = req.user._id.toString(); return next(); } // here we need id token retrieved from /session if (!req.headers.authorization) { return next({ status: 401 }); } try { const header = req.headers.authorization.split(' '); if (!header || header.length < 2) { log.debug('Authorization header is incorrect'); return next({ status: 401, message: CONSTANTS.ERROR_CODES.INVALID_HEADER }); } token = header[1]; payload = await TokenUtils.getAccountData(token); } catch (err) { log.warn('Failed to parse token', err); return next({ status: 401, message: CONSTANTS.ERROR_CODES.SESSION_EXPIRED }); } if (payload) { req.user = req.user || {}; return next(); } else { log.error('Token payload is empty or invalid', { payload }); return next({ status: 401, message: CONSTANTS.ERROR_CODES.VALIDATION_ERROR }); } }
第 3 阶段:最后,用户可以选择一个企业并请求 JWT 来访问它:
以下是获取某个企业的 JWT 的代码:
router.post('/token', authMiddleware.validateAuthentication, async (req, res, next) => { const { organization } = req.body; if (!organization) { return next({ status: 400, message: 'Missing organization' }); } try { if (await AccountDAO.userHasOrganization({ userId: req.user.userid, tenantId: organization })) { const jwtpayload = jwtUtils.getJwtPayload(await AccountDAO.findOne({ _id: req.user.userid })); const token = await jwtUtils.basic.sign(jwtpayload); req.headers.authorization = 'Bearer ${token}'; res.status(200).send({ token: token }); } else { res.sendStatus(403); } } catch (err) { logger.error(err); return next({ status: 500, message: CONSTANTS.ERROR_CODES.DEFAULT }); } });
JWT 令牌将提供有关用户身份的完整信息,包括用户的企业、角色和权限。之后,企业的外部 API 服务器可以验证 JWT 令牌和用户权限。
1.2. 授权
现在,我们来讨论如何确保授权功能,流程如下:
授权机制可以是 IAM 服务的一部分,也可以完全在企业的 API 服务一侧进行授权。这导致我们有两种可能的情况:
当使用基于哈希的消息身份验证代码(HMAC) 对算法进行签名并在本地使用 JWT 验证库时,外部 API 服务和 IAM 服务共享相同的密钥。这种方法的缺点是,每次 IAM 服务中签名密钥发生变化时,我们都需要手动更新签名密钥。
外部 API 服务首先获取JSON Web 密钥集(JWKS) 公钥,然后使用 JWKS 验证 JWT。本场景使用RSA算法,IAM服务负责维护和更新签名密钥。
在这两种情况下,企业的 API 服务都应依赖 JWT 库来验证 JWT。在第二种情况下,IAM 服务器包含在服务器启动时或按需生成的 RSA 公钥,并将其返回到外部 API 服务器:
router.get('/.well-known/jwks.json', async (req, res) => { if (CONF.jwt.algorithmType === CONSTANTS.JWT_ALGORITHMS.RSA) { const jwks = await keystore.getRsaKeys(); return res.send(jwks); } else { return res.status(423).send({ message: 'RSA algorithm is not activated' }); } });
以下是 keystore.js 文件中 RSA 密钥生成流程的代码:
const jose = require('node-jose'); const { JWK: { createKeyStore } } = jose; const keystore = createKeyStore(); const keyStorePath = path.join(__dirname, 'keystore/keystore.json'); const generateRSARaw = async (keySize) => { try { await keystore.generate('RSA', keySize, { kid: 'sig-rs-0', use: 'sig', }); } catch (err) { log.error(err); } return keystore.toJSON(true); }; generateRSAFile = async (keySize) => { const keystoreDir = path.dirname(keyStorePath); if (!fs.existsSync(keystoreDir)) { log.info('Creating keystore dir ${keystoreDir}'); fs.mkdirSync(keystoreDir); } if (!fs.existsSync(keyStorePath)) { log.info('Creating keystore file ${keyStorePath}'); const keystore = await generateRSARaw(keySize); return fs.writeFileSync(keyStorePath, JSON.stringify(keystore), 'utf8'); } }; getKeystoreFile = async () => { await generateRSAFile(); return require(keyStorePath); }; getKeystore = async () => { const keystoreFile = await getKeystoreFile(); return jose.JWK.asKeyStore(keystoreFile.keys); }; getRsaKeys = async () => { const keystore = await getKeystore(); return keystore.get({ kty: 'RSA', use: 'sig' }); };
1.3. 基于角色的访问控制 (RBAC)
让我们首先定义限制管理员和租户的最小权限集和基于角色的操作:
GET /api/v1/roles– 获取所有角色
POST /api/v1/roles – 创建一个角色
GET /api/v1/roles/:id– 通过ID获取角色
PATCH /api/v1/roles/:id – 修改角色
DELETE /api/v1/roles/:id– 删除角色
GET /api/v1/permission/:id– 通过ID获得许可
POST /api/v1/permission– 创建权限
PATCH /api/v1/permission/:id– 修改权限
DELETE /api/v1/permission/:id– 删除权限
要使用角色,用户租户应登录并具有租户特定的权限,例如tenant.roles.create和tenant.roles.update。此类权限是系统定义的并手动创建。有些受到限制,无法分配给自定义角色,以避免将系统关键角色分配给每个人。
以下是 access-control.js 文件中配置权限的代码:
const PERMISSIONS = { restricted: { 'all': 'all', 'iam.account.create': 'iam.account.create', 'iam.account.read': 'iam.account.read', 'iam.account.update': 'iam.account.update', 'iam.account.delete': 'iam.account.delete', // to do: other critical permissions than cannot be assigned to everyone }, common: { 'tenant.all': 'tenant.all', 'tenant.account.read': 'tenant.account.read', 'tenant.account.create': 'tenant.account.create', 'tenant.account.update': 'tenant.account.update', 'tenant.account.delete': 'tenant.account.delete', 'tenant.membership.create': 'tenant.membership.create', 'tenant.membership.update': 'tenant.membership.update', 'tenant.membership.delete': 'tenant.membership.delete', 'tenant.profile.read': 'tenant.profile.read', 'tenant.profile.update': 'tenant.profile.update', 'tenant.profile.delete': 'tenant.profile.delete', 'tenant.roles.read': 'tenant.roles.read', 'tenant.roles.create': 'tenant.roles.create', 'tenant.roles.update': 'tenant.roles.update', 'tenant.roles.delete': 'tenant.roles.delete', }, };
现在,让我们演示一些旨在管理 IAM 解决方案中的 RBAC 功能的操作。
以下示例显示了用户租户如何创建角色的流程。在这里,我们需要:
检查租户是否有创建角色的权限
验证输入数据并检查权限是否不在系统关键角色列表中
检查角色名称是否不是系统定义的
router.post('/', auth.hasTenantPermissions([PERMISSIONS['tenant.roles.create']]), async (req, res, next) => { const { name, description = '', permissions = [], } = req.body; if (!name || typeof permissions !== 'object') { return next({ status: 400, message: CONSTANTS.ERROR_CODES.INPUT_INVALID }); } if (!permissionsAreNotRestricted(permissions)) { logger.warn('Attempt to assign a restricted permission to a role by user ${req.user.userid}'); return next({ status: 403, message: CONSTANTS.ERROR_CODES.FORBIDDEN, details: 'Restricted permission used', }); } if (roleNameIsRestricted(name)) { return next({ status: 403, message: CONSTANTS.ERROR_CODES.FORBIDDEN, details: 'Restricted role name', }); } try { const newRole = await RolesDAO.create({ name, permissions, description, tenant: req.user.tenant, }); res.status(200).send(newRole); } catch (err) { logger.error(err); return next({ status: 500, message: CONSTANTS.ERROR_CODES.DEFAULT }); } });
让我们继续看一个用户租户如何创建用户的示例。在这里,我们首先检查租户是否具有帐户创建角色,然后验证用户数据。
在用户数据验证时,我们应该遵循最小权限原则,即:
不应将角色从租户分配给用户,这意味着租户无法将角色委派给普通用户。
不应将受限权限授予用户,这意味着普通用户不得拥有系统关键权限。
const rolesBelongToTenant = async (roles, tenant) => { const tenantRoles = (await RolesDAO.find({ tenant })).map((role) => role._id.toString()); const falseRoles = roles.filter((role) => tenantRoles.indexOf(role) < 0); return falseRoles.length === 0; }; const validateUserData = async (req, res, next) => { const userData = req.body; if (!req.user.isAdmin) { if (userData.roles && userData.roles.length && !(await rolesBelongToTenant(userData.roles, req.user.tenant))) { return next({ status: 403, message: CONSTANTS.ERROR_CODES.FORBIDDEN, details: 'Tenant role used', }); } if (userData.permissions && !permissionsAreNotRestricted(userData.permissions)) { return next({ status: 403, message: CONSTANTS.ERROR_CODES.FORBIDDEN, details: 'Restricted permission used', }); } req.body.tenant = req.user.tenant; } return next(); }; router.post('/', auth.hasTenantPermissions([PERMISSIONS['tenant.account.create']]), validateUserData, async (req, res, next) => { const userData = req.body; try { const user = await AccountDAO.create({ userData }); return res.send({ id: user._id }); } catch (err) { if (err.name === 'ValidationError') { log.debug(err); return next({ status: 400, message: CONSTANTS.ERROR_CODES.INPUT_INVALID, }); } else { return next(err); } } });
让我们总结一下已实现的 RBAC 功能并解释一下要点:
我们的 IAM 服务现在存储角色和权限并将其分配给用户。具有租户权限的用户可以在用户创建/更新过程中创建角色并分配角色。
有一组预定义的硬编码角色/权限,用于 IAM API 本身的授权。您可以动态创建其他角色/权限并将其保存在存储中。
每个角色都有一组权限。当用户令牌经过验证时,有效负载中包含的用户权限可以被其他服务解释为调用某些函数的权限。IAM 服务描述所有权限,每个单独的 API 服务仅理解一组有限的权限。
1.4. 审计日志记录
审核日志是有关服务器上特定活动的事件日志的记录。此功能通过将openintegrationhub/event-bus与 RabbitMQ 代理集成来实现。
为了确保审计日志记录,我们需要执行以下操作:
1. 在 event-manager.js 文件中配置一个保持连接的管理器:
const { EventBusManager } = require('@openintegrationhub/event-bus'); class EventManager { constructor(opts) { this.eventBus = opts && opts.eventBus; } async start() { EventBusManager.init({ eventBus: this.eventBus, serviceName: conf.general.loggingNameSpace }); } async stop() { await EventBusManager.destroy(); } } module.exports = EventManager;
2. 配置初始化事件管理器的 API 入口点。它通常位于index.js文件中:
const { EventBus } = require('@openintegrationhub/event-bus'); const EventManager = require('./event-manager'); (async () => { try { // configuring the event manager const eventBus = new EventBus({ serviceName: conf.general.loggingNameSpace, rabbitmqUri: conf.general.rabbitmqUrl }); await eventBus.connect(); const eventManager = new EventManager({ eventBus }); // other code that runs at startup } catch (err) { log.error(err); process.exit(1); } })();
3. 这是 file.js 文件中的日志记录示例:
const { Event, EventBusManager } = require('@openintegrationhub/event-bus'); func: (req, res, next) => { data = await someAction(); if (!data) { const event = new Event({ headers: { name: 'service.action.dataMissing', }, payload: { user: req.body.username }, }); EventBusManager.getEventBus().publish(event); } }
完成这些步骤后,我们需要构建一个消费者来侦听事件并将其保存在存储中,同时提供从 API 收集日志的能力。
在下一章节中,我们将探讨在构建基于云服务的解决方案时,如何确保IAM系统具有相同的功能。
发表评论