Weird-Sheep-Labs / django-azure-auth

A simple Django app for user authentication with Azure Active Directory/Entra ID.
MIT License
17 stars 10 forks source link

Authentication with Django Rest Framework #14

Closed kenricci closed 6 months ago

kenricci commented 9 months ago

Have you considered adding the ability for a user to request a token via RestAPI using username and password with Azure AD authentication?

regoawt commented 6 months ago

Hi, I have in fact implemented what you have suggested in a prior project but chose not to include it in this package as there is no single/correct way of doing it, and I wanted to keep this package for standalone Django apps only.

In any case, it is quite simple to achieve by subclassing the DRF authentication.BasicAuthentication class like so:

class BaseAzureAuthentication(authentication.BasicAuthentication, GetUserMixin):
    """
    A BasicAuthentication subclass that allows a user to authenticate directly
    with the API using their Azure AD username and password.
    Raises AuthenticationFailed if the username/password is incorrect or if
    there was a problem authenticating with Azure AD (i.e. user suspended).
    Implemented as a base class so we can more obviously show how a user can
    authenticate

    Usage:
    import requests
    resp = requests.get(<api_url>, auth=(<azure username>, <azure password>))
    """

    def authenticate_credentials(self, userid, password, request=None):
        """
        For the given username and password will authenticate the user with Azure
        and then return a ~users.models.User instance instantiated with the
        users Azure credentials
        :param userid: The Azure users username (email address)
        :param password: The Azure users password
        :param request: The request object
        :return: Instance of ~users.models.User
        :raises: exceptions.AuthenticationFailed
        """

        msal_app = msal.ConfidentialClientApplication(
            client_id=settings.AZURE_CLIENT_ID,
            client_credential=settings.AZURE_CLIENT_SECRET,
            authority=settings.AZURE_AUTHORITY,
        )
        token_result = msal_app.acquire_token_by_username_password(
            userid, password, scopes=["User.Read"]
        )
        if "error" not in token_result:
            access_token = token_result["access_token"]
            sub_claim = token_result["id_token_claims"]["sub"]
            resp = requests.get(
                settings.GRAPH_API_URL,
                headers={"Authorization": f"Bearer {access_token}"},
            )
            if resp.ok:
                user = resp.json()

                # The Azure `user["id"]` contains the `oid` claim which is the same
                # across different apps for the same tenant, for any given user, so
                # we need to replace it with the `sub` claim in `user_id`
                user["id"] = sub_claim
                return self.get_user(user)

            # Give the user a hint about what could be wrong with their account
            raise exceptions.AuthenticationFailed(
                _(f"Failed Azure authentication: `{resp.json()['error']}`")
            )

        # Returning a generic error avoids leaking Azure details to the client
        raise exceptions.AuthenticationFailed(_("Invalid username/password."))