使用supabase/auth实现OAuth登录

June 24, 2024

前面已经介绍过了如何使用supabase/auth来实现邮箱用户的认证以及supabase的源码分析,本次重点介绍如何使用supabase/auth来实现OAuth2.0的认证,从应用的web/backend端、supabse以及provider的角度来分析其流程及原理。

相关代码已经在Next-Blogger中实现,细节可以自行参考。

整体流程

角色

涉及到的角色包括:

  • web:本文指应用前端,浏览器,即用户
  • backend:本文这里指Nextjs实现的后端,代表应用服务端后端
  • supabase:suapbase服务端缩写,这里使用官方部署。自己部署时地址、cgi path可能略有差异
  • provider:github、google等

流程图

传统的OAuth2.0中Authorization Code Grant模式如下:

抛开资源访问的Resource Server,只有Resource Owner(类似web)、Application(类似backend)、Authorization Server(Provider)三个角色。流程上仅有获取 Authorization Code(redirect)和 Exchange Token两步。而引入Supabase作为认证代理之后,整体流程是会变得复杂较多,但是对用户体验基本无感知。

 

Supabase的主要思路就是对上面两步的封装

  • Authorization Code:在跳转到Resouce Server(Provider)之前需要先跳转Supabase的authorize地址。以获取Provider的参数信息。同时在Oauth中的callback地址也变为了Supabase提供的callback地址,Supabase Server处理完毕后才跳转应用想要的callback地址,同时携带的code已经是Supabase自己的auth code。
  • Exchange Token:使用Auth code交换Token,跟OAuth的流程一致。不过这里的Code和Token都已经是Supabase内部的code&token了。应用基本无需感知Provider存在。

引入Supabase后的整体流程如下

整体流程大致如下:

  1. 应用发起登录,跳转Supaase的authroize地址。
  2. Supaase的authorize根据provider配置,将用户302跳转到真正的Authorization Server的authorize地址。
  3. 用户完成登录&授权,provider server会将code、state作为参数跳转到Supabase的callback地址。
  4. Supaase的callback会将provider的code转换为相关token信息。确认本次认证的合法性。同时将内部的auth code作为参数回跳到应用指定的callback地址。
  5. 应用callback页面会将supbase返回的auth code转换为Supabase的session、jwt等。完成本次认证。
  6. 应用可以在Suapbase的JWT信息解析用户在provider中的信息,包括名称、头像等。具体跟provider、scope等相关。

流程解析

发起登录:web → backend

Web发起某个provider类型的登录,以获取provider对应的OAuth authorize接口地址以及client_id等信息,这部分信息是配置在Supabase服务端侧的(包括client_id、auth callback等),所以第一跳的地址应该是Supbase服务端地址,而非直接的Provider authorize地址。

应用服务端会调用SDK内的_getUrlForProvider接口,构建好Supabse对应的跳转地址,以302跳转的方式返回给web端开始执行后续的一系列跳转逻辑。

应用服务端的生成的Supabase服务端的地址格式大致如下:

Loading...

URL中的参数说明:

  • providerredirect_to: web端指定。期望的provider和授权完成的callback地址。类似OAuth2.0的exchange token流程,callback页拿Supabase的auth code去换取JWT、session等。
  • code_chalangecode_challenge_method:生成的PKCE的参数。

关于PKCE模式的说明:

@supabase/ssr 中createClient时会将flowType模式定义为PKCE。所以会在backend端生成随机串 code_verifier ,并根据其生成 code_challenge 以及 code_challenge_method 。其中:

  • code_verifier 用于向Supabase获取token时使用。因为是在backend端生成,所以直接通过cookie下发给了web,避免了backend的数据存储以及映射。
  • code_challengecode_challenge_method 用于在向Supabase获取code(authorize接口)时使用。算法code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))

PKCE 协议本身是对 OAuth 2.0 的扩展, 它和之前的授权码流程大体上是一致的, 区别在于, 在向授权服务器的 authorize endpoint 请求时,需要额外的 code_challenge 和 code_challenge_method 参数, 向 token endpoint 请求时, 需要额外的 code_verifier 参数, 最后授权服务器会对这三个参数进行对比验证, 通过后颁发令牌。这样就避免了client_secret的明文传递

在 OAuth 2.0 核心规范中, 要求授权服务器的 anthorize endpoint 和 token endpoint 必须使用 TLS(安全传输层协议)保护, 但是授权服务器携带授权码code返回到客户端的回调地址时(redirect), 有可能不受TLS 的保护, 恶意程序就可以在这个过程中拦截授权码code, 拿到 code 之后, 接下来就是通过 code 向授权服务器换取访问令牌 access_token , 对于机密的客户端来说, 请求 access_token 时需要携带客户端的密钥 client_secret , 而密钥保存在后端服务器上, 所以恶意程序通过拦截拿到授权码code 也没有用, 而对于公开的客户端(手机App, 桌面应用)来说, 本身没有能力保护 client_secret, 因为可以通过反编译等手段, 拿到客户端 client_secret, 也就可以通过授权码 code 换取 access_token, 到这一步,恶意应用就可以拿着 token 请求资源服务器了。

