Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keycloak modules retry request on authentication error, support refresh token parameter #9494

Open
wants to merge 27 commits into
base: main
Choose a base branch
from

Conversation

armkeh
Copy link

@armkeh armkeh commented Dec 30, 2024

SUMMARY

Fixes #8857.

Wraps all requests to Keycloak in the Keycloak modules (keycloak_authentication, keycloak_authz_authorization_scope, keycloak_authz_custom_policy, etc.) with retry logic to make use of a new refresh_token module parameter.

This improves the user experience when using Keycloak modules with the auth_token parameter; previously if that token expired during playbook execution, subsequent tasks would fail. Now they "fall back" to using the refresh_token, or, if it is not provided or is expired itself, to using the auth_username and auth_password.

    def _request(self, url, method, data=None):
        def make_request_ignoring_401():
            try:
                return open_url(url, method=method, data=data,
                                http_agent=self.http_agent, headers=self.restheaders,
                                timeout=self.connection_timeout,
                                validate_certs=self.validate_certs)
            except HTTPError as e:
                if e.code != 401:
                    raise e

            return None

        r = make_request_ignoring_401()
        if r is not None:
            return r

        # Authentication may have expired, re-authenticate with refresh token and retry
        refresh_token = self.module.params.get('refresh_token')
        if refresh_token is not None:
            token = _get_token_using_refresh_token(self.module.params)
            self.restheaders['Authorization'] = 'Bearer ' + token

        r = make_request_ignoring_401()
        if r is not None:
            return r

        # Retry once more with username and password
        auth_username = self.module.params.get('auth_username')
        auth_password = self.module.params.get('auth_password')
        if auth_username is not None and auth_password is not None:
            token = _get_token_using_credentials(self.module.params)
            self.restheaders['Authorization'] = 'Bearer ' + token

        return make_request_ignoring_401()
ISSUE TYPE
  • Feature Pull Request
COMPONENT NAME

keycloak

Copy link
Collaborator

@russoz russoz left a comment

Choose a reason for hiding this comment

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

hi @armkeh thanks for your contribution!

Couple of comments on the PR.

plugins/modules/keycloak_authentication.py Outdated Show resolved Hide resolved
Comment on lines 362 to 365
required_together=([['auth_realm', 'auth_username', 'auth_password']]),
required_by={
'refresh_token': 'auth_realm',
})
Copy link
Collaborator

Choose a reason for hiding this comment

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

A bit of a pain, but being helpful to those coming after us:

Suggested change
required_together=([['auth_realm', 'auth_username', 'auth_password']]),
required_by={
'refresh_token': 'auth_realm',
})
required_together=([['auth_realm', 'auth_username', 'auth_password']]),
required_by={'refresh_token': 'auth_realm'},
)

We get leaner PRs with this, like one for plugins/modules/keycloak_clientscope_type.py below

Copy link
Author

Choose a reason for hiding this comment

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

No problem, done.

@felixfontein felixfontein added check-before-release PR will be looked at again shortly before release and merged if possible. backport-10 Automatically create a backport for the stable-10 branch labels Dec 31, 2024
Copy link
Collaborator

@russoz russoz left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link
Collaborator

@felixfontein felixfontein left a comment

Choose a reason for hiding this comment

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

Thanks for your contribution! Please add a changelog fragment. Thanks.


return self.get_client_role_scope_from_realm(clientid, realm)

def fail_open_url(self, e, msg, **kwargs):
def fail_request(self, e, msg, **kwargs):
Copy link
Collaborator

Choose a reason for hiding this comment

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

The method fail_open_url must be kept (simply add a wrapper that calls fail_request) for backwards compatibility. The module utils is part of the public API of the collection.

token = _get_token_using_credentials(self.module.params)
self.restheaders['Authorization'] = 'Bearer ' + token

return make_request_ignoring_401()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Shouldn't the last request be made without ignoring 401?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backport-10 Automatically create a backport for the stable-10 branch check-before-release PR will be looked at again shortly before release and merged if possible.
Projects
None yet
3 participants