Skip to content

scholar_auth API Documentation

authorization

Created on 2024-10-20

@author: wf

moved from snapquery/authorization.py

Authorization

Authorization check.

Source code in scholar_auth/authorization.py
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
@lod_storable
class Authorization:
    """
    Authorization check.
    """

    user_rights: dict[str, UserRights] = field(default_factory=dict)

    @classmethod
    def default_yaml_path(cls) -> Path:
        """
        get the default path of the user rights file

        Returns:
            Path: ~/.solutions/scholar_auth/userrights.yaml
        """
        yaml_path = Path.home() / ".solutions/scholar_auth/userrights.yaml"
        return yaml_path

    @classmethod
    def load(cls, yaml_path: Optional[str] = None) -> "Authorization":
        """
        Load user rights from a YAML file.

        Args:
            yaml_path: the path of the user rights file, default: see default_yaml_path

        Returns:
            Authorization: the loaded user rights, without any rights if the file does not exist
        """
        if yaml_path is None:
            yaml_path = str(cls.default_yaml_path())
        if Path(yaml_path).exists():
            authorization = cls.load_from_yaml_file(yaml_path)
        else:
            print(f"YAML file not found: {yaml_path}")
            authorization = cls()
        return authorization

    def check_right_by_orcid(self, orcid: str, rights: Optional[str] = None) -> bool:
        """
        Check if the user with the given ORCID has rights.

        Args:
            orcid: the ORCID iD of the user
            rights: the specific right to check, if None any known user passes

        Returns:
            bool: True if the user is known and has the given right
        """
        ok = False
        user_right = self.user_rights.get(orcid)
        if user_right is not None:
            ok = rights is None or rights in user_right.get_rights()
        return ok

check_right_by_orcid(orcid, rights=None)

Check if the user with the given ORCID has rights.

Parameters:

Name Type Description Default
orcid str

the ORCID iD of the user

required
rights Optional[str]

the specific right to check, if None any known user passes

None

Returns:

Name Type Description
bool bool

True if the user is known and has the given right

Source code in scholar_auth/authorization.py
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
def check_right_by_orcid(self, orcid: str, rights: Optional[str] = None) -> bool:
    """
    Check if the user with the given ORCID has rights.

    Args:
        orcid: the ORCID iD of the user
        rights: the specific right to check, if None any known user passes

    Returns:
        bool: True if the user is known and has the given right
    """
    ok = False
    user_right = self.user_rights.get(orcid)
    if user_right is not None:
        ok = rights is None or rights in user_right.get_rights()
    return ok

default_yaml_path() classmethod

get the default path of the user rights file

Returns:

Name Type Description
Path Path

~/.solutions/scholar_auth/userrights.yaml

Source code in scholar_auth/authorization.py
44
45
46
47
48
49
50
51
52
53
@classmethod
def default_yaml_path(cls) -> Path:
    """
    get the default path of the user rights file

    Returns:
        Path: ~/.solutions/scholar_auth/userrights.yaml
    """
    yaml_path = Path.home() / ".solutions/scholar_auth/userrights.yaml"
    return yaml_path

load(yaml_path=None) classmethod

Load user rights from a YAML file.

Parameters:

Name Type Description Default
yaml_path Optional[str]

the path of the user rights file, default: see default_yaml_path

None

Returns:

Name Type Description
Authorization Authorization

the loaded user rights, without any rights if the file does not exist

Source code in scholar_auth/authorization.py
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
@classmethod
def load(cls, yaml_path: Optional[str] = None) -> "Authorization":
    """
    Load user rights from a YAML file.

    Args:
        yaml_path: the path of the user rights file, default: see default_yaml_path

    Returns:
        Authorization: the loaded user rights, without any rights if the file does not exist
    """
    if yaml_path is None:
        yaml_path = str(cls.default_yaml_path())
    if Path(yaml_path).exists():
        authorization = cls.load_from_yaml_file(yaml_path)
    else:
        print(f"YAML file not found: {yaml_path}")
        authorization = cls()
    return authorization

UserRights

the rights of a single user

Source code in scholar_auth/authorization.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@lod_storable
class UserRights:
    """
    the rights of a single user
    """

    name: str
    rights: str

    def get_rights(self) -> list[str]:
        """
        get the single rights of this user

        Returns:
            list[str]: the rights, separated by blanks or commas in the rights string
        """
        rights_list = self.rights.replace(",", " ").split()
        return rights_list

get_rights()

get the single rights of this user

Returns:

Type Description
list[str]

list[str]: the rights, separated by blanks or commas in the rights string