Supbase的Authorize接口

目的:Supabase的Authorize接口( /auth/v1/authorize)的主要作用是根据用户请求参数(provider、scopes、code_challenge、code_challenge_method)跳转到对应的第三方provider的Authorize接口地址,以获取OAuth流程中的code授权码。

构建authorize URL时大部分采用的是第三方lib OAuth2 for Go ,部分provider自定义实现或采用其他第三方lib。

Loading...

其他逻辑包括:

  1. 如果是pkce flowType,会在 flow_state 表中新增一条记录,记录pkce相关参数,用于记录本次请求的状态,等待后续access-token、refresh-token等值有了之后会更新。表中会两个uuid字段:id以及auth_code,id用于关联Provider的授权记录,会被作为加密串state的一部分;auth_code用作授权完成后backend取token的场景,用于定位flow_state 表记录。
  2. 构造第三方provider的 state 参数。用于关联本次 authorize 和后面的 token 请求。state使用类似token的算法,加密的元数据包括:
  • flowStateID: flow_state 中插入记录的id,用于后续更新 access/refresh-token。
  • providerType:provider类型,用于校验
  • 时间戳:用于防止重放等
  • redirectURL:原始的redirect_to ,用于授权完毕后的页面跳转。

页面获取到Supbase返回的provider对应的授权接口地址后,就会按照标准的OAuth流程,完成provider的登录、授权流程。然后携带code、state参数跳转到Supbase提供的callback地址。

Supbase的callback接口

目的:接收Provider用户授权完毕后的code、state参数,以换取access/refresh-token。

作为标准OAuth2中Authorization Code Grant模式的一环,Callback接口地址一般配置在provider提供的配置环境中,各Provider的配置方法参考官网文档。Supbase中的Callback地址可以在Provider对应的配置中取到,如图:

 

流程

  1. 解析state,查找对应的flow_state 表记录。( api.loadFlowState()
  2. 根据userdata中的email,创建用户身份 useridentitycreateAccountFromExternalIdentity
  3. 更新flow_state 中的token、userid等。非pkce flowType时直接颁发token,写入cookie。
  4. flow_state 表中的auth_code作为参数以302的方式给到web,跳转地址为web发起跳转时的redirect url(或者supabase中的默认配置)。

至此,Supabase与Provider之间的OAuth授权流程基本完毕了,Supabase服务端已经存储了access/refresh-token以及用户数据。现在要做的就是通知backend从Supabase来提取相关信息,并构建session写入web的cookie中,完成web端的登录。

完成登录:web → backend

目的:web发起登录时携带 redirect_url 为最终的登录结果地址,Supabase完成授权流程后,会将code透传给到此地址,应用端需要使用此code换取session、JWT等,实现页面的登录效果。

backend逻辑

backend会调用Supabase的 /token 接口(通过ssr SDK中的 supabase.auth.exchangeCodeForSession(code)实现),将code转换为session以及JWT。

Supabase逻辑

API.PKCE() 方法中,完成code的校验以及登录态的下发。主要流程包括:

  1. 参数校验: auth_code 为上一步中返回的code, code_verifier 为之前Backend存储在cookie中的PKCE参数,调用Supabase时会从cookie中取出并删除cookie项。
  2. 根据auth_code 查找对应的flow_state 记录。校验合法性(user_id非空、过期时间FlowStateExpiryDuration)
  3. 取出flow_state 中数据code_challengecode_challenge_method,校验PKCE参数:code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
  4. 记录审计日志。
  5. 颁发access/refresh-token:包括生成session、token,参考 supabase/auth源码分析
  6. 相对邮箱认证,也会将provider返回的access-token、refresh-token(如果有)一并返回。
Loading...

最后,web端SDK会将上面的数据写入cookie。表示最终的OAuth登录流程已完成。

小结

  1. 调试过程中遇到了cookie设置未生效的问题,网上也有部分人遇到过,cookie的数据过长(超过4k)会导致写入登录态无效。但是非必现,看解决方案新版sdk做了优化,对cookie中的数据做了一些裁剪。
  2. Supabase作为oauth的代理,需要兼容各种Provider的流程,所以在supbase/auth的代码中对不同的Provider做了部分特殊流程处理。如果发现部分Provider的问题,可能跟这种自定义实现有关,可以通过排查代码、文档定位。
  3. 一直觉得Supabase的cookie太长了,在cookie最大限制的边缘。如果应用中发现登录态丢失的问题,可能是业务自定义cookie与登录态超长导致。在部署过程中遇到了Nginx header超长的问题 “upstream sent too big header while reading response header from upstream”。调整最大长度后才通过。
See all postsSee all posts