OAuth 2 provider for Frappe Apps

Pull Request:
https://github.com/frappe/frappe/pull/2167

How to Setup:
https://github.com/revant/frappe/blob/develop/frappe/docs/user/en/guides/integration/how_to_setup_oauth.md

Usage:
https://github.com/revant/frappe/blob/develop/frappe/docs/user/en/guides/integration/using_oauth.md

6 Likes

https://github.com/frappe/frappe/pull/2167

2 Likes

Can you update the OP with latest changes and links to docs and how-tos? I’ll then pin it to the top.

1 Like

Testing for Frappe Social Logins

In [1]: from rauth import OAuth2Service

In [2]: frappe_oauth_server = OAuth2Service(client_id='48214acfa6', client_secret=None, name='acumen_erpnext', authorize_url='http://0.0.0.0:8000
   ...: /api/method/frappe.integrations.oauth2.authorize', access_token_url='http://0.0.0.0:8000/api/method/frappe.integrations.oauth
   ...: 2.get_token', base_url='http://0.0.0.0:8000/api/resource/')

In [3]: redirect_uri = "http://0.0.0.0:8000/redir"

In [4]: params = {'scope': 'project', 'response_type': 'code', 'redirect_uri': redirect_uri}

In [5]: url = frappe_oauth_server.get_authorize_url(**params)

In [6]: url
Out[6]: u'http://0.0.0.0:8000/api/method/frappe.integration_broker.oauth2.authorize?scope=project&redirect_uri=http%3A%2F%2F0.0.0.0%3A8000%2Fredir&response_type=code&client_id=48214acfa6'

In [7]: # the code should be returned upon the redirect from the authorize step, be sure to use it here (hint: it's in the URL!)

In [8]: data={'code': 'kCtF03mzXKnHobYBnrsoUzT3P7SmS6', 'grant_type':'authorization_code', 'redirect_uri': redirect_uri}

In [9]: session = frappe_oauth_server.get_auth_session(data=data,decoder=json.loads)

In [10]: print session.get("ToDo", params={"fields":'["name","description"]'}).json()
{u'data': [{u'name': u'469bb963fc', u'description': u'XYZ'}, {u'name': u'9f1f217dbf', u'description': u'PQR'}, {u'name': u'9a631eabee', u'description': u'ABC'}]}
1 Like

Notes :

  • Oauthlib - oauthlib · PyPI
  • Workflow : Authorization Code Request - Grant - Authorization Code - Bearer Token used for Web Apps

Planned in consequent pull requests OAuth 2 Provider for frappe by revant · Pull Request #2167 · frappe/frappe · GitHub

  1. Add OAuth Client Scope Child doctype instead of a scopes semi-colon separated value text field
  2. Fields in OAuth Client Scope
  3. Label:Scope, Type:Link, Option:DocType
  4. Check Boxes for Read, Write, Create, Delete, etc.
  5. Only allow resource defined in scope for the given access_token, right now roles and permissions are controlling access to resource. It should be scope restriction first and then user / role restrictions

Testing OAuth 2.0 with Postman

  1. Under Authorization tab select Type as OAuth 2.0

  1. Click on Get New Access Token button and enter the details about client_id, urls, scope.
    Note: redirect_uri must be set to the one given by Postman so that the token is received.

  1. Login to frappe with your credentials (If skip authorization checked in Oauth Client, no confirmation dialog will be shown)

  1. On successful login token will be received

  1. Use token in request header to access protected resource with user’s credentials
    e.g. /api/resource/ToDo

4 Likes

Thank you for contribution.
Does this provider support OAuth 1 as well? Just curious because oauthlib supports both 1 and 2 versions of OAuth.

After reading Wikipedia article, thought that this guide can be useful: http://www.oauthsecurity.com/

1 Like

I’ve only added OAuth2 support.

Further development:

https://github.com/frappe/frappe/pull/2227

2 Likes

OpenID connect and Social Login for Frappe is now added

https://github.com/revant/frappe/blob/develop/frappe/docs/user/en/guides/integration/openid_connect_and_frappe_social_login.md

I’ve tested it few times.

Developers outside frappe ecosystem can now use their platforms along with frappe.

Now we can develop microservices!

example bearer token with id_token

{
  "token_type": "Bearer",
  "id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6Imp3dCJ9.eyJpc3MiOiJodHRwczovL21udGVjaG5pcXVlLmNvbSIsImF0X2hhc2giOiJOQlFXbExJUy1lQ1BXd1d4Y0EwaVpnIiwiYXVkIjoiYjg3NzJhZWQ1YyIsImV4cCI6MTQ3Nzk1NTYzMywic3ViIjoiNWFjNDE2NThkZjFiZTE1MjI4M2QxYTk0YjhmYzcwNDIifQ.1GRvhk5wNoR4GWoeQfleEDgtLS5nvj9nsO4xd8QE-Uk",
  "access_token": "ZJD04ldyyvjuAngjgBrgHwxcOig4vW",
  "scope": "openid",
  "expires_in": 3600,
  "refresh_token": "2pBTDTGhjzs2EWRkcNV1N67yw0nizS"
}

Part 1 : on Frappe Identity Provider (IDP)

Login to IDP


Add OAuth Client on IDP


Set Server URL on IDP


Part 2 : on Frappe App Server

Set Frappe Client ID and Frappe Client Secret on App server (refer the client set on IDP)


Note: Frappe Server URL is the main server where identities from your organization are stored.

Login Screen on App Server (login using frappe)


Part 3 : Redirected on IDP

login with user on IDP


Confirm Access on IDP


Part 4 : Back on App Server

Logged in on app server with ID from IDP

4 Likes

Thanks! This was really usefull!

I was wondering if there is any way to easily import roles from IDP to App Server.

As of right now I am thinking about making the following steps:

  • Allow to specify scope in frappe provider in OAuth Provider Settings

  • Create a new scope called roles

  • Include active roles on the profile callback

    • Change “openid_profile” to a more generic profile?
  • Modify “update_oauth_user” in order to update roles on login

Do you think I am missing any steps?

As a last question, I saw on Github you were planning on using the OAUTH Token to query the API, is there any progress on this?

Regards!

1 Like

OAuth 2 Token from all request headers is validated.
this way it is working with many standard oauth2 clients like python rauth, postman.
I managed to connect Android Authenticator/SyncAdapter using standard OAuth2 Flow.

Community is also discussing about Magento OAuth 2 connector

reference:
https://github.com/frappe/frappe/blob/develop/frappe/api.py#L38
https://github.com/frappe/frappe/blob/develop/frappe/api.py#L131

Right now, access_token stores user and set the stored user in validate_oauth()

This gives access_token all the permissions user has.

Scopes are validated, i.e only the scopes stored in oauth 2 client are valid.

also if openid is present in scope id_token is sent along with response

So if you have ideas to connect scopes and roles it’ll be awesome!

For the steps, go for it! Fork Frappe develop branch and create a feature branch on your fork. Tag me on PR I’ll collaborate.

All above apps must not break after upgrade, If there is some change required we will also have to update documentation.

openid_profile endpoint
Standard Claims Draft: OpenID Connect Basic Client Profile 1.0 - draft 28

Roles Can be additional claim as mentioned Draft: OpenID Connect Basic Client Profile 1.0 - draft 28

I am having JSONDecodeError here. Any idea?

Have you set Frappe Server URL under social login keys?

Hi revant_one,

I kept reading every post related to OAuth2 on the forum, but I’m still scratching my head what’s next.

I have a form login on android which user login through api http://frappe.local:8000/api/method/login
I use CookieManager but I want user keep login unless they logout. Then I started reading Oauth2 on forum as session never expired by using refresh_token.

I successfully setup OAuth2 on server and I expected it works like:
User log in the form in the App, by username & password then got access_token and refresh_token back… in response.
But it seems not. I might misunderstand about OAuth2. https://frappe.io/docs/user/en/guides/integration/using_oauth

Can you just tell the flow of user login by form and get get_token back?

Following is explanation of Authorization Code / Refresh Token grant.

first call : authorize

It checks if user is logged in,

if user is logged and authorizes the access to resource the server return a “Authorization Code” to the redirect uri. i.e your app

Processing response of first call:

there is an endpoint on your app that accepts GET request. the Auth Code comes back here as a parameter. e.g. /process_code?code=abc123

Second call : get_token

take the Auth code caught on the redirect url endpoint in the processing step above, and ask for a token with this code. (make POST request or use oauth client libraries available)

This time the response is the bearer token. Use this bearer_token.access_token for access.

Third call get_token (on expiry of previous bearer token)

Use bearer_token.refresh_token to get new bearer_token seamlessly.

Fourth call (to keep the server clean from used up tokens, optional but recommended)

you can revoke_token the expired bearer token after you refresh token.

2 Likes

Thanks revant_one,

I’m crystal clear how it works now.

1 Like