Source code in scholar_auth/authorization.py
25
26
27
28
29
30
31
32
33
def get_rights(self) -> list[str]:
    """
    get the single rights of this user

    Returns:
        list[str]: the rights, separated by blanks or commas in the rights string
    """
    rights_list = self.rights.replace(",", " ").split()
    return rights_list

nicegui_login

Created on 2026-09-30

@author: wf

ORCID login for nicegui applications - needs the optional nicegui dependency

NiceGuiScholarLogin

Bases: ScholarLogin

ORCID login that keeps the session in the nicegui user storage and serves the ORCID callback and the logout

Source code in scholar_auth/nicegui_login.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
class NiceGuiScholarLogin(ScholarLogin):
    """
    ORCID login that keeps the session in the nicegui user storage
    and serves the ORCID callback and the logout
    """

    def __init__(
        self,
        base_path: Path,
        callback_path: str = "/orcid_callback",
        logout_path: str = "/logout",
        home_path: str = "/",
        config_file_name: str = "orcid_config.yaml",
        rights_file_name: str = "userrights.yaml",
    ):
        """
        constructor

        Args:
            base_path: the directory of the system with the ORCID configuration and the user rights file
            callback_path: the path of the redirect uri registered at ORCID
            logout_path: the path that logs the user out
            home_path: the path to go to after login and logout
            config_file_name: the name of the ORCID configuration file
            rights_file_name: the name of the user rights file
        """
        super().__init__(
            base_path,
            storage_provider=self.get_user_storage,
            config_file_name=config_file_name,
            rights_file_name=rights_file_name,
        )
        self.callback_path = callback_path
        self.logout_path = logout_path
        self.home_path = home_path

    def get_user_storage(self) -> MutableMapping:
        """
        get the nicegui storage of the current user session

        Returns:
            MutableMapping: app.storage.user
        """
        storage = app.storage.user
        return storage

    def handle_callback(self, code: str) -> RedirectResponse:
        """
        handle the redirect from ORCID

        Args:
            code: the authorization code

        Returns:
            RedirectResponse: the redirect to the home path

        Raises:
            HTTPException: 401 if the login fails
        """
        try:
            self.login(code)
        except Exception as ex:
            raise HTTPException(status_code=401, detail="ORCID login failed") from ex
        response = RedirectResponse(self.home_path)
        return response

    def handle_logout(self) -> RedirectResponse:
        """
        logout the user of the current session

        Returns:
            RedirectResponse: the redirect to the home path
        """
        self.logout()
        response = RedirectResponse(self.home_path)
        return response

    def register_routes(self) -> None:
        """
        register the ORCID callback and the logout route with the nicegui app
        """

        @app.get(self.callback_path)
        async def orcid_callback(code: str):
            response = self.handle_callback(code)
            return response

        @app.get(self.logout_path)
        async def orcid_logout():
            response = self.handle_logout()
            return response

__init__(base_path, callback_path='/orcid_callback', logout_path='/logout', home_path='/', config_file_name='orcid_config.yaml', rights_file_name='userrights.yaml')

constructor

Parameters:

Name Type Description Default
base_path Path

the directory of the system with the ORCID configuration and the user rights file

required
callback_path str

the path of the redirect uri registered at ORCID

'/orcid_callback'
logout_path str

the path that logs the user out

'/logout'
home_path str

the path to go to after login and logout

'/'
config_file_name str

the name of the ORCID configuration file

'orcid_config.yaml'
rights_file_name str

the name of the user rights file

'userrights.yaml'
Source code in scholar_auth/nicegui_login.py
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
def __init__(
    self,
    base_path: Path,
    callback_path: str = "/orcid_callback",
    logout_path: str = "/logout",
    home_path: str = "/",
    config_file_name: str = "orcid_config.yaml",
    rights_file_name: str = "userrights.yaml",
):
    """
    constructor

    Args:
        base_path: the directory of the system with the ORCID configuration and the user rights file
        callback_path: the path of the redirect uri registered at ORCID
        logout_path: the path that logs the user out
        home_path: the path to go to after login and logout
        config_file_name: the name of the ORCID configuration file
        rights_file_name: the name of the user rights file
    """
    super().__init__(
        base_path,
        storage_provider=self.get_user_storage,
        config_file_name=config_file_name,
        rights_file_name=rights_file_name,
    )
    self.callback_path = callback_path
    self.logout_path = logout_path
    self.home_path = home_path

get_user_storage()

get the nicegui storage of the current user session

Returns:

Name Type Description
MutableMapping MutableMapping

app.storage.user

