Skip to content

Conversation

@thomass-dev
Copy link
Collaborator

@thomass-dev thomass-dev commented Sep 19, 2025

Closes #2037.


Create a new URI object that is persisted next to the token during skore-hub-login.
Also, check at runtime the coherence of the URI that user wants to reach: if the token's URI is different of the envar URI, raise an exception.

Comment on lines -125 to -140


def test_post_oauth_refresh_token(respx_mock):
route = respx_mock.post(urljoin(URI, "identity/oauth/token/refresh")).mock(
Response(
200,
json={"access_token": "A", "refresh_token": "B", "expires_at": "C"},
)
)

access_token, refresh_token, expires_at = api.post_oauth_refresh_token("token")

assert route.calls.last.request.read() == b'{"refresh_token":"token"}'
assert access_token == "A"
assert refresh_token == "B"
assert expires_at == "C"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to test_token.py.

Comment on lines -140 to -174
def post_oauth_refresh_token(refresh_token: str):
"""Refresh an access token using a provided refresh token.
This endpoint allows a client to obtain a new access token
by providing a valid refresh token.
Parameters
----------
refresh_token : str
A valid refresh token
Returns
-------
tuple
A tuple containing:
- access_token : str
The OAuth access token
- refresh_token : str
The OAuth refresh token
- expires_at : str
The expiration datetime as ISO 8601 str of the access token
"""
from skore_hub_project.client.client import HUBClient

url = "identity/oauth/token/refresh"
json = {"refresh_token": refresh_token}

with HUBClient(authenticated=False) as client:
response = client.post(url, json=json).json()

return (
response["access_token"],
response["refresh_token"],
response["expires_at"],
)
Copy link
Collaborator Author

@thomass-dev thomass-dev Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to token.py.

Comment on lines 9 to 137
def get_oauth_device_login(success_uri: str | None = None):
"""Initiate device OAuth flow.
Initiates the OAuth device flow.
Provides the user with a URL and a OTP code to authenticate the device.
Parameters
----------
success_uri : str, optional
The URI to redirect to after successful authentication.
If not provided, defaults to None.
Returns
-------
tuple
A tuple containing:
- authorization_url: str
The URL to which the user needs to navigate
- device_code: str
The device code used for authentication
- user_code: str
The user code that needs to be entered on the authorization page
"""
from skore_hub_project.client.client import HUBClient

url = "identity/oauth/device/login"
params = {"success_uri": success_uri} if success_uri is not None else {}

with HUBClient(authenticated=False) as client:
response = client.get(url, params=params).json()

return (
response["authorization_url"],
response["device_code"],
response["user_code"],
)


def post_oauth_device_callback(state: str, user_code: str):
"""Validate the user-provided device code.
This endpoint verifies the code entered by the user during the device auth flow.
Parameters
----------
state: str
The unique value identifying the device flow.
user_code: str
The code entered by the user.
"""
from skore_hub_project.client.client import HUBClient

url = "identity/oauth/device/callback"
data = {"state": state, "user_code": user_code}

with HUBClient(authenticated=False) as client:
client.post(url, data=data)


def get_oauth_device_token(device_code: str):
"""Exchanges the device code for an access token.
This endpoint completes the device authorization flow
by exchanging the validated device code for an access token.
Parameters
----------
device_code : str
The device code to exchange for tokens
Returns
-------
tuple
A tuple containing:
- access_token : str
The OAuth access token
- refresh_token : str
The OAuth refresh token
- expires_at : str
The expiration datetime as ISO 8601 str of the access token
"""
from skore_hub_project.client.client import HUBClient

url = "identity/oauth/device/token"
params = {"device_code": device_code}

with HUBClient(authenticated=False) as client:
response = client.get(url, params=params).json()
tokens = response["token"]

return (
tokens["access_token"],
tokens["refresh_token"],
tokens["expires_at"],
)


def get_oauth_device_code_probe(device_code: str, *, timeout=600):
"""Ensure authorization code is acknowledged.
Start polling, wait for the authorization code to be acknowledged by the hub.
This is mandatory to be authorize to exchange with a token.
Parameters
----------
device_code : str
The device code to exchange for tokens.
"""
from skore_hub_project.client.client import HUBClient

url = "identity/oauth/device/code-probe"
params = {"device_code": device_code}

with HUBClient(authenticated=False) as client:
start = datetime.now()

while True:
try:
client.get(url, params=params)
except HTTPError as exc:
if exc.response.status_code != 400:
raise

if (datetime.now() - start).total_seconds() >= timeout:
raise TimeoutException("Authentication timeout") from exc

sleep(0.5)
else:
break
Copy link
Collaborator Author

@thomass-dev thomass-dev Oct 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to login.py.

Comment on lines -11 to -124
@mark.parametrize("success_uri", [None, "toto"])
def test_get_oauth_device_login(respx_mock, success_uri):
respx_mock.get(urljoin(URI, "identity/oauth/device/login")).mock(
Response(
200,
json={
"authorization_url": "A",
"device_code": "B",
"user_code": "C",
},
)
)

params = {} if success_uri is None else {"success_uri": "toto"}
authorization_url, device_code, user_code = api.get_oauth_device_login(
success_uri=success_uri
)

assert dict(respx_mock.calls.last.request.url.params) == params
assert authorization_url == "A"
assert device_code == "B"
assert user_code == "C"


