Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions arxiv/auth/legacy/sessions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Provides API for legacy user sessions."""
from datetime import datetime, timedelta
from datetime import datetime
from pytz import timezone, UTC

import logging
Expand Down Expand Up @@ -154,13 +154,21 @@ def create(authorizations: domain.Authorizations,
if db is None:
db = ScopedSession
logger.debug('create session for user %s', user.user_id)
start = datetime.now(tz=UTC)
end = start + timedelta(seconds=util.get_session_duration())
# iat - "Issued at" - the time at which the token was issued.
# util.epoch() rounds, which can land ~0.5s in the future. start_time later becomes a token's iat
# (via generate_cookie -> util.epoch), and a future iat trips PyJWT's not-yet-valid check (ImmatureSignatureError).
# Floor start to whole seconds so epoch() is exact and the issued time never leads the clock;
# start_epoch and end both derive from this same floored instant.
start = datetime.now(tz=UTC).replace(microsecond=0)
start_epoch = int((start - datetime.fromtimestamp(0, tz=util.EASTERN)).total_seconds())
# Derive end from start_epoch (not start) so the session window is anchored
# to the exact whole-second instant that is persisted and issued as iat.
end = datetime.fromtimestamp(start_epoch + util.get_session_duration(), tz=UTC)
try:
tapir_session = TapirSession(
user_id=int(user.user_id),
last_reissue=util.epoch(start),
start_time=util.epoch(start),
last_reissue=start_epoch,
start_time=start_epoch,
end_time=0
)
db.add(tapir_session)
Expand Down
44 changes: 41 additions & 3 deletions arxiv/auth/user_claims.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,11 +351,49 @@ def encode_jwt_token(self, secret: str, algorithm: str = 'HS256') -> str:


@classmethod
def decode_jwt_payload(cls, tokens: dict, jwt_payload: str, secret: str, algorithm: str = 'HS256') -> "ArxivUserClaims":
def decode_jwt_payload(cls, tokens: dict, jwt_payload: str, secret: str, algorithm: str = 'HS256',
leeway: int = 30) -> "ArxivUserClaims":
"""
Decodes the user claims.
Decode a signed JWT into an :class:`ArxivUserClaims`.

Verifies the token signature, then reassembles the claims. Registered
RFC 7519 claims (sub/exp/iat/...) live at the JWT top level, while the
arXiv custom claims travel as a JSON "hitchhiker" blob under
``NG_COOKIE_HITCHHIKER_NAME``; both are merged into ``tokens`` and
validated into the model.

Parameters
----------
tokens : dict
Base claim dict to populate; updated in place with the decoded
registered and custom claims.
jwt_payload : str
The encoded JWT to decode and verify.
secret : str
Shared secret used to verify the signature.
algorithm : str
Signing algorithm to verify against (default ``'HS256'``).
leeway : int
Seconds of clock-skew tolerance between the issuing and verifying
hosts when validating the time claims (iat/nbf/exp), so a token
issued "now" is not rejected with ImmatureSignatureError. Per
RFC 7519 this should be kept small -- "no more than a few minutes", and
30 is the lower-end of normal leeway.

Returns
-------
:class:`ArxivUserClaims`

Raises
------
ValueError
If the claims cannot be validated into an
:class:`ArxivUserClaimsModel`, or the payload cannot be decoded.
jwt.exceptions.InvalidTokenError
If signature verification or time-claim validation fails
(e.g. expired or not-yet-valid token).
"""
payload_ng = jwt.decode(jwt_payload, secret, algorithms = [algorithm])
payload_ng = jwt.decode(jwt_payload, secret, algorithms = [algorithm], leeway=leeway)
try:
payload = json.loads(payload_ng.get(NG_COOKIE_HITCHHIKER_NAME) or '{}')
except Exception as e:
Expand Down
Loading
Loading