OAuth 2.0 Client Credentials grant for server to server API calls

There seems to be no easy way to set up server to server authorization in the existing Frappe oauth2 integration using client_credentials grant_type. In such a case redirection to a login page is not an option.
e.g. How to use OAuth2 grant type Client Credential?

Based on test_login_using_implicit_token from frappe/frappe/tests/test_oauth20.py

This approach maybe used in such a situation.

  1. Create an OAuth Client that will provide the client_id, client_secret, scope etc

  2. Create a service account user that will be the User for generating the access_token


import frappe
import requests
import json
from urllib.parse import parse_qs, urljoin, urlparse, unquote
from werkzeug.wrappers import Response  # type: ignore
from frappe.utils.password import get_decrypted_password
from frappe.integrations.oauth2 import encode_params


def get_full_url(endpoint):
    """Turn '/endpoint' into 'http://127.0.0.1:8000/endpoint'."""
    return urljoin(frappe.utils.get_url(), endpoint)


def login(session):
    # The usr, pwd which will be the login. Can be from a Settings DocType
    usr = frappe.conf.oauth_usr
    pwd = frappe.conf.oauth_pwd

    session.post(get_full_url("/api/method/login"), data={"usr": usr, "pwd": pwd})

    return unquote(session.cookies.get("user_id", "")) == usr


@frappe.whitelist(allow_guest=True)
def token(**kwargs):
    """return access_token for grant_type client_credentials, without redirect to login"""

    if not kwargs.get("grant_type", "") == "client_credentials":
        return Response(
            json.dumps(
                {
                    "error": "unsupported_grant_type",
                    "error_description": "The grant_type should be client_credentials",
                }
            ),
            status=400,
        )

    client_id = kwargs.get("client_id")
    client_secret = kwargs.get("client_secret")

    oauth_client = frappe.get_value(
        "OAuth Client", {"client_id": client_id, "client_secret": client_secret}
    )

    if not oauth_client:
        return Response(
            json.dumps(
                {
                    "error": "invalid_client",
                    "error_description": "The client_credentials are invalid.",
                }
            ),
            status=401,
        )

    oauth_client = frappe.get_doc("OAuth Client", oauth_client)

    session = requests.Session()

    data = login(session)

    if not login(session):
        return Response(
            json.dumps(
                {
                    "error": "invalid_grant",
                    "error_description": "The API Service User credential is invalid.",
                }
            ),
            status=401,
        )

    # Go to Authorize url. 
    # The redirect url will have the access_token when OAuth Client is setup with Implicit and Token
    try:
        out = session.get(
            get_full_url("/api/method/frappe.integrations.oauth2.authorize"),
            params=encode_params(
                {
                    "client_id": client_id,
                    "scope": "openid all",
                    "response_type": "token",
                    "redirect_uri": oauth_client.default_redirect_uri,
                }
            ),
        )
        redirect_destination = out.url
    except requests.exceptions.ConnectionError as ex:
        redirect_destination = ex.request.url

    response_dict = parse_qs(urlparse(redirect_destination).fragment)

    data = {}

    for key, val in response_dict.items():
        data[key] = val[0]

    return Response(
        json.dumps(data), status=200, content_type="application/json; charset=utf-8"
    )

Implement this Client Credentials Grant — OAuthLib 3.2.2 documentation in frappe.oauth and send a PR.

1 Like

So in the implementation, go with the same approach as above ? Using OAuth Client for client_id and client_secret, scope etc and create a user as a service account with roles as per scope?

Add a grant type here,

Everything else is already there. Might need to add a “System Manager” user to link in “OAuth Settings” or Just set the token user to Administrator as it happens after bench migrate (app itself) does the db changes.

We’ve already implemented it as per this Creating a Provider — OAuthLib 3.2.2 documentation

from oauthlib.openid.connect.core.endpoints.pre_configured import Server as WebApplicationServer