Coverage for heritrace / routes / auth.py: 100%

63 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-07-02 10:16 +0000

1# SPDX-FileCopyrightText: 2024-2025 Arcangelo Massari <arcangelo.massari@unibo.it> 

2# 

3# SPDX-License-Identifier: ISC 

4 

5import os 

6from datetime import timedelta 

7 

8from flask import Blueprint, current_app, flash, redirect, request, session, url_for 

9from flask_babel import gettext 

10from flask_login import current_user, login_user, logout_user 

11from requests.exceptions import RequestException 

12from requests_oauthlib import OAuth2Session 

13from werkzeug.wrappers import Response as WerkzeugResponse 

14 

15from heritrace.apis.orcid import extract_orcid_id, is_orcid_url 

16from heritrace.models import User 

17 

18auth_bp = Blueprint("auth", __name__) 

19 

20 

21@auth_bp.route("/login") 

22def login() -> WerkzeugResponse: 

23 if current_user.is_authenticated: 

24 return redirect(url_for("main.catalogue")) 

25 

26 if os.environ.get("FLASK_ENV") == "demo": 

27 user_id_from_env = os.environ.get("USER_ID", "demo_user") 

28 demo_uri = f"http://example.org/demo/{user_id_from_env}" 

29 user_name = f"Demo User ({user_id_from_env})" 

30 

31 user = User(user_id=demo_uri, name=user_name, orcid=demo_uri) 

32 session["user_name"] = user_name 

33 session.permanent = True 

34 current_app.permanent_session_lifetime = timedelta(days=30) 

35 login_user(user) 

36 

37 flash( 

38 gettext("Welcome! You've been automatically logged in to the demo"), "info" 

39 ) 

40 return redirect(url_for("main.catalogue")) 

41 

42 callback_url = url_for("auth.callback", _external=True, _scheme="https") 

43 orcid = OAuth2Session( 

44 current_app.config["ORCID_CLIENT_ID"], 

45 redirect_uri=callback_url, 

46 scope=["/authenticate", "openid"], 

47 ) 

48 authorization_url, state = orcid.authorization_url( 

49 "https://orcid.org/oauth/authorize", 

50 prompt="login", # Forza il re-login 

51 nonce=os.urandom(16).hex(), # Aggiungiamo un nonce per sicurezza 

52 ) 

53 session["oauth_state"] = state 

54 return redirect(authorization_url) 

55 

56 

57@auth_bp.route("/callback") 

58def callback() -> WerkzeugResponse: 

59 if request.url.startswith("http://"): 

60 secure_url = request.url.replace("http://", "https://", 1) 

61 else: 

62 secure_url = request.url 

63 

64 orcid = OAuth2Session( 

65 current_app.config["ORCID_CLIENT_ID"], state=session["oauth_state"] 

66 ) 

67 try: 

68 token = orcid.fetch_token( 

69 "https://orcid.org/oauth/token", 

70 client_secret=current_app.config["ORCID_CLIENT_SECRET"], 

71 authorization_response=secure_url, 

72 ) 

73 except (RequestException, ValueError, KeyError): 

74 flash( 

75 gettext("An error occurred during authentication. Please try again"), 

76 "danger", 

77 ) 

78 return redirect(url_for("auth.login")) 

79 orcid_id = str(token["orcid"]) 

80 

81 safelist = current_app.config["ORCID_SAFELIST"] 

82 if safelist: 

83 normalized_safelist = { 

84 extract_orcid_id(item) if is_orcid_url(item) else item for item in safelist 

85 } 

86 if orcid_id not in normalized_safelist: 

87 flash( 

88 gettext("Your ORCID is not authorized to access this application"), 

89 "danger", 

90 ) 

91 return redirect(url_for("auth.login")) 

92 

93 session["user_name"] = token["name"] 

94 user = User(user_id=orcid_id, name=str(token["name"]), orcid=orcid_id) 

95 session.permanent = True 

96 current_app.permanent_session_lifetime = timedelta(days=30) 

97 login_user(user) 

98 flash(gettext("Welcome back %(name)s!", name=current_user.name), "success") 

99 return redirect(url_for("main.catalogue")) 

100 

101 

102@auth_bp.route("/logout") 

103def logout() -> tuple[str, int] | WerkzeugResponse: 

104 if not current_user.is_authenticated: 

105 return "", 401 

106 logout_user() 

107 flash(gettext("You have been logged out"), "info") 

108 return redirect(url_for("main.index"))