Welcome to Idpy OIDC-op Documentation

OIDC Certified

This project is a Python implementation of an OIDC Provider on top of jwtconnect.io that shows you how to ‘build’ an OP using the classes and functions provided by oidc-op.

If you are just going to build a standard OP you only have to write the configuration file. If you want to add or replace functionality this documentation should be able to tell you how.

Idpy OIDC-op implements the following standards:

It also comes with the following add_on modules.

The entire project code is open sourced and therefore licensed under the Apache 2.0.

The OpenID Connect Provider

Introduction

This documentation are here to show you how to ‘build’ an OP using the classes and functions provided by oidcop.

OAuth2 and thereby OpenID Connect (OIDC) are built on a request-response paradigm. The RP issues a request and the OP returns a response.

The OIDC core standard defines a set of such request-responses. This is a basic list of request-responses and the normal sequence in which they occur:

  1. Provider discovery (WebFinger)

  2. Provider Info Discovery

  3. Client registration

  4. Authorization/Authentication

  5. Access token

  6. User info

If you are just going to build a standard OP you only have to write the configuration file and of course add authentication and user consent services. If you want to add or replace functionality this document should be able to tell you how.

Setting up an OP means making a number if decisions. Like, should the OP support WebFinger , dynamic discovery and/or dynamic client registration .

All these are services you can access at endpoints. The total set of endpoints that this package supports are

  • webfinger

  • provider_info

  • registration

  • authorization

  • token

  • refresh_token

  • userinfo

  • end_session

Endpoint layout

When an endpoint receives a request it has to do a number of things:

  • Verify that the client can issue the request (client authentication/authorization)

  • Verify that the request is correct and that it contains the necessary information.

  • Process the request, which includes applying server policies and gathering information.

  • Construct the response

I should note at this point that this package is expected to work within the confines of a web server framework such that the actual receiving and sending of the HTTP messages are dealt with by the framework.

Based on the actions an endpoint has to perform a method call structure has been constructed. It looks like this:

  1. parse_request

    • client_authentication (*)

    • post_parse_request (*)

  2. process_request

  3. do_response

    • response_info
      • construct
        • pre_construct (*)

        • _parse_args

        • post_construct (*)

    • update_http_args

Steps marked with ‘*’ are places where extensions can be applied.

parse_request expects as input the request itself in a number of formats and also, if available, information about client authentication. The later is normally the authorization element of the HTTP header.

do_response returns a dictionary that can look like this:

{
  'response':
    _response as a string or as a Message instance_
  'http_headers': [
    ('Content-type', 'application/json'),
    ('Pragma', 'no-cache'),
    ('Cache-Control', 'no-store')
  ],
  'cookie': _list of cookies_,
  'response_placement': 'body'
}
cookie

MAY be present

http_headers

MAY be present

http_response

Already clear and formatted HTTP response

response

MUST be present

response_placement

If absent defaults to the endpoints response_placement parameter value or if that is also missing ‘url’

redirect_location

Where to send a redirect

Setup

Create an environment

virtualenv -ppython3 env
source env/bin/activate

Install

pip install oidcop

Get the usage examples

git clone https://github.com/identitypython/oidc-op.git
cd oidc-op/example/flask_op/
bash run.sh

To configure a standard OIDC Provider you have to edit the oidcop configuration file. See example/flask_op/config.json to get in.

~/DEV/IdentityPython/OIDC/oidc-op/example/flask_op$ bash run.sh
2021-05-02 14:57:44,727 root DEBUG Configured logging using dictionary
2021-05-02 14:57:44,728 oidcop.configure DEBUG Set server password to {'kty': 'oct', 'use': 'sig', 'k': 'n4G9OjOixYMOotXvP15grwq0peN2zq9I'}
 * Serving Flask app "oidc_op" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
2021-05-02 14:57:44,764 werkzeug INFO  * Running on https://127.0.0.1:5000/ (Press CTRL+C to quit)
2021-05-02 14:57:44,765 werkzeug INFO  * Restarting with stat
2021-05-02 14:57:45,011 root DEBUG Configured logging using dictionary
2021-05-02 14:57:45,011 oidcop.configure DEBUG Set server password to {'kty': 'oct', 'use': 'sig', 'k': 'bceYal7bK9zvlBAA7-23lsi5crcv_8Cd'}
2021-05-02 14:57:45,037 werkzeug WARNING  * Debugger is active!
2021-05-02 14:57:45,092 werkzeug INFO  * Debugger PIN: 560-973-597

Then open your browser to https://127.0.0.1:5000/.well-known/openid-configuration to get the OpenID Provider Configuration resource.


JWK Set (JWKS) files

see: cryptojwt documentation

You can use cryptojwt.key_jar.init_key_jar to create JWKS file. An easy way can be to configure the auto creation of JWKS files directly in your conf.yaml file. Using read_only: False in OIDC_KEYS it will create the path within the JWKS files. Change it to True if you don’t want to overwrite them on each execution.

In genral configuration:

OIDC_KEY_DEFS = [
    {
      "type": "RSA",
      "use": [
        "sig"
      ]
    },
    {
      "type": "EC",
      "crv": "P-256",
      "use": [
        "sig"
      ]
    }
]

