REST API for Web user authentication

Suggest the recommended standard practice for Login REST API creation for web user. This is for developing Mobile App – web user ID authentication.

The following flow we implemented,

  1. Signup will create user by mobile number in erpnext and won’t allow to create new user with the same mobile no.
  2. If user document already exists then generate new session keys and send to app. We can use the generated keys to other api’s.
  3. For login from the app can be done by otp validation. To keep store the otp in user doctype or as a cache then validate with mobile no.
  4. Once otp matches generate the new keys and keep store those keys in app cache to access othe api’s.

Sample code:

@frappe.whitelist(allow_guest=True)
def generate_otp(mobile_number):
    import re
    from base64 import b32encode
    import os
    import pyotp
    import requests


    if not mobile_number:
        frappe.response["http_status_code"] = 400
        return {
            "status": "error",
            "message": "Please Enter Mobile Number"
        }
    if not re.fullmatch(r'[6-9]\d{9}', mobile_number):
        frappe.response["http_status_code"] = 400
        return {
            "status": "error",
            "message": "Please Enter Valid Mobile Number"
        }

   
    otp_secret = b32encode(os.urandom(10)).decode("utf-8")
    totp = pyotp.TOTP(otp_secret, digits=4)
    otp = totp.now()

    try:
        url = "Your external vendor url to send OTP"
        payload = {
            "data": [
                {
                    "TransactionId": "",
                    "countrycode": "91",
                    "number": mobile_number,
                    "message": f"Your OTP for App login is {otp}. It is valid for 10 minutes. Do not share this code with anyone.",
                    "url": ""
                }
            ]
        }

        send_otp = requests.post(url, json=payload)
        if send_otp.status_code != 200:
            frappe.response["http_status_code"] = 400
            frappe.response["status"] = "error"
            frappe.response["message"] = "Failed to send OTP. Please try again later."
            return
        
        # Store OTP in cache
  
        frappe.cache().setex(f"otp_{mobile_number}", 300, otp)

        return {
            "status": "success",
            "message": message,
            "otp": otp
        }

    except Exception as e:
        frappe.response["http_status_code"] = 400
        frappe.response["status"] = "error"
        frappe.response["message"] = "Something went wrong while sending OTP. Please try again later."
        return
@frappe.whitelist(allow_guest=True)
def validate_otp(mobile_number, entered_otp):
    cache = frappe.cache()
    otp_key = f"otp_{mobile_number}"
    attempt_key = f"otp_attempts_{mobile_number}"
    lockout_key = f"otp_lockout_{mobile_number}"

    otp = cache.get(otp_key)
    if not otp:
        frappe.response["http_status_code"] = 400
        return {"status": "error", "message": "Please Enter the OTP"}

    otp = otp.decode("utf-8") if isinstance(otp, bytes) else otp

    if entered_otp != otp:
        frappe.response["http_status_code"] = 400
        return {
            "status": "error",
            "message": f"The Entered OTP is Invalid"
        }

    # OTP matched, clear cache
    cache.delete(otp_key)
    cache.delete(attempt_key)
    cache.delete(lockout_key)

    user_exists = frappe.db.get_value("User", {"phone": mobile_number,"enabled":1}, "name") or frappe.db.get_value("User", {"mobile_no": mobile_number, "enabled":1}, "name")

    if user_exists:

        return {
            "status": "success",
            "message": "OTP verified Sucessfully",
            "token": generate_keys(user_exists)
        }

    user_name = ""
    if not user_exists:
        user_ = frappe.db.get_value("User", {"phone": mobile_number,"enabled":0}, "name") or frappe.db.get_value("User", {"mobile_no": mobile_number, "enabled":0}, "name")
        if user_:
            frappe.db.set_value("User",user_, "enabled", 1)
            user_exists = user_
        else:
            user_doc = frappe.get_doc({
                "doctype": "User",
                "email": user_email,
                "first_name": mobile_number,
                "enabled": 1,
                "send_welcome_email": 0,
                "phone": mobile_number
            })
            user_doc.insert(ignore_permissions=True)
            user_exists = user_doc.name

    
    user_name=user_name if user_name else user_exists
    return {
        "status": "success",
        "message": "OTP verified Sucessfully",
        "token": generate_keys(user_name)
    }

@frappe.whitelist(allow_guest=True, methods=["POST"])
def generate_keys(user):
    user_details = frappe.get_doc("User", user)
    api_key = user_details.get("api_key")
    api_secret = None
    if not api_key:
        api_secret = frappe.generate_hash(length=15)
        api_key = frappe.generate_hash(length=15)
        user_details.api_key = api_key
        user_details.api_secret = api_secret
        user_details.save(ignore_permissions=True)
    else:
        api_secret = get_decrypted_password("User", user, "api_secret", raise_exception=False)
   # return base64.b64encode(f'{api_key}:{api_secret}'.encode('utf-8')).decode('utf-8')
    return f"{api_key}:{api_secret}"
1 Like

Thank you for the info..

is it possible to incorporate JWT with token and refresh token in ERP Next ?

How to implement the same

Please ellaborate your request

we need token and refresh token for our app development, is it possible to create api for the same ?

we need to set token expiry …

Please Explore