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,
- Signup will create user by mobile number in erpnext and won’t allow to create new user with the same mobile no.
- If user document already exists then generate new session keys and send to app. We can use the generated keys to other api’s.
- 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.
- 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