Source code in scholar_auth/nicegui_login.py
55
56
57
58
59
60
61
62
63
def get_user_storage(self) -> MutableMapping:
    """
    get the nicegui storage of the current user session

    Returns:
        MutableMapping: app.storage.user
    """
    storage = app.storage.user
    return storage

handle_callback(code)

handle the redirect from ORCID

Parameters:

Name Type Description Default
code str

the authorization code

required

Returns:

Name Type Description
RedirectResponse RedirectResponse

the redirect to the home path

Raises:

Type Description
HTTPException

401 if the login fails

Source code in scholar_auth/nicegui_login.py
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
def handle_callback(self, code: str) -> RedirectResponse:
    """
    handle the redirect from ORCID

    Args:
        code: the authorization code

    Returns:
        RedirectResponse: the redirect to the home path

    Raises:
        HTTPException: 401 if the login fails
    """
    try:
        self.login(code)
    except Exception as ex:
        raise HTTPException(status_code=401, detail="ORCID login failed") from ex
    response = RedirectResponse(self.home_path)
    return response

handle_logout()

logout the user of the current session

Returns:

Name Type Description
RedirectResponse RedirectResponse

the redirect to the home path

Source code in scholar_auth/nicegui_login.py
85
86
87
88
89
90
91
92
93
94
def handle_logout(self) -> RedirectResponse:
    """
    logout the user of the current session

    Returns:
        RedirectResponse: the redirect to the home path
    """
    self.logout()
    response = RedirectResponse(self.home_path)
    return response

register_routes()

register the ORCID callback and the logout route with the nicegui app

Source code in scholar_auth/nicegui_login.py
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
def register_routes(self) -> None:
    """
    register the ORCID callback and the logout route with the nicegui app
    """

    @app.get(self.callback_path)
    async def orcid_callback(code: str):
        response = self.handle_callback(code)
        return response

    @app.get(self.logout_path)
    async def orcid_logout():
        response = self.handle_logout()
        return response

orcid

Created on 2026-09-30

@author: wf

ORCID OAuth login - moved from snapquery/orcid.py and made independent of the web framework

OrcidAccessToken

orcid access token response

Source code in scholar_auth/orcid.py
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
@lod_storable
class OrcidAccessToken:
    """
    orcid access token response
    """

    orcid: str
    access_token: str
    token_type: str
    refresh_token: str
    expires_in: int
    scope: str
    name: str
    login_timestamp: int = field(default_factory=current_timestamp)

    @classmethod
    def get_samples(cls) -> list["OrcidAccessToken"]:
        """
        get sample access tokens

        Returns:
            list[OrcidAccessToken]: the samples
        """
        lod = [
            {
                "access_token": "f5af9f51-07e6-4332-8f1a-c0c11c1e3728",
                "token_type": "bearer",
                "refresh_token": "f725f747-3a65-49f6-a231-3e8944ce464d",
                "expires_in": 631138518,
                "scope": "/activities/update /read-limited",
                "name": "Sofia Garcia",
                "orcid": "0000-0001-2345-6789",
            }
        ]
        samples = [OrcidAccessToken.from_dict2(d) for d in lod]
        return samples

    def is_valid(self) -> bool:
        """
        check whether this access token has not expired yet

        Returns:
            bool: True if the token is still valid
        """
        time_passed = current_timestamp() - self.login_timestamp
        valid = self.expires_in - time_passed >= 0
        return valid

get_samples() classmethod

get sample access tokens

Returns:

Type Description
list[OrcidAccessToken]

list[OrcidAccessToken]: the samples

Source code in scholar_auth/orcid.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
@classmethod
def get_samples(cls) -> list["OrcidAccessToken"]:
    """
    get sample access tokens

    Returns:
        list[OrcidAccessToken]: the samples
    """
    lod = [
        {
            "access_token": "f5af9f51-07e6-4332-8f1a-c0c11c1e3728",
            "token_type": "bearer",
            "refresh_token": "f725f747-3a65-49f6-a231-3e8944ce464d",
            "expires_in": 631138518,
            "scope": "/activities/update /read-limited",
            "name": "Sofia Garcia",
            "orcid": "0000-0001-2345-6789",
        }
    ]
    samples = [OrcidAccessToken.from_dict2(d) for d in lod]
    return samples

is_valid()

check whether this access token has not expired yet

Returns:

Name Type Description
bool bool

True if the token is still valid

Source code in scholar_auth/orcid.py
113
114
115
116
117
118
119
120
121
122
def is_valid(self) -> bool:
    """
    check whether this access token has not expired yet

    Returns:
        bool: True if the token is still valid
    """
    time_passed = current_timestamp() - self.login_timestamp
    valid = self.expires_in - time_passed >= 0
    return valid

