Authentik 集成钉钉 OAuth2 登录
背景
Authentik 是一个开源的身份验证和授权服务,支持多种身份验证方式。钉钉提供了 OAuth2 授权机制,但其接口是非标准的,需要通过自定义转换服务来适配。
本文介绍如何使用 Authentik 的 OAuth2 提供程序集成钉钉登录,让用户可以使用钉钉账户登录应用。
实现步骤
1. 创建钉钉应用
参考以下文档创建钉钉应用:
1.1 钉钉 OAuth 接口说明
官方文档:获取登录用户的访问凭证
OAuth2 流程(钉钉版本):
接口示例
钉钉 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
import requests
import json
from base64 import b64decode
from urllib.parse import parse_qs
TOKEN_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 = {
# 'refresh_token': res.get('refreshToken'),
'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
import requests
import json
URL = '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 = {
# 'issuer': userInfoURL,
# 'picture': user_info.get('avatarUrl'),
'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。
相关源码:
错误日志示例:
{
"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 不同(如
clientIdvsclient_id) - 必须在用户信息中返回
sub字段 - 使用 Serverless 服务作为中间层进行协议转换