import jwt

from social_core.backends.oauth import BaseOAuth2


class KeycloakOAuth2(BaseOAuth2):  # pylint: disable=abstract-method
    """Keycloak OAuth2 authentication backend

    This backend has been tested working with a standard Keycloak installation,
    but you might have to specialize it and tune the parameters per your
    configuration.

    This setup specializes the OAuth2 backend which, strictly speaking, offers
    authorization without authentication capabilities.

    Keycloak does offer a full OpenID Connect implementation, but the
    implementation is rather labor intensive to implement.

    This backend is configured to get an access token instead, and assume that
    the access token contains the necessary user details for authentication.

    The integrity of the authentication process is followed by public key
    verification for the `access_token` along with OpenID Connect specification
    `aud` field checking.

    To set up, please take the following steps:

    1. Create a new Keycloak client in the Clients section:

        a. Choose the `Client ID` in the `General Settings` pane.

        b. Select `Client authentication` and `Authorization` in the
           `Capability config` pane.

    2. Configure the following parameters in the Client setup:

        Settings >
            Client ID (copy to settings as `KEY` value)
        Credentials >
            Client Authenticator >
                Use `Client Id and Secret` and copy the `Client secret` value
                to settings as `SECRET` value

    3. For the tokens to work with the JWT setup the following configuration has
       to be made in Keycloak:

        Advanced >
            Fine grain OpenID Connect configuration >
                User Info Signed Response Algorithm >
                    RS256
        Advanced >
            Fine grain OpenID Connect configuration >
                Request Object Signature Algorithm >
                    RS256

    4. Re-enable the audience (see https://issues.redhat.com/browse/KEYCLOAK-6638
       for context):

       Go to Client scopes > YOUR-CLIENT-ID-dedicated > Add mapper > Audience, pick
       a name for the mapper and select the Client ID corresponding to your client
       in `Included Client Audience`.

    5. Get the public key (copy to settings as `PUBLIC_KEY` value) to be used
       with the backend:

        Realm Settings > Keys > Public key

    6. Configure access token fields are configured via the Keycloak Client
       mappers:

        Clients > Client ID > Mappers

    They have to include at least the `ID_KEY` value and the dictionary keys
    defined in the `get_user_details` method.

    7. Configure your web backend. Example setting values for Django settings
       could be:

        SOCIAL_AUTH_KEYCLOAK_KEY = 'example'
        SOCIAL_AUTH_KEYCLOAK_SECRET = '1234abcd-1234-abcd-1234-abcd1234adcd'
        SOCIAL_AUTH_KEYCLOAK_PUBLIC_KEY = \
          'pempublickeythatis2048bitsinbase64andhaseg392characters'
        SOCIAL_AUTH_KEYCLOAK_AUTHORIZATION_URL = \
          'https://sso.com/auth/realms/example/protocol/openid-connect/auth'
        SOCIAL_AUTH_KEYCLOAK_ACCESS_TOKEN_URL = \
          'https://sso.com/auth/realms/example/protocol/openid-connect/token'

    8. The default behaviour is to associate users via username field, but you
       can change the key with e.g.

            SOCIAL_AUTH_KEYCLOAK_ID_KEY = 'email'

    Please make sure your Keycloak user database and Django user database do not
    conflict and that there is no risk of user account hijacking by false
    account association.
    """

    name = "keycloak"
    ID_KEY = "username"
    REDIRECT_STATE = False

    def authorization_url(self):
        return self.setting("AUTHORIZATION_URL")

    def access_token_url(self):
        return self.setting("ACCESS_TOKEN_URL")

    def audience(self):
        return self.setting("KEY")

    def algorithm(self):
        return self.setting("ALGORITHM", default="RS256")

    def public_key(self):
        return "\n".join(
            [
                "-----BEGIN PUBLIC KEY-----",
                self.setting("PUBLIC_KEY"),
                "-----END PUBLIC KEY-----",
            ]
        )

    def user_data(self, access_token, *args, **kwargs):  # pylint: disable=unused-argument
        """Decode user data from the access_token

        You can specialize this method to e.g. get information
        from the Keycloak backend if you do not want to include
        the user information in the access_token.
        """

        return jwt.decode(
            access_token,
            key=self.public_key(),
            algorithms=self.algorithm(),
            audience=self.audience(),
        )

    def get_user_details(self, response):
        """Map fields in user_data into Django User fields"""
        return {
            "username": response.get("preferred_username"),
            "email": response.get("email"),
            "fullname": response.get("name"),
            "first_name": response.get("given_name"),
            "last_name": response.get("family_name"),
        }

    def get_user_id(self, details, response):
        """Get and associate Django User by the field indicated by ID_KEY"""
        return details.get(self.ID_KEY)
