diff --git a/.gitignore b/.gitignore
index 831dbd0f..d2508374 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@ conf.yaml
flask_op/debug.log
flask_op/static/
debug.log
+.pytest_cache/
# Created by .ignore support plugin (hsz.mobi)
### Python template
# Byte-compiled / optimized / DLL files
diff --git a/docs/source/_images/oid-l-certification-mark-l-rgb-150dpi-90mm-300x157.png b/docs/source/_images/oid-l-certification-mark-l-rgb-150dpi-90mm-300x157.png
new file mode 100644
index 00000000..f226aa8c
Binary files /dev/null and b/docs/source/_images/oid-l-certification-mark-l-rgb-150dpi-90mm-300x157.png differ
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 34382356..2cc78e31 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -13,7 +13,7 @@
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
-from recommonmark.parser import CommonMarkParser
+# from recommonmark.parser import CommonMarkParser
# -- Project information -----------------------------------------------------
diff --git a/docs/source/contents/clients.rst b/docs/source/contents/clients.rst
new file mode 100644
index 00000000..f5644c91
--- /dev/null
+++ b/docs/source/contents/clients.rst
@@ -0,0 +1,75 @@
+********************
+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.
+
+* https://openid.net/specs/openid-connect-rpinitiated-1_0.html
+ + post_logout_redirect_uri
+* https://openid.net/specs/openid-connect-frontchannel-1_0.html
+ + frontchannel_logout_uri
+ + frontchannel_logout_session_required
+* https://openid.net/specs/openid-connect-backchannel-1_0.html#Backchannel
+ + backchannel_logout_uri
+ + backchannel_logout_session_required
+* https://openid.net/specs/openid-connect-federation-1_0.html#rfc.section.3.1
+ + client_registration_types
+ + organization_name
+ + signed_jwks_uri
+
+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.
+
+
diff --git a/docs/source/contents/conf.rst b/docs/source/contents/conf.rst
index e5450105..30ed142f 100644
--- a/docs/source/contents/conf.rst
+++ b/docs/source/contents/conf.rst
@@ -199,9 +199,9 @@ 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 this.
+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 in the session database makes total sense.
+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.
diff --git a/docs/source/contents/session_management.rst b/docs/source/contents/session_management.rst
index 26e99371..8bb60fbf 100644
--- a/docs/source/contents/session_management.rst
+++ b/docs/source/contents/session_management.rst
@@ -481,7 +481,7 @@ add_grant
+++++++++
.. _add_grant:
- add_grant(self, user_id, client_id, **kwargs)
+ add_grant(self, user_id, client_id, \*\*kwargs)
find_token
++++++++++
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 99a12111..051a7b27 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -1,6 +1,10 @@
Welcome to Idpy OIDC-op Documentation
======================================
+.. image:: _images/oid-l-certification-mark-l-rgb-150dpi-90mm-300x157.png
+ :width: 300
+ :alt: 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.
@@ -66,6 +70,12 @@ under the `Apache 2.0 `_.
contents/developers.md
+.. toctree::
+ :maxdepth: 2
+ :caption: Client database
+
+ contents/clients.rst
+
.. toctree::
:maxdepth: 2
:caption: FAQ
diff --git a/example/fastapi/__init__.py b/example/fastapi/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/example/fastapi/config.json b/example/fastapi/config.json
new file mode 100644
index 00000000..0dc7fd0d
--- /dev/null
+++ b/example/fastapi/config.json
@@ -0,0 +1,330 @@
+{
+ "add_on": {
+ "pkce": {
+ "function": "oidcop.oidc.add_on.pkce.add_pkce_support",
+ "kwargs": {
+ "essential": false,
+ "code_challenge_method": "S256 S384 S512"
+ }
+ },
+ "claims": {
+ "function": "oidcop.oidc.add_on.custom_scopes.add_custom_scopes",
+ "kwargs": {
+ "research_and_scholarship": [
+ "name",
+ "given_name",
+ "family_name",
+ "email",
+ "email_verified",
+ "sub",
+ "iss",
+ "eduperson_scoped_affiliation"
+ ]
+ }
+ }
+ },
+ "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
+ }
+ }
+ },
+ "authentication": {
+ "user": {
+ "acr": "oidcop.user_authn.authn_context.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": {
+ "subject_types_supported": [
+ "public",
+ "pairwise"
+ ],
+ "grant_types_supported": [
+ "authorization_code",
+ "implicit",
+ "urn:ietf:params:oauth:grant-type:jwt-bearer",
+ "refresh_token"
+ ]
+ },
+ "cookie_handler": {
+ "class": "oidcop.cookie_handler.CookieHandler",
+ "kwargs": {
+ "keys": {
+ "private_path": "private/cookie_jwks.json",
+ "key_defs": [
+ {
+ "type": "OCT",
+ "use": [
+ "enc"
+ ],
+ "kid": "enc"
+ },
+ {
+ "type": "OCT",
+ "use": [
+ "sig"
+ ],
+ "kid": "sig"
+ }
+ ],
+ "read_only": false
+ },
+ "name": {
+ "session": "oidc_op",
+ "register": "oidc_op_rp",
+ "session_management": "sman"
+ }
+ }
+ },
+ "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": null,
+ "client_secret_expiration_time": 432000
+ }
+ },
+ "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"
+ ],
+ "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"
+ ]
+ }
+ },
+ "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"
+ }
+ }
+ },
+ "httpc_params": {
+ "verify": false
+ },
+ "id_token": {
+ "class": "oidcop.id_token.IDToken",
+ "kwargs": {
+ "default_claims": {
+ "email": {
+ "essential": true
+ },
+ "email_verified": {
+ "essential": true
+ }
+ }
+ }
+ },
+ "issuer": "https://{domain}:{port}",
+ "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"
+ },
+ "login_hint2acrs": {
+ "class": "oidcop.login_hint.LoginHint2Acrs",
+ "kwargs": {
+ "scheme_map": {
+ "email": [
+ "oidcop.user_authn.authn_context.INTERNETPROTOCOLPASSWORD"
+ ]
+ }
+ }
+ },
+ "session_key": {
+ "filename": "private/session_jwk.json",
+ "type": "OCT",
+ "use": "sig"
+ },
+ "template_dir": "templates",
+ "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_claim_by_scope": true,
+ "aud": [
+ "https://example.org/appl"
+ ]
+ }
+ },
+ "refresh": {
+ "kwargs": {
+ "lifetime": 86400
+ }
+ }
+ },
+ "userinfo": {
+ "class": "oidcop.user_info.UserInfo",
+ "kwargs": {
+ "db_file": "users.json"
+ }
+ }
+}
diff --git a/example/fastapi/main.py b/example/fastapi/main.py
new file mode 100644
index 00000000..e7e64c4b
--- /dev/null
+++ b/example/fastapi/main.py
@@ -0,0 +1,64 @@
+import json
+import logging
+
+from fastapi import Depends
+from fastapi import FastAPI
+from fastapi import HTTPException
+from fastapi.logger import logger
+from fastapi.openapi.models import Response
+from models import AuthorizationRequest
+from models import WebFingerRequest
+from utils import verify
+
+from oidcop.exception import FailedAuthentication
+from oidcop.server import Server
+
+logger.setLevel(logging.DEBUG)
+
+app = FastAPI()
+app.server = None
+
+
+def get_app():
+ return app
+
+
+@app.on_event("startup")
+def op_startup():
+ _str = open('config.json').read()
+ cnf = json.loads(_str)
+ server = Server(cnf, cwd="/oidc")
+ app.server = server
+
+
+@app.get("/.well-known/webfinger")
+async def well_known(model: WebFingerRequest = Depends()):
+ endpoint = app.server.server_get("endpoint", "discovery")
+ args = endpoint.process_request(model.dict())
+ response = endpoint.do_response(**args)
+ resp = json.loads(response["response"])
+ return resp
+
+
+@app.get("/.well-known/openid-configuration")
+async def openid_config():
+ endpoint = app.server.server_get("endpoint", "provider_config")
+ args = endpoint.process_request()
+ response = endpoint.do_response(**args)
+ resp = json.loads(response["response"])
+ return resp
+
+
+@app.post('/verify/user', status_code=200)
+def verify_user(kwargs: dict, response: Response):
+ authn_method = app.server.server_get(
+ "endpoint_context").authn_broker.get_method_by_id('user')
+ try:
+ return verify(app, authn_method, kwargs, response)
+ except FailedAuthentication as exc:
+ raise HTTPException(404, "Failed authentication")
+
+
+@app.get('/authorization')
+def authorization(model: AuthorizationRequest = Depends()):
+ return service_endpoint(app.server.server_get("endpoint", 'authorization'))
diff --git a/example/fastapi/models.py b/example/fastapi/models.py
new file mode 100644
index 00000000..63b7a45e
--- /dev/null
+++ b/example/fastapi/models.py
@@ -0,0 +1,31 @@
+from typing import List
+from typing import Optional
+
+from pydantic import BaseModel
+
+
+class WebFingerRequest(BaseModel):
+ rel: Optional[str] = 'http://openid.net/specs/connect/1.0/issuer'
+ resource: str
+
+
+class AuthorizationRequest(BaseModel):
+ acr_values: Optional[List[str]]
+ claims: Optional[dict]
+ claims_locales: Optional[List[str]]
+ client_id: str
+ display: Optional[str]
+ id_token_hint: Optional[str]
+ login_hint: Optional[str]
+ max_age: Optional[int]
+ nonce: Optional[str]
+ prompt: Optional[List[str]]
+ redirect_uri: str
+ registration: Optional[dict]
+ request: Optional[str]
+ request_uri: Optional[str]
+ response_mode: Optional[str]
+ response_type: List[str]
+ scope: List[str]
+ state: Optional[str]
+ ui_locales: Optional[List[str]]
diff --git a/unsupported/chpy/passwd.json b/example/fastapi/passwd.json
similarity index 100%
rename from unsupported/chpy/passwd.json
rename to example/fastapi/passwd.json
diff --git a/unsupported/chpy/users.json b/example/fastapi/users.json
similarity index 100%
rename from unsupported/chpy/users.json
rename to example/fastapi/users.json
diff --git a/example/fastapi/utils.py b/example/fastapi/utils.py
new file mode 100644
index 00000000..13efdf6c
--- /dev/null
+++ b/example/fastapi/utils.py
@@ -0,0 +1,136 @@
+import json
+
+from fastapi import HTTPException
+from fastapi import status
+from oic.oic import AuthorizationRequest
+from oidcmsg.oauth2 import ResponseMessage
+
+
+def do_response(endpoint, req_args, response, error='', **args):
+ info = endpoint.do_response(request=req_args, error=error, **args)
+
+ try:
+ _response_placement = info['response_placement']
+ except KeyError:
+ _response_placement = endpoint.response_placement
+
+ if error:
+ if _response_placement == 'body':
+ raise HTTPException(400, info['response'])
+ else: # _response_placement == 'url':
+ response.status_code = status.HTTP_307_TEMPORARY_REDIRECT
+ resp = json.loads(info['response'])
+ else:
+ if _response_placement == 'body':
+ resp = json.loads(info['response'])
+ else: # _response_placement == 'url':
+ response.status_code = status.HTTP_307_TEMPORARY_REDIRECT
+ resp = json.loads(info['response'])
+
+ for key, value in info['http_headers']:
+ response.headers[key] = value
+
+ if 'cookie' in info:
+ for d in info["cookie"]:
+ response.set_cookie(key=d["name"], value=d["value"])
+
+ return resp
+
+
+def verify(app, authn_method, kwargs, response):
+ """
+ Authentication verification
+
+ :param kwargs: response arguments
+ :return: HTTP redirect
+ """
+
+ #kwargs = dict([(k, v) for k, v in request.form.items()])
+ username = authn_method.verify(**kwargs)
+ if not username:
+ raise HTTPException(403, "Authentication failed")
+
+ auth_args = authn_method.unpack_token(kwargs['token'])
+ authz_request = AuthorizationRequest().from_urlencoded(auth_args['query'])
+
+ endpoint = app.server.server_get("endpoint", 'authorization')
+ _session_id = endpoint.create_session(authz_request, username, auth_args['authn_class_ref'],
+ auth_args['iat'], authn_method)
+
+ args = endpoint.authz_part2(request=authz_request, session_id=_session_id)
+
+ if isinstance(args, ResponseMessage) and 'error' in args:
+ raise HTTPException(400, args.to_json())
+
+ return do_response(endpoint, kwargs, response, **args)
+
+
+IGNORE = ["cookie", "user-agent"]
+
+
+def service_endpoint(app, endpoint):
+ _log = app.srv_config.logger
+ _log.info('At the "{}" endpoint'.format(endpoint.name))
+
+ http_info = {
+ "headers": {k: v for k, v in request.headers.items(lower=True) if k not in IGNORE},
+ "method": request.method,
+ "url": request.url,
+ # name is not unique
+ "cookie": [{"name": k, "value": v} for k, v in request.cookies.items()]
+ }
+
+ if request.method == 'GET':
+ try:
+ req_args = endpoint.parse_request(request.args.to_dict(), http_info=http_info)
+ except (InvalidClient, UnknownClient) as err:
+ _log.error(err)
+ return make_response(json.dumps({
+ 'error': 'unauthorized_client',
+ 'error_description': str(err)
+ }), 400)
+ except Exception as err:
+ _log.error(err)
+ return make_response(json.dumps({
+ 'error': 'invalid_request',
+ 'error_description': str(err)
+ }), 400)
+ else:
+ if request.data:
+ if isinstance(request.data, str):
+ req_args = request.data
+ else:
+ req_args = request.data.decode()
+ else:
+ req_args = dict([(k, v) for k, v in request.form.items()])
+ try:
+ req_args = endpoint.parse_request(req_args, http_info=http_info)
+ except Exception as err:
+ _log.error(err)
+ err_msg = ResponseMessage(error='invalid_request', error_description=str(err))
+ return make_response(err_msg.to_json(), 400)
+
+ _log.info('request: {}'.format(req_args))
+ if isinstance(req_args, ResponseMessage) and 'error' in req_args:
+ return make_response(req_args.to_json(), 400)
+
+ try:
+ if isinstance(endpoint, Token):
+ args = endpoint.process_request(AccessTokenRequest(**req_args), http_info=http_info)
+ else:
+ args = endpoint.process_request(req_args, http_info=http_info)
+ except Exception as err:
+ message = traceback.format_exception(*sys.exc_info())
+ _log.error(message)
+ err_msg = ResponseMessage(error='invalid_request', error_description=str(err))
+ return make_response(err_msg.to_json(), 400)
+
+ _log.info('Response args: {}'.format(args))
+
+ if 'redirect_location' in args:
+ return redirect(args['redirect_location'])
+ if 'http_response' in args:
+ return make_response(args['http_response'], 200)
+
+ response = do_response(endpoint, req_args, **args)
+ return response
diff --git a/example/flask_op/config.json b/example/flask_op/config.json
index 1f0fb783..e3fe080d 100644
--- a/example/flask_op/config.json
+++ b/example/flask_op/config.json
@@ -116,6 +116,20 @@
"implicit",
"urn:ietf:params:oauth:grant-type:jwt-bearer",
"refresh_token"
+ ],
+ "request_object_signing_alg_values_supported": [
+ "RS256",
+ "RS384",
+ "RS512",
+ "ES256",
+ "ES384",
+ "ES512",
+ "HS256",
+ "HS384",
+ "HS512",
+ "PS256",
+ "PS384",
+ "PS512"
]
},
"cookie_handler": {
diff --git a/example/flask_op/config.yaml b/example/flask_op/config.yaml
deleted file mode 100644
index b79a5fc9..00000000
--- a/example/flask_op/config.yaml
+++ /dev/null
@@ -1,268 +0,0 @@
-logging:
- version: 1
- root:
- handlers:
- - default
- - console
- level: DEBUG
- loggers:
- bobcat_idp:
- level: DEBUG
- handlers:
- default:
- class: logging.FileHandler
- filename: 'debug.log'
- formatter: default
- console:
- class: logging.StreamHandler
- stream: 'ext://sys.stdout'
- formatter: default
- formatters:
- default:
- format: '%(asctime)s %(name)s %(levelname)s %(message)s'
-
-port: &port 5000
-domain: &domain 192.168.1.158
-server_name: '{domain}:{port}'
-base_url: &base_url 'https://{domain}:{port}'
-
-key_def: &key_def
- -
- type: RSA
- use:
- - sig
- -
- type: EC
- crv: "P-256"
- use:
- - sig
-
-
-op:
- server_info:
- issuer: *base_url
- httpc_params:
- verify: False
- capabilities:
- subject_types_supported:
- - public
- - pairwise
- grant_types_supported:
- - authorization_code
- - implicit
- - urn:ietf:params:oauth:grant-type:jwt-bearer
- - refresh_token
- template_dir: templates
- 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:
- lifetime: 600
- id_token:
- class: oidcop.token.id_token.IDToken
- kwargs:
- base_claims:
- email:
- essential: True
- email_verified:
- essential: True
- token:
- class: oidcop.token.jwt_token.JWTToken
- lifetime: 3600
- add_claims:
- - email
- - email_verified
- - phone_number
- - phone_number_verified
- add_claims_by_scope: True
- aud:
- - https://example.org/appl
- refresh:
- lifetime: 86400
- keys:
- private_path: "private/jwks.json"
- key_defs: *key_def
- public_path: 'static/jwks.json'
- read_only: False
- # otherwise OP metadata will have jwks_uri: https://127.0.0.1:5000/None!
- uri_path: 'static/jwks.json'
- 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: null
- client_secret_expiration_time: 432000
- 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
- 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
- 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'
- userinfo:
- class: oidcop.user_info.UserInfo
- kwargs:
- db_file: users.json
- 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"
- #anon:
- #acr: oidcop.user_authn.authn_context.UNSPECIFIED
- #class: oidcop.user_authn.user.NoAuthn
- #kwargs:
- #user: diana
- cookie_handler:
- class: oidcop.cookie_handler.CookieHandler
- kwargs":
- keys:
- private_path: "private/cookie_jwks.json"
- key_defs:
- -
- type: OCT
- kid: enc
- use:
- - enc
- -
- type: OCT
- kid: sig
- use:
- - sig
- read_only: false
- name:
- session: "oidc_op"
- register: "oidc_op_rp"
- session_management: "sman"
- login_hint2acrs:
- class: oidcop.login_hint.LoginHint2Acrs
- kwargs:
- scheme_map:
- email:
- - urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocolPassword
-
- # this adds PKCE support as mandatory - disable it if needed (essential: False)
- add_on:
- pkce:
- function: oidcop.oidc.add_on.pkce.add_pkce_support
- kwargs:
- essential: false
- code_challenge_method:
- #plain
- S256
- S384
- S512
-
- claims:
- function: oidcop.oidc.add_on.custom_scopes.add_custom_scopes
- kwargs:
- research_and_scholarship:
- - name
- - given_name
- - family_name
- - email
- - email_verified
- - sub
- - iss
- - eduperson_scoped_affiliation
-
-webserver:
- server_cert: 'certs/89296913_127.0.0.1.cert'
- server_key: 'certs/89296913_127.0.0.1.key'
- ca_bundle: null
- verify_user: false
- port: *port
- domain: 0.0.0.0
- debug: true
diff --git a/example/flask_op/seed.txt b/example/flask_op/seed.txt
deleted file mode 100644
index e5bd6105..00000000
--- a/example/flask_op/seed.txt
+++ /dev/null
@@ -1 +0,0 @@
-5gRv9mEEqMwO1xwkNblGXNsvbMVMghbaZbCAYMzBYOxj2Sji
\ No newline at end of file
diff --git a/example/flask_op/views.py b/example/flask_op/views.py
index d17b7347..648c3de6 100644
--- a/example/flask_op/views.py
+++ b/example/flask_op/views.py
@@ -304,9 +304,9 @@ def check_session_iframe():
return 'error'
return 'OK'
- current_app.logger.debug(
- 'check_session_iframe: {}'.format(req_args))
+ current_app.logger.debug('check_session_iframe: {}'.format(req_args))
doc = open('templates/check_session_iframe.html').read()
+ current_app.logger.debug(f"check_session_iframe response: {doc}")
return doc
diff --git a/example/flask_op/yaml_to_json.py b/example/flask_op/yaml_to_json.py
new file mode 100755
index 00000000..4f7052d4
--- /dev/null
+++ b/example/flask_op/yaml_to_json.py
@@ -0,0 +1,11 @@
+#! /usr/bin/env python3
+import json
+import sys
+
+import yaml
+
+"""Load a YAML configuration file."""
+with open(sys.argv[1], "rt", encoding='utf-8') as file:
+ config_dict = yaml.safe_load(file)
+
+print(json.dumps(config_dict))
diff --git a/pyproject.toml b/pyproject.toml
index 7564b0ee..f63cfeb0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[metadata]
name = "oidcop"
-version = "2.1.0"
+version = "2.3.0"
author = "Roland Hedberg"
author_email = "roland@catalogix.se"
description = "Python implementation of an OAuth2 AS and an OIDC Provider"
diff --git a/setup.py b/setup.py
index d707fd8a..47b7b04b 100644
--- a/setup.py
+++ b/setup.py
@@ -72,8 +72,7 @@ def run_tests(self):
"Programming Language :: Python :: 3.9",
"Topic :: Software Development :: Libraries :: Python Modules"],
install_requires=[
- "oidcmsg==1.4.0",
- "cryptojwt==1.5.2",
+ "oidcmsg==1.5.3",
"pyyaml",
"jinja2>=2.11.3",
"responses>=0.13.0"
diff --git a/src/oidcop/__init__.py b/src/oidcop/__init__.py
index 29c1f2a8..de2c3aa2 100644
--- a/src/oidcop/__init__.py
+++ b/src/oidcop/__init__.py
@@ -1,6 +1,6 @@
import secrets
-__version__ = "2.2.1"
+__version__ = "2.3.0"
DEF_SIGN_ALG = {
"id_token": "RS256",
diff --git a/src/oidcop/authn_event.py b/src/oidcop/authn_event.py
index 0b5bf0e8..6b7bae0a 100644
--- a/src/oidcop/authn_event.py
+++ b/src/oidcop/authn_event.py
@@ -2,7 +2,7 @@
from oidcmsg.message import SINGLE_OPTIONAL_STRING
from oidcmsg.message import SINGLE_REQUIRED_STRING
from oidcmsg.message import Message
-from oidcmsg.time_util import time_sans_frac
+from oidcmsg.time_util import utc_time_sans_frac
DEFAULT_AUTHN_EXPIRES_IN = 3600
@@ -20,10 +20,10 @@ def is_valid(self, now=0):
if now:
return self["valid_until"] > now
else:
- return self["valid_until"] > time_sans_frac()
+ return self["valid_until"] > utc_time_sans_frac()
def expires_in(self):
- return self["valid_until"] - time_sans_frac()
+ return self["valid_until"] - utc_time_sans_frac()
def create_authn_event(
@@ -59,7 +59,7 @@ def create_authn_event(
if _ts:
args["authn_time"] = _ts
else:
- args["authn_time"] = time_sans_frac()
+ args["authn_time"] = utc_time_sans_frac()
if valid_until:
args["valid_until"] = valid_until
diff --git a/src/oidcop/authz/__init__.py b/src/oidcop/authz/__init__.py
index a22a11bb..72b69797 100755
--- a/src/oidcop/authz/__init__.py
+++ b/src/oidcop/authz/__init__.py
@@ -57,7 +57,10 @@ def usage_rules_for(self, client_id, token_type):
return {}
def __call__(
- self, session_id: str, request: Union[dict, Message], resources: Optional[list] = None,
+ self,
+ session_id: str,
+ request: Union[dict, Message],
+ resources: Optional[list] = None,
) -> Grant:
session_info = self.server_get("endpoint_context").session_manager.get_session_info(
session_id=session_id, grant=True
@@ -93,7 +96,10 @@ def __call__(
class Implicit(AuthzHandling):
def __call__(
- self, session_id: str, request: Union[dict, Message], resources: Optional[list] = None,
+ self,
+ session_id: str,
+ request: Union[dict, Message],
+ resources: Optional[list] = None,
) -> Grant:
args = self.grant_config.copy()
grant = self.server_get("endpoint_context").session_manager.get_grant(session_id=session_id)
diff --git a/src/oidcop/client_authn.py b/src/oidcop/client_authn.py
index 7678419a..8c540b01 100755
--- a/src/oidcop/client_authn.py
+++ b/src/oidcop/client_authn.py
@@ -141,8 +141,7 @@ def verify(self, request, **kwargs):
class BearerHeader(ClientSecretBasic):
- """
- """
+ """"""
tag = "bearer_header"
@@ -348,7 +347,7 @@ def verify_client(
authorization_token = None
auth_info = {}
- _methods = getattr(endpoint, 'client_authn_method', [])
+ _methods = getattr(endpoint, "client_authn_method", [])
for _method in _methods:
if _method is None:
@@ -356,7 +355,9 @@ def verify_client(
if _method.is_usable(request, authorization_token):
try:
auth_info = _method.verify(
- request=request, authorization_token=authorization_token, endpoint=endpoint,
+ request=request,
+ authorization_token=authorization_token,
+ endpoint=endpoint,
)
except Exception as err:
logger.warning("Verifying auth using {} failed: {}".format(_method.tag, err))
diff --git a/src/oidcop/configure.py b/src/oidcop/configure.py
index 5f51bac9..2b9cc6d9 100755
--- a/src/oidcop/configure.py
+++ b/src/oidcop/configure.py
@@ -24,7 +24,7 @@
"private_path",
"public_path",
"db_file",
- "jwks_file"
+ "jwks_file",
]
OP_DEFAULT_CONFIG = {
@@ -48,8 +48,11 @@
],
"read_only": False,
},
- "name": {"session": "oidc_op", "register": "oidc_op_rp",
- "session_management": "sman", },
+ "name": {
+ "session": "oidc_op",
+ "register": "oidc_op_rp",
+ "session_management": "sman",
+ },
},
},
"claims_interface": {"class": "oidcop.session.claims.ClaimsInterface", "kwargs": {}},
@@ -59,13 +62,17 @@
"grant_config": {
"usage_rules": {
"authorization_code": {
- "supports_minting": ["access_token", "refresh_token", "id_token", ],
+ "supports_minting": [
+ "access_token",
+ "refresh_token",
+ "id_token",
+ ],
"max_usage": 1,
},
"access_token": {},
"refresh_token": {
"supports_minting": ["access_token", "refresh_token"],
- "expires_in": -1
+ "expires_in": -1,
},
},
"expires_in": 43200,
@@ -78,8 +85,14 @@
"token_handler_args": {
"jwks_file": "private/token_jwks.json",
"code": {"kwargs": {"lifetime": 600}},
- "token": {"class": "oidcop.token.jwt_token.JWTToken", "kwargs": {"lifetime": 3600}, },
- "refresh": {"class": "oidcop.token.jwt_token.JWTToken", "kwargs": {"lifetime": 86400}, },
+ "token": {
+ "class": "oidcop.token.jwt_token.JWTToken",
+ "kwargs": {"lifetime": 3600},
+ },
+ "refresh": {
+ "class": "oidcop.token.jwt_token.JWTToken",
+ "kwargs": {"lifetime": 86400},
+ },
"id_token": {"class": "oidcop.token.id_token.IDToken", "kwargs": {}},
},
"scopes_to_claims": SCOPE2CLAIMS,
@@ -87,7 +100,8 @@
AS_DEFAULT_CONFIG = copy.deepcopy(OP_DEFAULT_CONFIG)
AS_DEFAULT_CONFIG["claims_interface"] = {
- "class": "oidcop.session.claims.OAuth2ClaimsInterface", "kwargs": {}
+ "class": "oidcop.session.claims.OAuth2ClaimsInterface",
+ "kwargs": {},
}
@@ -128,13 +142,13 @@ def set_domain_and_port(conf: dict, uris: List[str], domain: str, port: int):
def create_from_config_file(
- cls,
- filename: str,
- base_path: str = "",
- entity_conf: Optional[List[dict]] = None,
- file_attributes: Optional[List[str]] = None,
- domain: Optional[str] = "",
- port: Optional[int] = 0,
+ cls,
+ filename: str,
+ base_path: str = "",
+ entity_conf: Optional[List[dict]] = None,
+ file_attributes: Optional[List[str]] = None,
+ domain: Optional[str] = "",
+ port: Optional[int] = 0,
):
if filename.endswith(".yaml"):
"""Load configuration as YAML"""
@@ -166,7 +180,10 @@ class Base(dict):
parameter = {}
def __init__(
- self, conf: Dict, base_path: str = "", file_attributes: Optional[List[str]] = None,
+ self,
+ conf: Dict,
+ base_path: str = "",
+ file_attributes: Optional[List[str]] = None,
):
dict.__init__(self)
@@ -182,12 +199,12 @@ def __getattr__(self, item):
def __setattr__(self, key, value):
if key in self:
- raise KeyError('{} has already been set'.format(key))
+ raise KeyError("{} has already been set".format(key))
super(Base, self).__setitem__(key, value)
def __setitem__(self, key, value):
if key in self:
- raise KeyError('{} has already been set'.format(key))
+ raise KeyError("{} has already been set".format(key))
super(Base, self).__setitem__(key, value)
@@ -214,13 +231,13 @@ class EntityConfiguration(Base):
}
def __init__(
- self,
- conf: Dict,
- base_path: Optional[str] = "",
- entity_conf: Optional[List[dict]] = None,
- domain: Optional[str] = "",
- port: Optional[int] = 0,
- file_attributes: Optional[List[str]] = None,
+ self,
+ conf: Dict,
+ base_path: Optional[str] = "",
+ entity_conf: Optional[List[dict]] = None,
+ domain: Optional[str] = "",
+ port: Optional[int] = 0,
+ file_attributes: Optional[List[str]] = None,
):
conf = copy.deepcopy(conf)
@@ -240,19 +257,20 @@ def __init__(
if not _val:
if key in self.default_config:
_val = copy.deepcopy(self.default_config[key])
- self.format(_val, base_path=base_path, file_attributes=file_attributes,
- domain=domain, port=port)
+ self.format(
+ _val,
+ base_path=base_path,
+ file_attributes=file_attributes,
+ domain=domain,
+ port=port,
+ )
else:
continue
if key not in DEFAULT_EXTENDED_CONF:
- logger.warning(
- f"{key} not seems to be a valid configuration parameter"
- )
+ logger.warning(f"{key} not seems to be a valid configuration parameter")
elif not _val:
- logger.warning(
- f"{key} not configured, using default configuration values"
- )
+ logger.warning(f"{key} not configured, using default configuration values")
if key == "template_dir":
_val = os.path.abspath(_val)
@@ -314,38 +332,45 @@ def __init__(
port=port,
file_attributes=file_attributes,
)
- scopes_to_claims = self.scopes_to_claims
+ self.scopes_to_claims
class ASConfiguration(EntityConfiguration):
"Authorization server configuration"
def __init__(
- self,
- conf: Dict,
- base_path: Optional[str] = "",
- entity_conf: Optional[List[dict]] = None,
- domain: Optional[str] = "",
- port: Optional[int] = 0,
- file_attributes: Optional[List[str]] = None,
+ self,
+ conf: Dict,
+ base_path: Optional[str] = "",
+ entity_conf: Optional[List[dict]] = None,
+ domain: Optional[str] = "",
+ port: Optional[int] = 0,
+ file_attributes: Optional[List[str]] = None,
):
- EntityConfiguration.__init__(self, conf=conf, base_path=base_path,
- entity_conf=entity_conf, domain=domain, port=port,
- file_attributes=file_attributes)
+ EntityConfiguration.__init__(
+ self,
+ conf=conf,
+ base_path=base_path,
+ entity_conf=entity_conf,
+ domain=domain,
+ port=port,
+ file_attributes=file_attributes,
+ )
class Configuration(Base):
"""Server Configuration"""
+
uris = ["issuer", "base_url"]
def __init__(
- self,
- conf: Dict,
- entity_conf: Optional[List[dict]] = None,
- base_path: str = "",
- file_attributes: Optional[List[str]] = None,
- domain: Optional[str] = "",
- port: Optional[int] = 0,
+ self,
+ conf: Dict,
+ entity_conf: Optional[List[dict]] = None,
+ base_path: str = "",
+ file_attributes: Optional[List[str]] = None,
+ domain: Optional[str] = "",
+ port: Optional[int] = 0,
):
Base.__init__(self, conf, base_path, file_attributes)
@@ -415,13 +440,17 @@ def __init__(
"grant_config": {
"usage_rules": {
"authorization_code": {
- "supports_minting": ["access_token", "refresh_token", "id_token", ],
+ "supports_minting": [
+ "access_token",
+ "refresh_token",
+ "id_token",
+ ],
"max_usage": 1,
},
"access_token": {},
"refresh_token": {
"supports_minting": ["access_token", "refresh_token"],
- "expires_in": -1
+ "expires_in": -1,
},
},
"expires_in": 43200,
@@ -435,7 +464,10 @@ def __init__(
"kwargs": {
"verify_endpoint": "verify/user",
"template": "user_pass.jinja2",
- "db": {"class": "oidcop.util.JSONDictDB", "kwargs": {"filename": "passwd.json"}, },
+ "db": {
+ "class": "oidcop.util.JSONDictDB",
+ "kwargs": {"filename": "passwd.json"},
+ },
"page_header": "Testing log in",
"submit_btn": "Get me in!",
"user_label": "Nickname",
@@ -463,8 +495,11 @@ def __init__(
],
"read_only": False,
},
- "name": {"session": "oidc_op", "register": "oidc_op_rp",
- "session_management": "sman", },
+ "name": {
+ "session": "oidc_op",
+ "register": "oidc_op_rp",
+ "session_management": "sman",
+ },
},
},
"endpoint": {
@@ -481,7 +516,10 @@ def __init__(
"registration": {
"path": "registration",
"class": "oidcop.oidc.registration.Registration",
- "kwargs": {"client_authn_method": None, "client_secret_expiration_time": 432000, },
+ "kwargs": {
+ "client_authn_method": None,
+ "client_secret_expiration_time": 432000,
+ },
},
"registration_api": {
"path": "registration_api",
@@ -491,7 +529,10 @@ def __init__(
"introspection": {
"path": "introspection",
"class": "oidcop.oauth2.introspection.Introspection",
- "kwargs": {"client_authn_method": ["client_secret_post"], "release": ["username"], },
+ "kwargs": {
+ "client_authn_method": ["client_secret_post"],
+ "release": ["username"],
+ },
},
"authorization": {
"path": "authorization",
@@ -562,7 +603,8 @@ def __init__(
"class": "oidcop.login_hint.LoginHint2Acrs",
"kwargs": {
"scheme_map": {
- "email": ["urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocolPassword"]}
+ "email": ["urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocolPassword"]
+ }
},
},
"template_dir": "templates",
@@ -583,7 +625,10 @@ def __init__(
},
"refresh": {
"class": "oidcop.token.jwt_token.JWTToken",
- "kwargs": {"lifetime": 3600, "aud": ["https://example.org/appl"], },
+ "kwargs": {
+ "lifetime": 3600,
+ "aud": ["https://example.org/appl"],
+ },
},
"id_token": {
"class": "oidcop.token.id_token.IDToken",
@@ -595,24 +640,20 @@ def __init__(
},
},
},
- "userinfo": {"class": "oidcop.user_info.UserInfo", "kwargs": {"db_file": "users.json"}, },
+ "userinfo": {
+ "class": "oidcop.user_info.UserInfo",
+ "kwargs": {"db_file": "users.json"},
+ },
"scopes_to_claims": SCOPE2CLAIMS,
"session_params": {
- "password": "ses_key",
- "salt": "ses_salt",
- "sub_func": {
- "public": {
- "class": "oidcop.session.manager.PublicID",
- "kwargs": {
- "salt": "mysalt"
- }
+ "password": "ses_key",
+ "salt": "ses_salt",
+ "sub_func": {
+ "public": {"class": "oidcop.session.manager.PublicID", "kwargs": {"salt": "mysalt"}},
+ "pairwise": {
+ "class": "oidcop.session.manager.PairWiseID",
+ "kwargs": {"salt": "mysalt"},
+ },
},
- "pairwise": {
- "class": "oidcop.session.manager.PairWiseID",
- "kwargs": {
- "salt": "mysalt"
- }
- }
- }
},
}
diff --git a/src/oidcop/cookie_handler.py b/src/oidcop/cookie_handler.py
index 7812f825..ea2549b5 100755
--- a/src/oidcop/cookie_handler.py
+++ b/src/oidcop/cookie_handler.py
@@ -37,7 +37,7 @@ def __init__(
keys: Optional[dict] = None,
sign_alg: [str] = "SHA256",
name: Optional[dict] = None,
- **kwargs
+ **kwargs,
):
if keys:
@@ -79,12 +79,12 @@ def __init__(
self.name = name
self.flags = kwargs.get(
- 'flags',
+ "flags",
{
- "samesite": "None",
- "httponly": True,
- "secure": True,
- }
+ "samesite": "None",
+ "httponly": True,
+ "secure": True,
+ },
)
def _sign_enc_payload(self, payload: str, timestamp: Optional[Union[int, str]] = 0):
@@ -153,7 +153,9 @@ def _ver_dec_content(self, parts):
mac = base64.b64decode(b64_mac)
verifier = HMACSigner(algorithm=self.sign_alg)
if verifier.verify(
- payload.encode("utf-8") + timestamp.encode("utf-8"), mac, self.sign_key.key,
+ payload.encode("utf-8") + timestamp.encode("utf-8"),
+ mac,
+ self.sign_key.key,
):
return payload, timestamp
else:
@@ -194,7 +196,7 @@ def make_cookie_content(
typ: Optional[str] = "",
timestamp: Optional[Union[int, str]] = "",
max_age: Optional[int] = 0,
- **kwargs
+ **kwargs,
) -> dict:
"""
Create and return information to put in a cookie
@@ -228,7 +230,7 @@ def make_cookie_content(
elif max_age:
content["max-age"] = epoch_in_a_while(seconds=max_age)
- for k,v in self.flags.items():
+ for k, v in self.flags.items():
content[k] = v
return content
@@ -253,7 +255,7 @@ def parse_cookie(self, name: str, cookies: List[dict]) -> Optional[List[dict]]:
LOGGER.debug("Looking for '{}' cookies".format(name))
res = []
for _cookie in cookies:
- LOGGER.debug('Cookie: {}'.format(_cookie))
+ LOGGER.debug("Cookie: {}".format(_cookie))
if "name" in _cookie and _cookie["name"] == name:
_content = self._ver_dec_content(_cookie["value"].split("|"))
if _content:
diff --git a/src/oidcop/endpoint.py b/src/oidcop/endpoint.py
index f5ea2bf8..854b85ae 100755
--- a/src/oidcop/endpoint.py
+++ b/src/oidcop/endpoint.py
@@ -1,6 +1,5 @@
import logging
from typing import Callable
-from typing import List
from typing import Optional
from typing import Union
from urllib.parse import urlparse
@@ -9,6 +8,7 @@
from oidcmsg.exception import MissingRequiredValue
from oidcmsg.message import Message
from oidcmsg.oauth2 import ResponseMessage
+from oidcmsg.oidc import RegistrationRequest
from oidcop import sanitize
from oidcop.client_authn import client_auth_setup
@@ -128,6 +128,10 @@ def __init__(self, server_get: Callable, **kwargs):
self.allowed_targets = [self.name]
self.client_verification_method = []
+ def process_verify_error(self, exception):
+ _error = "invalid_request"
+ return self.error_cls(error=_error, error_description="%s" % exception)
+
def parse_request(
self, request: Union[Message, dict, str], http_info: Optional[dict] = None, **kwargs
):
@@ -185,13 +189,21 @@ def parse_request(
try:
req.verify(keyjar=keyjar, opponent_id=_client_id)
except (MissingRequiredAttribute, ValueError, MissingRequiredValue) as err:
- return self.error_cls(error="invalid_request", error_description="%s" % err)
+ return self.process_verify_error(err)
+ _error = "invalid_request"
+ if isinstance(err, ValueError) and self.request_cls == RegistrationRequest:
+ if len(err.args) > 1:
+ if err.args[1] == "initiate_login_uri":
+ _error = "invalid_client_metadata"
+
+ return self.error_cls(error=_error, error_description="%s" % err)
LOGGER.info("Parsed and verified request: %s" % sanitize(req))
# Do any endpoint specific parsing
- return self.do_post_parse_request(request=req, client_id=_client_id, http_info=http_info,
- **kwargs)
+ return self.do_post_parse_request(
+ request=req, client_id=_client_id, http_info=http_info, **kwargs
+ )
def get_client_id_from_token(
self,
diff --git a/src/oidcop/endpoint_context.py b/src/oidcop/endpoint_context.py
index 51629b67..fb513a58 100755
--- a/src/oidcop/endpoint_context.py
+++ b/src/oidcop/endpoint_context.py
@@ -15,7 +15,6 @@
from oidcop.configure import OPConfiguration
from oidcop.scopes import SCOPE2CLAIMS
from oidcop.scopes import Scopes
-from oidcop.session.claims import STANDARD_CLAIMS
from oidcop.session.manager import SessionManager
from oidcop.template_handler import Jinja2TemplateHandler
from oidcop.util import get_http_params
diff --git a/src/oidcop/logging.py b/src/oidcop/logging.py
index d04c577e..10dfdc1e 100755
--- a/src/oidcop/logging.py
+++ b/src/oidcop/logging.py
@@ -17,7 +17,9 @@
def configure_logging(
- debug: Optional[bool] = False, config: Optional[dict] = None, filename: Optional[str] = "",
+ debug: Optional[bool] = False,
+ config: Optional[dict] = None,
+ filename: Optional[str] = "",
) -> logging.Logger:
"""Configure logging"""
diff --git a/src/oidcop/login_hint.py b/src/oidcop/login_hint.py
index cd64a3c9..904a64a2 100644
--- a/src/oidcop/login_hint.py
+++ b/src/oidcop/login_hint.py
@@ -24,6 +24,7 @@ class LoginHint2Acrs(object):
"""
OIDC Login hint support
"""
+
def __init__(self, scheme_map, server_get=None):
self.scheme_map = scheme_map
self.server_get = server_get
diff --git a/src/oidcop/oauth2/authorization.py b/src/oidcop/oauth2/authorization.py
index 2d13ad50..86b7ce82 100755
--- a/src/oidcop/oauth2/authorization.py
+++ b/src/oidcop/oauth2/authorization.py
@@ -45,11 +45,18 @@
# For the time being. This is JAR specific and should probably be configurable.
ALG_PARAMS = {
- "sign": ["request_object_signing_alg", "request_object_signing_alg_values_supported", ],
- "enc_alg": ["request_object_encryption_alg",
- "request_object_encryption_alg_values_supported", ],
- "enc_enc": ["request_object_encryption_enc",
- "request_object_encryption_enc_values_supported", ],
+ "sign": [
+ "request_object_signing_alg",
+ "request_object_signing_alg_values_supported",
+ ],
+ "enc_alg": [
+ "request_object_encryption_alg",
+ "request_object_encryption_alg_values_supported",
+ ],
+ "enc_enc": [
+ "request_object_encryption_enc",
+ "request_object_encryption_enc_values_supported",
+ ],
}
FORM_POST = """
@@ -83,10 +90,10 @@ def max_age(request):
def verify_uri(
- endpoint_context: EndpointContext,
- request: Union[dict, Message],
- uri_type: str,
- client_id: Optional[str] = None,
+ endpoint_context: EndpointContext,
+ request: Union[dict, Message],
+ uri_type: str,
+ client_id: Optional[str] = None,
):
"""
A redirect URI
@@ -116,20 +123,25 @@ def verify_uri(
raise URIError("Contains fragment")
(_base, _query) = split_uri(_redirect_uri)
- # if _query:
- # _query = parse_qs(_query)
# Get the clients registered redirect uris
client_info = endpoint_context.cdb.get(_cid)
if client_info is None:
raise KeyError("No such client")
- redirect_uris = client_info.get("{}s".format(uri_type))
+ redirect_uris = client_info.get(f"{uri_type}s")
+
if redirect_uris is None:
- raise ValueError(f"No registered {uri_type} for {_cid}")
+ raise RedirectURIError(f"No registered {uri_type} for {_cid}")
else:
match = False
- for regbase, rquery in redirect_uris:
+ for _item in redirect_uris:
+ if isinstance(_item, str):
+ regbase = _item
+ rquery = {}
+ else:
+ regbase, rquery = _item
+
# The URI MUST exactly match one of the Redirection URI
if _base == regbase:
# every registered query component must exist in the uri
@@ -177,7 +189,7 @@ def join_query(base, query):
def get_uri(endpoint_context, request, uri_type):
- """ verify that the redirect URI is reasonable.
+ """verify that the redirect URI is reasonable.
:param endpoint_context: An EndpointContext instance
:param request: The Authorization request
@@ -208,7 +220,10 @@ def get_uri(endpoint_context, request, uri_type):
def authn_args_gather(
- request: Union[AuthorizationRequest, dict], authn_class_ref: str, cinfo: dict, **kwargs,
+ request: Union[AuthorizationRequest, dict],
+ authn_class_ref: str,
+ cinfo: dict,
+ **kwargs,
):
"""
Gather information to be used by the authentication method
@@ -251,9 +266,7 @@ def check_unknown_scopes_policy(request_info, client_id, endpoint_context):
return
scope = request_info["scope"]
- filtered_scopes = set(
- endpoint_context.scopes_handler.filter_scopes(scope, client_id=client_id)
- )
+ filtered_scopes = set(endpoint_context.scopes_handler.filter_scopes(scope, client_id=client_id))
scopes = set(scope)
# this prevents that authz would be released for unavailable scopes
if scopes != filtered_scopes:
@@ -377,10 +390,16 @@ def _do_request_uri(self, request, client_id, endpoint_context, **kwargs):
)
if _ver_request.jwe_header is not None:
self.allowed_request_algorithms(
- client_id, endpoint_context, _ver_request.jws_header.get("alg"), "enc_alg",
+ client_id,
+ endpoint_context,
+ _ver_request.jws_header.get("alg"),
+ "enc_alg",
)
self.allowed_request_algorithms(
- client_id, endpoint_context, _ver_request.jws_header.get("enc"), "enc_enc",
+ client_id,
+ endpoint_context,
+ _ver_request.jws_header.get("enc"),
+ "enc_enc",
)
# The protected info overwrites the non-protected
for k, v in _ver_request.items():
@@ -404,18 +423,18 @@ def _post_parse_request(self, request, client_id, endpoint_context, **kwargs):
"""
if not request:
logger.debug("No AuthzRequest")
- return self.authentication_error_response(request,
- error="invalid_request",
- error_description="Can not parse AuthzRequest"
- )
+ return self.authentication_error_response(
+ request, error="invalid_request", error_description="Can not parse AuthzRequest"
+ )
request = self.filter_request(endpoint_context, request)
_cinfo = endpoint_context.cdb.get(client_id)
if not _cinfo:
logger.error("Client ID ({}) not in client database".format(request["client_id"]))
- return self.authentication_error_response(request, error="unauthorized_client",
- error_description="unknown client")
+ return self.authentication_error_response(
+ request, error="unauthorized_client", error_description="unknown client"
+ )
# Is the asked for response_type among those that are permitted
if not self.verify_response_type(request, _cinfo):
@@ -452,9 +471,7 @@ def pick_authn_method(self, request, redirect_uri, acr=None, **kwargs):
try:
res = pick_auth(_context, request)
except Exception as exc:
- logger.exception(
- f"An error occurred while picking the authN broker: {exc}"
- )
+ logger.exception(f"An error occurred while picking the authN broker: {exc}")
if res:
return res
else:
@@ -468,7 +485,11 @@ def pick_authn_method(self, request, redirect_uri, acr=None, **kwargs):
def create_session(self, request, user_id, acr, time_stamp, authn_method):
_context = self.server_get("endpoint_context")
_mngr = _context.session_manager
- authn_event = create_authn_event(user_id, authn_info=acr, time_stamp=time_stamp, )
+ authn_event = create_authn_event(
+ user_id,
+ authn_info=acr,
+ time_stamp=time_stamp,
+ )
_exp_in = authn_method.kwargs.get("expires_in")
if _exp_in and "valid_until" in authn_event:
authn_event["valid_until"] = utc_time_sans_frac() + _exp_in
@@ -494,15 +515,26 @@ def _login_required_error(self, redirect_uri, request):
logger.debug("Login required error: {}".format(_res))
return _res
+ def _unwrap_identity(self, identity):
+ if isinstance(identity, dict):
+ try:
+ _id = b64d(as_bytes(identity["uid"]))
+ except BadSyntax:
+ return identity
+ else:
+ _id = b64d(as_bytes(identity))
+
+ return json.loads(as_unicode(_id))
+
def setup_auth(
- self,
- request: Optional[Union[Message, dict]],
- redirect_uri: str,
- cinfo: dict,
- cookie: List[dict] = None,
- acr: str = None,
- **kwargs,
- ):
+ self,
+ request: Optional[Union[Message, dict]],
+ redirect_uri: str,
+ cinfo: dict,
+ cookie: List[dict] = None,
+ acr: str = None,
+ **kwargs,
+ ) -> dict:
"""
:param request: The authorization/authentication request
@@ -527,7 +559,7 @@ def setup_auth(
_max_age = 0
else:
_max_age = max_age(request)
- logger.debug(f'Max age: {_max_age}')
+ logger.debug(f"Max age: {_max_age}")
identity, _ts = authn.authenticated_as(
client_id, cookie, authorization=_auth_info, max_age=_max_age
)
@@ -544,18 +576,16 @@ def setup_auth(
_ts = 0
else:
if identity:
- try: # If identity['uid'] is in fact a base64 encoded JSON string
- _id = b64d(as_bytes(identity["uid"]))
- except BadSyntax:
- pass
- else:
- identity = json.loads(as_unicode(_id))
-
+ identity = self._unwrap_identity(identity)
+ _sid = identity.get("sid")
+ if _sid:
try:
- _csi = _context.session_manager[identity.get("sid")]
+ _csi = _context.session_manager[_sid]
except Revoked:
+ logger.debug("Authentication session revoked!!")
identity = None
else:
+ logger.debug(f"Session info type: {_csi.__class__.__name__}")
if _csi.is_active() is False:
identity = None
@@ -574,7 +604,7 @@ def setup_auth(
else:
return {"function": authn, "args": authn_args}
else:
- logger.info("Active authentication")
+ logger.info(f"Active authentication: {identity}")
if re_authenticate(request, authn):
# demand re-authentication
return {"function": authn, "args": authn_args}
@@ -626,12 +656,12 @@ def aresp_check(self, aresp, request):
return ""
def response_mode(
- self,
- request: Union[dict, AuthorizationRequest],
- response_args: Optional[AuthorizationResponse] = None,
- return_uri: Optional[str] = "",
- fragment_enc: Optional[bool] = None,
- **kwargs,
+ self,
+ request: Union[dict, AuthorizationRequest],
+ response_args: Optional[Union[dict, AuthorizationResponse]] = None,
+ return_uri: Optional[str] = "",
+ fragment_enc: Optional[bool] = None,
+ **kwargs,
) -> dict:
resp_mode = request["response_mode"]
if resp_mode == "form_post":
@@ -639,9 +669,21 @@ def response_mode(
_args = response_args.to_dict()
else:
_args = response_args
- msg = FORM_POST.format(inputs=inputs(_args), action=return_uri, )
+
+ if "error" in _args:
+ if not return_uri:
+ return_uri = _args["return_uri"]
+ del _args["return_uri"]
+ if "return_type" in _args:
+ del _args["return_type"]
+
+ msg = FORM_POST.format(inputs=inputs(_args), action=return_uri)
kwargs.update(
- {"response_msg": msg, "content_type": "text/html", "response_placement": "body", }
+ {
+ "response_msg": msg,
+ "content_type": "text/html",
+ "response_placement": "body",
+ }
)
elif resp_mode == "fragment":
if fragment_enc is False:
@@ -662,12 +704,19 @@ def response_mode(
return kwargs
def error_response(self, response_info, request, error, error_description):
- resp = self.authentication_error_response(request,
- error=error,
- error_description=str(error_description))
+ resp = self.authentication_error_response(
+ request, error=error, error_description=str(error_description)
+ )
response_info["response_args"] = resp
return response_info
+ def error_by_response_mode(self, response_info, request, error, error_description):
+ response_info = self.error_response(response_info, request, error, error_description)
+ if "return_uri" not in response_info:
+ response_info["return_uri"] = request["redirect_uri"]
+ response_info = self.response_mode(request, **response_info)
+ return response_info
+
def create_authn_response(self, request: Union[dict, Message], sid: str) -> dict:
"""
:param request:
@@ -702,7 +751,9 @@ def create_authn_response(self, request: Union[dict, Message], sid: str) -> dict
if "code" in request["response_type"]:
_code = self.mint_token(
- token_class="authorization_code", grant=grant, session_id=_sinfo["session_id"],
+ token_class="authorization_code",
+ grant=grant,
+ session_id=_sinfo["session_id"],
)
aresp["code"] = _code.value
handled_response_type.append("code")
@@ -711,7 +762,9 @@ def create_authn_response(self, request: Union[dict, Message], sid: str) -> dict
if "token" in rtype:
_access_token = self.mint_token(
- token_class="access_token", grant=grant, session_id=_sinfo["session_id"],
+ token_class="access_token",
+ grant=grant,
+ session_id=_sinfo["session_id"],
)
aresp["access_token"] = _access_token.value
aresp["token_type"] = "Bearer"
@@ -730,11 +783,15 @@ def create_authn_response(self, request: Union[dict, Message], sid: str) -> dict
elif {"id_token", "token"}.issubset(rtype):
kwargs = {"access_token": _access_token.value}
+ if request["response_type"] == ["id_token"]:
+ kwargs["as_if"] = "userinfo"
+
try:
id_token = self.mint_token(
token_class="id_token",
grant=grant,
session_id=_sinfo["session_id"],
+ scope=request["scope"],
**kwargs,
)
# id_token = _context.idtoken.make(sid, **kwargs)
@@ -754,7 +811,8 @@ def create_authn_response(self, request: Union[dict, Message], sid: str) -> dict
if not_handled:
resp = self.authentication_error_response(
request,
- error="invalid_request", error_description="unsupported_response_type",
+ error="invalid_request",
+ error_description="unsupported_response_type",
)
return {"response_args": resp, "fragment_enc": fragment_enc}
@@ -786,8 +844,9 @@ def post_authentication(self, request: Union[dict, Message], session_id: str, **
try:
_mngr.set([user_id, client_id, grant_id], grant)
except Exception as err:
- return self.error_response(response_info, request, "server_error",
- "{}".format(err.args))
+ return self.error_response(
+ response_info, request, "server_error", "{}".format(err.args)
+ )
logger.debug("response type: %s" % request["response_type"])
@@ -799,8 +858,9 @@ def post_authentication(self, request: Union[dict, Message], session_id: str, **
try:
redirect_uri = get_uri(_context, request, "redirect_uri")
except (RedirectURIError, ParameterError) as err:
- return self.error_response(response_info, request, "invalid_request",
- "{}".format(err.args))
+ return self.error_response(
+ response_info, request, "invalid_request", "{}".format(err.args)
+ )
else:
response_info["return_uri"] = redirect_uri
@@ -812,8 +872,9 @@ def post_authentication(self, request: Union[dict, Message], session_id: str, **
try:
response_info = self.response_mode(request, **response_info)
except InvalidRequest as err:
- return self.error_response(response_info, request, "invalid_request",
- "{}".format(err.args))
+ return self.error_response(
+ response_info, request, "invalid_request", "{}".format(err.args)
+ )
_cookie_info = _context.new_cookie(
name=_context.cookie_handler.name["session"],
@@ -837,40 +898,47 @@ def authz_part2(self, request, session_id, **kwargs):
try:
resp_info = self.post_authentication(request, session_id, **kwargs)
except Exception as err:
- return self.error_response({}, request, "server_error", err)
+ return self.error_by_response_mode({}, request, "server_error", err)
_context = self.server_get("endpoint_context")
+ logger.debug(f"resp_info: {resp_info}")
+
if "check_session_iframe" in _context.provider_info:
salt = rndstr()
try:
authn_event = _context.session_manager.get_authentication_event(session_id)
except KeyError:
- return self.error_response({}, request, "server_error", "No such session")
+ return self.error_by_response_mode({}, request, "server_error", "No such session")
else:
if authn_event.is_valid() is False:
- return self.error_response({}, request, "server_error",
- "Authentication has timed out")
+ return self.error_by_response_mode(
+ {}, request, "server_error", "Authentication has timed out"
+ )
_state = b64e(as_bytes(json.dumps({"authn_time": authn_event["authn_time"]})))
_session_cookie_content = _context.new_cookie(
- name=_context.cookie_handler.name["session_management"], state=as_unicode(_state),
+ name=_context.cookie_handler.name["session_management"],
+ state=as_unicode(_state),
)
opbs_value = _session_cookie_content["value"]
+ if "return_uri" in resp_info:
+ re_uri = resp_info["return_uri"]
+ else:
+ re_uri = request["redirect_uri"]
+
logger.debug(
"compute_session_state: client_id=%s, origin=%s, opbs=%s, salt=%s",
request["client_id"],
- resp_info["return_uri"],
+ re_uri,
opbs_value,
salt,
)
- _session_state = compute_session_state(
- opbs_value, salt, request["client_id"], resp_info["return_uri"]
- )
+ _session_state = compute_session_state(opbs_value, salt, request["client_id"], re_uri)
if _session_cookie_content:
if "cookie" in resp_info:
@@ -878,7 +946,8 @@ def authz_part2(self, request, session_id, **kwargs):
else:
resp_info["cookie"] = [_session_cookie_content]
- resp_info["response_args"]["session_state"] = _session_state
+ if "response_args" in resp_info:
+ resp_info["response_args"]["session_state"] = _session_state
# Mix-Up mitigation
if "response_args" in resp_info:
@@ -891,12 +960,12 @@ def do_request_user(self, request_info, **kwargs):
return kwargs
def process_request(
- self,
- request: Optional[Union[Message, dict]] = None,
- http_info: Optional[dict] = None,
- **kwargs,
+ self,
+ request: Optional[Union[Message, dict]] = None,
+ http_info: Optional[dict] = None,
+ **kwargs,
):
- """ The AuthorizationRequest endpoint
+ """The AuthorizationRequest endpoint
:param http_info: Information on the HTTP request
:param request: The authorization request as a Message instance
@@ -930,7 +999,10 @@ def process_request(
info = self.setup_auth(request, request["redirect_uri"], cinfo, _my_cookies, **kwargs)
if "error" in info:
- return info
+ if "response_mode" in request:
+ return self.response_mode(request, info)
+ else:
+ return info
_function = info.get("function")
if not _function:
diff --git a/src/oidcop/oauth2/token.py b/src/oidcop/oauth2/token.py
index 768c1734..bfee6dff 100755
--- a/src/oidcop/oauth2/token.py
+++ b/src/oidcop/oauth2/token.py
@@ -3,13 +3,12 @@
from typing import Union
from cryptojwt.jwe.exception import JWEException
-from cryptojwt.jwt import utc_time_sans_frac
from oidcmsg.message import Message
from oidcmsg.oauth2 import AccessTokenResponse
from oidcmsg.oauth2 import ResponseMessage
from oidcmsg.oidc import RefreshAccessTokenRequest
from oidcmsg.oidc import TokenErrorResponse
-from oidcmsg.time_util import time_sans_frac
+from oidcmsg.time_util import utc_time_sans_frac
from oidcop import sanitize
from oidcop.constant import DEFAULT_TOKEN_LIFETIME
@@ -54,7 +53,7 @@ def _mint_token(
based_on: Optional[SessionToken] = None,
scope: Optional[list] = None,
token_args: Optional[dict] = None,
- token_type: Optional[str] = ""
+ token_type: Optional[str] = "",
) -> SessionToken:
_context = self.endpoint.server_get("endpoint_context")
_mngr = _context.session_manager
@@ -90,7 +89,7 @@ def _mint_token(
_exp_in = int(_exp_in)
if _exp_in:
- token.expires_at = time_sans_frac() + _exp_in
+ token.expires_at = utc_time_sans_frac() + _exp_in
_context.session_manager.set(_context.session_manager.unpack_session_key(session_id), grant)
@@ -296,7 +295,8 @@ def process_request(self, req: Union[Message, dict], **kwargs):
token.register_usage()
- if ("client_id" in req
+ if (
+ "client_id" in req
and req["client_id"] in _context.cdb
and "revoke_refresh_on_issue" in _context.cdb[req["client_id"]]
):
@@ -477,7 +477,9 @@ def process_request(self, request: Optional[Union[Message, dict]] = None, **kwar
name=_context.cookie_handler.name["session"],
sub=_session_info["grant"].sub,
sid=_context.session_manager.session_key(
- _session_info["user_id"], _session_info["user_id"], _session_info["grant"].id,
+ _session_info["user_id"],
+ _session_info["client_id"],
+ _session_info["grant"].id,
),
)
diff --git a/src/oidcop/oidc/add_on/custom_scopes.py b/src/oidcop/oidc/add_on/custom_scopes.py
index e3981e18..5d3d655a 100644
--- a/src/oidcop/oidc/add_on/custom_scopes.py
+++ b/src/oidcop/oidc/add_on/custom_scopes.py
@@ -19,7 +19,7 @@ def add_custom_scopes(endpoint, **kwargs):
_scopes2claims = SCOPE2CLAIMS.copy()
_scopes2claims.update(kwargs)
_context = _endpoint.server_get("endpoint_context")
- _context.scopes_handler.scopes_to_claims = _scopes2claims
+ _context.scopes_handler.set_scopes_mapping(_scopes2claims)
pi = _context.provider_info
_scopes = set(pi.get("scopes_supported", []))
diff --git a/src/oidcop/oidc/add_on/pkce.py b/src/oidcop/oidc/add_on/pkce.py
index 6825c38c..3e7fa054 100644
--- a/src/oidcop/oidc/add_on/pkce.py
+++ b/src/oidcop/oidc/add_on/pkce.py
@@ -43,12 +43,11 @@ def post_authn_parse(request, client_id, endpoint_context, **kwargs):
if "pkce_essential" in client:
essential = client["pkce_essential"]
else:
- essential = endpoint_context.args["pkce"].get(
- "essential", False
- )
+ essential = endpoint_context.args["pkce"].get("essential", False)
if essential and "code_challenge" not in request:
return AuthorizationErrorResponse(
- error="invalid_request", error_description="Missing required code_challenge",
+ error="invalid_request",
+ error_description="Missing required code_challenge",
)
if "code_challenge_method" not in request:
@@ -93,7 +92,8 @@ def post_token_parse(request, client_id, endpoint_context, **kwargs):
:return:
"""
if isinstance(
- request, (AuthorizationErrorResponse, RefreshAccessTokenRequest, TokenExchangeRequest),
+ request,
+ (AuthorizationErrorResponse, RefreshAccessTokenRequest, TokenExchangeRequest),
):
return request
@@ -109,13 +109,16 @@ def post_token_parse(request, client_id, endpoint_context, **kwargs):
if "code_challenge" in _authn_req:
if "code_verifier" not in request:
return TokenErrorResponse(
- error="invalid_grant", error_description="Missing code_verifier",
+ error="invalid_grant",
+ error_description="Missing code_verifier",
)
_method = _authn_req["code_challenge_method"]
if not verify_code_challenge(
- request["code_verifier"], _authn_req["code_challenge"], _method,
+ request["code_verifier"],
+ _authn_req["code_challenge"],
+ _method,
):
return TokenErrorResponse(error="invalid_grant", error_description="PKCE check failed")
diff --git a/src/oidcop/oidc/authorization.py b/src/oidcop/oidc/authorization.py
index 670b42a7..fb4d73e8 100755
--- a/src/oidcop/oidc/authorization.py
+++ b/src/oidcop/oidc/authorization.py
@@ -43,9 +43,18 @@ def host_component(url):
ALG_PARAMS = {
- "sign": ["request_object_signing_alg", "request_object_signing_alg_values_supported",],
- "enc_alg": ["request_object_encryption_alg", "request_object_encryption_alg_values_supported",],
- "enc_enc": ["request_object_encryption_enc", "request_object_encryption_enc_values_supported",],
+ "sign": [
+ "request_object_signing_alg",
+ "request_object_signing_alg_values_supported",
+ ],
+ "enc_alg": [
+ "request_object_encryption_alg",
+ "request_object_encryption_alg_values_supported",
+ ],
+ "enc_enc": [
+ "request_object_encryption_enc",
+ "request_object_encryption_enc_values_supported",
+ ],
}
diff --git a/src/oidcop/oidc/registration.py b/src/oidcop/oidc/registration.py
index cc67dcd1..529664a9 100755
--- a/src/oidcop/oidc/registration.py
+++ b/src/oidcop/oidc/registration.py
@@ -3,7 +3,6 @@
import json
import logging
import secrets
-import time
from typing import List
from urllib.parse import urlencode
from urllib.parse import urlparse
@@ -86,31 +85,33 @@ def verify_url(url: str, urlset: List[list]) -> bool:
def secret(seed: str, sid: str):
- msg = "{}{}{}".format(time.time(), secrets.token_urlsafe(16), sid).encode("utf-8")
+ msg = "{}{}{}".format(utc_time_sans_frac(), secrets.token_urlsafe(16), sid).encode("utf-8")
csum = hmac.new(as_bytes(seed), msg, hashlib.sha224)
return csum.hexdigest()
def comb_uri(args):
- for param in ["redirect_uris", "post_logout_redirect_uris"]:
- if param not in args:
- continue
-
+ redirect_uris = args.get("redirect_uris")
+ if redirect_uris:
val = []
- for base, query_dict in args[param]:
+ for base, query_dict in redirect_uris:
if query_dict:
- query_string = urlencode(
- [
- (key, v)
- for key in query_dict
- for v in query_dict[key]
- ]
- )
- val.append("{base}?{query_string}")
+ query_string = urlencode([(key, v) for key in query_dict for v in query_dict[key]])
+ val.append(f"{base}?{query_string}")
else:
val.append(base)
- args[param] = val
+ args["redirect_uris"] = val
+
+ post_logout_redirect_uri = args.get("post_logout_redirect_uri")
+ if post_logout_redirect_uri:
+ base, query_dict = post_logout_redirect_uri
+ if query_dict:
+ query_string = urlencode([(key, v) for key in query_dict for v in query_dict[key]])
+ val = f"{base}?{query_string}"
+ else:
+ val = base
+ args["post_logout_redirect_uri"] = val
request_uris = args.get("request_uris")
if request_uris:
@@ -179,17 +180,15 @@ def do_client_registration(self, request, client_id, ignore=None):
if key not in ignore:
_cinfo[key] = val
- if "post_logout_redirect_uris" in request:
- plruri = []
- for uri in request["post_logout_redirect_uris"]:
- if urlparse(uri).fragment:
- err = self.error_cls(
- error="invalid_configuration_parameter",
- error_description="post_logout_redirect_uris contains fragment",
- )
- return err
- plruri.append(split_uri(uri))
- _cinfo["post_logout_redirect_uris"] = plruri
+ _uri = request.get("post_logout_redirect_uri")
+ if _uri:
+ if urlparse(_uri).fragment:
+ err = self.error_cls(
+ error="invalid_configuration_parameter",
+ error_description="post_logout_redirect_uri contains fragment",
+ )
+ return err
+ _cinfo["post_logout_redirect_uri"] = split_uri(_uri)
if "redirect_uris" in request:
try:
@@ -217,9 +216,10 @@ def do_client_registration(self, request, client_id, ignore=None):
if "sector_identifier_uri" in request:
try:
- (_cinfo["si_redirects"], _cinfo["sector_id"],) = self._verify_sector_identifier(
- request
- )
+ (
+ _cinfo["si_redirects"],
+ _cinfo["sector_id"],
+ ) = self._verify_sector_identifier(request)
except InvalidSectorIdentifier as err:
return ResponseMessage(
error="invalid_configuration_parameter", error_description=str(err)
@@ -292,7 +292,9 @@ def verify_redirect_uris(registration_request):
pass
else:
logger.error(
- "InvalidRedirectURI: scheme:%s, hostname:%s", p.scheme, p.hostname,
+ "InvalidRedirectURI: scheme:%s, hostname:%s",
+ p.scheme,
+ p.hostname,
)
raise InvalidRedirectURIError(
"Redirect_uri must use custom " "scheme or http and localhost"
@@ -384,17 +386,21 @@ def client_registration_setup(self, request, new_id=True, set_secret=True):
try:
request.verify()
except (MessageException, ValueError) as err:
- logger.error("request.verify() on %s", request)
- return ResponseMessage(
- error="invalid_configuration_request", error_description="%s" % err
- )
+ logger.error("request.verify() error on %s", request)
+ _error = "invalid_configuration_request"
+ if len(err.args) > 1:
+ if err.args[1] == "initiate_login_uri":
+ _error = "invalid_client_metadata"
+
+ return ResponseMessage(error=_error, error_description="%s" % err)
request.rm_blanks()
try:
self.match_client_request(request)
except CapabilitiesMisMatch as err:
return ResponseMessage(
- error="invalid_request", error_description="Don't support proposed %s" % err,
+ error="invalid_request",
+ error_description="Don't support proposed %s" % err,
)
_context = self.server_get("endpoint_context")
@@ -429,7 +435,9 @@ def client_registration_setup(self, request, new_id=True, set_secret=True):
_context.cdb[client_id] = _cinfo
_cinfo = self.do_client_registration(
- request, client_id, ignore=["redirect_uris", "policy_uri", "logo_uri", "tos_uri"],
+ request,
+ client_id,
+ ignore=["redirect_uris", "policy_uri", "logo_uri", "tos_uri"],
)
if isinstance(_cinfo, ResponseMessage):
return _cinfo
@@ -470,7 +478,17 @@ def process_request(self, request=None, new_id=True, set_secret=True, **kwargs):
else:
_context = self.server_get("endpoint_context")
_cookie = _context.new_cookie(
- name=_context.cookie_handler.name["register"], client_id=reg_resp["client_id"],
+ name=_context.cookie_handler.name["register"],
+ client_id=reg_resp["client_id"],
)
return {"response_args": reg_resp, "cookie": _cookie, "response_code": 201}
+
+ def process_verify_error(self, exception):
+ _error = "invalid_request"
+ if isinstance(exception, ValueError):
+ if len(exception.args) > 1:
+ if exception.args[1] == "initiate_login_uri":
+ _error = "invalid_client_metadata"
+
+ return self.error_cls(error=_error, error_description=f"{exception}")
diff --git a/src/oidcop/oidc/session.py b/src/oidcop/oidc/session.py
index 4e35141e..79a7f9dc 100644
--- a/src/oidcop/oidc/session.py
+++ b/src/oidcop/oidc/session.py
@@ -51,6 +51,8 @@ def do_front_channel_logout_iframe(cinfo, iss, sid):
except KeyError:
flsr = False
+ logger.debug(f"frontchannel_logout_uri: {frontchannel_logout_uri}")
+ logger.debug(f"frontchannel_logout_session_required: {flsr}")
if flsr:
_query = {"iss": iss, "sid": sid}
if "?" in frontchannel_logout_uri:
@@ -61,6 +63,7 @@ def do_front_channel_logout_iframe(cinfo, iss, sid):
_np = p._replace(query="")
frontchannel_logout_uri = _np.geturl()
+ logger.debug(f"IFrame query: {_query}")
_iframe = '