From e349635a3d74298e5c8e639672ef4c50f61d9773 Mon Sep 17 00:00:00 2001 From: Chaoxi Weng Date: Thu, 8 May 2025 10:23:19 +0800 Subject: [PATCH] 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 --- api/apps/auth/README.md | 4 ++-- api/apps/auth/oauth.py | 4 +++- api/apps/auth/oidc.py | 8 +++----- api/apps/user_app.py | 32 +++++++++++++++++++++++++++---- conf/service_conf.yaml | 4 +++- docker/service_conf.yaml.template | 4 +++- 6 files changed, 42 insertions(+), 14 deletions(-) diff --git a/api/apps/auth/README.md b/api/apps/auth/README.md index e2c42d0b4..5dca3218c 100644 --- a/api/apps/auth/README.md +++ b/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/" + "redirect_uri": "https://your-app.com/v1/user/oauth/callback/" } # 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/" + "redirect_uri": "https://your-app.com/v1/user/oauth/callback/" } # Get client instance diff --git a/api/apps/auth/oauth.py b/api/apps/auth/oauth.py index 5f2188fb2..a908e81ea 100644 --- a/api/apps/auth/oauth.py +++ b/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) diff --git a/api/apps/auth/oidc.py b/api/apps/auth/oidc.py index 318f00ad1..2fcdb6f5d 100644 --- a/api/apps/auth/oidc.py +++ b/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 diff --git a/api/apps/user_app.py b/api/apps/user_app.py index 3749c8a12..be6eb5c10 100644 --- a/api/apps/user_app.py +++ b/api/apps/user_app.py @@ -116,7 +116,30 @@ def login(): ) -@manager.route("/login/") # 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/", 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)}") diff --git a/conf/service_conf.yaml b/conf/service_conf.yaml index 6e2a8d0d8..dfb936a9b 100644 --- a/conf/service_conf.yaml +++ b/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 diff --git a/docker/service_conf.yaml.template b/docker/service_conf.yaml.template index c35373988..3354925be 100644 --- a/docker/service_conf.yaml.template +++ b/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