OrcidAuth

authenticate with orcid

the session storage is supplied by the calling application as a function returning the mapping that belongs to the current user session

Source code in scholar_auth/orcid.py
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
class OrcidAuth:
    """
    authenticate with orcid

    the session storage is supplied by the calling application as a function
    returning the mapping that belongs to the current user session
    """

    TOKEN_KEY = "orcid_token"

    def __init__(
        self,
        base_path: Optional[Path] = None,
        config_file_name: str = "orcid_config.yaml",
        storage_provider: Optional[Callable[[], MutableMapping]] = None,
    ):
        """
        constructor

        Args:
            base_path: the directory of the configuration file, default: ~/.solutions/scholar_auth
            config_file_name: the name of the configuration file
            storage_provider: function returning the storage of the current user session,
                default: a single in-memory storage
        """
        if base_path is None:
            base_path = Path.home() / ".solutions/scholar_auth"
        self.base_path = base_path
        self.config_file_name = config_file_name
        self.memory_storage: dict = {}
        if storage_provider is None:
            storage_provider = self.get_memory_storage
        self.storage_provider = storage_provider
        self.config = self.load_config()

    def get_memory_storage(self) -> MutableMapping:
        """
        get the in-memory storage used when the application supplies none

        Returns:
            MutableMapping: the in-memory storage
        """
        storage = self.memory_storage
        return storage

    def get_config_path(self) -> Path:
        """
        get the path of the configuration file

        Returns:
            Path: the configuration file path
        """
        config_path = self.base_path / self.config_file_name
        return config_path

    def config_exists(self) -> bool:
        """
        check whether the configuration file exists

        Returns:
            bool: True if the configuration file exists
        """
        exists = self.get_config_path().exists()
        return exists

    def available(self) -> bool:
        """
        check whether the ORCID login is configured

        Returns:
            bool: True if a configuration has been loaded
        """
        is_available = self.config is not None
        return is_available

    def load_config(self) -> Optional[OrcidConfig]:
        """
        load the configuration

        Returns:
            Optional[OrcidConfig]: the configuration or None if there is no configuration file
        """
        config = None
        if self.config_exists():
            config = OrcidConfig.load_from_yaml_file(str(self.get_config_path()))
        return config

    def store_config(self) -> None:
        """
        store the configuration
        """
        self.config.save_to_yaml_file(str(self.get_config_path()))

    def authenticate_url(self) -> str:
        """
        get the url that starts the login at ORCID

        Returns:
            str: the authorize url
        """
        url = self.config.authenticate_url()
        return url

    def authenticated(self) -> bool:
        """
        check whether the user of the current session is logged in

        Returns:
            bool: True if there is a valid access token for the current session
        """
        is_authenticated = False
        if self.available():
            orcid_token = self.get_cached_user_access_token()
            if orcid_token is not None:
                is_authenticated = orcid_token.is_valid()
        return is_authenticated

    def get_cached_user_access_token(self) -> Optional[OrcidAccessToken]:
        """
        get the access token of the current session

        Returns:
            Optional[OrcidAccessToken]: the access token or None if the user is not logged in
        """
        orcid_token = None
        orcid_token_record = self.storage_provider().get(self.TOKEN_KEY, None)
        if orcid_token_record:
            orcid_token = OrcidAccessToken.from_dict2(orcid_token_record)
        return orcid_token

    def login(self, access_code: str) -> bool:
        """
        login with the code that ORCID handed to the callback

        Args:
            access_code: the authorization code

        Returns:
            bool: True if the login succeeded

        Raises:
            requests.RequestException: if the token request fails
        """
        orcid_token = self._retrieve_token(access_code)
        self.storage_provider()[self.TOKEN_KEY] = asdict(orcid_token)
        is_authenticated = True
        return is_authenticated

    def _retrieve_token(self, code: str) -> OrcidAccessToken:
        """
        exchange the authorization code for an access token

        URL=https://sandbox.orcid.org/oauth/token
         HEADER: Accept: application/json
         HEADER: Content-Type: application/x-www-form-urlencoded
         METHOD: POST
         DATA:
           client_id=[Your client ID]
           client_secret=[Your client secret]
           grant_type=authorization_code
           code=Six-digit code

        Args:
            code: the authorization code

        Returns:
            OrcidAccessToken: the access token
        """
        url = f"{self.config.url}/oauth/token"
        data = {
            "client_id": self.config.client_id,
            "client_secret": self.config.client_secret,
            "grant_type": "authorization_code",
            "code": code,
        }
        resp = requests.post(url, data=data)
        resp.raise_for_status()
        resp_json = resp.json()
        orcid_token = OrcidAccessToken.from_dict2(resp_json)
        return orcid_token

    def logout(self) -> None:
        """
        logout the user of the current session by deleting the cached access token
        """
        self.storage_provider().pop(self.TOKEN_KEY, None)

