浏览代码

Feat: Add `/login/channels` route and improve auth logic for frontend third-party login integration (#7521)

### What problem does this PR solve?

Add `/login/channels` route and improve auth logic to support frontend
integration with third-party login providers:

- Add `/login/channels` route to provide authentication channel list
with `display_name` and `icon`
- Optimize user info parsing logic by prioritizing `avatar_url` and
falling back to `picture`
- Simplify OIDC token validation by removing unnecessary `kid` checks
- Ensure `client_id` is safely cast to string during `audience`
validation
- Fix typo

---
- Related pull request: #7379 

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
- [x] Documentation Update
tags/v0.19.0
Chaoxi Weng 5 个月前
父节点
当前提交
e349635a3d
没有帐户链接到提交者的电子邮件
共有 6 个文件被更改,包括 42 次插入14 次删除
  1. 2
    2
      api/apps/auth/README.md
  2. 3
    1
      api/apps/auth/oauth.py
  3. 3
    5
      api/apps/auth/oidc.py
  4. 28
    4
      api/apps/user_app.py
  5. 3
    1
      conf/service_conf.yaml
  6. 3
    1
      docker/service_conf.yaml.template

+ 2
- 2
api/apps/auth/README.md 查看文件

@@ -20,7 +20,7 @@ oauth_config = {
"authorization_url": "https://provider.com/oauth/authorize",
"token_url": "https://provider.com/oauth/token",
"userinfo_url": "https://provider.com/oauth/userinfo",
"redirect_uri": "https://your-app.com/oauth/callback/<channel>"
"redirect_uri": "https://your-app.com/v1/user/oauth/callback/<channel>"
}

# OIDC configuration
@@ -29,7 +29,7 @@ oidc_config = {
"issuer": "https://provider.com/v1/oidc",
"client_id": "your_client_id",
"client_secret": "your_client_secret",
"redirect_uri": "https://your-app.com/oauth/callback/<channel>"
"redirect_uri": "https://your-app.com/v1/user/oauth/callback/<channel>"
}

# Get client instance

+ 3
- 1
api/apps/auth/oauth.py 查看文件

@@ -102,5 +102,7 @@ class OAuthClient:
email = user_info.get("email")
username = user_info.get("username", str(email).split("@")[0])
nickname = user_info.get("nickname", username)
avatar_url = user_info.get("picture", "")
avatar_url = user_info.get("avatar_url", None)
if avatar_url is None:
avatar_url = user_info.get("picture", "")
return UserInfo(email=email, username=username, nickname=nickname, avatar_url=avatar_url)

+ 3
- 5
api/apps/auth/oidc.py 查看文件

@@ -39,6 +39,7 @@ class OIDCClient(OAuthClient):
})

super().__init__(config)
self.issuer = config['issuer']
self.jwks_uri = config['jwks_uri']


@@ -60,11 +61,8 @@ class OIDCClient(OAuthClient):
Parse and validate OIDC ID Token (JWT format) with signature verification.
"""
try:
# Decode JWT header to extract key ID (kid) without verifying signature
# Decode JWT header without verifying signature
headers = jwt.get_unverified_header(id_token)
kid = headers.get("kid")
if not kid:
raise ValueError("ID Token missing 'kid' in header")
# OIDC usually uses `RS256` for signing
alg = headers.get("alg", "RS256")
@@ -79,7 +77,7 @@ class OIDCClient(OAuthClient):
id_token,
key=signing_key,
algorithms=[alg],
audience=self.client_id,
audience=str(self.client_id),
issuer=self.issuer,
)
return decoded_token

+ 28
- 4
api/apps/user_app.py 查看文件

@@ -116,7 +116,30 @@ def login():
)


@manager.route("/login/<channel>") # noqa: F821
@manager.route("/login/channels", methods=["GET"]) # noqa: F821
def get_login_channels():
"""
Get all supported authentication channels.
"""
try:
channels = []
for channel, config in settings.OAUTH_CONFIG.items():
channels.append({
"channel": channel,
"display_name": config.get("display_name", channel.title()),
"icon": config.get("icon", "sso"),
})
return get_json_result(data=channels)
except Exception as e:
logging.exception(e)
return get_json_result(
data=[],
message=f"Load channels failure, error: {str(e)}",
code=settings.RetCode.EXCEPTION_ERROR
)


@manager.route("/login/<channel>", methods=["GET"]) # noqa: F821
def oauth_login(channel):
channel_config = settings.OAUTH_CONFIG.get(channel)
if not channel_config:
@@ -171,7 +194,7 @@ def oauth_callback(channel):
users = user_register(
user_id,
{
"access_token": access_token,
"access_token": get_uuid(),
"email": user_info.email,
"avatar": avatar,
"nickname": user_info.nickname,
@@ -189,7 +212,7 @@ def oauth_callback(channel):
# Try to log in
user = users[0]
login_user(user)
return redirect(f"/?auth_success=true&user_id={user.get_id()}")
return redirect(f"/?auth={user.get_id()}")

except Exception as e:
rollback_user_registration(user_id)
@@ -201,8 +224,9 @@ def oauth_callback(channel):
user.access_token = get_uuid()
login_user(user)
user.save()
return redirect(f"/?auth_success=true&user_id={user.get_id()}")
return redirect(f"/?auth={user.get_id()}")
except Exception as e:
logging.exception(e)
return redirect(f"/?error={str(e)}")



+ 3
- 1
conf/service_conf.yaml 查看文件

@@ -75,11 +75,13 @@ redis:
# grant_type: 'authorization_code'
# custom_channel:
# type: oidc
# icon: sso
# display_name: "Custom Channel"
# issuer: https://provider.com/v1/oidc
# client_id: xxxxxxxxxxxxxxxxxxxxxxxxx
# client_secret: xxxxxxxxxxxxxxxxxxxxxxxx
# scope: "openid email profile"
# redirect_uri: https://your-app.com/oauth/callback/custom_channel
# redirect_uri: https://your-app.com/v1/user/oauth/callback/custom_channel
# authentication:
# client:
# switch: false

+ 3
- 1
docker/service_conf.yaml.template 查看文件

@@ -87,11 +87,13 @@ redis:
# grant_type: 'authorization_code'
# custom_channel:
# type: oidc
# icon: sso
# display_name: "Custom Channel"
# issuer: https://provider.com/v1/oidc
# client_id: xxxxxxxxxxxxxxxxxxxxxxxxx
# client_secret: xxxxxxxxxxxxxxxxxxxxxxxx
# scope: "openid email profile"
# redirect_uri: https://your-app.com/oauth/callback/custom_channel
# redirect_uri: https://your-app.com/v1/user/oauth/callback/custom_channel
# authentication:
# client:
# switch: false

正在加载...
取消
保存