Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. import uuid
  2. from datetime import UTC, datetime, timedelta
  3. from flask import request
  4. from flask_restx import Resource
  5. from sqlalchemy import func, select
  6. from werkzeug.exceptions import NotFound, Unauthorized
  7. from configs import dify_config
  8. from controllers.web import web_ns
  9. from controllers.web.error import WebAppAuthRequiredError
  10. from extensions.ext_database import db
  11. from libs.passport import PassportService
  12. from models.model import App, EndUser, Site
  13. from services.enterprise.enterprise_service import EnterpriseService
  14. from services.feature_service import FeatureService
  15. from services.webapp_auth_service import WebAppAuthService, WebAppAuthType
  16. @web_ns.route("/passport")
  17. class PassportResource(Resource):
  18. """Base resource for passport."""
  19. @web_ns.doc("get_passport")
  20. @web_ns.doc(description="Get authentication passport for web application access")
  21. @web_ns.doc(
  22. responses={
  23. 200: "Passport retrieved successfully",
  24. 401: "Unauthorized - missing app code or invalid authentication",
  25. 404: "Application or user not found",
  26. }
  27. )
  28. def get(self):
  29. system_features = FeatureService.get_system_features()
  30. app_code = request.headers.get("X-App-Code")
  31. user_id = request.args.get("user_id")
  32. web_app_access_token = request.args.get("web_app_access_token")
  33. if app_code is None:
  34. raise Unauthorized("X-App-Code header is missing.")
  35. # exchange token for enterprise logined web user
  36. enterprise_user_decoded = decode_enterprise_webapp_user_id(web_app_access_token)
  37. if enterprise_user_decoded:
  38. # a web user has already logged in, exchange a token for this app without redirecting to the login page
  39. return exchange_token_for_existing_web_user(
  40. app_code=app_code, enterprise_user_decoded=enterprise_user_decoded
  41. )
  42. if system_features.webapp_auth.enabled:
  43. app_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_code(app_code=app_code)
  44. if not app_settings or not app_settings.access_mode == "public":
  45. raise WebAppAuthRequiredError()
  46. # get site from db and check if it is normal
  47. site = db.session.scalar(select(Site).where(Site.code == app_code, Site.status == "normal"))
  48. if not site:
  49. raise NotFound()
  50. # get app from db and check if it is normal and enable_site
  51. app_model = db.session.scalar(select(App).where(App.id == site.app_id))
  52. if not app_model or app_model.status != "normal" or not app_model.enable_site:
  53. raise NotFound()
  54. if user_id:
  55. end_user = db.session.scalar(
  56. select(EndUser).where(EndUser.app_id == app_model.id, EndUser.session_id == user_id)
  57. )
  58. if end_user:
  59. pass
  60. else:
  61. end_user = EndUser(
  62. tenant_id=app_model.tenant_id,
  63. app_id=app_model.id,
  64. type="browser",
  65. is_anonymous=True,
  66. session_id=user_id,
  67. )
  68. db.session.add(end_user)
  69. db.session.commit()
  70. else:
  71. end_user = EndUser(
  72. tenant_id=app_model.tenant_id,
  73. app_id=app_model.id,
  74. type="browser",
  75. is_anonymous=True,
  76. session_id=generate_session_id(),
  77. )
  78. db.session.add(end_user)
  79. db.session.commit()
  80. payload = {
  81. "iss": site.app_id,
  82. "sub": "Web API Passport",
  83. "app_id": site.app_id,
  84. "app_code": app_code,
  85. "end_user_id": end_user.id,
  86. }
  87. tk = PassportService().issue(payload)
  88. return {
  89. "access_token": tk,
  90. }
  91. def decode_enterprise_webapp_user_id(jwt_token: str | None):
  92. """
  93. Decode the enterprise user session from the Authorization header.
  94. """
  95. if not jwt_token:
  96. return None
  97. decoded = PassportService().verify(jwt_token)
  98. source = decoded.get("token_source")
  99. if not source or source != "webapp_login_token":
  100. raise Unauthorized("Invalid token source. Expected 'webapp_login_token'.")
  101. return decoded
  102. def exchange_token_for_existing_web_user(app_code: str, enterprise_user_decoded: dict):
  103. """
  104. Exchange a token for an existing web user session.
  105. """
  106. user_id = enterprise_user_decoded.get("user_id")
  107. end_user_id = enterprise_user_decoded.get("end_user_id")
  108. session_id = enterprise_user_decoded.get("session_id")
  109. user_auth_type = enterprise_user_decoded.get("auth_type")
  110. if not user_auth_type:
  111. raise Unauthorized("Missing auth_type in the token.")
  112. site = db.session.scalar(select(Site).where(Site.code == app_code, Site.status == "normal"))
  113. if not site:
  114. raise NotFound()
  115. app_model = db.session.scalar(select(App).where(App.id == site.app_id))
  116. if not app_model or app_model.status != "normal" or not app_model.enable_site:
  117. raise NotFound()
  118. app_auth_type = WebAppAuthService.get_app_auth_type(app_code=app_code)
  119. if app_auth_type == WebAppAuthType.PUBLIC:
  120. return _exchange_for_public_app_token(app_model, site, enterprise_user_decoded)
  121. elif app_auth_type == WebAppAuthType.EXTERNAL and user_auth_type != "external":
  122. raise WebAppAuthRequiredError("Please login as external user.")
  123. elif app_auth_type == WebAppAuthType.INTERNAL and user_auth_type != "internal":
  124. raise WebAppAuthRequiredError("Please login as internal user.")
  125. end_user = None
  126. if end_user_id:
  127. end_user = db.session.scalar(select(EndUser).where(EndUser.id == end_user_id))
  128. if session_id:
  129. end_user = db.session.scalar(
  130. select(EndUser).where(
  131. EndUser.session_id == session_id,
  132. EndUser.tenant_id == app_model.tenant_id,
  133. EndUser.app_id == app_model.id,
  134. )
  135. )
  136. if not end_user:
  137. if not session_id:
  138. raise NotFound("Missing session_id for existing web user.")
  139. end_user = EndUser(
  140. tenant_id=app_model.tenant_id,
  141. app_id=app_model.id,
  142. type="browser",
  143. is_anonymous=True,
  144. session_id=session_id,
  145. )
  146. db.session.add(end_user)
  147. db.session.commit()
  148. exp_dt = datetime.now(UTC) + timedelta(minutes=dify_config.ACCESS_TOKEN_EXPIRE_MINUTES)
  149. exp = int(exp_dt.timestamp())
  150. payload = {
  151. "iss": site.id,
  152. "sub": "Web API Passport",
  153. "app_id": site.app_id,
  154. "app_code": site.code,
  155. "user_id": user_id,
  156. "end_user_id": end_user.id,
  157. "auth_type": user_auth_type,
  158. "granted_at": int(datetime.now(UTC).timestamp()),
  159. "token_source": "webapp",
  160. "exp": exp,
  161. }
  162. token: str = PassportService().issue(payload)
  163. return {
  164. "access_token": token,
  165. }
  166. def _exchange_for_public_app_token(app_model, site, token_decoded):
  167. user_id = token_decoded.get("user_id")
  168. end_user = None
  169. if user_id:
  170. end_user = db.session.scalar(
  171. select(EndUser).where(EndUser.app_id == app_model.id, EndUser.session_id == user_id)
  172. )
  173. if not end_user:
  174. end_user = EndUser(
  175. tenant_id=app_model.tenant_id,
  176. app_id=app_model.id,
  177. type="browser",
  178. is_anonymous=True,
  179. session_id=generate_session_id(),
  180. )
  181. db.session.add(end_user)
  182. db.session.commit()
  183. payload = {
  184. "iss": site.app_id,
  185. "sub": "Web API Passport",
  186. "app_id": site.app_id,
  187. "app_code": site.code,
  188. "end_user_id": end_user.id,
  189. }
  190. tk = PassportService().issue(payload)
  191. return {
  192. "access_token": tk,
  193. }
  194. def generate_session_id():
  195. """
  196. Generate a unique session ID.
  197. """
  198. while True:
  199. session_id = str(uuid.uuid4())
  200. existing_count = db.session.scalar(
  201. select(func.count()).select_from(EndUser).where(EndUser.session_id == session_id)
  202. )
  203. if existing_count == 0:
  204. return session_id