Connect ODS Server

There are two ways to connect to an ASAM ODS server: directly using ConI, or using the ConIFactory helper class.


Direct ConI Usage

ConI is the core session class. You can use it directly without the factory for full control over authentication.

Username / Password

The simplest way to connect is by passing a (username, password) tuple as the auth argument:

from odsbox import ConI

with ConI(
    url="https://my.asam.server.com:8443/api",
    auth=("employee_id", "password")
) as con_i:
    units = con_i.query_data({"AoUnit": {}})

For self-signed certificates:

with ConI(
    url="https://localhost:8443/api",
    auth=("user", "pass"),
    verify_certificate=False  # Development only!
) as con_i:
    pass

Custom Auth (requests.auth.AuthBase)

For token-based or other auth schemes, pass any requests.auth.AuthBase subclass as the auth argument.

Bearer token example:

import requests
from odsbox import ConI

class BearerAuth(requests.auth.AuthBase):
    def __init__(self, token):
        self.token = token

    def __call__(self, r):
        r.headers["authorization"] = "Bearer " + self.token
        return r

with ConI(
    url="https://my.asam.server.com:8443/api",
    auth=BearerAuth("YOUR_BEARER_TOKEN")
) as con_i:
    units = con_i.query_data({"AoUnit": {}})

You can implement any requests.auth.AuthBase subclass this way — digest auth, HMAC signing, API key headers, etc.


Custom Session

If you have a fully pre-configured requests.Session (e.g., from an OAuth2 library), pass it as custom_session. When custom_session is provided, the auth and verify_certificate parameters are ignored.

import requests
from odsbox import ConI

session = requests.Session()
session.headers.update({"Authorization": "Bearer YOUR_TOKEN"})
session.verify = False  # or path to CA bundle

with ConI(
    url="https://my.asam.server.com:8443/api",
    custom_session=session
) as con_i:
    units = con_i.query_data({"AoUnit": {}})

This is particularly useful when integrating with OAuth2 libraries that provide their own requests.Session subclass.


ConIFactory Usage

ConIFactory is a convenience factory class that wraps ConI and handles the authentication setup for the most common scenarios. All methods return a ready-to-use ConI connection instance.

Authentication Flows

1. Basic Authentication (Username/Password)

Use this method for direct username/password authentication to an ODS server.

When to use:

  • Direct user credentials available

  • Development and testing environments

  • Legacy systems requiring basic auth

Method signature:

@staticmethod
def basic(
    url: str,
    username: str,
    password: str,
    *,
    verify_certificate: bool = True,
    **kwargs: Any,
) -> Any

Example:

from odsbox.con_i_factory import ConIFactory

con = ConIFactory.basic(
    url="https://my.asam.server.com:8443/api",
    username="employee_id",
    password="password"
)

with con:
    # Use the connection
    # Connection is automatically closed when exiting the with block
    pass

For self-signed certificates:

con = ConIFactory.basic(
    url="https://localhost:8443/api",
    username="user",
    password="pass",
    verify_certificate=False  # Development only!
)

2. Machine-to-Machine (M2M) Authentication

Use this method for service-to-service communication where a client authenticates directly with the OAuth2 token endpoint using a client ID and secret, without user interaction.

When to use:

  • Service-to-service communication

  • Automated scripts and batch jobs

  • Background processing

  • No user interaction available

Method signature:

@staticmethod
def m2m(
    url: str,
    token_endpoint: str,
    client_id: str,
    client_secret: str,
    *,
    scope: list[str] | None = None,
    verify_certificate: bool = True,
    **kwargs: Any,
) -> Any

Example with Keyring:

from odsbox.con_i_factory import ConIFactory
from keyring import get_password

token_endpoint = "https://example.com/auth/realms/myrealm/protocol/openid-connect/token"
client_id = "f0a8cec0-e980-48c4-9898-8a11f40da518"

# Retrieve secret from secure storage (keyring, password manager, etc.)
client_secret = get_password(token_endpoint, client_id)
assert client_secret is not None, f"Secret for {client_id} not found"

con = ConIFactory.m2m(
    url="https://my.asam.server.com:8443/api",
    token_endpoint=token_endpoint,
    client_id=client_id,
    client_secret=client_secret
)

