Browse Source

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 months ago
parent
commit
e349635a3d
No account linked to committer's email address

+ 2
- 2
api/apps/auth/README.md View File

"authorization_url": "https://provider.com/oauth/authorize", "authorization_url": "https://provider.com/oauth/authorize",
"token_url": "https://provider.com/oauth/token", "token_url": "https://provider.com/oauth/token",
"userinfo_url": "https://provider.com/oauth/userinfo", "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 # OIDC configuration
"issuer": "https://provider.com/v1/oidc", "issuer": "https://provider.com/v1/oidc",
"client_id": "your_client_id", "client_id": "your_client_id",
"client_secret": "your_client_secret", "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 # Get client instance

+ 3
- 1
api/apps/auth/oauth.py View File

email = user_info.get("email") email = user_info.get("email")
username = user_info.get("username", str(email).split("@")[0]) username = user_info.get("username", str(email).split("@")[0])
nickname = user_info.get("nickname", username) 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) return UserInfo(email=email, username=username, nickname=nickname, avatar_url=avatar_url)

+ 3
- 5
api/apps/auth/oidc.py View File

}) })


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




Parse and validate OIDC ID Token (JWT format) with signature verification. Parse and validate OIDC ID Token (JWT format) with signature verification.
""" """
try: 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) 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 # OIDC usually uses `RS256` for signing
alg = headers.get("alg", "RS256") alg = headers.get("alg", "RS256")
id_token, id_token,
key=signing_key, key=signing_key,
algorithms=[alg], algorithms=[alg],
audience=self.client_id,
audience=str(self.client_id),
issuer=self.issuer, issuer=self.issuer,
) )
return decoded_token return decoded_token

+ 28
- 4
api/apps/user_app.py View File

) )




@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): def oauth_login(channel):
channel_config = settings.OAUTH_CONFIG.get(channel) channel_config = settings.OAUTH_CONFIG.get(channel)
if not channel_config: if not channel_config:
users = user_register( users = user_register(
user_id, user_id,
{ {
"access_token": access_token,
"access_token": get_uuid(),
"email": user_info.email, "email": user_info.email,
"avatar": avatar, "avatar": avatar,
"nickname": user_info.nickname, "nickname": user_info.nickname,
# Try to log in # Try to log in
user = users[0] user = users[0]
login_user(user) 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: except Exception as e:
rollback_user_registration(user_id) rollback_user_registration(user_id)
user.access_token = get_uuid() user.access_token = get_uuid()
login_user(user) login_user(user)
user.save() user.save()
return redirect(f"/?auth_success=true&user_id={user.get_id()}")
return redirect(f"/?auth={user.get_id()}")
except Exception as e: except Exception as e:
logging.exception(e)
return redirect(f"/?error={str(e)}") return redirect(f"/?error={str(e)}")





+ 3
- 1
conf/service_conf.yaml View File

# grant_type: 'authorization_code' # grant_type: 'authorization_code'
# custom_channel: # custom_channel:
# type: oidc # type: oidc
# icon: sso
# display_name: "Custom Channel"
# issuer: https://provider.com/v1/oidc # issuer: https://provider.com/v1/oidc
# client_id: xxxxxxxxxxxxxxxxxxxxxxxxx # client_id: xxxxxxxxxxxxxxxxxxxxxxxxx
# client_secret: xxxxxxxxxxxxxxxxxxxxxxxx # client_secret: xxxxxxxxxxxxxxxxxxxxxxxx
# scope: "openid email profile" # 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: # authentication:
# client: # client:
# switch: false # switch: false

+ 3
- 1
docker/service_conf.yaml.template View File

# grant_type: 'authorization_code' # grant_type: 'authorization_code'
# custom_channel: # custom_channel:
# type: oidc # type: oidc
# icon: sso
# display_name: "Custom Channel"
# issuer: https://provider.com/v1/oidc # issuer: https://provider.com/v1/oidc
# client_id: xxxxxxxxxxxxxxxxxxxxxxxxx # client_id: xxxxxxxxxxxxxxxxxxxxxxxxx
# client_secret: xxxxxxxxxxxxxxxxxxxxxxxx # client_secret: xxxxxxxxxxxxxxxxxxxxxxxx
# scope: "openid email profile" # 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: # authentication:
# client: # client:
# switch: false # switch: false

Loading…
Cancel
Save