__init__(base_path=None, config_file_name='orcid_config.yaml', storage_provider=None)

constructor

Parameters:

Name Type Description Default
base_path Optional[Path]

the directory of the configuration file, default: ~/.solutions/scholar_auth

None
config_file_name str

the name of the configuration file

'orcid_config.yaml'
storage_provider Optional[Callable[[], MutableMapping]]

function returning the storage of the current user session, default: a single in-memory storage

None
Source code in scholar_auth/orcid.py
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
def __init__(
    self,
    base_path: Optional[Path] = None,
    config_file_name: str = "orcid_config.yaml",
    storage_provider: Optional[Callable[[], MutableMapping]] = None,
):
    """
    constructor

    Args:
        base_path: the directory of the configuration file, default: ~/.solutions/scholar_auth
        config_file_name: the name of the configuration file
        storage_provider: function returning the storage of the current user session,
            default: a single in-memory storage
    """
    if base_path is None:
        base_path = Path.home() / ".solutions/scholar_auth"
    self.base_path = base_path
    self.config_file_name = config_file_name
    self.memory_storage: dict = {}
    if storage_provider is None:
        storage_provider = self.get_memory_storage
    self.storage_provider = storage_provider
    self.config = self.load_config()

authenticate_url()

get the url that starts the login at ORCID

Returns:

Name Type Description
str str

the authorize url

Source code in scholar_auth/orcid.py
218
219
220
221
222
223
224
225
226
def authenticate_url(self) -> str:
    """
    get the url that starts the login at ORCID

    Returns:
        str: the authorize url
    """
    url = self.config.authenticate_url()
    return url

authenticated()

check whether the user of the current session is logged in

Returns:

Name Type Description
bool bool

True if there is a valid access token for the current session

Source code in scholar_auth/orcid.py
228
229
230
231
232
233
234
235
236
237
238
239
240
def authenticated(self) -> bool:
    """
    check whether the user of the current session is logged in

    Returns:
        bool: True if there is a valid access token for the current session
    """
    is_authenticated = False
    if self.available():
        orcid_token = self.get_cached_user_access_token()
        if orcid_token is not None:
            is_authenticated = orcid_token.is_valid()
    return is_authenticated

available()

check whether the ORCID login is configured

Returns:

Name Type Description
bool bool

True if a configuration has been loaded

Source code in scholar_auth/orcid.py
190
191
192
193
194
195
196
197
198
def available(self) -> bool:
    """
    check whether the ORCID login is configured

    Returns:
        bool: True if a configuration has been loaded
    """
    is_available = self.config is not None
    return is_available

config_exists()

check whether the configuration file exists

Returns:

Name Type Description
bool bool

True if the configuration file exists

Source code in scholar_auth/orcid.py
180
181
182
183
184
185
186
187
188
def config_exists(self) -> bool:
    """
    check whether the configuration file exists

    Returns:
        bool: True if the configuration file exists
    """
    exists = self.get_config_path().exists()
    return exists

get_cached_user_access_token()

get the access token of the current session

Returns:

Type Description
Optional[OrcidAccessToken]

Optional[OrcidAccessToken]: the access token or None if the user is not logged in

Source code in scholar_auth/orcid.py
242
243
244
245
246
247
248
249
250
251
252
253
def get_cached_user_access_token(self) -> Optional[OrcidAccessToken]:
    """
    get the access token of the current session

    Returns:
        Optional[OrcidAccessToken]: the access token or None if the user is not logged in
    """
    orcid_token = None
    orcid_token_record = self.storage_provider().get(self.TOKEN_KEY, None)
    if orcid_token_record:
        orcid_token = OrcidAccessToken.from_dict2(orcid_token_record)
    return orcid_token

get_config_path()

get the path of the configuration file

Returns:

Name Type Description
Path Path

the configuration file path

Source code in scholar_auth/orcid.py
170
171
172
173
174
175
176
177
178
def get_config_path(self) -> Path:
    """
    get the path of the configuration file

    Returns:
        Path: the configuration file path
    """
    config_path = self.base_path / self.config_file_name
    return config_path

get_memory_storage()

get the in-memory storage used when the application supplies none

Returns:

Name Type Description
MutableMapping MutableMapping

