| 
									
										
										
										
											2023-07-11 15:21:20 +08:00
										 |  |  | import uuid | 
					
						
							| 
									
										
										
										
											2025-06-09 17:19:53 +09:00
										 |  |  | from datetime import UTC, datetime, timedelta | 
					
						
							| 
									
										
										
										
											2024-01-12 12:34:01 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | from flask import request | 
					
						
							| 
									
										
										
										
											2025-05-06 11:58:49 +08:00
										 |  |  | from flask_restful import Resource | 
					
						
							| 
									
										
										
										
											2024-02-06 13:21:13 +08:00
										 |  |  | from werkzeug.exceptions import NotFound, Unauthorized | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-09 17:19:53 +09:00
										 |  |  | from configs import dify_config | 
					
						
							| 
									
										
										
										
											2024-02-06 13:21:13 +08:00
										 |  |  | from controllers.web import api | 
					
						
							| 
									
										
										
										
											2025-05-20 12:07:50 +08:00
										 |  |  | from controllers.web.error import WebAppAuthRequiredError | 
					
						
							| 
									
										
										
										
											2024-02-06 13:21:13 +08:00
										 |  |  | from extensions.ext_database import db | 
					
						
							| 
									
										
										
										
											2023-07-11 15:21:20 +08:00
										 |  |  | from libs.passport import PassportService | 
					
						
							| 
									
										
										
										
											2024-01-12 12:34:01 +08:00
										 |  |  | from models.model import App, EndUser, Site | 
					
						
							| 
									
										
										
										
											2024-08-25 18:47:02 +08:00
										 |  |  | from services.enterprise.enterprise_service import EnterpriseService | 
					
						
							| 
									
										
										
										
											2024-05-15 16:14:49 +08:00
										 |  |  | from services.feature_service import FeatureService | 
					
						
							| 
									
										
										
										
											2025-06-09 17:19:53 +09:00
										 |  |  | from services.webapp_auth_service import WebAppAuthService, WebAppAuthType | 
					
						
							| 
									
										
										
										
											2024-01-12 12:34:01 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-07-11 15:21:20 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | class PassportResource(Resource): | 
					
						
							|  |  |  |     """Base resource for passport.""" | 
					
						
							| 
									
										
										
										
											2024-08-26 15:29:10 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-07-11 15:21:20 +08:00
										 |  |  |     def get(self): | 
					
						
							| 
									
										
										
										
											2024-05-15 16:14:49 +08:00
										 |  |  |         system_features = FeatureService.get_system_features() | 
					
						
							| 
									
										
										
										
											2024-08-26 15:29:10 +08:00
										 |  |  |         app_code = request.headers.get("X-App-Code") | 
					
						
							| 
									
										
										
										
											2025-03-31 18:55:42 +08:00
										 |  |  |         user_id = request.args.get("user_id") | 
					
						
							| 
									
										
										
										
											2025-06-09 17:19:53 +09:00
										 |  |  |         web_app_access_token = request.args.get("web_app_access_token") | 
					
						
							| 
									
										
										
										
											2025-03-31 18:55:42 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-08-13 17:32:23 +08:00
										 |  |  |         if app_code is None: | 
					
						
							| 
									
										
										
										
											2024-08-26 15:29:10 +08:00
										 |  |  |             raise Unauthorized("X-App-Code header is missing.") | 
					
						
							| 
									
										
										
										
											2023-07-11 15:21:20 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-09 17:19:53 +09:00
										 |  |  |         # exchange token for enterprise logined web user | 
					
						
							|  |  |  |         enterprise_user_decoded = decode_enterprise_webapp_user_id(web_app_access_token) | 
					
						
							|  |  |  |         if enterprise_user_decoded: | 
					
						
							|  |  |  |             # a web user has already logged in, exchange a token for this app without redirecting to the login page | 
					
						
							|  |  |  |             return exchange_token_for_existing_web_user( | 
					
						
							|  |  |  |                 app_code=app_code, enterprise_user_decoded=enterprise_user_decoded | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-20 12:07:50 +08:00
										 |  |  |         if system_features.webapp_auth.enabled: | 
					
						
							|  |  |  |             app_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_code(app_code=app_code) | 
					
						
							|  |  |  |             if not app_settings or not app_settings.access_mode == "public": | 
					
						
							|  |  |  |                 raise WebAppAuthRequiredError() | 
					
						
							| 
									
										
										
										
											2024-08-26 15:29:10 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-07-11 15:21:20 +08:00
										 |  |  |         # get site from db and check if it is normal | 
					
						
							| 
									
										
										
										
											2024-08-26 15:29:10 +08:00
										 |  |  |         site = db.session.query(Site).filter(Site.code == app_code, Site.status == "normal").first() | 
					
						
							| 
									
										
										
										
											2023-07-11 15:21:20 +08:00
										 |  |  |         if not site: | 
					
						
							|  |  |  |             raise NotFound() | 
					
						
							|  |  |  |         # get app from db and check if it is normal and enable_site | 
					
						
							|  |  |  |         app_model = db.session.query(App).filter(App.id == site.app_id).first() | 
					
						
							| 
									
										
										
										
											2024-08-26 15:29:10 +08:00
										 |  |  |         if not app_model or app_model.status != "normal" or not app_model.enable_site: | 
					
						
							| 
									
										
										
										
											2023-07-11 15:21:20 +08:00
										 |  |  |             raise NotFound() | 
					
						
							| 
									
										
										
										
											2024-05-15 16:14:49 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-31 18:55:42 +08:00
										 |  |  |         if user_id: | 
					
						
							|  |  |  |             end_user = ( | 
					
						
							|  |  |  |                 db.session.query(EndUser).filter(EndUser.app_id == app_model.id, EndUser.session_id == user_id).first() | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if end_user: | 
					
						
							|  |  |  |                 pass | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 end_user = EndUser( | 
					
						
							|  |  |  |                     tenant_id=app_model.tenant_id, | 
					
						
							|  |  |  |                     app_id=app_model.id, | 
					
						
							|  |  |  |                     type="browser", | 
					
						
							|  |  |  |                     is_anonymous=True, | 
					
						
							|  |  |  |                     session_id=user_id, | 
					
						
							|  |  |  |                 ) | 
					
						
							|  |  |  |                 db.session.add(end_user) | 
					
						
							|  |  |  |                 db.session.commit() | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             end_user = EndUser( | 
					
						
							|  |  |  |                 tenant_id=app_model.tenant_id, | 
					
						
							|  |  |  |                 app_id=app_model.id, | 
					
						
							|  |  |  |                 type="browser", | 
					
						
							|  |  |  |                 is_anonymous=True, | 
					
						
							|  |  |  |                 session_id=generate_session_id(), | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  |             db.session.add(end_user) | 
					
						
							|  |  |  |             db.session.commit() | 
					
						
							| 
									
										
										
										
											2023-07-11 15:21:20 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |         payload = { | 
					
						
							|  |  |  |             "iss": site.app_id, | 
					
						
							| 
									
										
										
										
											2024-08-26 15:29:10 +08:00
										 |  |  |             "sub": "Web API Passport", | 
					
						
							|  |  |  |             "app_id": site.app_id, | 
					
						
							|  |  |  |             "app_code": app_code, | 
					
						
							|  |  |  |             "end_user_id": end_user.id, | 
					
						
							| 
									
										
										
										
											2023-07-11 15:21:20 +08:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         tk = PassportService().issue(payload) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return { | 
					
						
							| 
									
										
										
										
											2024-08-26 15:29:10 +08:00
										 |  |  |             "access_token": tk, | 
					
						
							| 
									
										
										
										
											2023-07-11 15:21:20 +08:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-15 16:14:49 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-26 15:29:10 +08:00
										 |  |  | api.add_resource(PassportResource, "/passport") | 
					
						
							| 
									
										
										
										
											2023-07-11 15:21:20 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-15 16:14:49 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-09 17:19:53 +09:00
										 |  |  | def decode_enterprise_webapp_user_id(jwt_token: str | None): | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Decode the enterprise user session from the Authorization header. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     if not jwt_token: | 
					
						
							|  |  |  |         return None | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     decoded = PassportService().verify(jwt_token) | 
					
						
							|  |  |  |     source = decoded.get("token_source") | 
					
						
							|  |  |  |     if not source or source != "webapp_login_token": | 
					
						
							|  |  |  |         raise Unauthorized("Invalid token source. Expected 'webapp_login_token'.") | 
					
						
							|  |  |  |     return decoded | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def exchange_token_for_existing_web_user(app_code: str, enterprise_user_decoded: dict): | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Exchange a token for an existing web user session. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     user_id = enterprise_user_decoded.get("user_id") | 
					
						
							|  |  |  |     end_user_id = enterprise_user_decoded.get("end_user_id") | 
					
						
							|  |  |  |     session_id = enterprise_user_decoded.get("session_id") | 
					
						
							|  |  |  |     user_auth_type = enterprise_user_decoded.get("auth_type") | 
					
						
							|  |  |  |     if not user_auth_type: | 
					
						
							|  |  |  |         raise Unauthorized("Missing auth_type in the token.") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     site = db.session.query(Site).filter(Site.code == app_code, Site.status == "normal").first() | 
					
						
							|  |  |  |     if not site: | 
					
						
							|  |  |  |         raise NotFound() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     app_model = db.session.query(App).filter(App.id == site.app_id).first() | 
					
						
							|  |  |  |     if not app_model or app_model.status != "normal" or not app_model.enable_site: | 
					
						
							|  |  |  |         raise NotFound() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     app_auth_type = WebAppAuthService.get_app_auth_type(app_code=app_code) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if app_auth_type == WebAppAuthType.PUBLIC: | 
					
						
							|  |  |  |         return _exchange_for_public_app_token(app_model, site, enterprise_user_decoded) | 
					
						
							|  |  |  |     elif app_auth_type == WebAppAuthType.EXTERNAL and user_auth_type != "external": | 
					
						
							|  |  |  |         raise WebAppAuthRequiredError("Please login as external user.") | 
					
						
							|  |  |  |     elif app_auth_type == WebAppAuthType.INTERNAL and user_auth_type != "internal": | 
					
						
							|  |  |  |         raise WebAppAuthRequiredError("Please login as internal user.") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     end_user = None | 
					
						
							|  |  |  |     if end_user_id: | 
					
						
							|  |  |  |         end_user = db.session.query(EndUser).filter(EndUser.id == end_user_id).first() | 
					
						
							|  |  |  |     if session_id: | 
					
						
							|  |  |  |         end_user = ( | 
					
						
							|  |  |  |             db.session.query(EndUser) | 
					
						
							|  |  |  |             .filter( | 
					
						
							|  |  |  |                 EndUser.session_id == session_id, | 
					
						
							|  |  |  |                 EndUser.tenant_id == app_model.tenant_id, | 
					
						
							|  |  |  |                 EndUser.app_id == app_model.id, | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  |             .first() | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |     if not end_user: | 
					
						
							|  |  |  |         if not session_id: | 
					
						
							|  |  |  |             raise NotFound("Missing session_id for existing web user.") | 
					
						
							|  |  |  |         end_user = EndUser( | 
					
						
							|  |  |  |             tenant_id=app_model.tenant_id, | 
					
						
							|  |  |  |             app_id=app_model.id, | 
					
						
							|  |  |  |             type="browser", | 
					
						
							|  |  |  |             is_anonymous=True, | 
					
						
							|  |  |  |             session_id=session_id, | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |         db.session.add(end_user) | 
					
						
							|  |  |  |         db.session.commit() | 
					
						
							|  |  |  |     exp_dt = datetime.now(UTC) + timedelta(hours=dify_config.ACCESS_TOKEN_EXPIRE_MINUTES * 24) | 
					
						
							|  |  |  |     exp = int(exp_dt.timestamp()) | 
					
						
							|  |  |  |     payload = { | 
					
						
							|  |  |  |         "iss": site.id, | 
					
						
							|  |  |  |         "sub": "Web API Passport", | 
					
						
							|  |  |  |         "app_id": site.app_id, | 
					
						
							|  |  |  |         "app_code": site.code, | 
					
						
							|  |  |  |         "user_id": user_id, | 
					
						
							|  |  |  |         "end_user_id": end_user.id, | 
					
						
							|  |  |  |         "auth_type": user_auth_type, | 
					
						
							|  |  |  |         "granted_at": int(datetime.now(UTC).timestamp()), | 
					
						
							|  |  |  |         "token_source": "webapp", | 
					
						
							|  |  |  |         "exp": exp, | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     token: str = PassportService().issue(payload) | 
					
						
							|  |  |  |     return { | 
					
						
							|  |  |  |         "access_token": token, | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def _exchange_for_public_app_token(app_model, site, token_decoded): | 
					
						
							|  |  |  |     user_id = token_decoded.get("user_id") | 
					
						
							|  |  |  |     end_user = None | 
					
						
							|  |  |  |     if user_id: | 
					
						
							|  |  |  |         end_user = ( | 
					
						
							|  |  |  |             db.session.query(EndUser).filter(EndUser.app_id == app_model.id, EndUser.session_id == user_id).first() | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if not end_user: | 
					
						
							|  |  |  |         end_user = EndUser( | 
					
						
							|  |  |  |             tenant_id=app_model.tenant_id, | 
					
						
							|  |  |  |             app_id=app_model.id, | 
					
						
							|  |  |  |             type="browser", | 
					
						
							|  |  |  |             is_anonymous=True, | 
					
						
							|  |  |  |             session_id=generate_session_id(), | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         db.session.add(end_user) | 
					
						
							|  |  |  |         db.session.commit() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     payload = { | 
					
						
							|  |  |  |         "iss": site.app_id, | 
					
						
							|  |  |  |         "sub": "Web API Passport", | 
					
						
							|  |  |  |         "app_id": site.app_id, | 
					
						
							|  |  |  |         "app_code": site.code, | 
					
						
							|  |  |  |         "end_user_id": end_user.id, | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     tk = PassportService().issue(payload) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return { | 
					
						
							|  |  |  |         "access_token": tk, | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-07-11 15:21:20 +08:00
										 |  |  | def generate_session_id(): | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Generate a unique session ID. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     while True: | 
					
						
							|  |  |  |         session_id = str(uuid.uuid4()) | 
					
						
							| 
									
										
										
										
											2024-08-26 15:29:10 +08:00
										 |  |  |         existing_count = db.session.query(EndUser).filter(EndUser.session_id == session_id).count() | 
					
						
							| 
									
										
										
										
											2023-07-11 15:21:20 +08:00
										 |  |  |         if existing_count == 0: | 
					
						
							|  |  |  |             return session_id |