OIDCOP_CONF = {
  "port": PORT,
  "domain": DOMAIN,
  "server_name": SERVER_NAME,
  "base_url": f"https://{SERVER_NAME}",
  "keys": {
    "private_path": "data/oidc_op/private/jwks.json",
    "key_defs": OIDC_KEY_DEFS,
    "public_path": "data/static/jwks.json",
    "read_only": False,
    "uri_path": "static/jwks.json"
  },

In the JWTConnect-Python-CryptoJWT distribution there is also a script you can use to construct a JWK. You can for instance do:

$ jwkgen --kty=RSA
{
    "d": "b9ucfay9vxDvz_nRZMVSUR9eRvHNMo0tc8Bl7tWkwxTis7LBXxmbMH1yzLs8omUil_u2a-Z_6VlKENxacuejYYcOhs6bfaU3iOqJbGi2p4t2i1oxjuF-cX6BZ5aHB5Wfb1uTXXobHokjcjVVDmBr_fNYBEPtZsVYqyN9sR9KE_ZLHEPks3IER09aX9G3wiB_PgcxQDRAl72qucsBz9_W9KS-TVWs-qCEqtXLmx9AAN6P8SjUcHAzEb0ZCJAYCkVu34wgNjxVaGyYN1qMA-1iOOVz--wtMyBwc5atSDBDgUApxFyj_DHSeBl81IHedcPjS9azxqFhumP7oJJyfecfSQ",
    "e": "AQAB",
    "kid": "cHZQbWRrMzRZak53U1pfSUNjY0dKd2xXaXRKenktdUduUjVBVTl3VE5ndw",
    "kty": "RSA",
    "n": "73XCXV2iiubSCEaFe26OpVnsBFlXwXh_yDCDyBqFgAFi5WdZTpRMJZoK0nn_vv2MvrXqFnw6IfXkwdsRGlMsNldVy36003gKa584CNksxfenwJZcF-huASUrSJEFr-3c0fMT_pLyAc7yf3rNCdRegzbBXSvIGKQpaeIjIFYftAPd9tjGA_SuYWVQDsSh3MeGbB4wt0lArAyFZ4f5o7SSxSDRCUF3ng3CB_QKUAaDHHgXrcNG_gPpgqQZjsDJ0VwMXjFKxQmskbH-dfsQ05znQsYn3pjcd_TEZ-Yu765_L5uxUrkEy_KnQXe1iqaQHcnfBWKXt18NAuBfgmKsv8gnxQ",
    "p": "_RPgbiQcFu8Ekp-tC-Kschpag9iaLc9aDqrxE6GWuThEdExGngP_p1I7Qd7gXHHTMXLp1c4gH2cKx4AkfQyKny2RJGtV2onQButUU5r0gwnlqqycIA2Dc9JiH85PX2Z889TKJUlVETfYbezHbKhdsazjjsXCQ6p9JfkmgfBQOXM",
    "q": "8jmgnadtwjMt96iOaoL51irPRXONO82tLM2AAZAK5Obsj23bZ9LFiw2Joh5oCSFdoUcRhbbIhCIv2aT4T_XKnDGnddrkxpF5Xgu0-hPNYnJx5m4kuzerot4j79Tx6qO-bshaaGz50MHs1vHSeFaDVN4fvh_hDWpV1BCNI0PKK-c"
}
SHA-256: pvPmdk34YjNwSZ_ICccGJwlWitJzy-uGnR5AU9wTNgw

Example: create a JWK for cookie signing

jwkgen --kty=SYM --kid cookie > private/cookie_sign_jwk.json

Configuration directives

issuer

The issuer ID of the OP, a unique value in URI format.

session params

Configuration parameters used by session manager:

"session_params": {
  "password": "__password_used_to_encrypt_access_token_sid_value",
  "salt": "salt involved in session sub hash ",
  "sub_func": {
    "public": {
      "class": "oidcop.session.manager.PublicID",
      "kwargs": {
        "salt": "sdfsdfdsf"
      }
    },
    "pairwise": {
      "class": "oidcop.session.manager.PairWiseID",
      "kwargs": {
        "salt": "sdfsdfsdf"
      }
    }
 }
},

password

Optional. Encryption key used to encrypt the SessionID (sid) in access_token. If unset it will be random.

salt

Optional. Salt, value or filename, used in sub_funcs (pairwise, public) for creating the opaque hash of sub claim.

sub_funcs

Optional. Functions involved in subject value creation.

scopes_to_claims

A dict defining the scopes that are allowed to be used per client and the claims they map to (defaults to the scopes mapping described in the spec). If we want to define a scope that doesn’t map to claims (e.g. offline_access) then we simply map it to an empty list. E.g.:

{
    "scope_a": ["claim1", "claim2"],
    "scope_b": []
}

Note: For OIDC the openid scope must be present in this mapping.

allowed_scopes

A list with the scopes that are allowed to be used (defaults to the keys in scopes_to_claims).

scopes_supported

A list with the scopes that will be advertised in the well-known endpoint (defaults to allowed_scopes).

add_on

An example:

"add_on": {
    "pkce": {
      "function": "oidcop.oidc.add_on.pkce.add_pkce_support",
      "kwargs": {
        "essential": false,
        "code_challenge_method": "S256 S384 S512"
      }
    },
  }

The provided add-ons can be seen in the following sections.

pkce

The pkce add on is activated using the oidcop.oidc.add_on.pkce.add_pkce_support function. The possible configuration options can be found below.

essential

Whether pkce is mandatory, authentication requests without a code_challenge will fail if this is True. This option can be overridden per client by defining pkce_essential in the client metadata.

code_challenge_method

The allowed code_challenge methods. The supported code challenge methods are: plain, S256, S384, S512

authentication

An example:

"authentication": {
    "user": {
      "acr": "urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocolPassword",
      "class": "oidcop.user_authn.user.UserPassJinja2",
      "kwargs": {
        "verify_endpoint": "verify/user",
        "template": "user_pass.jinja2",
        "db": {
          "class": "oidcop.util.JSONDictDB",
          "kwargs": {
            "filename": "passwd.json"
          }
        },
        "page_header": "Testing log in",
        "submit_btn": "Get me in!",
        "user_label": "Nickname",
        "passwd_label": "Secret sauce"
      }
    }
  },

capabilities

This covers most of the basic functionality of the OP. The key words are the same as defined in OIDC Discovery. A couple of things are defined else where. Like the endpoints, issuer id, jwks_uri and the authentication methods at the token endpoint.

An example:

response_types_supported:
    - code
    - token
    - id_token
    - "code token"
    - "code id_token"
    - "id_token token"
    - "code id_token token"
    - none
  response_modes_supported:
    - query
    - fragment
    - form_post
  subject_types_supported:
    - public
    - pairwise
  grant_types_supported:
    - authorization_code
    - implicit
    - urn:ietf:params:oauth:grant-type:jwt-bearer
    - refresh_token
  claim_types_supported:
    - normal
    - aggregated
    - distributed
  claims_parameter_supported: True
  request_parameter_supported: True
  request_uri_parameter_supported: True
  frontchannel_logout_supported: True
  frontchannel_logout_session_supported: True
  backchannel_logout_supported: True
  backchannel_logout_session_supported: True
  check_session_iframe: https://127.0.0.1:5000/check_session_iframe
  scopes_supported: ["openid", "profile", "random"]
  claims_supported: ["sub", "given_name", "birthdate"]

client_db

If you’re running an OP with static client registration you want to keep the registered clients in a database separate from the session database since it will change independent of the OP process. In this case you need client_db. If you are on the other hand only allowing dynamic client registration then keeping registered clients only in the session database makes total sense.

The class you reference in the specification MUST be a subclass of oidcmsg.storage.DictType and have some of the methods a dictionary has.

Note also that this class MUST support the dump and load methods as defined in oidcmsg.impexp.ImpExp.

An example:

client_db: {
    "class": 'oidcmsg.abfile.AbstractFileSystem',
    "kwargs": {
        'fdir': full_path("afs"),
        'value_conv': 'oidcmsg.util.JSON'
    }
}

endpoint

An example:

"endpoint": {
  "webfinger": {
    "path": ".well-known/webfinger",
    "class": "oidcop.oidc.discovery.Discovery",
    "kwargs": {
      "client_authn_method": null
    }
  },
  "provider_info": {
    "path": ".well-known/openid-configuration",
    "class": "oidcop.oidc.provider_config.ProviderConfiguration",
    "kwargs": {
      "client_authn_method": null
    }
  },
  "registration": {
    "path": "registration",
    "class": "oidcop.oidc.registration.Registration",
    "kwargs": {
      "client_authn_method": None,
      "client_secret_expiration_time": 432000,
      "client_id_generator": {
         "class": 'oidcop.oidc.registration.random_client_id',
         "kwargs": {
              "seed": "that-optional-random-value"
         }
     }
    }
  },
  "registration_api": {
    "path": "registration_api",
    "class": "oidcop.oidc.read_registration.RegistrationRead",
    "kwargs": {
      "client_authn_method": [
        "bearer_header"
      ]
    }
  },
  "introspection": {
    "path": "introspection",
    "class": "oidcop.oauth2.introspection.Introspection",
    "kwargs": {
      "client_authn_method": [
        "client_secret_post",
        "client_secret_basic",
        "client_secret_jwt",
        "private_key_jwt"
      ]
      "release": [
        "username"
      ]
    }
  },
  "authorization": {
    "path": "authorization",
    "class": "oidcop.oidc.authorization.Authorization",
    "kwargs": {
      "client_authn_method": null,
      "claims_parameter_supported": true,
      "request_parameter_supported": true,
      "request_uri_parameter_supported": true,
      "response_types_supported": [
        "code",
        "token",
        "id_token",
        "code token",
        "code id_token",
        "id_token token",
        "code id_token token",
        "none"
      ],
      "response_modes_supported": [
        "query",
        "fragment",
        "form_post"
      ]
    }
  },
  "token": {
    "path": "token",
    "class": "oidcop.oidc.token.Token",
    "kwargs": {
      "client_authn_method": [
        "client_secret_post",
        "client_secret_basic",
        "client_secret_jwt",
        "private_key_jwt",
      ],
      "revoke_refresh_on_issue": True
    }
  },
  "userinfo": {
    "path": "userinfo",
    "class": "oidcop.oidc.userinfo.UserInfo",
    "kwargs": {
      "claim_types_supported": [
        "normal",
        "aggregated",
        "distributed"
      ]
    }
  },
  "end_session": {
    "path": "session",
    "class": "oidcop.oidc.session.Session",
    "kwargs": {
      "logout_verify_url": "verify_logout",
      "post_logout_uri_path": "post_logout",
      "signing_alg": "ES256",
      "frontchannel_logout_supported": true,
      "frontchannel_logout_session_supported": true,
      "backchannel_logout_supported": true,
      "backchannel_logout_session_supported": true,
      "check_session_iframe": "check_session_iframe"
    }
  }
}

You can specify which algoritms are supported, for example in userinfo_endpoint:

"userinfo_signing_alg_values_supported": OIDC_SIGN_ALGS,
"userinfo_encryption_alg_values_supported": OIDC_ENC_ALGS,

Or in authorization endpoint:

"request_object_encryption_alg_values_supported": OIDC_ENC_ALGS,

httpc_params

Parameters submitted to the web client (python requests). In this case the TLS certificate will not be verified, to be intended exclusively for development purposes

Example

"httpc_params": {
    "verify": false
  },

keys

An example:

"keys": {
    "private_path": "private/jwks.json",
    "key_defs": [
      {
        "type": "RSA",
        "use": [
          "sig"
        ]
      },
      {
        "type": "EC",
        "crv": "P-256",
        "use": [
          "sig"
        ]
      }
    ],
    "public_path": "static/jwks.json",
    "read_only": false,
    "uri_path": "static/jwks.json"
  },

read_only means that on each restart the keys will created and overwritten with new ones. This can be useful during the first time the project have been executed, then to keep them as they are read_only would be configured to True.

login_hint2acrs

OIDC Login hint support, it’s optional. It matches the login_hint paramenter to one or more Authentication Contexts.

An example:

"login_hint2acrs": {
  "class": "oidcop.login_hint.LoginHint2Acrs",
  "kwargs": {
    "scheme_map": {
      "email": [
        "urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocolPassword"
      ]
    }
  }
},

oidc-op supports the following authn contexts:

authz

This configuration section refers to the authorization/authentication endpoint behaviour. Scopes bound to an access token are strictly related to grant management, as part of what that endpoint does. Regarding grant authorization we should have something like the following example.

If you omit this section from the configuration (thus using some sort of default profile) you’ll have an Implicit grant authorization that leads granting nothing. Add the below to your configuration and you’ll see things changing.

An example:

"authz": {
  "class": "oidcop.authz.AuthzHandling",
  "kwargs": {
      "grant_config": {
          "usage_rules": {
              "authorization_code": {
                  "supports_minting": ["access_token", "refresh_token", "id_token"],
                  "max_usage": 1
              },
              "access_token": {},
              "refresh_token": {
                  "supports_minting": ["access_token", "refresh_token"]
              }
          },
          "expires_in": 43200
      }
  }
},

template_dir

The HTML Template directory used by Jinja2, used by endpoint context

template loader, as:

Environment(loader=FileSystemLoader(template_dir), autoescape=True)

An example:

"template_dir": "templates"

For any further customization of template here an example of what used in django-oidc-op:

"authentication": {
  "user": {
    "acr": "urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocolPassword",
    "class": "oidc_provider.users.UserPassDjango",
    "kwargs": {
      "verify_endpoint": "verify/oidc_user_login/",
      "template": "oidc_login.html",

      "page_header": "Testing log in",
      "submit_btn": "Get me in!",
      "user_label": "Nickname",
      "passwd_label": "Secret sauce"
    }
  }
},

token_handler_args

Token handler is an intermediate interface used by and endpoint to manage

the tokens’ default behaviour, like lifetime and minting policies. With it we can create a token that’s linked to another, and keep relations between many tokens in session and grants management.

An example:

"token_handler_args": {
    "jwks_def": {
      "private_path": "private/token_jwks.json",
      "read_only": false,
      "key_defs": [
        {
          "type": "oct",
          "bytes": 24,
          "use": [
            "enc"
          ],
          "kid": "code"
        },
        {
          "type": "oct",
          "bytes": 24,
          "use": [
            "enc"
          ],
          "kid": "refresh"
        }
      ]
    },
    "code": {
      "kwargs": {
        "lifetime": 600
      }
    },
    "token": {
      "class": "oidcop.token.jwt_token.JWTToken",
      "kwargs": {
          "lifetime": 3600,
          "add_claims": [
            "email",
            "email_verified",
            "phone_number",
            "phone_number_verified"
          ],
          "add_claims_by_scope": true,
          "aud": ["https://example.org/appl"]
       }
    },
    "refresh": {
        "kwargs": {
            "lifetime": 86400
        }
    }
    "id_token": {
        "class": "oidcop.token.id_token.IDToken",
        "kwargs": {
            "base_claims": {
                "email": None,
                "email_verified": None,
        },
    }
  }

jwks_defs can be replaced eventually by jwks_file:

"jwks_file": f"{OIDC_JWKS_PRIVATE_PATH}/token_jwks.json",

You can even select wich algorithms to support in id_token, eg:

"id_token": {
    "class": "oidcop.token.id_token.IDToken",
    "kwargs": {
        "id_token_signing_alg_values_supported": [
                "RS256",
                "RS512",
                "ES256",
                "ES512",
                "PS256",
                "PS512",
            ],
        "id_token_encryption_alg_values_supported": [
                "RSA-OAEP",
                "RSA-OAEP-256",
                "A192KW",
                "A256KW",
                "ECDH-ES",
                "ECDH-ES+A128KW",
                "ECDH-ES+A192KW",
                "ECDH-ES+A256KW",
            ],
        "id_token_encryption_enc_values_supported": [
                'A128CBC-HS256',
                'A192CBC-HS384',
                'A256CBC-HS512',
                'A128GCM',
                'A192GCM',
                'A256GCM'
            ],
    }
}

userinfo

An example:

"userinfo": {
    "class": "oidcop.user_info.UserInfo",
    "kwargs": {
      "db_file": "users.json"
    }
}

This is somethig that can be customized. For example in the django-oidc-op implementation is used something like the following:

"userinfo": {
    "class": "oidc_provider.users.UserInfo",
    "kwargs": {
        "claims_map": {
            "phone_number": "telephone",
            "family_name": "last_name",
            "given_name": "first_name",
            "email": "email",
            "verified_email": "email",
            "gender": "gender",
            "birthdate": "get_oidc_birthdate",
            "updated_at": "get_oidc_lastlogin"
        }
    }
}

Clients

In this section there are some client configuration examples. That can be used to override the global configuration of the OP.

How to configure the release of the user claims per clients:

endpoint_context.cdb["client_1"] = {
    "client_secret": "hemligt",
    "redirect_uris": [("https://example.com/cb", None)],
    "response_types": ["code", "token", "code id_token", "id_token"],
    "add_claims": {
        "always": {
            "introspection": ["nickname", "eduperson_scoped_affiliation"],
            "userinfo": ["picture", "phone_number"],
        },
        # this overload the general endpoint configuration for this client
        # self.server.server_get("endpoint", "id_token").kwargs = {"add_claims_by_scope": True}
        "by_scope": {
            "id_token": False,
        },
    },

The available configuration options are:

client_secret

The client secret. This parameter is required.

client_secret_expires_at

When the client_secret expires.

redirect_uris

The client’s redirect uris.

auth_method

The auth_method that can be used per endpoint. E.g:

{
    "AccessTokenRequest": "client_secret_basic",
    ...
}

request_uris

A list of request_uris.

See https://openid.net/specs/openid-connect-registration-1_0-29.html#ClientMetadata.

response_types

The allowed response_types for this client.

See https://openid.net/specs/openid-connect-registration-1_0-29.html#ClientMetadata.

grant_types_supported

Configure the allowed grant types on the token endpoint.

See https://openid.net/specs/openid-connect-registration-1_0-29.html#ClientMetadata.

scopes_to_claims

A dict defining the scopes that are allowed to be used per client and the claims they map to (defaults to the scopes mapping described in the spec). If we want to define a scope that doesn’t map to claims (e.g. offline_access) then we simply map it to an empty list. E.g.:

{
  "scope_a": ["claim1", "claim2"],
  "scope_b": []
}

allowed_scopes

A list with the scopes that are allowed to be used (defaults to the keys in the clients scopes_to_claims).

revoke_refresh_on_issue

Configure whether to revoke the refresh token that was used to issue a new refresh token.

add_claims

A dictionary with the following keys

always

A dictionary with the following keys: userinfo, id_token, introspection, access_token. The keys are used to describe the claims we want to add to the corresponding interface. The keys can be a list of claims to be added or a dict in the format described in https://openid.net/specs/openid-connect-core-1_0.html#IndividualClaimsRequests E.g.:

{
    "add_claims": {
        "always": {
          "userinfo": ["email", "phone"], # Always add "email" and "phone" in the userinfo response if such claims exists
          "id_token": {"email": null}, # Always add "email" in the id_token if such a claim exists
          "introspection": {"email": {"value": "a@a.com"}}, # Add "email" in the introspection response only if its value is "a@a.com"
        }
    }
}

by_scope

A dictionary with the following keys: userinfo, id_token, introspection, access_token. The keys are boolean values that describe whether the scopes should be mapped to claims and added to the response. E.g.:

{
    "add_claims": {
        "by_scope": {
            id_token: True, # Map the requested scopes to claims and add them to the id token
}

token_usage_rules

The usage rules for each token type. E.g.:

{
    "usage_rules": {
        "authorization_code": {
            "expires_in": 3600,
            "supports_minting": [
                "access_token",
                "id_token",
            ],
            "max_usage": 1,
        },
        "access_token": {
            "expires_in": self.params["access_token_lifetime"],
        },
    }
}

pkce_essential

Whether pkce is essential for this client.

post_logout_redirect_uri

The client’s post logout redirect uris.

See https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout.

backchannel_logout_uri

The client’s backchannel_logout_uri.

See https://openid.net/specs/openid-connect-backchannel-1_0.html#BCRegistration

frontchannel_logout_uri

The client’s frontchannel_logout_uri.

See https://openid.net/specs/openid-connect-frontchannel-1_0.html#RPLogout

request_object_signing_alg

A list with the allowed algorithms for signing the request object.

See https://openid.net/specs/openid-connect-registration-1_0-29.html#ClientMetadata

request_object_encryption_alg

A list with the allowed alg algorithms for encrypting the request object.

See https://openid.net/specs/openid-connect-registration-1_0-29.html#ClientMetadata

request_object_encryption_enc

A list with the allowed enc algorithms for signing the request object.

See https://openid.net/specs/openid-connect-registration-1_0-29.html#ClientMetadata

userinfo_signed_response_alg

JWS alg algorithm [JWA] REQUIRED for signing UserInfo Responses.

See https://openid.net/specs/openid-connect-registration-1_0-29.html#ClientMetadata

userinfo_encrypted_response_enc

The alg algorithm [JWA] REQUIRED for encrypting UserInfo Responses.

See https://openid.net/specs/openid-connect-registration-1_0-29.html#ClientMetadata

userinfo_encrypted_response_alg

JWE enc algorithm [JWA] REQUIRED for encrypting UserInfo Responses.

See https://openid.net/specs/openid-connect-registration-1_0-29.html#ClientMetadata

id_token_signed_response_alg

JWS alg algorithm [JWA] REQUIRED for signing ID Token issued to this Client.

See https://openid.net/specs/openid-connect-registration-1_0-29.html#ClientMetadata

id_token_encrypted_response_enc

The alg algorithm [JWA] REQUIRED for encrypting ID Token issued to this Client.

See https://openid.net/specs/openid-connect-registration-1_0-29.html#ClientMetadata

id_token_encrypted_response_alg

JWE enc algorithm [JWA] REQUIRED for encrypting ID Token issued to this Client.

See https://openid.net/specs/openid-connect-registration-1_0-29.html#ClientMetadata

dpop_jkt

Usage

Some examples, how to run flask_op and django_op but also some typical configuration in relation to common use cases.

Configure flask-rp

JWTConnect-Python-OidcRP is Relaing Party for tests, see related page. You can run a working instance of JWTConnect-Python-OidcRP.flask_rp with:

pip install git+https://github.com/openid/JWTConnect-Python-OidcRP.git

# get entire project to have examples files
git clone https://github.com/openid/JWTConnect-Python-OidcRP.git
cd JWTConnect-Python-OidcRP/example/flask_rp

# run it as it come
bash run.sh

Now you can connect to https://127.0.0.1:8090/ to see the RP landing page and select your authentication endpoint.

Authentication examples

_images/1.pngRP

Get to the RP landing page to choose your authentication endpoint. The first option aims to use Provider Discovery.


_images/2.pngOP Auth

The AS/OP supports dynamic client registration, it accepts the authentication request and prompt to us the login form. Read passwd.json file to get credentials.


_images/3.pngAccess

The identity representation with the information fetched from the user info endpoint.


_images/4.pngLogout

We can even test the single logout

Refresh token

To obtain a refresh token, you have to use response_type=code, add offline_access to scope and also use prompt=consent, otherwise there will be an error (based on OpenID Connect specification).

To refresh a token:

import requests

CLIENT_ID = "DBP60x3KUQfCYWZlqFaS_Q"
CLIENT_SECRET="8526270403788522b2444e87ea90c53bcafb984119cec92eeccc12f1"
REFRESH_TOKEN = "Z0FBQUFBQ ... lN2JNODYtZThjMnFsZUNDcg=="

data = {
    "grant_type" : "refresh_token",
    "client_id" : f"{CLIENT_ID}",
    "client_secret" : f"{CLIENT_SECRET}",
    "refresh_token" : f"{REFRESH_TOKEN}"
}
headers = {'Content-Type': "application/x-www-form-urlencoded" }
response = requests.post(
    'https://127.0.0.1:8000/oidcop/token', verify=False, data=data, headers=headers
)

oidc-op will return a json response like this:

{
 'access_token': 'eyJhbGc ... CIOH_09tT_YVa_gyTqg',
 'token_type': 'Bearer',
 'scope': 'openid profile email address phone offline_access',
 'refresh_token': 'Z0FBQ ... 1TE16cm1Tdg=='
}

Introspection endpoint

Here an example about how to consume oidc-op introspection endpoint. This example uses a client with an HTTP Basic Authentication::

import base64
import requests

TOKEN = "eyJhbGciOiJFUzI1NiIsImtpZCI6IlQwZGZTM1ZVYUcxS1ZubG9VVTQwUXpJMlMyMHpjSHBRYlMxdGIzZ3hZVWhCYzNGaFZWTlpTbWhMTUEifQ.eyJzY29wZSI6IFsib3BlbmlkIiwgInByb2ZpbGUiLCAiZW1haWwiLCAiYWRkcmVzcyIsICJwaG9uZSJdLCAiYXVkIjogWyJvTHlSajdzSkozWHZBWWplRENlOHJRIl0sICJqdGkiOiAiOWQzMjkzYjZiYmNjMTFlYmEzMmU5ODU0MWIwNzE1ZWQiLCAiY2xpZW50X2lkIjogIm9MeVJqN3NKSjNYdkFZamVEQ2U4clEiLCAic3ViIjogIm9MeVJqN3NKSjNYdkFZamVEQ2U4clEiLCAic2lkIjogIlowRkJRVUZCUW1keGJIVlpkRVJKYkZaUFkxQldaa0pQVUVGc1pHOUtWWFZ3VFdkZmVEY3diMVprYmpSamRrNXRMVzB4YTNnelExOHlRbHBHYTNRNVRHZEdUUzF1UW1sMlkzVnhjRE5sUm01dFRFSmxabGRXYVhJeFpFdHVSV2xtUzBKcExWTmFaRzV3VjJodU0yNXlSbTU0U1ZWVWRrWTRRM2x2UWs1TlpVUk9SazlGVlVsRWRteGhjWGx2UWxWRFdubG9WbTFvZGpORlVUSnBkaTFaUTFCcFptZFRabWRDVWt0YVNuaGtOalZCWVhkcGJFNXpaV2xOTTFCMk0yaE1jMDV0ZGxsUlRFc3dObWxsYUcxa1lrTkhkemhuU25OaWFWZE1kVUZzZDBwWFdWbzFiRWhEZFhGTFFXWTBPVzl5VjJOUk4zaGtPRDA9IiwgInR0eXBlIjogIlQiLCAiaXNzIjogImh0dHBzOi8vMTI3LjAuMC4xOjgwMDAiLCAiaWF0IjogMTYyMTc3NzMwNSwgImV4cCI6IDE2MjE3ODA5MDV9.pVqxUNznsoZu9ND18IEMJIHDOT6_HxzoFiTLsniNdbAdXTuOoiaKeRTqtDyjT9WuUPszdHkVjt5xxeFX8gQMuA"

data = {
 'token': TOKEN,
 'token_type_hint': 'access_token'
}

_basic_secret = base64.b64encode(
    f'{"oLyRj7sJJ3XvAYjeDCe8rQ"}:{"53fb49f2a6501ec775355c89750dc416744a3253138d5a04e409b313"}'.encode()
)
headers = {
    'Authorization': f"Basic {_basic_secret.decode()}"
}

requests.post('https://127.0.0.1:8000/introspection', verify=False, data=data, headers=headers)

oidc-op will return a json response like this::

{
  "active": true,
  "scope": "openid profile email address phone",
  "client_id": "oLyRj7sJJ3XvAYjeDCe8rQ",
  "token_type": "access_token",
  "exp": 0,
  "iat": 1621777305,
  "sub": "a7b0dea2958aec275a789d7d7dc8e7d09c6316dd4fc6ae92742ed3297e14dded",
  "iss": "https://127.0.0.1:8000",
  "aud": [
    "oLyRj7sJJ3XvAYjeDCe8rQ"
  ]
}

Session Management

About session management

The OIDC Session Management draft defines session to be:

Continuous period of time during which an End-User accesses a Relying Party relying on the Authentication of the End-User performed by the OpenID Provider.

Note that we are dealing with a Single Sign On (SSO) context here. If for some reason the OP does not want to support SSO then the session management has to be done a bit differently. In that case each session (user_id,client_id) would have its own authentication event. Not one shared between the sessions.

Design criteria

So a session is defined by a user and a Relying Party. If one adds to that that a user can have several sessions active at the same time each one against a unique Relying Party we have the bases for session management.

Furthermore the user may well decide on different rules for different relying parties for releasing user attributes, where and how issued access tokens could be used and whether refresh tokens should be issued or not.

We also need to keep track on which tokens where used to mint new tokens such that we can easily revoked a suite of tokens all with a common ancestor.

Database layout

The database is organized in 3 levels. The top one being the users. Below that the Relying Parties and at the bottom what is called grants.

Grants organize authorization codes, access tokens and refresh tokens (and possibly other types of tokens) in a comprehensive way. More about that below.

There may be many Relying Parties below a user and many grants below a Relying Party.

The information structure

As stated above there are 3 layers: user session information, client session information and grants. But first the keys to the information.

Session key

A key to the session information is based on a list. The first item being the user identifier, the second the client identifier and the third the grant identifier. If you only want the user session information then the key is a list with one item the user id. If you want the client session information the key is a list with 2 items (user_id, client_id). And lastly if you want a grant then the key is a list with 3 elements (user_id, client_id, grant_id).

Example::

“diana;;KtEST70jZx1x;;85544c9cace411ebab53559c5425fcc0”

A session identifier is constructed using the session_key function. It takes as input the 3 elements list.:

session_id = session_key(user_id, client_id, grant_id)

Using the function unpack_session_key you can get the elements from a session_id.:

user_id, client_id, grant_id = unpack_session_id(session_id)

User session information

Houses the authentication event information which is the same for all session connected to a user. Here we also have a list of all the clients that this user has a session with. Expressed as a dictionary this can look like this:

{
    'authentication_event': {
        'uid': 'diana',
        'authn_info': "urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocolPassword",
        'authn_time': 1605515787,
        'valid_until': 1605519387
    },
    'subordinate': ['client_1']
}

Client session information

The client specific information of the session information. Presently only the authorization request and the subject identifier (sub). The subordinates to this set of information are the grants:

{
    'authorization_request':{
        'client_id': 'client_1',
        'redirect_uri': 'https://example.com/cb',
        'scope': ['openid', 'research_and_scholarship'],
        'state': 'STATE',
        'response_type': ['code']
    },
    'sub': '117afe8d7bb0ace8e7fb2706034ab2d3fbf17f0fd4c949aa9c23aedd051cc9e3',
    'subordinate': ['e996c61227e711eba173acde48001122'],
    'revoked': False
}

Grant information

Grants are created by an authorization subsystem in an OP. If the grant is created in connection with an user authentication the authorization system might normally ask the user for usage consent and then base the construction of the grant on that consent.

If an authorization server can act as a Security Token Service (STS) as defined by Token Exchange [RFC-8693] then no user is involved. In the context of session management the STS is equivalent to a user.

Grant information contains information about user consent and issued tokens.:

{
    "type": "grant",
    "scope": ["openid", "research_and_scholarship"],
    "authorization_details": null,
    "claims": {
        "userinfo": {
            "sub": null,
            "name": null,
            "given_name": null,
            "family_name": null,
            "email": null,
            "email_verified": null,
            "eduperson_scoped_affiliation": null
        }
    },
    "resources": ["client_1"],
    "issued_at": 1605452123,
    "not_before": 0,
    "expires_at": 0,
    "revoked": false,
    "issued_token": [
        {
            "type": "authorization_code",
            "issued_at": 1605452123,
            "not_before": 0,
            "expires_at": 1605452423,
            "revoked": false,
            "value": "Z0FBQUFBQmZzVUZieDFWZy1fbjE2ckxvZkFTVC1ZTHJIVlk0Z09tOVk1M0RsOVNDbkdfLTIxTUhILWs4T29kM1lmV015UEN1UGxrWkxLTkVXOEg1WVJLNjh3MGlhMVdSRWhYcUY4cGdBQkJEbzJUWUQ3UGxTUWlJVDNFUHFlb29PWUFKcjNXeHdRM1hDYzRIZnFrYjhVZnIyTFhvZ2Y0NUhjR1VBdzE0STVEWmJ3WkttTk1OYXQtTHNtdHJwYk1nWnl3MUJqSkdWZGFtdVNfY21VNXQxY3VzalpIczBWbGFueVk0TVZ2N2d2d0hVWTF4WG56TDJ6bz0=",
            "usage_rules": {
                "expires_in": 300,
                "supports_minting": [
                    "access_token",
                    "refresh_token",
                    "id_token"
                ],
                "max_usage": 1
                },
            "used": 0,
            "based_on": null,
            "id": "96d19bea275211eba43bacde48001122"
       },
       {
            "type": "access_token",
            "issued_at": 1605452123,
            "not_before": 0,
            "expires_at": 1605452723,
            "revoked": false,
            "value": "Z0FBQUFBQmZzVUZiaWVRbi1IS2k0VW4wVDY1ZmJHeEVCR1hVODBaQXR6MWkzelNBRFpOS2tRM3p4WWY5Y1J6dk5IWWpnelRETGVpSG52b0d4RGhjOWphdWp4eW5xZEJwQzliaS16cXFCcmRFbVJqUldsR1Z3SHdTVVlWbkpHak54TmJaSTV2T3NEQ0Y1WFkxQkFyamZHbmd4V0RHQ3k1MVczYlYwakEyM010SGoyZk9tUVVxbWdYUzBvMmRRNVlZMUhRSnM4WFd2QzRkVmtWNVJ1aVdJSXQyWnpVTlRiZnMtcVhKTklGdzBzdDJ3RkRnc1A1UEw2Yz0=",
            "usage_rules": {
                "expires_in": 600,
            },
            "used": 0,
            "based_on": "Z0FBQUFBQmZzVUZieDFWZy1fbjE2ckxvZkFTVC1ZTHJIVlk0Z09tOVk1M0RsOVNDbkdfLTIxTUhILWs4T29kM1lmV015UEN1UGxrWkxLTkVXOEg1WVJLNjh3MGlhMVdSRWhYcUY4cGdBQkJEbzJUWUQ3UGxTUWlJVDNFUHFlb29PWUFKcjNXeHdRM1hDYzRIZnFrYjhVZnIyTFhvZ2Y0NUhjR1VBdzE0STVEWmJ3WkttTk1OYXQtTHNtdHJwYk1nWnl3MUJqSkdWZGFtdVNfY21VNXQxY3VzalpIczBWbGFueVk0TVZ2N2d2d0hVWTF4WG56TDJ6bz0=",
            "id": "96d1c840275211eba43bacde48001122"
       }
    ],
    "id": "96d16d3c275211eba43bacde48001122"
}

The parameters are described below

scope

This is the scope that was chosen for this grant. Either by the user or by some rules that the Authorization Server runs by.

authorization_details

Presently a place hold. But this is expected to be information on how the authorization was performed. What input was used and so on.

claims

The set of claims that should be returned in different circumstances. The syntax that is defined in https://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter is used. With one addition, beside userinfo and id_token we have added introspection.

resources

This are the resource servers and other entities that should be accepted as users of issued access tokens.

issued_at

When the grant was created. Its value is a JSON number representing the number of seconds from 1970-01-01T0:0:0Z as measured in UTC until the date/time.

not_before

If the usage of the grant should be delay, this is when it can start being used. Its value is a JSON number representing the number of seconds from 1970-01-01T0:0:0Z as measured in UTC until the date/time.

expires_at

When the grant expires. Its value is a JSON number representing the number of seconds from 1970-01-01T0:0:0Z as measured in UTC until the date/time.

revoked

If the grant has been revoked.

issued_token

Tokens that has been issued based on this grant. There is no limitation as to which tokens can be issued. Though presently we only have:

  • authorization_code,

  • access_token and

  • refresh_token

id

The grant identifier.

Token

As mention above there are presently only 3 token types that are defined:

  • authorization_code,

  • access_token and

  • refresh_token

A token is described as follows:

{
    "type": "authorization_code",
    "issued_at": 1605452123,
    "not_before": 0,
    "expires_at": 1605452423,
    "revoked": false,
    "value": "Z0FBQUFBQmZzVUZieDFWZy1fbjE2ckxvZkFTVC1ZTHJIVlk0Z09tOVk1M0RsOVNDbkdfLTIxTUhILWs4T29kM1lmV015UEN1UGxrWkxLTkVXOEg1WVJLNjh3MGlhMVdSRWhYcUY4cGdBQkJEbzJUWUQ3UGxTUWlJVDNFUHFlb29PWUFKcjNXeHdRM1hDYzRIZnFrYjhVZnIyTFhvZ2Y0NUhjR1VBdzE0STVEWmJ3WkttTk1OYXQtTHNtdHJwYk1nWnl3MUJqSkdWZGFtdVNfY21VNXQxY3VzalpIczBWbGFueVk0TVZ2N2d2d0hVWTF4WG56TDJ6bz0=",
    "usage_rules": {
        "expires_in": 300,
        "supports_minting": [
            "access_token",
            "refresh_token",
            "id_token"
        ],
        "max_usage": 1
        },
    "used": 0,
    "based_on": null,
    "id": "96d19bea275211eba43bacde48001122"
}
type

The type of token.

issued_at

When the token was created. Its value is a JSON number representing the number of seconds from 1970-01-01T0:0:0Z as measured in UTC until the date/time.

not_before

If the start of the usage of the token is to be delay, this is until when. Its value is a JSON number representing the number of seconds from 1970-01-01T0:0:0Z as measured in UTC until the date/time.

expires_at

When the token expires. Its value is a JSON number representing the number of seconds from 1970-01-01T0:0:0Z as measured in UTC until the date/time.

revoked

If the token has been revoked.

value

This is the value that appears in OIDC protocol exchanges.

usage_rules

Rules as to how this token can be used:

expires_in

Used to calculate expires_at

supports_minting

The tokens types that can be minted based on this token. Typically a code can be used to mint ID tokens and access and refresh tokens.

max_usage

How many times this token can be used (being used is presently defined as used to mint other tokens). An authorization_code token can according to the OIDC standard only be used once but then to, in the same session, mint more then one token.

used

How many times the token has been used

based_on

Reference to the token that was used to mint this token. Might be empty if the token was minted based on the grant it belongs to.

id

Token identifier

Session Info API

add_subordinate

remove_subordinate

revoke

is_revoked

to_json

from_json

Grant API

Token API

Session Manager API

create_session

Creating a new session is done by running the create_session method of the class SessionManager. The create_session methods takes the following arguments.

authn_event

An AuthnEvent class instance that describes the authentication event.

auth_req

The Authentication request

client_id

The client Identifier

user_id

The user identifier

sector_identifier

A possible sector identifier to be used when constructing a pairwise subject identifier

sub_type

The type of subject identifier that should be constructed. It can either be pairwise or public.

So a typical command would look like this:

authn_event = create_authn_event(self.user_id)
session_manager.create_session(authn_event=authn_event, auth_req=auth_req,
                               user_id=self.user_id, client_id=client_id,
                               sub_type=sub_type, sector_identifier=sector_identifier)

add_grant

add_grant(self, user_id, client_id, **kwargs)

find_token

find_token(self, session_id, token_value)

get_authentication_event

get_authentication_event(self, session_id)

get_client_session_info

get_client_session_info(self, session_id)

get_grant_by_response_type

get_grant_by_response_type(self, user_id, client_id)

get_session_info

get_session_info(self, session_id)

get_session_info_by_token

get_session_info_by_token(self, token_value)

get_sids_by_user_id

get_sids_by_user_id(self, user_id)

get_user_info

get_user_info(self, uid)

grants

grants(self, session_id)

revoke_client_session

revoke_client_session(self, session_id)

revoke_grant

revoke_grant(self, session_id)

revoke_token

revoke_token(self, session_id, token_value, recursive=False)

Tests

pip install -r requirements-dev.txt
pytest --cov=oidcop tests/

The clients database

Information kept about clients in the client database are to begin with the client metadata as defined in https://openid.net/specs/openid-connect-registration-1_0.html .

To that we have the following additions specified in OIDC extensions.

And finally we add a number of parameters that are OidcOP specific. These are described in this document.

allowed_scopes

Which scopes that can be returned to a client. This is used to filter the set of scopes a user can authorize release of.

token_usage_rules

There are usage rules for tokens. Rules are set per token type (the basic set is authorization_code, refresh_token, access_token and id_token). The possible rules are:

  • how many times they can be used

  • if other tokens can be minted based on this token

  • how fast they expire

A typical example (this is the default) would be:

"token_usage_rules": {
    "authorization_code": {
        "max_usage": 1
        "supports_minting": ["access_token", "refresh_token"],
        "expires_in": 600,
    },
    "refresh_token": {
        "supports_minting": ["access_token"],
        "expires_in": -1
    },
}

This then means that access_tokens can be used any number of times, can not be used to mint other tokens and will expire after 300 seconds which is the default for any token. An authorization_code can only used once and it can be used to mint access_tokens and refresh_tokens. Note that normally an authorization_code is used to mint an access_token and a refresh_token at the same time. Such a dual minting is counted as one usage. And lastly an refresh_token can be used to mint access_tokens any number of times. An expires_in of -1 means that the token will never expire.

If token_usage_rules are defined in the client metadata then it will be used whenever a token is minted unless circumstances makes the OP modify the rules.

Also this does not mean that what is valid for a token can not be changed during run time.

FAQ