Best way to connect a Angular App with ERPNext

Hello guys,

I was wondering what the best way is to implement Authentification from an Angular App with User Credentials (Username + PW) AND OR an ID (like an RFID chip [currently implemented with the username instead of an ID]).

Attempt 1:
My first attempt was to create an API_Key+Secret by my own (but here I am still using the Username as an ID):

@frappe.whitelist(allow_guest=True)
def get_login_data(username):
try:
    user_details = frappe.get_doc("User", username)
except:
    frappe.throw(frappe._(""), frappe.DoesNotExistError)

api_secret = frappe.generate_hash(length=15)
# if api key is not set generate api key
if not user_details.api_key:
    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)

return {"api_key": user_details.api_key, "api_secret": api_secret}

This is working pretty fine the only problem I have here is that the User needs to generate the first key- secret pair manually in the UI.
Is there a way arround this problem?

Attempt 2:
So the second thing i tried was to create a Session with a Cookie:

@frappe.whitelist(allow_guest=True)
def generateCookie(username, resume=False):
    user = frappe.get_doc("User", username)
    full_name = " ".join(filter(None, [user.first_name, user.last_name]))
    make_session(user, False, full_name)
    set_user_info(user, full_name, False)

def make_session(user, full_name, resume):
    frappe.local.session_obj = Session(user=user, resume=resume, full_name=full_name, user_type=user.user_type)
    print(frappe.local.session_obj.data)
    frappe.local.session = frappe.local.session_obj.data

def set_user_info(user, full_name, resume=False):
    frappe.local.cookie_manager.init_cookies()

    if user.user_type=="Website User":
        frappe.local.cookie_manager.set_cookie("system_user", "no")
        if not resume:
            frappe.local.response["message"] = "No App"
    else:
        frappe.local.cookie_manager.set_cookie("system_user", "yes")
        if not resume:
            frappe.local.response['message'] = 'Logged In'
            frappe.local.response["home_page"] = "/desk"
            frappe.local.response["cookie"] = frappe.local.session.get('sid')

    if not resume:
        frappe.response["full_name"] = full_name

    frappe.local.cookie_manager.set_cookie("full_name", full_name)

So the thing is I want a user to be able to log in here, but when a guest calls this method the SessionManager recognizes him as a Guest.
That means that he will return a sid with “guest” inside. But I want a correct SID instead.

Would it be possible to create a SID for a given user and to return it to the Angular App?

These were my attempts to make a custom Login.
I am not very happy with them, what do you guys think?
Are there better solutions?

Help would be really appreciated!

check this if it helps. Add Single Page Application to website portal page

This app is served as a www page from frappe framework. That makes it part of same domain served as static files so there is no issue with cors and requests can be made with cookie already there in browser for rest of the site.

Thank you for your fast reply! @revant_one

This sounds like a nice solution but in my particular case I need to use it as a standalone App.
Do you have any suggestions how to implement it like this?

Check this out what i have used for one usecase.

@frappe.whitelist(allow_guest=True)
def login(usr,pwd):
    try:
        login_manager = frappe.auth.LoginManager()
        login_manager.authenticate(user=usr,pwd=pwd)
        login_manager.post_login()
    except frappe.exceptions.AuthenticationError:
        frappe.clear_messages()
        frappe.local.response["message"] =  {
            "success_key":0,
            "message":"Authentication Failed"
            }
        return

    api_generate=generate_keys(frappe.session.user)
    user = frappe.get_doc('User',frappe.session.user)
 
    frappe.response["message"] =	{
            "success_key": 1,
            "message":"Authentication Success"
            "sid": frappe.session.sid,
            "api_key":user.api_key,
            "api secret": api_generate
            }
    return

def generate_keys(user):
    user_details = frappe.get_doc('User', user)
    api_secret = frappe.generate_hash(length=15)
    # if api key is not set, generate api key
    if not user_details.api_key:
        api_key = frappe.generate_hash(length=15)
        user_details.api_key = api_key
    user_details.api_secret = api_secret
    user_details.save()
    return api_secret

You can generate api key and api secret everytime someone logs in.

1 Like

and you don’t need to generate the first key-secret pair manually. it works for me using the above for the login. further requests work with api key and api secret.

Will this change the api_secret if same user logs in from 2 devices?

Actually yes. Only the last logged in device works. In my case, the other devices gets logged out automatically and redirects back to the login page.

thx @hashir that helps a lot

but i still have the usecase where i need to login a User only with an ID (RFID or in my current case his email address)

do you have any advice how I can solve this?

the ‘usr’ parameter in my above example can be username, email address or mobile number (from user doctype) provided that the following are enabled in the system settings

If you want seamless single sign on in your custom app, create OAuth Client and use OAuth 2.
That will make sure multiple devices can login and get separate bearer token for each device.

Still CORS needs to be configured if it is browser only app.

This is Angular/Ionic sample mobile app, it uses OAuth 2 code grant with refresh token.
It is using native http calls from mobile platform to workaround CORS.

you can make it into a PWA to be served from browser if you enable CORS for domain from where app will be served.

1 Like

@revant_one i have my users registering themselves from the app as new users. Can OAuth be applied in this scenario?

As long as user is able to login using standard login page, it’ll work. It will also work if user selects social login option.

1 Like

I was able to run this Ionic mobile App and login and fetching sales invoice was successful.

I changed appUrl: ‘frappe://’ to ‘http://localhost:8100’ so that i can test in browser also
Also i call this.token.processCode(window.location.href) in ngOnInit at home page so that the auth code returned is exchanged for access token

All is good but the problem is when i logout, frappe.integrations.oauth2.revoke_token endpoint is called an it fails with Bad Request error

When this happens repeatedly, fail2ban block my ip for 10 mins.

How to ensure that this doesnt happen or how to make the revoke token call successful?

What’s the response? Check the response tab.

Check this as well, it supports ionic capacitor castlecraft / capacitor-starter · GitLab

Check request, it should be like this frappe/test_oauth20.py at b4d57b0174a16035ffe3024ac04f1f87944a448d · frappe/frappe · GitHub

This is the response

As per the test, it requires headers and token which are being passed as expected from browser

request-header in this image seems to be Content-Type: text/plain

Can you try the same request without any other header and only {"content-type": "application/x-www-form-urlencoded"}

Try it in postman.

I’ll try on my end today.

Can you try the other starter? Just try the basic login and logout, On logout it should revoke token.

The OAuth Bearer Token doctype will show status Revoked

Sure, will try and post the outcome

This works. I receive success response and i checked at the table as well, it sets the status to “Revoked”
Tested using Postman

No sure if i am doing it the right way, but still posting my observation

  • I revoked the token using postman
  • The token is no more valid but still my request is passing through and i get response for get Sales Invoice in the angular App
  • I removed the token from the header of the request by clearing the local storage in Chrome
  • Still the get Sales Invoice was successful
  • Realized that since while approving the Authorization request, i login at the frappe frontend, it sets the cookie. So even after the token becomes invalid, this cookie is used to process the request of get Sales Invoice
  • Even if i logout, it only will clear the local storage i guess, the request will still pass through using the cookie.