背景 Authentik 是一个开源的身份验证和授权服务,支持多种身份验证方式。钉钉提供了 OAuth2 授权机制,但其接口是非标准的,需要通过自定义转换服务来适配。
本文介绍如何使用 Authentik 的 OAuth2 提供程序集成钉钉登录,让用户可以使用钉钉账户登录应用。
参考文档:Authentik OAuth Sources
实现步骤 1. 创建钉钉应用 参考以下文档创建钉钉应用:
1.1 钉钉 OAuth 接口说明 官方文档:获取登录用户的访问凭证
OAuth2 流程(钉钉版本):
接口示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 钉钉 OAuth2 协议流程 1. 授权请求 GET https://login.dingtalk.com/oauth2/auth? redirect_uri=https%3A%2F%2Fwww.example.com%2F&response_type=code&client_id=dingyourclientid&scope=openid&prompt=consent 2. 回调地址格式 https://www.example.com/?authCode=6b427e8bfab83e93bedd13f16a430702 3. 获取 Token POST https://api.dingtalk.com/v1.0/oauth2/userAccessToken Content-Type: application/json { "clientId": "ding your id", "clientSecret": "your secret", "code": "6b427e8bfab83e93bedd13f16a430702", "grantType": "authorization_code" } 响应: { "expireIn": 7200, "accessToken": "a8f4e3215a703ce9a7164e91dbab53c0", "refreshToken": "b13e5a61b421342d95d86c9e64c275c6" } 4. 获取用户信息 GET https://api.dingtalk.com/v1.0/contact/users/me x-acs-dingtalk-access-token: a8f4e3215a703ce9a7164e91dbab53c0 Content-Type: application/json 响应: { "nick": "AWIS ME", "unionId": "D578iS5hxxxx", "avatarUrl": "https://static-legacy.dingtalk.com/media/lADPGT5i9m5ZyXDNA4LNAtA_720.jpg", "openId": "WySPOpXqxE", "mobile": "1350xxxxxxxx", "stateCode": "86", "email": "[email protected] " }
参考实现:
2. 配置 Authentik 参考:Twitch 集成文档
关键配置项:
配置项
值
身份验证类型
OpenID Connect
Scopes
openid
Authorization URL
https://login.dingtalk.com/oauth2/auth?prompt=consent
Token URL
自定义转换服务 URL(见下文)
User Info URL
自定义转换服务 URL(见下文)
注意 :钉钉的 OAuth2 接口是非标准的(命名方法和参数格式有差异),需要自己实现转换服务。参考:知乎文章
3. 实现 OAuth2 转换服务 使用 AWS Serverless(Lambda)实现,将钉钉接口转换为标准 OAuth2 格式。
3.1 Token 接口
📄 /auth/dingtalk/token
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import requestsimport jsonfrom base64 import b64decodefrom urllib.parse import parse_qsTOKEN_URL = 'https://api.dingtalk.com/v1.0/oauth2/userAccessToken' def parse_form_data_to_json (form_data ): parsed_data = parse_qs(form_data) result = {k: v[0 ] for k, v in parsed_data.items()} return result def main (event, context ): print (f"event:\n{event} " ) s = event.get("body" ) if event.get("isBase64Encoded" ) and s: s = b64decode(s).decode("utf-8" ) body = parse_form_data_to_json(s) headers = {"Content-Type" : "application/json" } response = requests.post(TOKEN_URL, json={ 'clientId' : body.get('client_id' ), 'clientSecret' : body.get('client_secret' ), 'code' : body.get('code' ), 'grantType' : body.get('grant_type' ), }, headers=headers) response.raise_for_status() res = response.json() result = { 'access_token' : res.get('accessToken' ), 'expires_in' : res.get('expiresIn' ), 'token_type' : 'Bearer' , } return {"statusCode" : 200 , "body" : json.dumps(result)}
3.2 用户信息接口
📄 /auth/dingtalk/profile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import requestsimport jsonURL = 'https://api.dingtalk.com/v1.0/contact/users/me' def main (event, context ): print (f"event:\n{event} " ) access_token = event.get('headers' , {}).get('authorization' , '' ) access_token = access_token.replace('Bearer ' , '' ) print (access_token) headers = { "Content-Type" : "application/json" , 'x-acs-dingtalk-access-token' : access_token, } response = requests.get(URL, headers=headers) response.raise_for_status() user_info = response.json() print (user_info) result = { 'sub' : user_info['openId' ], 'nickname' : user_info['nick' ], 'name' : user_info['nick' ], 'email' : user_info['email' ] } return {"statusCode" : 200 , "body" : json.dumps(result)}
常见问题 错误:Could not determine id. 原因 :返回的用户信息中缺少 sub 字段。
解决 :确保转换服务返回包含 sub 字段的 JSON,sub 通常对应钉钉的 openId。
相关源码:
错误日志示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 { "auth_via" : "unauthenticated" , "domain_url" : "example.com" , "event" : "Authentication Failure" , "host" : "example.com" , "level" : "warning" , "logger" : "authentik.sources.oauth.views.callback" , "pid" : 4721 , "reason" : "Could not determine id." , "request_id" : "28a8d8818c63441da41051455c32d437" , "schema_name" : "public" , "timestamp" : "2024-04-09T10:30:48.464283" }
总结 通过自定义转换服务,成功将钉钉的非标准 OAuth2 接口适配为 Authentik 可识别的标准格式,实现了钉钉登录集成。核心要点:
钉钉接口参数命名与标准 OAuth2 不同(如 clientId vs client_id)
必须在用户信息中返回 sub 字段
使用 Serverless 服务作为中间层进行协议转换