diff --git a/CHANGES.rst b/CHANGES.rst index 6a6767da..bde662e6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,7 @@ Added :attr:`~canaille.core.configuration.CoreSettings.ENABLE_PASSWORD_COMPROMISSION_CHECK` and :attr:`~canaille.core.configuration.CoreSettings.API_URL_HIBP` :issue:`179` +- Implement OIDC client_credentials flow. :issue:`207` Changed ^^^^^^^ diff --git a/canaille/backends/sql/models.py b/canaille/backends/sql/models.py index 754b2113..fc09a021 100644 --- a/canaille/backends/sql/models.py +++ b/canaille/backends/sql/models.py @@ -238,7 +238,7 @@ class Token(canaille.oidc.models.Token, Base, SqlAlchemyModel): access_token: Mapped[str] = mapped_column(String, nullable=True) client_id: Mapped[str] = mapped_column(ForeignKey("client.id")) client: Mapped["Client"] = relationship() - subject_id: Mapped[str] = mapped_column(ForeignKey("user.id")) + subject_id: Mapped[str] = mapped_column(ForeignKey("user.id"), nullable=True) subject: Mapped["User"] = relationship() type: Mapped[str] = mapped_column(String, nullable=True) refresh_token: Mapped[str] = mapped_column(String, nullable=True) diff --git a/canaille/oidc/endpoints/forms.py b/canaille/oidc/endpoints/forms.py index 1d78fa9c..ccba4e7f 100644 --- a/canaille/oidc/endpoints/forms.py +++ b/canaille/oidc/endpoints/forms.py @@ -82,6 +82,7 @@ class ClientAddForm(Form): ("implicit", "implicit"), ("hybrid", "hybrid"), ("refresh_token", "refresh_token"), + ("client_credentials", "client_credentials"), ], default=["authorization_code", "refresh_token"], ) diff --git a/canaille/oidc/endpoints/oauth.py b/canaille/oidc/endpoints/oauth.py index 2c0fb2a4..5f3529a7 100644 --- a/canaille/oidc/endpoints/oauth.py +++ b/canaille/oidc/endpoints/oauth.py @@ -192,7 +192,9 @@ def authorize_consent(client, user): @csrf.exempt def issue_token(): request_params = request.form.to_dict(flat=False) - grant_type = request_params["grant_type"][0] + grant_type = ( + request_params["grant_type"][0] if request_params["grant_type"] else None + ) current_app.logger.debug("token endpoint request: POST: %s", request_params) response = authorization.create_token_response() current_app.logger.debug("token endpoint response: %s", response.json) @@ -201,9 +203,15 @@ def issue_token(): access_token = response.json["access_token"] token = Backend.instance.get(models.Token, access_token=access_token) request_ip = request.remote_addr or "unknown IP" - current_app.logger.security( - f"Issued {grant_type} token for {token.subject.user_name} in client {token.client.client_name} from {request_ip}" - ) + if token.subject: + current_app.logger.security( + f"Issued {grant_type} token for {token.subject.user_name} in client {token.client.client_name} from {request_ip}" + ) + else: + current_app.logger.security( + f"Issued {grant_type} token for client {token.client.client_name} from {request_ip}" + ) + return response diff --git a/canaille/oidc/oauth.py b/canaille/oidc/oauth.py index 18328375..c7f1a424 100644 --- a/canaille/oidc/oauth.py +++ b/canaille/oidc/oauth.py @@ -509,6 +509,11 @@ require_oauth = ResourceProtector() def generate_access_token(client, grant_type, user, scope): + if grant_type == "client_credentials": + # Canaille could generate a JWT with iss/sub/aud/exp/iat/jti/scope/client_id + # instead of a random string + return gen_salt(48) + audience = [client.client_id for client in client.audience] bearer_token_generator = authorization._token_generators["default"] kwargs = { diff --git a/canaille/oidc/templates/partial/token_list.html b/canaille/oidc/templates/partial/token_list.html index 7228786c..8e7d5178 100644 --- a/canaille/oidc/templates/partial/token_list.html +++ b/canaille/oidc/templates/partial/token_list.html @@ -22,9 +22,15 @@ - - {{ token.subject.user_name }} - + {% if token.subject %} + + {{ token.subject.user_name }} + + {% else %} + + {{ token.client.client_name }} + + {% endif %} {{ token.issue_date }} diff --git a/canaille/oidc/templates/token_view.html b/canaille/oidc/templates/token_view.html index 40f52861..b1f1f817 100644 --- a/canaille/oidc/templates/token_view.html +++ b/canaille/oidc/templates/token_view.html @@ -52,9 +52,15 @@ {{ _("Subject") }} - - {{ token.subject.identifier }} - + {% if token.subject %} + + {{ token.subject.identifier }} + + {% else %} + + {{ token.client.client_name }} + + {% endif %} @@ -122,7 +128,7 @@ {{ _("Refresh token") }} - +