Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auth0 support #284

Merged
merged 7 commits into from
Oct 18, 2024
Merged
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
508 changes: 316 additions & 192 deletions poetry.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ qi = "quantuminspire.cli.command_list:app"
python = "^3.9"
typer = {extras = ["all"], version = "^0.12.5"}
pydantic = "^2.9.2"
qi-compute-api-client = ">=0.33.0"
qi-compute-api-client = "^0.38.0"
qxelarator = {version = "^0.7.1", optional = true}
pydantic-settings = "^2.6.0"
qiskit = "1.0.2"
Expand Down
9 changes: 6 additions & 3 deletions quantuminspire/cli/command_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,9 @@ def list_projects(

@projects_app.command("sync")
def sync_projects(
dest: Destination = typer.Option(Destination.LOCAL, help="The target system with which to synchronize the projects")
dest: Destination = typer.Option(
Destination.LOCAL, help="The target system with which to synchronize the projects"
),
) -> None:
"""Sync project.

Expand Down Expand Up @@ -311,7 +313,7 @@ def get_final_results(job_id: int = typer.Argument(..., help="The id of the run"
def login(
host: str = typer.Argument(
"https://api.qi2.quantum-inspire.com", help="The URL of the platform to which to connect"
)
),
) -> None:
"""Log in to Quantum Inspire.

Expand All @@ -325,6 +327,7 @@ def login(

login_info = auth_session.initialize_authorization()
typer.echo(f"Please continue logging in by opening: {login_info['verification_uri_complete']} in your browser")
typer.echo(f"If promped to verify a code, please confirm it is as follows: {login_info['user_code']}")
webbrowser.open(login_info["verification_uri_complete"], new=2)
tokens = auth_session.poll_for_tokens()
settings.store_tokens(host_url, tokens)
Expand All @@ -336,7 +339,7 @@ def login(
def logout(
host: str = typer.Argument(
"https://api.qi2.quantum-inspire.com", help="The URL of the platform from which to log out"
)
),
) -> None:
"""Log out of Quantum Inspire.

Expand Down
9 changes: 4 additions & 5 deletions quantuminspire/util/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,15 @@ def _get_endpoints(self) -> Tuple[str, str]:
config = response.json()
return config["token_endpoint"], config["device_authorization_endpoint"]

def initialize_authorization(self, scope: str = "api-access openid") -> Dict[str, Any]:
def initialize_authorization(self) -> Dict[str, Any]:
code_verifier = self._oauth_client.create_code_verifier(self._settings.code_verifyer_length)
self._oauth_client.create_code_challenge(code_verifier, self._settings.code_challenge_method)
data = {
"client_id": self._client_id,
"code_challenge_method": self._settings.code_challenge_method,
"code_challenge": self._oauth_client.code_challenge,
"scope": scope,
"audience": self._settings.audience,
"scope": self._settings.scope,
}

response = requests.post(self._device_endpoint, data=data, headers=self._headers).json()
Expand All @@ -69,7 +70,7 @@ def request_token(self) -> TokenInfo:

response = requests.post(self._token_endpoint, data=data, headers=self._headers)

if response.status_code == 400:
if response.status_code >= 400:
content = response.json()
if content["error"] == "authorization_pending":
raise AuthorisationPending(content["error"])
Expand All @@ -85,7 +86,6 @@ def request_token(self) -> TokenInfo:
raise AuthorisationError(f"Received status code: {response.status_code}\n {response.text}")

def poll_for_tokens(self) -> TokenInfo:

while time.monotonic() < self.expires_at:
try:
return self.request_token()
Expand Down Expand Up @@ -117,7 +117,6 @@ def refresh(self) -> TokenInfo:


class Configuration(compute_api_client.Configuration): # type: ignore[misc]

def __init__(self, host: str, oauth_session: OauthDeviceSession, **kwargs: Any):
self._oauth_session = oauth_session
super().__init__(host=host, **kwargs)
Expand Down
12 changes: 5 additions & 7 deletions quantuminspire/util/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
{
"auths": {
"https://staging.qi2.quantum-inspire.com": {
"well_known_endpoint": "https://auth.qi2.quantum-inspire.com/realms/oidc_staging/.well-known/openid-configuration"
"well_known_endpoint": "https://quantum-inspire-staging.eu.auth0.com/.well-known/openid-configuration"
},
"https://api.qi2.quantum-inspire.com": {
"well_known_endpoint": "https://auth.qi2.quantum-inspire.com/realms/oidc_production/.well-known/openid-configuration"
Expand Down Expand Up @@ -72,24 +72,22 @@ class TokenInfo(BaseModel):
access_token: str
expires_in: int
refresh_token: str
refresh_expires_in: int
generated_at: float = Field(default_factory=time.time)

@property
def access_expires_at(self) -> float:
"""Timestamp containing the time when the access token will expire."""
return self.generated_at + self.expires_in

@property
def refresh_expires_at(self) -> float:
"""Timestamp containing the time when the refresh token will expire."""
return self.generated_at + self.refresh_expires_in


class AuthSettings(BaseModel):
"""Pydantic model for storing all auth related settings for a given host."""

client_id: str = "compute-job-manager"
audience: str = "compute-job-manager"
# Keycloak requires api-access in scope for compute-job-manager audience
# Auth0 requires offline_access in scopefor sending a refresh token
scope: str = "api-access openid profile email offline_access"
code_challenge_method: str = "S256"
code_verifyer_length: int = 64
well_known_endpoint: Url = (
Expand Down
4 changes: 1 addition & 3 deletions tests/util/test_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def test_force_file_into_existence_file_does_not_exist(mocked_config_file: Magic
{
"auths": {
"https://staging.qi2.quantum-inspire.com": {
"well_known_endpoint": "https://auth.qi2.quantum-inspire.com/realms/oidc_staging/.well-known/openid-configuration"
"well_known_endpoint": "https://quantum-inspire-staging.eu.auth0.com/.well-known/openid-configuration"
},
"https://api.qi2.quantum-inspire.com": {
"well_known_endpoint": "https://auth.qi2.quantum-inspire.com/realms/oidc_production/.well-known/openid-configuration"
Expand Down Expand Up @@ -64,7 +64,6 @@ def test_json_config_settings_file_does_exist(mocked_config_file: MagicMock) ->


def test_json_config_settings_qi2_813(mocked_config_file: MagicMock) -> None:

settings = configuration.Settings()

assert settings.auths["https://host"].well_known_endpoint == "https://some_url"
Expand Down Expand Up @@ -96,7 +95,6 @@ def test_settings_from_init(mocked_config_file: MagicMock) -> None:

def test_tokeninfo() -> None:
assert EXAMPLE_TOKENINFO.access_expires_at == 10100
assert EXAMPLE_TOKENINFO.refresh_expires_at == 10200


def test_store_tokens(mocked_config_file: MagicMock, mocker: MockerFixture) -> None:
Expand Down