with con:
    # Use the connection
    pass

Custom scopes:

con = ConIFactory.m2m(
    url="https://my.asam.server.com:8443/api",
    token_endpoint=token_endpoint,
    client_id=client_id,
    client_secret=client_secret,
    scope=["api", "admin"]  # Custom scopes
)

3. OIDC (OpenID Connect) Authentication

Use this method for user-facing applications where users authenticate through their browser. The OIDC endpoints are automatically discovered via WebFinger if not explicitly provided.

When to use:

  • Interactive user login flows

  • Web applications

  • Desktop applications with browser support

  • Federated authentication

  • Single sign-on (SSO) scenarios

Method signature:

@staticmethod
def oidc(
    url: str,
    client_id: str,
    redirect_uri: str,
    *,
    redirect_url_allow_insecure: bool = False,
    client_secret: str | None = None,
    scope: list[str] | None = None,
    authorization_endpoint: str | None = None,
    token_endpoint: str | None = None,
    login_timeout: int = 60,
    verify_certificate: bool = True,
    webfinger_path_prefix: str = "",
    **kwargs: Any,
) -> Any
Explicit Endpoints

If you prefer to skip WebFinger discovery or know the endpoints:

con = ConIFactory.oidc(
    url="https://server:8015/api",
    client_id="your-client-id",
    redirect_uri="http://127.0.0.1:1234",
    redirect_url_allow_insecure=True,
    authorization_endpoint="https://auth.example.com/authorize",
    token_endpoint="https://auth.example.com/token"
)

with con:
    pass
Local Development with Insecure Redirects

For development with localhost redirects (HTTP instead of HTTPS):

con = ConIFactory.oidc(
    url="https://server:8015/api",
    client_id="dev-client-id",
    redirect_uri="http://127.0.0.1:1234",
    redirect_url_allow_insecure=True,  # Allow HTTP for local development
    webfinger_path_prefix="/ods"
)

with con:
    pass
Custom Scopes
con = ConIFactory.oidc(
    url="https://server:8015/api",
    client_id="client-id",
    redirect_uri="http://127.0.0.1:1234",
    redirect_url_allow_insecure=True,
    scope=["openid", "profile", "email", "custom_api"]
)

WebFinger Discovery

ConIFactory includes a built-in WebFinger discovery feature that automatically locates OIDC endpoints on the server.

Access the discovery function directly:

from odsbox.con_i_factory import ConIFactory

auth_endpoint, token_endpoint = ConIFactory.discover_endpoints(
    ods_base_url="https://server:8015/api",
    webfinger_path_prefix="/ods"  # Optional
)

print(f"Auth: {auth_endpoint}")
print(f"Token: {token_endpoint}")

This is useful if you need to:

  • Inspect the discovered endpoints before creating a connection

  • Cache endpoints for later use

  • Validate server configuration


Common Patterns

Pattern 1: Secret Retrieval from Keyring

Store secrets securely and retrieve them at runtime:

from odsbox.con_i_factory import ConIFactory
from keyring import get_password

# For basic auth
url = "https://server:8443/api"
user = "employee_id"
password = get_password(url, user)
assert password is not None, f"Password for {user} not found"

con = ConIFactory.basic(url=url, username=user, password=password)

Pattern 2: Configuration from Environment

Load connection details from environment variables:

import os
from odsbox.con_i_factory import ConIFactory

con = ConIFactory.oidc(
    url=os.getenv("ODS_URL"),
    client_id=os.getenv("ODS_OIDC_CLIENT_ID"),
    webfinger_path_prefix=os.getenv("ODS_OIDC_WEBFINGER_PREFIX", "")
)

Pattern 3: Fallback Authentication

Try OIDC first, fall back to basic auth:

from odsbox.con_i_factory import ConIFactory

try:
    con = ConIFactory.oidc(...)
except Exception:
    con = ConIFactory.basic(...)

Pattern 4: Additional ConI Parameters

Pass advanced options to the underlying ConI:

con = ConIFactory.basic(
    url="https://server/api",
    username="user",
    password="pass",
    timeout=30,           # Additional ConI parameter
    verify_certificate=False
)

Error Handling

OIDC Errors

from odsbox.con_i_factory import ConIFactory

