pyoidc

repository·master·Indexed 20 days ago

https://github.com/cz-nic/pyoidc

A Python implementation of OpenID Connect (OIDC) providing tools for both OpenID Providers (OP) and Relying Parties (RP). The library includes utilities for managing OIDC clients, handling authorization requests with specific claims, and supporting client assertions for authentication, including custom JWT assertions for providers like Microsoft Azure AD.

Tokens
24.6K
Snippets
80
Records
109
Agent score
70%

What's inside pyoidc

  1. Overview of the OIDCProvider example

    master
    The OIDCProvider is a simplified OpenID Connect (OIDC) Provider implementation based on the pyoidc library. It serves as a practical example of how to build an OIDC Provider using the core components of pyoidc. This specific implementation is a streamlined version of the more complex op2 example found in the main repository.
  2. Set up a multi-authentication chain

    master

    Multi-authentication allows requiring multiple methods (e.g., SAML + Username/Password) before a user is authenticated.

    Steps to implement:

    1. Configure the chain: In your OP configuration, add a key to the AUTHENTICATION dictionary representing the chain (e.g., "SamlPass") and assign it an ACR.
    2. Instantiate methods: Create singleton instances of the authentication method classes. If using SAML in both single and multi-auth contexts, use AuthnIndexedEndpointWrapper to provide unique indices to the SP endpoints.
    3. Create the chain: Use oic.utils.authn.multi_auth.setup_multi_auth with a list of tuples [(method_instance, callback_endpoint_regex), ...]. The regex should match the path the login page returns to.
    4. Register with Broker: Add the object returned by setup_multi_auth to your AuthnBroker instance.
    5. RP Request: The Relying Party (RP) can now request this chain by specifying the configured ACR.
  3. Run tests across multiple Python versions using Tox

    master

    PyOIDC uses tox to ensure compatibility across various Python versions.

    1. Install tox via pip: pip install tox

    2. List available test environments: tox -l

    3. Run tests in a specific environment (e.g., py36): tox -e <environment_name>

    $ pip install tox
    $ tox -l
    $ tox -e py36
  4. Set up the OP2 Example environment

    master

    To run the OP2 example, clone the repository, navigate to the example directory, and set up a Python virtual environment with the required dependencies using the provided constraints file.

    git clone https://github.com/CZ-NIC/pyoidc.git
    cd pyoidc/oidc_example/op2/
    python3 -m venv venv && . venv/bin/activate
    pip install -r requirements.txt -c constraints.txt
    git clone https://github.com/CZ-NIC/pyoidc.git
    cd pyoidc/oidc_example/op2/
    python3 -m venv venv && . venv/bin/activate
    pip install -r requirements.txt -c constraints.txt
  5. Configure a Client with static Provider and Registration info

    master

    If you cannot use dynamic discovery or registration, you can manually configure the Client instance using pre-obtained information.

    1. Provider Configuration: Use client.handle_provider_config() with a ProviderConfigurationResponse object.
    2. Client Registration: Use client.store_registration_info() with a RegistrationResponse object containing your client_id and client_secret.
    from oic.oic.message import ProviderConfigurationResponse, RegistrationResponse
    
    # 1. Static Provider Configuration
    op_info = ProviderConfigurationResponse(
        version="1.0", 
        issuer="https://example.org/OP/1",
        authorization_endpoint="https://example.org/OP/1/authz",
        token_endpoint="https://example.org/OP/1/token"
    )
    client.handle_provider_config(op_info, op_info['issuer'])
    
    # 2. Static Client Registration
    reg_data = {"client_id": "1234567890", "client_secret": "abcdefghijklmnop"}
    client_reg = RegistrationResponse(**reg_data)
    client.store_registration_info(client_reg)
  6. Run the simple_op and simple_rp examples together

    master

    You can run both the OpenID Provider (simple_op) and the Relying Party (simple_rp) on the same machine by assigning them different ports. Note that simple_op typically runs on the standard HTTPS port (443), while simple_rp can be configured to run on a different port (e.g., 8000).

    1. Start the OP server

    Run the server in the simple_op directory using a configuration file and specifying the port:

    python src/run.py settings.yaml.example -p 443

    2. Start the RP server

    Run the server in the simple_rp directory using a configuration file and specifying a different port:

    python src/rp.py settings.yaml.example -p 8000

    3. Complete the Authentication Flow

    1. Open <https://localhost:8000/> in your browser.
    2. When prompted, enter the UID localhost to connect to the simple_op server.
    3. Login using the credentials defined in simple_op/passwd.json (this file is referenced in the simple_op settings).
    4. Verify that the user info is successfully loaded by the RP server.
    # Start OP
    python src/run.py settings.yaml.example -p 443
    
    # Start RP
    python src/rp.py settings.yaml.example -p 8000
  7. Implement the Authorization Code Flow

    master

    The Authorization Code Flow is a multi-step process used to retrieve user information.

    1. Construct the Authentication Request: Create an authentication request using client.construct_AuthorizationRequest. You must provide a state (to track the request) and a nonce (to mitigate replay attacks). These should be stored in a user session.
    2. Redirect the User: Use the generated URL to redirect the user to the OP's authorization endpoint.
    3. Parse the Response: Once the user is redirected back to your redirect_uri, parse the response using client.parse_response with the AuthorizationResponse message type.
    4. Exchange Code for Token: Use the code from the response to call client.do_access_token_request.
    5. Fetch User Info: Use the resulting state to call client.do_user_info_request to get user details.
    from oic import rndstr
    from oic.utils.http_util import Redirect
    from oic.oic.message import AuthorizationResponse
    
    # 1. Construct and Redirect
    session["state"] = rndstr()
    session["nonce"] = rndstr()
    args = {
        "client_id": client.client_id,
        "response_type": "code",
        "scope": ["openid"],
        "nonce": session["nonce"],
        "redirect_uri": client.registration_response["redirect_uris"][0],
        "state": session["state"]
    }
    auth_req = client.construct_AuthorizationRequest(request_args=args)
    login_url = auth_req.request(client.authorization_endpoint)
    # return Redirect(login_url)
    
    # 2. Parse Response (after redirect back)
    # response = environ["QUERY_STRING"]
    # aresp = client.parse_response(AuthorizationResponse, info=response, sformat="urlencoded")
    
    # 3. Exchange Code for Token
    # args = {"code": aresp["code"]}
    # resp = client.do_access_token_request(state=aresp["state"], request_args=args, authn_method="client_secret_basic")
    
    # 4. Get User Info
    # userinfo = client.do_user_info_request(state=aresp["state"])
  8. Run the OP2 server

    master

    Start the OP2 server by executing server.py. You must specify a port using -p and provide a configuration file (e.g., config_simple.py).

    ./server.py -p 8040 config_simple.py

    Use ./server.py --help to view available command-line options.

    ./server.py -p 8040 config_simple.py
  9. Use Client Assertions for authentication

    master

    Instead of a client_secret, you can use client assertions (e.g., private_key_jwt) to authenticate at the IdP token endpoint. This is supported via the do_access_token_request method of the oic.oic.Client class using specific keyword arguments:

    • authn_method: Set to "private_key_jwt" to initiate assertion-based authentication.
    • algorithm: The signing algorithm (e.g., "RS256").
    • authn_endpoint: The endpoint used for authentication (e.g., 'token').

    To use this, the client's keyjar must contain the appropriate signing key. For example, a KeyBundle containing the key with use: "sig" must be assigned to the client's keyjar.

    kwargs = dict(algorithm="RS256", authn_endpoint='token',
                  authn_method="private_key_jwt")
    
    # The client must have the key in its keyjar
    # client.keyjar[""] = KeyBundle([{"key": _key, "kty": "RSA", "use": "sig"}])
    
    client.do_access_token_request(**kwargs)
  10. Manage OIDC clients with client_management.py

    master

    The client_management.py utility is used to manage the client database (e.g., client_db). You must create clients with valid redirect_uris before starting the server. The tool automatically generates a client_id and client_secret for you.

    Commands:

    • Add a new client: Use -c <database_file> and follow the interactive prompts.
    • List all clients: Use -l <database_file>.
    • Show specific client details: Use -s -i <client_id> <database_file> to see the JSON representation of a client's configuration.
    • Help: Use -h <database_file> to see all available options.
    python ../../src/oic/utils/client_management.py -c client_db