ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Reflex 企业版 OIDC Providers 完全指南:多身份源、PKCE 授权码流程与作用域管理

Reflex 企业版 OIDC Providers 完全指南:多身份源、PKCE 授权码流程与作用域管理 Reflex 企业版 OIDC Providers 完全指南多身份源、PKCE 授权码流程与作用域管理【免费下载链接】reflex️ Web apps in pure Python 项目地址: https://gitcode.com/GitHub_Trending/re/reflexrxe.AuthPlugin将 OpenID ConnectOIDC认证能力接入 Reflex 应用而OIDC Provider正是驱动整套认证的状态类——它负责对你的身份提供商IdP执行 OpenID Connect Authorization Code PKCE 流程。本文基于本仓库 providers.md 完整讲解 Provider 的默认行为、命名与多 Provider 配置、环境变量解析规则、作用域Scope与刷新令牌管理、iframe 弹窗登录以及十余个可覆写的高级扩展钩子。读完本文你将能够为 Reflex 应用接入任意符合 OIDC 标准的 IdP如 Google、Azure AD、Auth0、Okta并实现多身份源共存、按 Provider 定制请求作用域、用访问令牌调用下游 API 等实战方案。适用前提本特性随reflex-enterprisev0.9.1 提供应用必须使用rxe.App()而非rx.App()并将rxe.AuthPlugin加入rxe.Config(plugins[...])同时通过环境变量配置 OIDC 身份提供商。完整接入步骤见 Authentication Overview。Provider 的角色跑完 OIDC Authorization Code PKCE 流程的状态类一个 OIDC Provider 本质上是运行 OpenID Connect Authorization Code PKCE 流程的 state 类。它与你的 IdPIdentity Provider身份提供商交互完成将未登录用户重定向到 IdP 的授权端点接收 IdP 回调并校验 OAuthstate参数防 CSRF用授权码换取令牌access token、id token、可选的 refresh token将令牌持久化为安全 Cookie在令牌临近过期时用刷新令牌后台续期在多个浏览器标签页间协调。rxe.AuthPlugin内置了一个默认 Provider其配置完全从环境变量解析。关于插件如何保护页面、事件、字段与计算变量secure-by-default 模型以及整套插件的搭建步骤分别见 secure by default 与 overview。默认 ProviderGenericOIDCAuthState与OIDC_*环境变量GenericOIDCAuthState是内置的默认 Provider。它只读取三个环境变量OIDC_ISSUER_URIhttps://your-issuer.example.com OIDC_CLIENT_IDyour-client-id OIDC_CLIENT_SECRETyour-client-secret # 可选PKCE 流程不需要它也能工作AuthPlugin.auth_providers默认值为[GenericOIDCAuthState]。只要设置了OIDC_*变量就无需编写任何 Provider 类import reflex as rx import reflex_enterprise as rxe config rxe.Config( app_namemy_app, plugins[rxe.AuthPlugin()], # 使用 GenericOIDCAuthState OIDC_* 环境变量 )此时应用的认证流程为OIDC 发行者元数据.well-known/openid-configuration从OIDC_ISSUER_URI自动发现每个 Provider 始终运行带 PKCES256的授权码流程默认请求openid email profile三个 scope。由于client_secret可选IdP 客户端既可以是公有客户端仅 PKCE、无密钥也可以是保密客户端PKCE 密钥——流程机制完全一致只有 issuer URL 不同详见 deployment。请将插件的auth_callback_endpoint默认/callback注册为 IdP 客户端中允许的重定向 URI。该回调地址不是配置值而是在运行时根据浏览器可见的页面 URL 动态构建scheme host /callback因此注册时务必写全 scheme、host、port 与 path例如https://your-app.com/callback否则 IdP 会报redirect_uri_mismatch。提示即使 IdP 暂时不可达只要设置了OIDC_*环境变量应用也能正常导入与编译——OIDC 发现流程只在用户实际登录时才执行占位符值足够用于本地构建与 CI。命名 Provider__provider__与独立的环境变量命名空间当需要为不同 IdP 使用各自独立的环境变量、或在一个应用里注册多个不同 IdP 时可以子类化OIDCAuthState并设置__provider__。子类还必须同时继承rx.Stateimport reflex as rx from reflex_enterprise.auth import OIDCAuthState class OktaAuthState(OIDCAuthState, rx.State): __provider__ okta设置__provider__ okta后配置解析会优先使用OKTA_ISSUER_URI/OKTA_CLIENT_ID/OKTA_CLIENT_SECRET变量找不到时再回退到共享的OIDC_*键OKTA_ISSUER_URIhttps://your-org.okta.com OKTA_CLIENT_IDyour-okta-client-id OKTA_CLIENT_SECRETyour-okta-client-secretProvider 注册后默认的/login页面会为它显示一个登录按钮。按钮文案由display_name()控制默认返回__provider__的标题化Title Case形式okta→Oktaclass OktaAuthState(OIDCAuthState, rx.State): __provider__ okta classmethod def display_name(cls) - str: return Okta SSO环境变量解析规则每个 Provider 在解析任一配置键时都遵循先 Provider 专属、再共享回退的规则先尝试{PROVIDER}_{KEY}再回退到共享的OIDC_{KEY}。其中{PROVIDER}是__provider__的大写形式。配置键Provider 专属变量共享回退变量说明Issuer发行者{PROVIDER}_ISSUER_URIOIDC_ISSUER_URIIdP 的发行者 URL其.well-known/openid-configuration由此发现Client ID客户端 ID{PROVIDER}_CLIENT_IDOIDC_CLIENT_IDOAuth 客户端 IDClient Secret客户端密钥{PROVIDER}_CLIENT_SECRETOIDC_CLIENT_SECRET可选PKCE 流程不需要它默认的GenericOIDCAuthState__provider__ generic依次解析GENERIC_*、再回退OIDC_*。对默认 Provider 而言只需设置OIDC_*键即可。把 Provider 注册进插件类与 import-path 字符串AuthPlugin(auth_providers[...])接受 Provider类或module.ClassName形式的import-path 字符串。字符串会在编译期被惰性解析列表顺序被保留且两种形式可以混用。默认值为[GenericOIDCAuthState]。警告rxconfig.py中必须使用 import-path 字符串Provider 模块会导入reflex_enterprise而该包在导入时会加载rxconfig。如果在rxconfig.py里直接导入 Provider 类就会再次进入配置加载re-enter the config。因此在rxconfig.py中请把 Provider 传成module.ClassName字符串插件会在配置已存在后惰性解析它们。在rxconfig.py中传字符串import reflex_enterprise as rxe config rxe.Config( app_namemy_app, plugins[ rxe.AuthPlugin( auth_providers[my_app.auth.OktaAuthState], ), ], )在rxconfig.py之外例如测试代码中可以直接传类本身from my_app.auth import OktaAuthState rxe.AuthPlugin(auth_providers[OktaAuthState])多 Provider一个应用接入多个身份源/login页面会为每个Provider 显示一个登录按钮——即便只有一个 Provider 也不会自动跳转到 IdP。回调阶段通过 OAuthstate参数解析出发起登录的那个 Provider登出阶段则解析当前持有令牌的活动Providerrxe.AuthPlugin( auth_providers[ my_app.auth.OktaAuthState, my_app.auth.AzureAuthState, ], )警告运行多个 Provider 时必须为各自配置独立变量如果两个或更多 Provider 对某个必需键issuer 或 client id都回退到了共享的OIDC_*配置它们会解析到同一个 IdP 值。此时插件会在启动时抛出ConfigError并在错误中指明受影响的 Provider。请为每个 Provider 设置专属的{PROVIDER}_*变量例如OKTA_ISSUER_URI与AZURE_ISSUER_URI。读取用户与 Provider 无关User.name/.email/.sub/.picture绑定到AuthUserState由完成登录的那个 Provider填充。要在后端代码中按 Provider 分支使用await User.current_provider()import reflex as rx import reflex_enterprise as rxe from reflex_enterprise.auth import User class DemoState(rx.State): rxe.event async def show_provider(self): provider await User.current_provider() return rx.toast(provider.__provider__ if provider else anonymous)在 iframe 内运行弹窗登录/登出流程嵌入式应用使用弹窗popup登录/登出流程而不是顶层重定向。弹窗由用户点击时打开并通过postMessage把令牌回传给应用自身 origin 的页面。自定义/login页面时必须对每个 Provider 调用provider.get_login_button(*children)。该方法会挂载弹窗流程所需的消息监听器直接调用redirect_to_login不会挂载该监听器。大多数应用把用户链到/login即可只有默认布局不够时才去自定义登录页见 custom pages 中A custom login page一节。如需强制或禁用弹窗流程可在 Provider 子类上覆写_use_popup_flow(self) - bool。默认返回值取决于应用是否处于嵌入式iframe环境中。Scopes 与刷新令牌extra_scopes的作用与限制extra_scopes会被转发给每一个已配置的 Provider并合并进各自请求的 scope 集合中。合并是去重的并保留既有 scope默认为openid email profilerxe.AuthPlugin( auth_providers[my_app.auth.OktaAuthState], extra_scopes[offline_access], )关键行为extra_scopes[offline_access]请求 IdP 签发刷新令牌。一旦获得框架会在访问令牌临近过期时自动刷新并在多个浏览器标签页之间协调。刷新请求只包含 IdP 当初**实际授予granted**的 scope被请求但未被 IdP 授予的 scope如offline_access不会在刷新时触发invalid_scope错误。没有offline_access就没有刷新令牌。访问令牌过期即会话结束用户被送回/login。如果刷新失败刷新令牌被吊销或已过期会话被重置用户被登出。extra_scopes[groups]请求 group 声明claims可用于对ctx.auth_user_state.userinfo.get(groups)的授权检查具体用法见 secure by default。独立于令牌过期时间每个认证 Cookie 都有固定的 7 天生命周期这是会话的实际最大长度。生产环境的 HTTPS 与 Cookie 要求见 deployment。按 Provider 定制 scope_requested_scopesextra_scopes会均匀地作用于每个 Provider且只做追加。若想给某个 Provider 一套完全不同的 scope 集合在该子类上设置_requested_scopes类属性即可。与extra_scopes不同它替换默认的openid email profile而不是合并import reflex as rx from reflex_enterprise.auth import OIDCAuthState class DatabricksAuthState(OIDCAuthState, rx.State): __provider__ databricks _requested_scopes: str all-apis offline_access openid email profile读取实际授予的 scope每个 Provider 都暴露一个granted_scopesVar保存 IdP实际授予的、以空格分隔的 scope 列表。await User.current_provider()会解析出认证了当前用户的那个 Provider因此可以在自己的状态上派生一个计算变量去读取活动 Provider 的 scopes再用rx.cond做门控——无论用户用哪个 Provider 登录都成立import reflex as rx from reflex_enterprise.auth import User class DemoState(rx.State): rx.var(initial_valueFalse, auto_depsFalse, deps[]) async def has_offline_access(self) - bool: 当前活动的 Provider 是否被授予了 offline_access。 provider await User.current_provider() if provider is None: return False scopes (await self.get_state(provider)).granted_scopes.split() return offline_access in scopesrx.cond( DemoState.has_offline_access, rx.text(Long-lived session enabled), rx.text(Session ends at token expiry), )授予的 scopes 在用户登录那一刻就固定下来因此该变量没有任何响应式依赖auto_depsFalse, deps[]它在登录时解析一次并在整个会话期间保持稳定。Provider 返回的 claimsOIDCUserInfo与常见声明投影OIDCUserInfo是一个TypedDicttotalFalse只声明了sub——这是 OIDC 规范强制要求的声明。name、email、picture等个人资料声明只在对应 scope 被授予时才会出现。在运行时OIDCUserInfo就是一个普通 dict读取声明请用.get(...)。为了把 Provider 返回的额外声明文档化可在 Provider 上声明一个嵌套的UserInfo(OIDCUserInfo, totalFalse)from reflex_enterprise.auth import OIDCAuthState, OIDCUserInfo class OktaAuthState(OIDCAuthState, rx.State): __provider__ okta class UserInfo(OIDCUserInfo, totalFalse): name: str email: str picture: str groups: list[str]常见声明会被投影为User上的只读 VarUser.name、.email、.sub、.picture其他任何声明则通过await User.current()或ctx.auth_user_state.userinfo.get(...)从 dict 中读取。完整 API 见 secure by default 中Reading the current user一节。高级扩展点可覆写的异步钩子OIDCAuthState暴露了一系列可覆写的异步钩子用于 Provider 专属行为。只需覆写子类所需的那几个钩子用途_validate_tokens(self) - bool校验当前的 access token 与 ID token返回它们是否有效_verify_jwt(self, token_json) - Token校验 ID token 的 JWT覆写以自定义校验逻辑_valid_issuers(self) - list[str] \| None可接受的iss声明值覆写以支持如 Azure 多租户场景_set_tokens(self, access_token, id_tokenNone, refresh_tokenNone, granted_scopesNone, **kwargs)令牌交换后持久化令牌覆写以处理额外响应数据_set_tokens_payload_from_exchange(self, exchange) - dict构建传给_set_tokens的 kwargs覆写以转发令牌交换响应中的额外字段_validate_auth_callback_exchange(self, exchange) - dict \| None校验回调中的令牌交换响应_fetch_userinfo(self) - OIDCUserInfo从 IdP 的 userinfo 端点获取声明覆写以自定义获取或重塑声明_redirect_to_login_payload(self) - dict构建授权请求的查询参数scope、state、PKCE challenge覆写以支持非标准登录参数_redirect_to_logout_payload(self) - dict[str, str]构建 IdP 的 end-session 参数state、id_token_hint、post_logout_redirect_uri覆写以自定义post_logout_redirect_uri或非标准 end-session_on_access_token_change(self, new_access_token, refreshFalse)access token 被设置或被刷新时触发_on_refresh_access_token(self, new_access_token)专门在 access token 被刷新时触发登出行为说明登出会把 Provider 自动发现的end_session_endpoint与id_token_hint、post_logout_redirect_uri串联起来。如果 IdP 没有公布end_session_endpoint登出会清除本地令牌并重定向到应用首页。此时 IdP 侧的会话仍然活跃之后的登录可能会复用它。用访问令牌调用下游 API在你的OIDCAuthState子类的rx.event或计算变量内部await self._access_token即可取得当前的 OAuth 访问令牌然后把它作为 Bearer 令牌发给下游服务或 IdP。前导下划线在这里不表示直接字段访问_access_token是仅服务端可用的可等待 Var永远不会暴露给浏览器并由后台刷新保持新鲜import httpx import reflex as rx from reflex_enterprise.auth import OIDCAuthState class OktaAuthState(OIDCAuthState, rx.State): __provider__ okta rx.event async def fetch_profile(self): access_token await self._access_token async with httpx.AsyncClient() as client: resp await client.get( https://api.example.com/me, headers{Authorization: fBearer {access_token}}, ) resp.raise_for_status()若要在令牌签发或刷新时做出反应请在子类上覆写_on_access_token_change或_on_refresh_access_token。跨 Provider 共享行为mixinTrue状态混合若想在不重复代码的前提下把相同的字段、变量、事件处理器或钩子覆写应用到多个 Provider 上可以定义一个mixinTrue的rx.State并在每个 Provider 的基类列表中把它排在OIDCAuthState之前import reflex as rx from reflex_enterprise.auth import OIDCAuthState class SharedAuthBehavior(rx.State, mixinTrue): async def _on_access_token_change( self, new_access_token, refreshFalse ): ... # 对每个混入该 mixin 的 Provider 都会执行 class OktaAuthState(SharedAuthBehavior, OIDCAuthState, rx.State): __provider__ okta class AzureAuthState(SharedAuthBehavior, OIDCAuthState, rx.State): __provider__ azure基类顺序很关键mixin 必须排在OIDCAuthState/rx.State之前。每个 Provider 读取自己__provider__命名空间下的 Cookie共享代码操作的是该 Provider 自己的令牌。持久化额外的令牌交换数据_set_tokens_payload_from_exchange从 IdP 的令牌交换响应构建 payloadaccess_token以及交换响应中包含的id_token、refresh_token、granted_scopes中的任意项该 payload 就是_set_tokens在回调路径和刷新路径上接收到的全部 kwargs。要持久化交换响应中的自定义字段需要同时覆写两个钩子_set_tokens_payload_from_exchange负责把字段带进来_set_tokens负责接收并存储它import reflex as rx from reflex_enterprise.auth import OIDCAuthState class OrgAuthState(OIDCAuthState, rx.State): __provider__ myorg _org_id: str async def _set_tokens_payload_from_exchange(self, exchange): payload await super()._set_tokens_payload_from_exchange(exchange) if org_id in exchange: payload[org_id] exchange[org_id] return payload async def _set_tokens( self, access_token, id_tokenNone, refresh_tokenNone, granted_scopesNone, org_id, **kwargs, ): await super()._set_tokens( access_token, id_token, refresh_token, granted_scopes, **kwargs ) self._org_id org_id需要特别注意 iframe 弹窗流程下的行为弹窗与打开者opener是两个独立的客户端。弹窗负责运行回调并在自己的状态中捕获字段然后把令牌通过postMessage回传给打开者打开者通过on_iframe_auth_success应用令牌而不会经过_set_tokens_payload_from_exchange。只有被回传的令牌能跨过这条边界因此存储在弹窗self上的自定义字段不会自动到达打开者。请给额外的_set_tokens关键字参数提供默认值如上例的org_id这样打开者一侧的调用也能正常工作。从register_auth_endpoints迁移OIDCAuthState.register_auth_endpoints(app)已被弃用自 reflex-enterprise v0.9.1 起1.0 中移除。请改为在rxe.Config(plugins[...])中注册rxe.AuthPlugin。插件会注册/login、/logout、/callback与/forbidden四条路由并施加 secure-by-default 的各项保护。小结与延伸阅读OIDC Provider 是 Reflex 企业版认证体系的执行核心默认GenericOIDCAuthState加上OIDC_*环境变量即可开箱即用子类化OIDCAuthState并设置__provider__即可获得独立的环境变量命名空间与多身份源能力extra_scopes/_requested_scopes/granted_scopes构成完整的 scope 管理体系十余个异步钩子则覆盖了 JWT 校验、issuer 校验、令牌持久化、userinfo 获取、登录/登出参数构建等一切可定制点。继续深入本仓库可阅读secure by default被保护的表面积、auth包装器与授权检查custom pages自定义登录、回调、登出与 forbidden 页面构建器deploymentHTTPS、回调 URL 注册、反向代理与故障排查overview插件搭建与完整登录流程testing受保护表面积的单元测试与 mock-IdP 集成测试【免费下载链接】reflex️ Web apps in pure Python 项目地址: https://gitcode.com/GitHub_Trending/re/reflex创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表