the in-memory storage

Source code in scholar_auth/orcid.py
160
161
162
163
164
165
166
167
168
def get_memory_storage(self) -> MutableMapping:
    """
    get the in-memory storage used when the application supplies none

    Returns:
        MutableMapping: the in-memory storage
    """
    storage = self.memory_storage
    return storage

load_config()

load the configuration

Returns:

Type Description
Optional[OrcidConfig]

Optional[OrcidConfig]: the configuration or None if there is no configuration file

Source code in scholar_auth/orcid.py
200
201
202
203
204
205
206
207
208
209
210
def load_config(self) -> Optional[OrcidConfig]:
    """
    load the configuration

    Returns:
        Optional[OrcidConfig]: the configuration or None if there is no configuration file
    """
    config = None
    if self.config_exists():
        config = OrcidConfig.load_from_yaml_file(str(self.get_config_path()))
    return config

login(access_code)

login with the code that ORCID handed to the callback

Parameters:

Name Type Description Default
access_code str

the authorization code

required

Returns:

Name Type Description
bool bool

True if the login succeeded

Raises:

Type Description
RequestException

if the token request fails

Source code in scholar_auth/orcid.py
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
def login(self, access_code: str) -> bool:
    """
    login with the code that ORCID handed to the callback

    Args:
        access_code: the authorization code

    Returns:
        bool: True if the login succeeded

    Raises:
        requests.RequestException: if the token request fails
    """
    orcid_token = self._retrieve_token(access_code)
    self.storage_provider()[self.TOKEN_KEY] = asdict(orcid_token)
    is_authenticated = True
    return is_authenticated

logout()

logout the user of the current session by deleting the cached access token

Source code in scholar_auth/orcid.py
306
307
308
309
310
def logout(self) -> None:
    """
    logout the user of the current session by deleting the cached access token
    """
    self.storage_provider().pop(self.TOKEN_KEY, None)

store_config()

store the configuration

Source code in scholar_auth/orcid.py
212
213
214
215
216
def store_config(self) -> None:
    """
    store the configuration
    """
    self.config.save_to_yaml_file(str(self.get_config_path()))

OrcidConfig

orcid authentication configuration

Source code in scholar_auth/orcid.py
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
@lod_storable
class OrcidConfig:
    """
    orcid authentication configuration
    """

    url: str
    client_id: str
    client_secret: str
    redirect_uri: str = "http://127.0.0.1:9862/orcid_callback"
    api_endpoint: str = "https://pub.orcid.org/v3.0"
    search_token: Optional[str] = None

    @classmethod
    def get_samples(cls) -> list["OrcidConfig"]:
        """
        get sample configurations

        Returns:
            list[OrcidConfig]: the samples
        """
        lod = [
            {
                "url": "https://orcid.org",
                "client_id": "APP-123456789ABCDEFG",
                "client_secret": "<KEY>",
                "redirect_uri": "http://127.0.0.1:9862/orcid_callback",
                "api_endpoint": "https://sandbox.orcid.org/v3.0",
            }
        ]
        samples = [OrcidConfig.from_dict2(d) for d in lod]
        return samples

    def authenticate_url(self) -> str:
        """
        get the url that starts the login at ORCID

        Returns:
            str: the authorize url for the authorization code flow
        """
        url = (
            f"{self.url}/oauth/authorize?client_id={self.client_id}"
            f"&response_type=code&scope=/authenticate&redirect_uri={self.redirect_uri}"
        )
        return url

authenticate_url()

get the url that starts the login at ORCID

Returns:

Name Type Description
str str

the authorize url for the authorization code flow

Source code in scholar_auth/orcid.py
62
63
64
65
66
67
68
69
70
71
72
73
def authenticate_url(self) -> str:
    """
    get the url that starts the login at ORCID

    Returns:
        str: the authorize url for the authorization code flow
    """
    url = (
        f"{self.url}/oauth/authorize?client_id={self.client_id}"
        f"&response_type=code&scope=/authenticate&redirect_uri={self.redirect_uri}"
    )
    return url

get_samples() classmethod

get sample configurations

Returns:

Type Description
list[OrcidConfig]

list[OrcidConfig]: the samples

Source code in scholar_auth/orcid.py
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@classmethod
def get_samples(cls) -> list["OrcidConfig"]:
    """
    get sample configurations

    Returns:
        list[OrcidConfig]: the samples
    """
    lod = [
        {
            "url": "https://orcid.org",
            "client_id": "APP-123456789ABCDEFG",
            "client_secret": "<KEY>",
            "redirect_uri": "http://127.0.0.1:9862/orcid_callback",
            "api_endpoint": "https://sandbox.orcid.org/v3.0",
        }
    ]
    samples = [OrcidConfig.from_dict2(d) for d in lod]
    return samples

