Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
11 changes: 11 additions & 0 deletions centml/cli/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@


from centml.sdk import auth
from centml.sdk.api import get_centml_client
from centml.sdk.config import settings


Expand Down Expand Up @@ -95,6 +96,16 @@ def login(token_file):
if token_file:
auth.store_centml_cred(token_file)

# Try client credentials flow first if SERVICE_ACCOUNT_ID and SERVICE_ACCOUNT_SECRET are set
if settings.CENTML_SERVICE_ACCOUNT_ID and settings.CENTML_SERVICE_ACCOUNT_SECRET:
click.echo("Authenticating with client credentials...")
access_token = auth.authenticate_with_client_credentials()
if access_token:
with get_centml_client() as cclient:
cclient.initialize_user()
click.echo("✅ Login successful with client credentials")
return

cred = auth.load_centml_cred()
if cred is not None and auth.refresh_centml_token(cred.get("refresh_token")):
click.echo("Authenticating with stored credentials...\n")
Expand Down
3 changes: 3 additions & 0 deletions centml/sdk/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ def get_deployment_usage(
step=step,
).values

def initialize_user(self):
return self._api.setup_stripe_customer_payments_setup_post()


@contextmanager
def get_centml_client():
Expand Down
45 changes: 42 additions & 3 deletions centml/sdk/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,52 @@ def get_centml_token():
exp_time = int(jwt.decode(cred["access_token"], options={"verify_signature": False})["exp"])

if time.time() >= exp_time - 100:
cred = refresh_centml_token(cred["refresh_token"])
if cred is None:
sys.exit("Could not refresh credentials. Please login and try again...")
# Check if we have a refresh token (interactive flow) or should use client credentials
refresh_token = cred.get("refresh_token")
if refresh_token is not None:
# Use refresh token for interactive authentication
cred = refresh_centml_token(cred["refresh_token"])
if cred is None:
sys.exit("Could not refresh credentials. Please login and try again...")
else:
# No refresh token - try client credentials flow
access_token = authenticate_with_client_credentials()
if access_token is not None:
cred = {"access_token": access_token}
else:
sys.exit("Could not refresh credentials. Please login and try again...")

return cred["access_token"]


def authenticate_with_client_credentials():
"""
Authenticate using client credentials flow for service-to-service authentication.
Returns access token if successful, None otherwise.
"""
if not settings.CENTML_SERVICE_ACCOUNT_ID or not settings.CENTML_SERVICE_ACCOUNT_SECRET:
return None

params = {
'grant_type': 'client_credentials',
'client_id': settings.CENTML_SERVICE_ACCOUNT_ID,
'client_secret': settings.CENTML_SERVICE_ACCOUNT_SECRET,
'scope': 'openid profile email',
}
response = requests.post(settings.CENTML_SERVICE_ACCOUNT_TOKEN_URL, data=params, timeout=10)
response.raise_for_status()
response_data = response.json()
access_token = response_data.get('access_token')
if access_token:
# Store the access token (without refresh token for client credentials)
cred = {'access_token': access_token}
os.makedirs(os.path.dirname(settings.CENTML_CRED_FILE_PATH), exist_ok=True)
with open(settings.CENTML_CRED_FILE_PATH, "w") as f:
json.dump(cred, f)
return access_token
return None


def remove_centml_cred():
if os.path.exists(settings.CENTML_CRED_FILE_PATH):
os.remove(settings.CENTML_CRED_FILE_PATH)
8 changes: 8 additions & 0 deletions centml/sdk/config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
from typing import Optional
from pathlib import Path
from pydantic_settings import BaseSettings, SettingsConfigDict

Expand All @@ -16,5 +17,12 @@ class Config(BaseSettings):

CENTML_WORKOS_CLIENT_ID: str = os.getenv("CENTML_WORKOS_CLIENT_ID", default="client_01JP5TWW2997MF8AYQXHJEGYR0")

# Long-term credentials - can be set via environment variables
CENTML_SERVICE_ACCOUNT_SECRET: Optional[str] = os.getenv("CENTML_SERVICE_ACCOUNT_SECRET", default=None)
CENTML_SERVICE_ACCOUNT_ID: Optional[str] = os.getenv("CENTML_SERVICE_ACCOUNT_ID", default=None)
CENTML_SERVICE_ACCOUNT_TOKEN_URL: str = os.getenv(
"CENTML_SERVICE_ACCOUNT_TOKEN_URL", default="https://signin.centml.com/oauth2/token"
)


settings = Config()
Loading