def test_post_oauth_device_callback(respx_mock):
route = respx_mock.post(urljoin(URI, "identity/oauth/device/callback")).mock(
Response(201, json={})
)

api.post_oauth_device_callback("my_state", "my_user_code")

assert route.called
assert route.calls.last.request.read() == b"state=my_state&user_code=my_user_code"


def test_get_oauth_device_token(respx_mock):
respx_mock.get(urljoin(URI, "identity/oauth/device/token")).mock(
Response(
200,
json={
"token": {
"access_token": "A",
"refresh_token": "B",
"expires_at": "C",
}
},
)
)

access_token, refresh_token, expires_at = api.get_oauth_device_token(
"<device_code>"
)

assert access_token == "A"
assert refresh_token == "B"
assert expires_at == "C"
assert dict(respx_mock.calls.last.request.url.params) == {
"device_code": "<device_code>"
}


def test_get_oauth_device_code_probe(monkeypatch, respx_mock):
monkeypatch.setattr("skore_hub_project.client.api.sleep", lambda _: None)
respx_mock.get(urljoin(URI, "identity/oauth/device/code-probe")).mock(
side_effect=[
Response(400),
Response(400),
Response(200),
]
)

api.get_oauth_device_code_probe("<device_code>")

assert len(respx_mock.calls) == 3
assert dict(respx_mock.calls.last.request.url.params) == {
"device_code": "<device_code>",
}


def test_get_oauth_device_code_probe_exception(respx_mock):
respx_mock.get(urljoin(URI, "identity/oauth/device/code-probe")).mock(
side_effect=[
Response(404),
Response(400),
Response(200),
]
)

with raises(HTTPError) as excinfo:
api.get_oauth_device_code_probe("<device_code>")

assert excinfo.value.response.status_code == 404
assert len(respx_mock.calls) == 1
assert dict(respx_mock.calls.last.request.url.params) == {
"device_code": "<device_code>",
}


def test_get_oauth_device_code_probe_timeout(respx_mock):
respx_mock.get(urljoin(URI, "identity/oauth/device/code-probe")).mock(
side_effect=[
Response(400),
Response(400),
Response(200),
]
)

with raises(TimeoutException):
api.get_oauth_device_code_probe("<device_code>", timeout=0)

assert len(respx_mock.calls) == 1
assert dict(respx_mock.calls.last.request.url.params) == {
"device_code": "<device_code>",
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to test_login.py.

@thomass-dev thomass-dev force-pushed the skore-hub-project-store-uri-in-token branch from 9bf4946 to 05d47de Compare October 2, 2025 19:15
@github-actions
Copy link
Contributor

github-actions bot commented Oct 2, 2025

Coverage

Coverage Report for skore-hub-project/
FileStmtsMissCoverMissing
skore-hub-project/src/skore_hub_project
   __init__.py22195%39
   protocol.py290100% 
skore-hub-project/src/skore_hub_project/artifact
   __init__.py00100% 
   artifact.py230100% 
   serializer.py260100% 
   upload.py36488%175, 177–178, 180
skore-hub-project/src/skore_hub_project/artifact/media
   __init__.py50100% 
   data.py220100% 
   feature_importance.py340100% 
   media.py100100% 
   model.py90100% 
   performance.py440100% 
skore-hub-project/src/skore_hub_project/artifact/pickle
   __init__.py20100% 
   pickle.py240100% 
skore-hub-project/src/skore_hub_project/authentication
   __init__.py00100% 
   login.py660100% 
   logout.py40100% 
   token.py240100% 
   uri.py180100% 
skore-hub-project/src/skore_hub_project/client
   __init__.py00100% 
   client.py600100% 
skore-hub-project/src/skore_hub_project/metric
   __init__.py100100% 
   accuracy.py350100% 
   brier_score.py350100% 
   log_loss.py350100% 
   metric.py49197%25
   precision.py630100% 
   r2.py350100% 
   recall.py630100% 
   rmse.py350100% 
   roc_auc.py350100% 
   timing.py90495%52–53, 115–116
skore-hub-project/src/skore_hub_project/project
   __init__.py00100% 
   project.py102397%204, 308, 338
skore-hub-project/src/skore_hub_project/report
   __init__.py30100% 
   cross_validation_report.py64198%204
   estimator_report.py120100% 
   report.py520100% 
TOTAL11761498% 

Tests Skipped Failures Errors Time
139 0 💤 0 ❌ 0 🔥 1m 30s ⏱️

@thomass-dev thomass-dev force-pushed the skore-hub-project-store-uri-in-token branch from ee11cf5 to 8302602 Compare October 23, 2025 12:48
@thomass-dev thomass-dev changed the title feat(skore-hub-project): Store URI in the token feat(skore-hub-project): Persist URI used for authentication Oct 23, 2025
@thomass-dev thomass-dev marked this pull request as ready for review October 23, 2025 13:17
rouk1

This comment was marked as off-topic.

Copy link
Contributor

@rouk1 rouk1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like it fits the requirements.

@thomass-dev thomass-dev merged commit 414123f into main Oct 24, 2025
36 checks passed
@thomass-dev thomass-dev deleted the skore-hub-project-store-uri-in-token branch October 24, 2025 07:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(skore-hub-project): Make skore-hub-login persist API server URI for notebook use

3 participants