current_timestamp()

get the current time

Returns:

Name Type Description
int int

the current time in seconds since the epoch

Source code in scholar_auth/orcid.py
18
19
20
21
22
23
24
25
26
def current_timestamp() -> int:
    """
    get the current time

    Returns:
        int: the current time in seconds since the epoch
    """
    timestamp = int(time())
    return timestamp

scholar_login

Created on 2026-09-30

@author: wf

login and rights of a scholar for one system

ScholarLogin

ORCID login combined with the user rights of one system

a system keeps its ORCID configuration and its user rights file in its own directory

Source code in scholar_auth/scholar_login.py
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
class ScholarLogin:
    """
    ORCID login combined with the user rights of one system

    a system keeps its ORCID configuration and its user rights file in its own directory
    """

    def __init__(
        self,
        base_path: Path,
        storage_provider: Optional[Callable[[], MutableMapping]] = None,
        config_file_name: str = "orcid_config.yaml",
        rights_file_name: str = "userrights.yaml",
    ):
        """
        constructor

        Args:
            base_path: the directory of the system with the ORCID configuration and the user rights file
            storage_provider: function returning the storage of the current user session
            config_file_name: the name of the ORCID configuration file
            rights_file_name: the name of the user rights file
        """
        self.base_path = Path(base_path)
        self.orcid_auth = OrcidAuth(self.base_path, config_file_name, storage_provider)
        self.rights_path = self.base_path / rights_file_name
        self.authorization = Authorization.load(str(self.rights_path))

    def available(self) -> bool:
        """
        check whether the ORCID login is configured for this system

        Returns:
            bool: True if there is an ORCID configuration
        """
        is_available = self.orcid_auth.available()
        return is_available

    def login_url(self) -> Optional[str]:
        """
        get the url that starts the login at ORCID

        Returns:
            Optional[str]: the url or None if the ORCID login is not configured
        """
        url = None
        if self.available():
            url = self.orcid_auth.authenticate_url()
        return url

    def login(self, access_code: str) -> bool:
        """
        login with the code that ORCID handed to the callback

        Args:
            access_code: the authorization code

        Returns:
            bool: True if the login succeeded
        """
        is_authenticated = self.orcid_auth.login(access_code)
        return is_authenticated

    def logout(self) -> None:
        """
        logout the user of the current session
        """
        self.orcid_auth.logout()

    def authenticated(self) -> bool:
        """
        check whether the user of the current session is logged in

        Returns:
            bool: True if the user is logged in
        """
        is_authenticated = self.orcid_auth.authenticated()
        return is_authenticated

    def current_user(self) -> Optional[OrcidAccessToken]:
        """
        get the logged in user of the current session

        Returns:
            Optional[OrcidAccessToken]: the access token with name and ORCID iD or None if nobody is logged in
        """
        orcid_token = None
        if self.authenticated():
            orcid_token = self.orcid_auth.get_cached_user_access_token()
        return orcid_token

    def has_right(self, right: str) -> bool:
        """
        check whether the user of the current session is logged in and has the given right

        Args:
            right: the name of the right

        Returns:
            bool: True if the user is logged in and the user rights file grants the right
        """
        granted = False
        orcid_token = self.current_user()
        if orcid_token is not None:
            granted = self.authorization.check_right_by_orcid(orcid_token.orcid, right)
        return granted

    def user_description(self) -> Optional[str]:
        """
        describe the logged in user of the current session

        Returns:
            Optional[str]: name and ORCID iD or None if nobody is logged in
        """
        description = None
        orcid_token = self.current_user()
        if orcid_token is not None:
            description = f"{orcid_token.name} ({orcid_token.orcid})"
        return description

__init__(base_path, storage_provider=None, config_file_name='orcid_config.yaml', rights_file_name='userrights.yaml')

constructor

Parameters:

Name Type Description Default
base_path Path

the directory of the system with the ORCID configuration and the user rights file

required
storage_provider Optional[Callable[[], MutableMapping]]

function returning the storage of the current user session

None
config_file_name str

the name of the ORCID configuration file

'orcid_config.yaml'
rights_file_name str

the name of the user rights file

