Skip to content

Conversation

@ecodina
Copy link
Contributor

@ecodina ecodina commented Dec 14, 2025

closes: #59286

This PR implements the client_credentials grant flow to enable other services use of the Airflow API.

It modifies POST /auth/token to add a grant_type option (by default password to not break backwards compatibility).

When using grant_type=client_credentials it requires passing the client_id and client_secret in order to obtain the token for the associated service account.

When using grant_type=password it requires passing the username and password, as it was until now.

The client used must exist in the same realm and instance configured for the auth manager. The service account must have the appropriate permissions / roles for the resources it needs access.

I've tested obtaining the token and using it with airflow.sdk.api.client.Client and it works.

Some considerations:

  1. According to RFC6749 section 4.4.3, when using this flow, the user should not have a refresh token, so we need to handle that in the refresh_user method.
  2. I haven't tested this using airflowctl, but it should work by setting the env variable AIRFLOW_CLI_TOKEN with the obtained token.

^ Add meaningful description above
Read the Pull Request Guidelines for more information.
In case of fundamental code changes, an Airflow Improvement Proposal (AIP) is needed.
In case of a new dependency, check compliance with the ASF 3rd Party License Policy.
In case of backwards incompatible changes please leave a note in a newsfragment file, named {pr_number}.significant.rst or {issue_number}.significant.rst, in airflow-core/newsfragments.

@ecodina ecodina force-pushed the keycloak-client-credentials branch 2 times, most recently from 92e90a8 to 97a9019 Compare December 14, 2025 19:16
Copy link
Contributor

@vincbeck vincbeck left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty cool in general! On top of my comments, could you also update the documentation to mention this new way of authentication?

@ecodina
Copy link
Contributor Author

ecodina commented Dec 15, 2025

Thanks @vincbeck! I've implemented your suggestions (although mypy is complaining, which I'll fix). When I was doing the changes, I thought of another way to implement client credentials, which might align it more with the specs.

We could have just one endpoint, /token, that would accept a TokenBody which the standard grant_type param. Validation of the request is more difficult, but it may be cleaner from the user perspective, and allow to easily add other grant types without needing specific endpoints. WDYT?

class TokenBody(StrictBaseModel):
    """Token serializer for post bodies."""

    grant_type: Literal["password", "client_credentials"] = Field(default="password")
    username: str | None = Field(None)
    password: str | None = Field(None)
    client_id: str | None = Field(None)
    client_secret: str | None = Field(None)

    @field_validator("username", mode="after")
    @classmethod
    def validate_username(cls, v, info):
        if info.data.get("grant_type") == "password" and v is None:
            raise ValueError("username is required for password grant")
        return v

    @field_validator("password", mode="after")
    @classmethod
    def validate_password(cls, v, info):
        if info.data.get("grant_type") == "password" and v is None:
            raise ValueError("password is required for password grant")
        return v

    @field_validator("client_id", mode="after")
    @classmethod
    def validate_client_id(cls, v, info):
        if info.data.get("grant_type") == "client_credentials" and v is None:
            raise ValueError("client_id is required for client_credentials grant")
        return v

    @field_validator("client_secret", mode="after")
    @classmethod
    def validate_client_secret(cls, v, info):
        if info.data.get("grant_type") == "client_credentials" and v is None:
            raise ValueError("client_secret is required for client_credentials grant")
        return v

@vincbeck
Copy link
Contributor

vincbeck commented Dec 15, 2025

I like that! I agree, from a user standpoint, that will be easier to use one endpoint instead of multiple. Regarding TokenBody, I think we can do something cleaner using tagged union in Pydantic: https://docs.pydantic.dev/latest/concepts/unions/#discriminated-unions

@ecodina ecodina force-pushed the keycloak-client-credentials branch from ae42240 to 20339d4 Compare December 20, 2025 11:52
@ecodina
Copy link
Contributor Author

ecodina commented Dec 20, 2025

Sorry for the delay @vincbeck. Keeping just one endpoint and using pydantic unions makes everything very clean.

It has been a bit difficult to make it work since we should keep grant_type optional (default to password) and not change the body params (keep everything at the root). It was complicated to fill grant_type when it is not sent, with params at the root of the body.

Checks are green now.

@ecodina
Copy link
Contributor Author

ecodina commented Dec 21, 2025

Thanks for pointing that out, @dabla! It indeed makes the code even cleaner.

I had a ValueError there just for mypy. Pydantic already checks that the grant_type is of the correct type.

I've added the create_token to the Token**Body methods, not to the TokenBody since it is a RootModel and not a base class. Moreover, if grant_type were compulsory, TokenBody would not be needed (only TokenUnion).

@ecodina ecodina force-pushed the keycloak-client-credentials branch from e1cb208 to 36d57e4 Compare December 27, 2025 14:23
@ecodina ecodina force-pushed the keycloak-client-credentials branch from 36d57e4 to b58a613 Compare January 3, 2026 09:45
@vincbeck
Copy link
Contributor

vincbeck commented Jan 5, 2026

Can you please resolve the conflicts?

Copy link
Contributor

@vincbeck vincbeck left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very cool, only some nits

Co-authored-by: Vincent <97131062+vincbeck@users.noreply.github.com>
@vincbeck vincbeck merged commit 21a2fa5 into apache:main Jan 5, 2026
83 checks passed
@vincbeck
Copy link
Contributor

vincbeck commented Jan 5, 2026

I love that feature!

@ecodina
Copy link
Contributor Author

ecodina commented Jan 5, 2026

Thanks @vincbeck !

@bugraoz93
Copy link
Contributor

Nice!

@ecodina ecodina deleted the keycloak-client-credentials branch January 6, 2026 08:35
chirodip98 pushed a commit to chirodip98/airflow-contrib that referenced this pull request Jan 9, 2026
* kc: implement client_credentials grant

* Improve error log

Co-authored-by: Vincent <97131062+vincbeck@users.noreply.github.com>

* change client credentials service func name

Co-authored-by: Vincent <97131062+vincbeck@users.noreply.github.com>

* update client credentials service usage

* refresh token can be None

* add docs for client credentials

* implement pydantic union

* add tests for the new token route

* add docs for new token endpoint

* fix mypy error

* refactor token creation logic to use methods in data models

* lowercase

Co-authored-by: Vincent <97131062+vincbeck@users.noreply.github.com>

* lowercase

* remove unused import

---------

Co-authored-by: Vincent <97131062+vincbeck@users.noreply.github.com>
stegololz pushed a commit to stegololz/airflow that referenced this pull request Jan 9, 2026
* kc: implement client_credentials grant

* Improve error log

Co-authored-by: Vincent <97131062+vincbeck@users.noreply.github.com>

* change client credentials service func name

Co-authored-by: Vincent <97131062+vincbeck@users.noreply.github.com>

* update client credentials service usage

* refresh token can be None

* add docs for client credentials

* implement pydantic union

* add tests for the new token route

* add docs for new token endpoint

* fix mypy error

* refactor token creation logic to use methods in data models

* lowercase

Co-authored-by: Vincent <97131062+vincbeck@users.noreply.github.com>

* lowercase

* remove unused import

---------

Co-authored-by: Vincent <97131062+vincbeck@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Keycloak: token via client_credentials flow

4 participants