try:
    con = ConIFactory.oidc(
        url="https://server/api",
        client_id="client-id",
        redirect_uri="http://invalid-uri"  # Missing port
    )
except ValueError as e:
    print(f"Invalid redirect URI: {e}")

try:
    con = ConIFactory.oidc(
        url="https://server/api",
        client_id="client-id",
        login_timeout=5
    )
except ValueError as e:
    print(f"Login timed out: {e}")

WebFinger Discovery Errors

try:
    auth_ep, token_ep = ConIFactory.discover_endpoints(
        ods_base_url="https://invalid-server/api"
    )
except ValueError as e:
    print(f"Discovery failed: {e}")
    # Fall back to explicit endpoints

Performance Considerations
  • WebFinger Discovery: First call discovers endpoints, subsequent calls reuse them

  • Token Caching: OAuth2Session automatically caches and refreshes tokens

  • Connection Pooling: ConI handles HTTP connection pooling


Security Best Practices

  1. Never hardcode secrets:

    # ❌ DO NOT DO THIS
    con = ConIFactory.m2m(..., client_secret="hardcoded_secret")
    
    # ✅ Use keyring or environment variables instead
    con = ConIFactory.m2m(..., client_secret=get_password(...))
    
  2. Verify certificates in production:

    # ❌ Development only
    con = ConIFactory.basic(..., verify_certificate=False)
    
    # ✅ Production (default)
    con = ConIFactory.basic(...)  # verify_certificate=True by default
    
  3. Use HTTPS redirect URIs for OIDC:

    # ❌ Development only
    con = ConIFactory.oidc(..., redirect_uri="http://localhost:1234", redirect_url_allow_insecure=True)
    
    # ✅ Production
    con = ConIFactory.oidc(..., redirect_uri="https://app.example.com/callback")
    
  4. OIDC: Use client secrets securely:

    from keyring import get_password
    
    client_secret = get_password(token_endpoint, client_id)
    con = ConIFactory.oidc(..., client_secret=client_secret)
    

Troubleshooting

“OAUTHLIB_INSECURE_TRANSPORT” Error

Problem: OIDC with HTTP redirect fails

Solution: Use redirect_url_allow_insecure=True for local development

con = ConIFactory.oidc(
    ...,
    redirect_uri="http://127.0.0.1:1234",
    redirect_url_allow_insecure=True
)

WebFinger Discovery Fails

Problem: “OIDC issuer not found” or “WebFinger request failed”

Solution: Try explicit endpoints or adjust webfinger_path_prefix

# Option 1: Use explicit endpoints
con = ConIFactory.oidc(
    url=url,
    client_id=client_id,
    authorization_endpoint="https://...",
    token_endpoint="https://..."
)

# Option 2: Adjust WebFinger path prefix
con = ConIFactory.oidc(
    url=url,
    client_id=client_id,
    webfinger_path_prefix="/ods"  # or "/api", etc.
)

Certificate Verification Fails

Problem: “SSL: CERTIFICATE_VERIFY_FAILED” or similar

Solution:

For development:

con = ConIFactory.basic(..., verify_certificate=False)

For production, ensure your CA certificate bundle is up-to-date.


API Reference

See the docstrings in con_i_factory.py for full API details:

from odsbox.con_i_factory import ConIFactory

help(ConIFactory.basic)
help(ConIFactory.m2m)
help(ConIFactory.oidc)
help(ConIFactory.discover_endpoints)

Use this method for direct username/password authentication to an ODS server.

When to use:

  • Direct user credentials available

  • Development and testing environments

  • Legacy systems requiring basic auth

Method signature:

@staticmethod
def basic(
    url: str,
    username: str,
    password: str,
    *,
    verify_certificate: bool = True,
    **kwargs: Any,
) -> Any

Example:

from odsbox.con_i_factory import ConIFactory

con = ConIFactory.basic(
    url="https://my.asam.server.com:8443/api",
    username="employee_id",
    password="password"
)

with con:
    # Use the connection
    # Connection is automatically closed when exiting the with block
    pass

For self-signed certificates:

con = ConIFactory.basic(
    url="https://localhost:8443/api",
    username="user",
    password="pass",
    verify_certificate=False  # Development only!
)