{"id":18518428,"url":"https://github.com/identitypython/pyop","last_synced_at":"2025-04-08T04:10:37.999Z","repository":{"id":10769379,"uuid":"66910459","full_name":"IdentityPython/pyop","owner":"IdentityPython","description":"OpenID Connect Provider (OP) library in Python.","archived":false,"fork":false,"pushed_at":"2024-11-06T11:25:49.000Z","size":229,"stargazers_count":92,"open_issues_count":2,"forks_count":33,"subscribers_count":12,"default_branch":"master","last_synced_at":"2025-04-01T03:25:09.127Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/IdentityPython.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2016-08-30T05:46:52.000Z","updated_at":"2025-03-02T09:10:19.000Z","dependencies_parsed_at":"2023-01-13T16:08:38.099Z","dependency_job_id":"2001a2f3-63ae-44aa-aa14-bd58f2b9b9c4","html_url":"https://github.com/IdentityPython/pyop","commit_stats":{"total_commits":125,"total_committers":15,"mean_commits":8.333333333333334,"dds":0.696,"last_synced_commit":"c7b9ff666121a93933d132e8948b9b856e8059b5"},"previous_names":[],"tags_count":24,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/IdentityPython%2Fpyop","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/IdentityPython%2Fpyop/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/IdentityPython%2Fpyop/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/IdentityPython%2Fpyop/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/IdentityPython","download_url":"https://codeload.github.com/IdentityPython/pyop/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247773719,"owners_count":20993639,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-06T17:13:20.135Z","updated_at":"2025-04-08T04:10:37.980Z","avatar_url":"https://github.com/IdentityPython.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# pyOP\n[![Build Status](https://travis-ci.org/IdentityPython/pyop.svg)](https://travis-ci.org/IdentityPython/pyop)\n[![PyPI](https://img.shields.io/pypi/v/pyop.svg)](https://pypi.python.org/pypi/pyop)\n\n\nOpenID Connect Provider (OP) library in Python.\nUses [pyoidc](https://github.com/rohe/pyoidc/) and\n[pyjwkest](https://github.com/rohe/pyjwkest).\n\n# Provider implementations using pyOP\n* [se-leg-op](https://github.com/SUNET/se-leg-op)\n* [SATOSA OIDC frontend](https://github.com/its-dirg/SATOSA/blob/master/src/satosa/frontends/openid_connect.py)\n* [local example](example/views.py)\n\n# Introduction\n\npyOP is a high-level library intended to be usable in any web server application.\nBy only providing the core functionality for OpenID Connect the application can freely choose to implement any kind of\nauthentication mechanisms, while pyOP provides a simple interface for the OpenID Connect messages to send back to\nclients.\n\n## OpenID Connect support\n* [Dynamic Provider Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html)\n* [Dynamic Client Registration](https://openid.net/specs/openid-connect-registration-1_0.html)\n* [Core](http://openid.net/specs/openid-connect-core-1_0.html)\n    * [Authorization Code Flow](http://openid.net/specs/openid-connect-core-1_0.html#CodeFlowSteps)\n    * [Implicit Flow](http://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth)\n    * [Hybrid Flow](http://openid.net/specs/openid-connect-core-1_0.html#HybridFlowAuth)\n    * Claims\n        * [Requesting Claims using Scope Values](http://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims)\n        * [Claims Request Parameter](http://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter)\n* Crypto support\n    * Currently only supports issuing signed ID Tokens\n\n# Configuration\nThe provider instance can be configured through the provider configuration information. In the following example, a\nprovider instance is initiated to use a MongoDB instance as its backend storage:\n\n```python\nfrom jwkest.jwk import rsa_load, RSAKey\n\nfrom pyop.authz_state import AuthorizationState\nfrom pyop.provider import Provider\nfrom pyop.storage import MongoWrapper\nfrom pyop.subject_identifier import HashBasedSubjectIdentifierFactory\nfrom pyop.userinfo import Userinfo\n\nsigning_key = RSAKey(key=rsa_load('signing_key.pem'), use='sig', alg='RS256')\nconfiguration_information = {\n    'issuer': 'https://example.com',\n    'authorization_endpoint': 'https://example.com/authorization',\n    'token_endpoint': 'https://example.com/token',\n    'userinfo_endpoint': 'https://example.com/userinfo',\n    'registration_endpoint': 'https://example.com/registration',\n    'response_types_supported': ['code', 'id_token token'],\n    'id_token_signing_alg_values_supported': [signing_key.alg],\n    'response_modes_supported': ['fragment', 'query'],\n    'subject_types_supported': ['public', 'pairwise'],\n    'grant_types_supported': ['authorization_code', 'implicit'],\n    'claim_types_supported': ['normal'],\n    'claims_parameter_supported': True,\n    'claims_supported': ['sub', 'name', 'given_name', 'family_name'],\n    'request_parameter_supported': False,\n    'request_uri_parameter_supported': False,\n    'scopes_supported': ['openid', 'profile']\n}\n\nsubject_id_factory = HashBasedSubjectIdentifierFactory(sub_hash_salt)\nauthz_state = AuthorizationState(subject_id_factory,\n                                 MongoWrapper(db_uri, 'provider', 'authz_codes'),\n                                 MongoWrapper(db_uri, 'provider', 'access_tokens'),\n                                 MongoWrapper(db_uri, 'provider', 'refresh_tokens'),\n                                 MongoWrapper(db_uri, 'provider', 'subject_identifiers'))\nclient_db = MongoWrapper(db_uri, 'provider', 'clients')\nuser_db = MongoWrapper(db_uri, 'provider', 'users')\nprovider = Provider(signing_key, configuration_information, authz_state, client_db, Userinfo(user_db))\n```\n\nwhere `db_uri` is the [MongoDB connection URI](https://docs.mongodb.com/manual/reference/connection-string/) and\n`sub_hash_salt` is a secret string to use as a salt when creating hash based subject identifiers.\n\n## Token lifetimes\nThe ID token lifetime (in seconds) can be supplied to the `Provider` constructor with `id_token_lifetime`, e.g.:\n\n```python\nProvider(..., id_token_lifetime=600)\n```\nIf not specified it will default to 1 hour.\n\nThe lifetime of authorization codes, access tokens, and refresh tokens is configured in the `AuthorizationState`, e.g.:\n\n```python\nAuthorizationState(..., authorization_code_lifetime=300, access_token_lifetime=60*60*24,\n                   refresh_token_lifetime=60*60*24*365, refresh_token_threshold=None)\n```\n\nIf not specified the lifetimes defaults to the following values:\n* Authorization codes are valid for 10 minutes.\n* Access tokens are valid for 1 hour.\n* Refresh tokens are not issued.\n\nTo make sure refresh tokens are issued in response to code exchange token requests, specify a\n`refresh_token_lifetime` \u003e 0.\nTo make sure refresh tokens are renewed if they are close to expiry in response to refresh token requests,\nspecify a `refresh_token_threshold` \u003e 0.\n\n# Dynamic discovery: Provider Configuration Information\nTo publish the provider configuration information at an endpoint, use `Provider.provider_configuration`.\n\nThe following example illustrates the high-level idea:\n\n```python\n@app.route('/.well-known/openid-configuration')\ndef provider_config():\n    return HTTPResponse(provider.provider_configuration.to_json(), content_type=\"application/json\")\n```\n\n# Authorization endpoint\nAn incoming authentication request can be validated by the provider using `Provider.parse_authentication_request`.\nIf the request is valid, it should be stored and associated with the current user session to be able to retrieve it\nwhen the end-user authentication is completed.\n\n```python\nfrom pyop.exceptions import InvalidAuthenticationRequest\n\n@app.route('/authorization')\ndef authorization_endpoints(request):\n    try:\n        authn_req = provider.parse_authentication_request(request)\n    except InvalidAuthenticationRequest as e:\n        error_url = e.to_error_url()\n\n        if error_url:\n            return HTTPResponse(error_url, status=303)\n        else:\n            return HTTPResponse(\"Something went wrong: {}\".format(str(e)), status=400)\n\n    session['authn_req'] = authn_req.to_dict()\n    // TODO initiate end-user authentication\n```\n\nWhen the authentication is completed by the user, the provider must be notified to make an authentication response\nto the client's 'redirect_uri'. This is done with `Provider.authorize`, where the local user id supplied must exist\nin the user database supplied on initialization. When using the included `MongoWrapper`, no mapping is done between\nuser data and OpenID Connect claim names. Hence the underlying data source must contain the user information under the\nsame names as the [standard claims of OpenID Connect](http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims).\n\n```python\nfrom pyop.message import AuthorizationRequest\n\nfrom pyop.util import should_fragment_encode\n\nauthn_req = session['authn_req']\nauthn_response = provider.authorize(AuthorizationRequest().from_dict(authn_req), user_id)\nreturn_url = authn_response.request(authn_req['redirect_uri'], should_fragment_encode(authn_req))\n\nreturn HTTPResponse(return_url, status=303)\n```\n\n## Authentication request validation\nThe provider instance is by default configured to validate authentication requests according to the OpenID Connect\nCore specification. If you need to add additional custom validation of authentication requests, that's possible by\nadding such validation functions to the list of authentication request validators.\n\nIn this example an additional validator that checks that the 'nonce' parameter is included in all requests is added:\n\n```python\nfrom pyop.exceptions import InvalidAuthenticationRequest\n\ndef request_contains_nonce(authentication_request):\n    if 'nonce' not in authentication_request:\n        raise InvalidAuthenticationRequest('The request does not contain a nonce', authentication_request,\n                                           oauth_error='invalid_request')\n\nprovider.authentication_request_validators.append(request_contains_nonce)\n```\n\n# Token endpoint\nAn incoming token request is processed by `Provider.handle_token_request`. It will validate the request and issue all\nnecessary tokens (access token and possibly refresh token)\n\n```python\nfrom oic.oic.message import TokenErrorResponse\n\nfrom pyop.exceptions import InvalidClientAuthentication\nfrom pyop.exceptions import OAuthError\n\n@app.route('/token', methods=['POST', 'GET'])\ndef token_endpoint(request):\n    try:\n        token_response = provider.handle_token_request(request.get_data().decode('utf-8'),\n                                                       request.headers)\n        return HTTPResponse(token_response.to_json(), content_type='application/json')\n    except InvalidClientAuthentication as e:\n        error_resp = TokenErrorResponse(error='invalid_client', error_description=str(e))\n        http_response = HTTPResponse(error_resp.to_json(), status=401, content_type='application/json')\n        http_response.headers['WWW-Authenticate'] = 'Basic'\n        return http_response\n    except OAuthError as e:\n        error_resp = TokenErrorResponse(error=e.oauth_error, error_description=str(e))\n        return HTTPResponse(error_resp.to_json(), status=400, content_type='application/json')\n```\n\n\n# Userinfo endpoint\nAn incoming userinfo request is processed by `Provider.handle_userinfo_request`. It will validate the request and return\nall requested userinfo.\n\n```python\nfrom oic.oic.message import UserInfoErrorResponse\n\nfrom pyop.access_token import AccessToken\nfrom pyop.exceptions import BearerTokenError\nfrom pyop.exceptions import InvalidAccessToken\n\n@app.route('/userinfo', methods=['GET', 'POST'])\ndef userinfo_endpoint(request):\n    try:\n        response = provider.handle_userinfo_request(request.get_data().decode('utf-8'),\n                                                    request.headers)\n        return HTTPResponse(response.to_json(), content_type='application/json')\n    except (BearerTokenError, InvalidAccessToken) as e:\n        error_resp = UserInfoErrorResponse(error='invalid_token', error_description=str(e))\n        http_response = HTTPResponse(error_resp.to_json(), status=401, content_type='application/json')\n        http_response.headers['WWW-Authenticate'] = AccessToken.BEARER_TOKEN_TYPE\n        return http_response\n```\n\n\n# Dynamic client registration\n\nAn incoming client registration request is process by `Provider.handle_client_registration_request`. It will validate the request,\nstore the registered metadata and issue new client credentials.\n\n```python\nfrom pyop.exceptions import InvalidClientRegistrationRequest\n\n@app.route('/registration', methods=['POST'])\ndef registration_endpoint(request):\n    try:\n        response = provider.handle_client_registration_request(request.get_data().decode('utf-8'))\n        return HTTPResponse(response.to_json(), status=201, content_type='application/json')\n    except InvalidClientRegistrationRequest as e:\n        return HTTPResponse(e.to_json(), status=400, content_type='application/json')\n```\n\n## Registration request validation\nThe provider instance is by default configured to validate registration requests according to the OpenID Connect\nDynamic Registration specification. If you need to add additional custom validation of registration requests, that's\npossible by adding such validation functions to the list of registration request validators.\n\nIn this example an additional validator that checks that the 'software_statement' parameter is included in all requests\nis added:\n\n```python\ndef request_contains_software_statement(registration_request):\n    if 'software_statement' not in registration_request:\n        raise InvalidClientRegistrationRequest('The request does not contain a software_statement', registration_request,\n                                               oauth_error='invalid_request')\n\nprovider.registration_request_validators.append(request_contains_software_statement)\n```\n\n# User logout\n\nRP-initiated logout, as described in [Section 5 of OpenID Connect Session Management](http://openid.net/specs/openid-connect-session-1_0.html#RPLogout)\nis supported. The parsed request should be passed to `Provider.logout_user` together with any known subject identifier\nfor the user, and then `Provider.do_post_logout_redirect` should be called do obey any valid `post_logout_redirect_uri`\nincluded in the request.\n\n```python\nfrom oic.oic.message import EndSessionRequest\n\nfrom pyop.exceptions import InvalidSubjectIdentifier\n\n@app.route('/logout')\ndef end_session_endpoint(request):\n    end_session_request = EndSessionRequest().deserialize(request.get_data().decode('utf-8'))\n\n    try:\n        provider.logout_user(session.get('sub'), end_session_request)\n    except InvalidSubjectIdentifier as e:\n        return HTTPResponse('Logout unsuccessful!', content_type='text/html', status=400)\n\n    # TODO automagic logout, should ask user first!\n    redirect_url = provider.do_post_logout_redirect(end_session_request)\n    if redirect_url:\n        return HTTPResponse(redirect_url, status=303)\n\n    return HTTPResponse('Logout successful!', content_type='text/html')\n```\n\n# Exceptions\nAll exceptions, except `AuthorizationError`, inherits from `ValueError`. However it might be necessary to distinguish\nbetween them to send the correct error message back to the client according to the OpenID Connect specifications.\n\nAll OAuth errors contain the OAuth error code in `OAuthError.oauth_error`, together with the error description as the\nmessage of the exception (accessed by `str(exception)`).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fidentitypython%2Fpyop","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fidentitypython%2Fpyop","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fidentitypython%2Fpyop/lists"}