'userrights.yaml'
Source code in scholar_auth/scholar_login.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
def __init__(
    self,
    base_path: Path,
    storage_provider: Optional[Callable[[], MutableMapping]] = None,
    config_file_name: str = "orcid_config.yaml",
    rights_file_name: str = "userrights.yaml",
):
    """
    constructor

    Args:
        base_path: the directory of the system with the ORCID configuration and the user rights file
        storage_provider: function returning the storage of the current user session
        config_file_name: the name of the ORCID configuration file
        rights_file_name: the name of the user rights file
    """
    self.base_path = Path(base_path)
    self.orcid_auth = OrcidAuth(self.base_path, config_file_name, storage_provider)
    self.rights_path = self.base_path / rights_file_name
    self.authorization = Authorization.load(str(self.rights_path))

authenticated()

check whether the user of the current session is logged in

Returns:

Name Type Description
bool bool

True if the user is logged in

Source code in scholar_auth/scholar_login.py
85
86
87
88
89
90
91
92
93
def authenticated(self) -> bool:
    """
    check whether the user of the current session is logged in

    Returns:
        bool: True if the user is logged in
    """
    is_authenticated = self.orcid_auth.authenticated()
    return is_authenticated

available()

check whether the ORCID login is configured for this system

Returns:

Name Type Description
bool bool

True if there is an ORCID configuration

Source code in scholar_auth/scholar_login.py
44
45
46
47
48
49
50
51
52
def available(self) -> bool:
    """
    check whether the ORCID login is configured for this system

    Returns:
        bool: True if there is an ORCID configuration
    """
    is_available = self.orcid_auth.available()
    return is_available

current_user()

get the logged in user of the current session

Returns:

Type Description
Optional[OrcidAccessToken]

Optional[OrcidAccessToken]: the access token with name and ORCID iD or None if nobody is logged in

Source code in scholar_auth/scholar_login.py
 95
 96
 97
 98
 99
100
101
102
103
104
105
def current_user(self) -> Optional[OrcidAccessToken]:
    """
    get the logged in user of the current session

    Returns:
        Optional[OrcidAccessToken]: the access token with name and ORCID iD or None if nobody is logged in
    """
    orcid_token = None
    if self.authenticated():
        orcid_token = self.orcid_auth.get_cached_user_access_token()
    return orcid_token

has_right(right)

check whether the user of the current session is logged in and has the given right

Parameters:

Name Type Description Default
right str

the name of the right

required

Returns:

Name Type Description
bool bool

True if the user is logged in and the user rights file grants the right

Source code in scholar_auth/scholar_login.py
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
def has_right(self, right: str) -> bool:
    """
    check whether the user of the current session is logged in and has the given right

    Args:
        right: the name of the right

    Returns:
        bool: True if the user is logged in and the user rights file grants the right
    """
    granted = False
    orcid_token = self.current_user()
    if orcid_token is not None:
        granted = self.authorization.check_right_by_orcid(orcid_token.orcid, right)
    return granted

login(access_code)

login with the code that ORCID handed to the callback

Parameters:

Name Type Description Default
access_code str

the authorization code

required

Returns:

Name Type Description
bool bool

True if the login succeeded

Source code in scholar_auth/scholar_login.py
66
67
68
69
70
71
72
73
74
75
76
77
def login(self, access_code: str) -> bool:
    """
    login with the code that ORCID handed to the callback

    Args:
        access_code: the authorization code

    Returns:
        bool: True if the login succeeded
    """
    is_authenticated = self.orcid_auth.login(access_code)
    return is_authenticated

login_url()

get the url that starts the login at ORCID

Returns:

Type Description
Optional[str]

Optional[str]: the url or None if the ORCID login is not configured

Source code in scholar_auth/scholar_login.py
54
55
56
57
58
59
60
61
62
63
64
def login_url(self) -> Optional[str]:
    """
    get the url that starts the login at ORCID

    Returns:
        Optional[str]: the url or None if the ORCID login is not configured
    """
    url = None
    if self.available():
        url = self.orcid_auth.authenticate_url()
    return url

logout()

logout the user of the current session

Source code in scholar_auth/scholar_login.py
79
80
81
82
83
def logout(self) -> None:
    """
    logout the user of the current session
    """
    self.orcid_auth.logout()

user_description()

describe the logged in user of the current session

Returns:

Type Description
Optional[str]

Optional[str]: name and ORCID iD or None if nobody is logged in

Source code in scholar_auth/scholar_login.py
123
124
125
126
127
128
129
130
131
132
133
134
def user_description(self) -> Optional[str]:
    """
    describe the logged in user of the current session

    Returns:
        Optional[str]: name and ORCID iD or None if nobody is logged in
    """
    description = None
    orcid_token = self.current_user()
    if orcid_token is not None:
        description = f"{orcid_token.name} ({orcid_token.orcid})"
    return description