浏览代码

OpenID Connect for Frappe (#2227)

* Add field for client_secret in Oauth Client Doctype

* openid_profile endpoint in oauth2.py

* added stub methods for OpenID Connect in RequestValidator

* [Fix] using werkzeug url_fix on uri

* added 3 oidc methods in RequestValidator

* Added Frappe Section in Social Login Keys

Add section in `Social Login Keys` for fields `Frappe Client ID` and `Frappe Client Secret` and additional field Frappe OAuth 2 Server because github, facebook and google have fixed urls, Frappe server URL can change as per the hosting domain/server/company

* [Fix] accept code id_token param for oidc

* generate id_token jwt alg HS256

* Updates to OAuth 2 and OIDC

`OAuth Authorization Code` and `OAuth Bearer Token` DocType made RO
Delete Invalid Codes and Revoked Tokens periodically
generate and send `id_token` only if scope of token is `openid`

* [Fix] Periodically delete revoked tokens

* Social Logins untested

* Enabled Frappe social login

* [Docs] OpenID Connect and Frappe social login

* [Fix] Allow multiple scopes for OAuth 2

* [Docs] Added screenshot steps to configure openid and frappe social login

* saved doctype to solve merge conflict

* [fix] re-added client_secret after resolving merge conlict

* [Fix] frappe_userid and default scope
version-14
Revant Nandgaonkar 8 年前
committed by Rushabh Mehta
父节点
当前提交
a16e6a143f
共有 25 个文件被更改,包括 591 次插入35 次删除
  1. +29
    -1
      frappe/core/doctype/user/user.json
  2. +3
    -0
      frappe/core/doctype/user/user.py
  3. 二进制
      frappe/docs/assets/img/00-login-to-idp.png
  4. 二进制
      frappe/docs/assets/img/01-add-oauth-client-on-idp.png
  5. 二进制
      frappe/docs/assets/img/02-set-server-url-on-idp.png
  6. 二进制
      frappe/docs/assets/img/03-set-clientid-client-secret-server-on-app-server.png
  7. 二进制
      frappe/docs/assets/img/04-login-screen-on-app-server.png
  8. 二进制
      frappe/docs/assets/img/05-login-with-user-on-idp.png
  9. 二进制
      frappe/docs/assets/img/06-confirm-grant-access-on-idp.png
  10. 二进制
      frappe/docs/assets/img/07-logged-in-as-website-user-with-id-from-idp.png
  11. +1
    -0
      frappe/docs/user/en/guides/integration/index.txt
  12. +72
    -0
      frappe/docs/user/en/guides/integration/openid_connect_and_frappe_social_login.md
  13. +1
    -0
      frappe/hooks.py
  14. +3
    -3
      frappe/integration_broker/doctype/oauth_authorization_code/oauth_authorization_code.json
  15. +3
    -3
      frappe/integration_broker/doctype/oauth_bearer_token/oauth_bearer_token.json
  16. +33
    -4
      frappe/integration_broker/doctype/oauth_client/oauth_client.json
  17. +2
    -0
      frappe/integration_broker/doctype/oauth_client/oauth_client.py
  18. +69
    -6
      frappe/integration_broker/oauth2.py
  19. +163
    -2
      frappe/integrations/doctype/social_login_keys/social_login_keys.json
  20. +15
    -1
      frappe/integrations/doctype/social_login_keys/social_login_keys.py
  21. +159
    -13
      frappe/oauth.py
  22. +27
    -1
      frappe/utils/oauth.py
  23. +5
    -0
      frappe/www/login.html
  24. +5
    -1
      frappe/www/login.py
  25. +1
    -0
      requirements.txt

+ 29
- 1
frappe/core/doctype/user/user.json 查看文件

@@ -1712,6 +1712,34 @@
"search_index": 0, "search_index": 0,
"set_only_once": 0, "set_only_once": 0,
"unique": 0 "unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "frappe_userid",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Frappe User ID",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
} }
], ],
"hide_heading": 0, "hide_heading": 0,
@@ -1727,7 +1755,7 @@
"istable": 0, "istable": 0,
"max_attachments": 5, "max_attachments": 5,
"menu_index": 0, "menu_index": 0,
"modified": "2016-11-07 05:27:32.432906",
"modified": "2016-11-07 18:53:28.155954",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Core", "module": "Core",
"name": "User", "name": "User",


+ 3
- 0
frappe/core/doctype/user/user.py 查看文件

@@ -67,6 +67,9 @@ class User(Document):
if self.language == "Loading...": if self.language == "Loading...":
self.language = None self.language = None


if (self.name not in ["Administrator", "Guest"]) and (not self.frappe_userid):
self.frappe_userid = frappe.generate_hash(length=39)

def on_update(self): def on_update(self):
# clear new password # clear new password
self.validate_user_limit() self.validate_user_limit()


二进制
frappe/docs/assets/img/00-login-to-idp.png 查看文件

之前 之后
宽度: 1304  |  高度: 682  |  大小: 20 KiB

二进制
frappe/docs/assets/img/01-add-oauth-client-on-idp.png 查看文件

之前 之后
宽度: 1291  |  高度: 571  |  大小: 74 KiB

二进制
frappe/docs/assets/img/02-set-server-url-on-idp.png 查看文件

之前 之后
宽度: 1303  |  高度: 432  |  大小: 24 KiB

二进制
frappe/docs/assets/img/03-set-clientid-client-secret-server-on-app-server.png 查看文件

之前 之后
宽度: 1294  |  高度: 510  |  大小: 22 KiB

二进制
frappe/docs/assets/img/04-login-screen-on-app-server.png 查看文件

之前 之后
宽度: 1298  |  高度: 667  |  大小: 19 KiB

二进制
frappe/docs/assets/img/05-login-with-user-on-idp.png 查看文件

之前 之后
宽度: 1300  |  高度: 681  |  大小: 18 KiB

二进制
frappe/docs/assets/img/06-confirm-grant-access-on-idp.png 查看文件

之前 之后
宽度: 1317  |  高度: 681  |  大小: 22 KiB

二进制
frappe/docs/assets/img/07-logged-in-as-website-user-with-id-from-idp.png 查看文件

之前 之后
宽度: 1300  |  高度: 679  |  大小: 24 KiB

+ 1
- 0
frappe/docs/user/en/guides/integration/index.txt 查看文件

@@ -1,3 +1,4 @@
rest_api rest_api
how_to_setup_oauth how_to_setup_oauth
using_oauth using_oauth
openid_connect_and_frappe_social_login

+ 72
- 0
frappe/docs/user/en/guides/integration/openid_connect_and_frappe_social_login.md 查看文件

@@ -0,0 +1,72 @@
# OpenID Connect and Frappe social login

## OpenID Connect

Frappe also uses Open ID connect essential standard for authenticating users. To get `id_token` with `access_token`, pass `openid` as the value for the scope parameter during authorization request.

If the scope is `openid` the JSON response with `access_token` will also include a JSON Web Token (`id_token`) signed with `HS256` and `Client Secret`. The decoded `id_token` includes the `at_hash`.

Example Bearer Token with scope `openid`

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

## Frappe social login setup

In this example there are 2 servers,

### Primary Server
This is the main server hosting all the users. e.g. `https://frappe.io`. To setup this as the main server, go to *Setup* > *Integrations* > *Social Login Keys* and enter `https://frappe.io` in the field `Frappe Server URL`. This URL repeats in all other Frappe servers who connect to this server to authenticate. Effectively, this is the main Identity Provider (IDP).

Under this server add as many `OAuth Client`(s) as required. Because we are setting up one app server, add only one `OAuth Client`

### Frappe App Server
This is the client connecting to the IDP. Go to *Setup* > *Integrations* > *Social Login Keys* on this server and add appropriate values to `Frappe Client ID` and `Frappe Client Secret` (refer to client added in primary server). As mentioned before keep the `Frappe Server URL` as `https://frappe.io`

Now you will see Frappe icon on the login page. Click on this icon to login with account created in primary server (IDP) `https://frappe.io`

**Note**: If `Skip Authorization` is checked while registering a client, page to allow or deny the granting access to resource is not shown. This can be used if the apps are internal to one organization and seamless user experience is needed.

## Steps

### Part 1 : on Frappe Identity Provider (IDP)

Login to IDP
<img img class="screenshot" src="/assets/img/00-login-to-idp.png">

Add OAuth Client on IDP
<img img class="screenshot" src="/assets/img/01-add-oauth-client-on-idp.png">

Set Server URL on IDP
<img img class="screenshot" src="/assets/img/02-set-server-url-on-idp.png">

### Part 2 : on Frappe App Server

Set `Frappe Client ID` and `Frappe Client Secret` on App server (refer the client set on IDP)
<img img class="screenshot" src="/assets/img/03-set-clientid-client-secret-server-on-app-server.png">

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

Login Screen on App Server (login with frappe)
<img img class="screenshot" src="/assets/img/04-login-screen-on-app-server.png">

### Part 3 : Redirected on IDP

login with user on IDP
<img img class="screenshot" src="/assets/img/05-login-with-user-on-idp.png">

Confirm Access on IDP
<img img class="screenshot" src="/assets/img/06-confirm-grant-access-on-idp.png">

### Part 4 : Back on App Server

Logged in on app server with ID from IDP
<img img class="screenshot" src="/assets/img/07-logged-in-as-website-user-with-id-from-idp.png">

+ 1
- 0
frappe/hooks.py 查看文件

@@ -116,6 +116,7 @@ scheduler_events = {
"frappe.email.queue.flush", "frappe.email.queue.flush",
"frappe.email.doctype.email_account.email_account.pull", "frappe.email.doctype.email_account.email_account.pull",
"frappe.email.doctype.email_account.email_account.notify_unreplied", "frappe.email.doctype.email_account.email_account.notify_unreplied",
"frappe.oauth.delete_oauth2_data"
], ],
"hourly": [ "hourly": [
"frappe.model.utils.link_count.update_link_count", "frappe.model.utils.link_count.update_link_count",


+ 3
- 3
frappe/integration_broker/doctype/oauth_authorization_code/oauth_authorization_code.json 查看文件

@@ -217,7 +217,7 @@
"issingle": 0, "issingle": 0,
"istable": 0, "istable": 0,
"max_attachments": 0, "max_attachments": 0,
"modified": "2016-11-07 05:22:59.015955",
"modified": "2016-11-07 18:31:54.470173",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Integration Broker", "module": "Integration Broker",
"name": "OAuth Authorization Code", "name": "OAuth Authorization Code",
@@ -228,7 +228,7 @@
"amend": 0, "amend": 0,
"apply_user_permissions": 0, "apply_user_permissions": 0,
"cancel": 0, "cancel": 0,
"create": 1,
"create": 0,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 1, "export": 1,
@@ -243,7 +243,7 @@
"set_user_permissions": 0, "set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0, "submit": 0,
"write": 1
"write": 0
} }
], ],
"quick_entry": 0, "quick_entry": 0,


+ 3
- 3
frappe/integration_broker/doctype/oauth_bearer_token/oauth_bearer_token.json 查看文件

@@ -244,7 +244,7 @@
"issingle": 0, "issingle": 0,
"istable": 0, "istable": 0,
"max_attachments": 0, "max_attachments": 0,
"modified": "2016-11-07 05:54:52.149689",
"modified": "2016-11-07 18:31:32.243853",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Integration Broker", "module": "Integration Broker",
"name": "OAuth Bearer Token", "name": "OAuth Bearer Token",
@@ -255,7 +255,7 @@
"amend": 0, "amend": 0,
"apply_user_permissions": 0, "apply_user_permissions": 0,
"cancel": 0, "cancel": 0,
"create": 1,
"create": 0,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 1, "export": 1,
@@ -270,7 +270,7 @@
"set_user_permissions": 0, "set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0, "submit": 0,
"write": 1
"write": 0
} }
], ],
"quick_entry": 0, "quick_entry": 0,


+ 33
- 4
frappe/integration_broker/doctype/oauth_client/oauth_client.json 查看文件

@@ -12,6 +12,34 @@
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"fields": [ "fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "",
"fieldname": "client_id",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "App Client ID",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
@@ -101,8 +129,7 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"default": "",
"fieldname": "client_id",
"fieldname": "client_secret",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
@@ -110,10 +137,11 @@
"in_filter": 0, "in_filter": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0, "in_standard_filter": 0,
"label": "App Client ID",
"label": "App Client Secret",
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
"precision": "",
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0, "print_hide_if_no_value": 0,
"read_only": 1, "read_only": 1,
@@ -187,6 +215,7 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"default": "all openid",
"description": "A list of resources which the Client App will have access to after the user allows it.<br> e.g. project", "description": "A list of resources which the Client App will have access to after the user allows it.<br> e.g. project",
"fieldname": "scopes", "fieldname": "scopes",
"fieldtype": "Text", "fieldtype": "Text",
@@ -417,7 +446,7 @@
"issingle": 0, "issingle": 0,
"istable": 0, "istable": 0,
"max_attachments": 0, "max_attachments": 0,
"modified": "2016-11-07 05:22:49.074319",
"modified": "2016-11-07 18:53:59.549740",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Integration Broker", "module": "Integration Broker",
"name": "OAuth Client", "name": "OAuth Client",


+ 2
- 0
frappe/integration_broker/doctype/oauth_client/oauth_client.py 查看文件

@@ -9,3 +9,5 @@ from frappe.model.document import Document
class OAuthClient(Document): class OAuthClient(Document):
def validate(self): def validate(self):
self.client_id = self.name self.client_id = self.name
if not self.client_secret:
self.client_secret = frappe.generate_hash(length=10)

+ 69
- 6
frappe/integration_broker/oauth2.py 查看文件

@@ -3,8 +3,10 @@ import frappe, json
from frappe.oauth import OAuthWebRequestValidator, WebApplicationServer from frappe.oauth import OAuthWebRequestValidator, WebApplicationServer
from oauthlib.oauth2 import FatalClientError, OAuth2Error from oauthlib.oauth2 import FatalClientError, OAuth2Error
from urllib import quote, urlencode from urllib import quote, urlencode
from werkzeug import url_fix
from urlparse import urlparse from urlparse import urlparse
from frappe.integrations.doctype.oauth_provider_settings.oauth_provider_settings import get_oauth_settings from frappe.integrations.doctype.oauth_provider_settings.oauth_provider_settings import get_oauth_settings
from frappe import _


#Variables required across requests #Variables required across requests
oauth_validator = OAuthWebRequestValidator() oauth_validator = OAuthWebRequestValidator()
@@ -23,7 +25,7 @@ def get_urlparams_from_kwargs(param_kwargs):
@frappe.whitelist() @frappe.whitelist()
def approve(*args, **kwargs): def approve(*args, **kwargs):
r = frappe.request r = frappe.request
uri = r.url
uri = url_fix(r.url.replace("+"," "))
http_method = r.method http_method = r.method
body = r.get_data() body = r.get_data()
headers = r.headers headers = r.headers
@@ -60,7 +62,7 @@ def authorize(*args, **kwargs):
elif frappe.session['user']!='Guest': elif frappe.session['user']!='Guest':
try: try:
r = frappe.request r = frappe.request
uri = r.url
uri = url_fix(r.url)
http_method = r.method http_method = r.method
body = r.get_data() body = r.get_data()
headers = r.headers headers = r.headers
@@ -94,14 +96,41 @@ def authorize(*args, **kwargs):
def get_token(*args, **kwargs): def get_token(*args, **kwargs):
r = frappe.request r = frappe.request


uri = r.url
uri = url_fix(r.url)
http_method = r.method http_method = r.method
body = r.form body = r.form
headers = r.headers headers = r.headers
#Check whether frappe server URL is set
frappe_server_url = frappe.db.get_value("Social Login Keys", None, "frappe_server_url") or None
if not frappe_server_url:
frappe.throw(_("Define Frappe Server URL in Social Login Keys"))


try: try:
headers, body, status = oauth_server.create_token_response(uri, http_method, body, headers, credentials) headers, body, status = oauth_server.create_token_response(uri, http_method, body, headers, credentials)
frappe.local.response = frappe._dict(json.loads(body))
out = frappe._dict(json.loads(body))
if not out.error and "openid" in out.scope:
token_user = frappe.db.get_value("OAuth Bearer Token", out.access_token, "user")
token_client = frappe.db.get_value("OAuth Bearer Token", out.access_token, "client")
client_secret = frappe.db.get_value("OAuth Client", token_client, "client_secret")
if token_user in ["Guest", "Administrator"]:
frappe.throw(_("Logged in as Guest or Administrator"))
import hashlib
id_token_header = {
"typ":"jwt",
"alg":"HS256"
}
id_token = {
"aud": token_client,
"exp": int((frappe.db.get_value("OAuth Bearer Token", out.access_token, "expiration_time") - frappe.utils.datetime.datetime(1970, 1, 1)).total_seconds()),
"sub": frappe.db.get_value("User", token_user, "frappe_userid"),
"iss": frappe_server_url,
"at_hash": frappe.oauth.calculate_at_hash(out.access_token, hashlib.sha256)
}
import jwt
id_token_encoded = jwt.encode(id_token, client_secret, algorithm='HS256', headers=id_token_header)
out.update({"id_token":id_token_encoded})
frappe.local.response = out
except FatalClientError as e: except FatalClientError as e:
return e return e


@@ -109,7 +138,7 @@ def get_token(*args, **kwargs):
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
def revoke_token(*args, **kwargs): def revoke_token(*args, **kwargs):
r = frappe.request r = frappe.request
uri = r.url
uri = url_fix(r.url)
http_method = r.method http_method = r.method
body = r.form body = r.form
headers = r.headers headers = r.headers
@@ -120,4 +149,38 @@ def revoke_token(*args, **kwargs):
if status == 200: if status == 200:
return "success" return "success"
else: else:
return "bad request"
return "bad request"

@frappe.whitelist()
def openid_profile(*args, **kwargs):
picture = None
first_name, last_name, avatar, name, frappe_userid = frappe.db.get_value("User", frappe.session.user, ["first_name", "last_name", "user_image", "name", "frappe_userid"])
request_url = urlparse(frappe.request.url)

if avatar:
if validate_url(avatar):
picture = avatar
else:
picture = request_url.scheme + "://" + request_url.netloc + avatar

user_profile = frappe._dict({
"sub": frappe_userid,
"name": " ".join(filter(None, [first_name, last_name])),
"given_name": first_name,
"family_name": last_name,
"email": name,
"picture": picture
})
frappe.local.response = user_profile

def validate_url(url_string):
from urlparse import urlparse
try:
result = urlparse(url_string)
if result.scheme and result.scheme in ["http", "https", "ftp", "ftps"]:
return True
else:
return False
except:
return False

+ 163
- 2
frappe/integrations/doctype/social_login_keys/social_login_keys.json 查看文件

@@ -2,26 +2,33 @@
"allow_copy": 0, "allow_copy": 0,
"allow_import": 0, "allow_import": 0,
"allow_rename": 0, "allow_rename": 0,
"beta": 0,
"creation": "2014-03-04 08:29:52", "creation": "2014-03-04 08:29:52",
"custom": 0, "custom": 0,
"docstatus": 0, "docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "System", "document_type": "System",
"editable_grid": 0,
"fields": [ "fields": [
{ {
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0,
"fieldname": "facebook", "fieldname": "facebook",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0,
"label": "Facebook", "label": "Facebook",
"length": 0,
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -33,16 +40,21 @@
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0,
"fieldname": "facebook_client_id", "fieldname": "facebook_client_id",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0,
"label": "Facebook Client ID", "label": "Facebook Client ID",
"length": 0,
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -54,16 +66,21 @@
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0,
"fieldname": "facebook_client_secret", "fieldname": "facebook_client_secret",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0,
"label": "Facebook Client Secret", "label": "Facebook Client Secret",
"length": 0,
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -75,16 +92,21 @@
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0,
"fieldname": "google", "fieldname": "google",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0,
"label": "Google", "label": "Google",
"length": 0,
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -96,16 +118,21 @@
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0,
"fieldname": "google_client_id", "fieldname": "google_client_id",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0,
"label": "Google Client ID", "label": "Google Client ID",
"length": 0,
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -117,16 +144,21 @@
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0,
"fieldname": "google_client_secret", "fieldname": "google_client_secret",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0,
"label": "Google Client Secret", "label": "Google Client Secret",
"length": 0,
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -138,16 +170,21 @@
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0,
"fieldname": "github", "fieldname": "github",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0,
"label": "GitHub", "label": "GitHub",
"length": 0,
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -159,16 +196,21 @@
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0,
"fieldname": "github_client_id", "fieldname": "github_client_id",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0,
"label": "GitHub Client ID", "label": "GitHub Client ID",
"length": 0,
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -180,16 +222,129 @@
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0,
"fieldname": "github_client_secret", "fieldname": "github_client_secret",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0,
"label": "GitHub Client Secret", "label": "GitHub Client Secret",
"length": 0,
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "frappe",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Frappe",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "frappe_client_id",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Frappe Client ID",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "frappe_client_secret",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Frappe Client Secret",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "frappe_server_url",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Frappe Server URL",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -202,12 +357,14 @@
"hide_toolbar": 0, "hide_toolbar": 0,
"icon": "icon-signin", "icon": "icon-signin",
"idx": 1, "idx": 1,
"image_view": 0,
"in_create": 0, "in_create": 0,
"in_dialog": 0, "in_dialog": 0,
"is_submittable": 0, "is_submittable": 0,
"issingle": 1, "issingle": 1,
"istable": 0, "istable": 0,
"modified": "2015-08-05 08:14:52.667728",
"max_attachments": 0,
"modified": "2016-10-29 13:36:35.121599",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Integrations", "module": "Integrations",
"name": "Social Login Keys", "name": "Social Login Keys",
@@ -223,6 +380,7 @@
"export": 0, "export": 0,
"if_owner": 0, "if_owner": 0,
"import": 0, "import": 0,
"is_custom": 0,
"permlevel": 0, "permlevel": 0,
"print": 0, "print": 0,
"read": 1, "read": 1,
@@ -234,6 +392,9 @@
"write": 1 "write": 1
} }
], ],
"quick_entry": 0,
"read_only": 0, "read_only": 0,
"read_only_onload": 0
"read_only_onload": 0,
"sort_order": "ASC",
"track_seen": 0
} }

+ 15
- 1
frappe/integrations/doctype/social_login_keys/social_login_keys.py 查看文件

@@ -7,6 +7,20 @@ from __future__ import unicode_literals
import frappe import frappe


from frappe.model.document import Document from frappe.model.document import Document
from frappe import _


class SocialLoginKeys(Document): class SocialLoginKeys(Document):
pass
def validate(self):
self.validate_frappe_server_url()

def validate_frappe_server_url(self):
if self.frappe_server_url:
if self.frappe_server_url.endswith('/'):
self.frappe_server_url = self.frappe_server_url[:-1]
import requests
try:
r = requests.get(self.frappe_server_url + "/api/method/frappe.handler.version", timeout=5)
except:
frappe.throw(_("Unable to make request to the Frappe Server URL"))
if r.status_code != 200:
frappe.throw(_("Check Frappe Server URL"))

+ 159
- 13
frappe/oauth.py 查看文件

@@ -1,8 +1,9 @@
import frappe, urllib import frappe, urllib


from frappe import _
from urlparse import parse_qs, urlparse from urlparse import parse_qs, urlparse
from oauthlib.oauth2.rfc6749.tokens import BearerToken from oauthlib.oauth2.rfc6749.tokens import BearerToken
from oauthlib.oauth2.rfc6749.grant_types import AuthorizationCodeGrant, ImplicitGrant, ResourceOwnerPasswordCredentialsGrant, ClientCredentialsGrant, RefreshTokenGrant
from oauthlib.oauth2.rfc6749.grant_types import AuthorizationCodeGrant, ImplicitGrant, ResourceOwnerPasswordCredentialsGrant, ClientCredentialsGrant, RefreshTokenGrant, OpenIDConnectAuthCode
from oauthlib.oauth2 import RequestValidator from oauthlib.oauth2 import RequestValidator
from oauthlib.oauth2.rfc6749.endpoints.authorization import AuthorizationEndpoint from oauthlib.oauth2.rfc6749.endpoints.authorization import AuthorizationEndpoint
from oauthlib.oauth2.rfc6749.endpoints.token import TokenEndpoint from oauthlib.oauth2.rfc6749.endpoints.token import TokenEndpoint
@@ -10,6 +11,8 @@ from oauthlib.oauth2.rfc6749.endpoints.resource import ResourceEndpoint
from oauthlib.oauth2.rfc6749.endpoints.revocation import RevocationEndpoint from oauthlib.oauth2.rfc6749.endpoints.revocation import RevocationEndpoint
from oauthlib.common import Request from oauthlib.common import Request


separated_by = " "

class WebApplicationServer(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint, class WebApplicationServer(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint,
RevocationEndpoint): RevocationEndpoint):


@@ -32,10 +35,19 @@ class WebApplicationServer(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoin
""" """
auth_grant = AuthorizationCodeGrant(request_validator) auth_grant = AuthorizationCodeGrant(request_validator)
refresh_grant = RefreshTokenGrant(request_validator) refresh_grant = RefreshTokenGrant(request_validator)
openid_connect_auth = OpenIDConnectAuthCode(request_validator)
bearer = BearerToken(request_validator, token_generator, bearer = BearerToken(request_validator, token_generator,
token_expires_in, refresh_token_generator) token_expires_in, refresh_token_generator)
AuthorizationEndpoint.__init__(self, default_response_type='code', AuthorizationEndpoint.__init__(self, default_response_type='code',
response_types={'code': auth_grant},
response_types={
'code': auth_grant,
'code+token': openid_connect_auth,
'code+id_token': openid_connect_auth,
'code+token+id_token': openid_connect_auth,
'code token': openid_connect_auth,
'code id_token': openid_connect_auth,
'code token id_token': openid_connect_auth,
},
default_token_type=bearer) default_token_type=bearer)
TokenEndpoint.__init__(self, default_grant_type='authorization_code', TokenEndpoint.__init__(self, default_grant_type='authorization_code',
grant_types={ grant_types={
@@ -64,7 +76,7 @@ class OAuthWebRequestValidator(RequestValidator):
# Is the client allowed to use the supplied redirect_uri? i.e. has # Is the client allowed to use the supplied redirect_uri? i.e. has
# the client previously registered this EXACT redirect uri. # the client previously registered this EXACT redirect uri.


redirect_uris = frappe.db.get_value("OAuth Client", client_id, 'redirect_uris').split(';')
redirect_uris = frappe.db.get_value("OAuth Client", client_id, 'redirect_uris').split(separated_by)


if redirect_uri in redirect_uris: if redirect_uri in redirect_uris:
return True return True
@@ -80,7 +92,7 @@ class OAuthWebRequestValidator(RequestValidator):


def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs): def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs):
# Is the client allowed to access the requested scopes? # Is the client allowed to access the requested scopes?
client_scopes = frappe.db.get_value("OAuth Client", client_id, 'scopes').split(';')
client_scopes = frappe.db.get_value("OAuth Client", client_id, 'scopes').split(separated_by)


are_scopes_valid = True are_scopes_valid = True


@@ -92,7 +104,7 @@ class OAuthWebRequestValidator(RequestValidator):
def get_default_scopes(self, client_id, request, *args, **kwargs): def get_default_scopes(self, client_id, request, *args, **kwargs):
# Scopes a client will authorize for if none are supplied in the # Scopes a client will authorize for if none are supplied in the
# authorization request. # authorization request.
scopes = frappe.db.get_value("OAuth Client", client_id, 'scopes').split(';')
scopes = frappe.db.get_value("OAuth Client", client_id, 'scopes').split(separated_by)
request.scopes = scopes #Apparently this is possible. request.scopes = scopes #Apparently this is possible.
return scopes return scopes


@@ -100,8 +112,11 @@ class OAuthWebRequestValidator(RequestValidator):
# Clients should only be allowed to use one type of response type, the # Clients should only be allowed to use one type of response type, the
# one associated with their one allowed grant type. # one associated with their one allowed grant type.
# In this case it must be "code". # In this case it must be "code".
allowed_response_types = [client.response_type.lower(),
"code token", "code id_token", "code token id_token",
"code+token", "code+id_token", "code+token id_token"]


return (client.response_type.lower() == response_type)
return (response_type in allowed_response_types)




# Post-authorization # Post-authorization
@@ -111,7 +126,7 @@ class OAuthWebRequestValidator(RequestValidator):
cookie_dict = get_cookie_dict_from_headers(request) cookie_dict = get_cookie_dict_from_headers(request)


oac = frappe.new_doc('OAuth Authorization Code') oac = frappe.new_doc('OAuth Authorization Code')
oac.scopes = ';'.join(request.scopes)
oac.scopes = separated_by.join(request.scopes)
oac.redirect_uri_bound_to_authorization_code = request.redirect_uri oac.redirect_uri_bound_to_authorization_code = request.redirect_uri
oac.client = client_id oac.client = client_id
oac.user = urllib.unquote(cookie_dict['user_id']) oac.user = urllib.unquote(cookie_dict['user_id'])
@@ -130,8 +145,10 @@ class OAuthWebRequestValidator(RequestValidator):
#Extract token, instantiate OAuth Bearer Token and use clientid from there. #Extract token, instantiate OAuth Bearer Token and use clientid from there.
if frappe.form_dict.has_key("refresh_token"): if frappe.form_dict.has_key("refresh_token"):
oc = frappe.get_doc("OAuth Client", frappe.db.get_value("OAuth Bearer Token", {"refresh_token": frappe.form_dict["refresh_token"]}, 'client')) oc = frappe.get_doc("OAuth Client", frappe.db.get_value("OAuth Bearer Token", {"refresh_token": frappe.form_dict["refresh_token"]}, 'client'))
else:
elif frappe.form_dict.has_key("token"):
oc = frappe.get_doc("OAuth Client", frappe.db.get_value("OAuth Bearer Token", frappe.form_dict["token"], 'client')) oc = frappe.get_doc("OAuth Client", frappe.db.get_value("OAuth Bearer Token", frappe.form_dict["token"], 'client'))
else:
oc = frappe.get_doc("OAuth Client", frappe.db.get_value("OAuth Bearer Token", frappe.get_request_header("Authorization").split(" ")[1], 'client'))
try: try:
request.client = request.client or oc.as_dict() request.client = request.client or oc.as_dict()
except Exception, e: except Exception, e:
@@ -159,7 +176,7 @@ class OAuthWebRequestValidator(RequestValidator):
checkcodes.append(vcode["name"]) checkcodes.append(vcode["name"])


if code in checkcodes: if code in checkcodes:
request.scopes = frappe.db.get_value("OAuth Authorization Code", code, 'scopes').split(';')
request.scopes = frappe.db.get_value("OAuth Authorization Code", code, 'scopes').split(separated_by)
request.user = frappe.db.get_value("OAuth Authorization Code", code, 'user') request.user = frappe.db.get_value("OAuth Authorization Code", code, 'user')
return True return True
else: else:
@@ -185,7 +202,7 @@ class OAuthWebRequestValidator(RequestValidator):
otoken = frappe.new_doc("OAuth Bearer Token") otoken = frappe.new_doc("OAuth Bearer Token")
otoken.client = request.client['name'] otoken.client = request.client['name']
otoken.user = request.user otoken.user = request.user
otoken.scopes = ";".join(request.scopes)
otoken.scopes = separated_by.join(request.scopes)
otoken.access_token = token['access_token'] otoken.access_token = token['access_token']
otoken.refresh_token = token['refresh_token'] otoken.refresh_token = token['refresh_token']
otoken.expires_in = token['expires_in'] otoken.expires_in = token['expires_in']
@@ -209,7 +226,7 @@ class OAuthWebRequestValidator(RequestValidator):
otoken = frappe.get_doc("OAuth Bearer Token", token) #{"access_token": str(token)}) otoken = frappe.get_doc("OAuth Bearer Token", token) #{"access_token": str(token)})
is_token_valid = (frappe.utils.datetime.datetime.now() < otoken.expiration_time) \ is_token_valid = (frappe.utils.datetime.datetime.now() < otoken.expiration_time) \
and otoken.status != "Revoked" and otoken.status != "Revoked"
client_scopes = frappe.db.get_value("OAuth Client", otoken.client, 'scopes').split(';')
client_scopes = frappe.db.get_value("OAuth Client", otoken.client, 'scopes').split(separated_by)
are_scopes_valid = True are_scopes_valid = True
for scp in scopes: for scp in scopes:
are_scopes_valid = are_scopes_valid and True if scp in client_scopes else False are_scopes_valid = are_scopes_valid and True if scp in client_scopes else False
@@ -261,7 +278,6 @@ class OAuthWebRequestValidator(RequestValidator):
# - Resource Owner Password Credentials Grant (also indirectly) # - Resource Owner Password Credentials Grant (also indirectly)
# - Refresh Token Grant # - Refresh Token Grant
# """ # """
# raise NotImplementedError('Subclasses must implement this method.')


otoken = frappe.get_doc("OAuth Bearer Token", {"refresh_token": refresh_token, "status": "Active"}) otoken = frappe.get_doc("OAuth Bearer Token", {"refresh_token": refresh_token, "status": "Active"})


@@ -270,7 +286,101 @@ class OAuthWebRequestValidator(RequestValidator):
else: else:
return True return True


#TODO: Validate scopes.
# OpenID Connect
def get_id_token(self, token, token_handler, request):
"""
In the OpenID Connect workflows when an ID Token is requested this method is called.
Subclasses should implement the construction, signing and optional encryption of the
ID Token as described in the OpenID Connect spec.

In addition to the standard OAuth2 request properties, the request may also contain
these OIDC specific properties which are useful to this method:

- nonce, if workflow is implicit or hybrid and it was provided
- claims, if provided to the original Authorization Code request

The token parameter is a dict which may contain an ``access_token`` entry, in which
case the resulting ID Token *should* include a calculated ``at_hash`` claim.

Similarly, when the request parameter has a ``code`` property defined, the ID Token
*should* include a calculated ``c_hash`` claim.

http://openid.net/specs/openid-connect-core-1_0.html (sections `3.1.3.6`_, `3.2.2.10`_, `3.3.2.11`_)

.. _`3.1.3.6`: http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken
.. _`3.2.2.10`: http://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDToken
.. _`3.3.2.11`: http://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken

:param token: A Bearer token dict
:param token_handler: the token handler (BearerToken class)
:param request: the HTTP Request (oauthlib.common.Request)
:return: The ID Token (a JWS signed JWT)
"""
# the request.scope should be used by the get_id_token() method to determine which claims to include in the resulting id_token

def validate_silent_authorization(self, request):
"""Ensure the logged in user has authorized silent OpenID authorization.

Silent OpenID authorization allows access tokens and id tokens to be
granted to clients without any user prompt or interaction.

:param request: The HTTP Request (oauthlib.common.Request)
:rtype: True or False

Method is used by:
- OpenIDConnectAuthCode
- OpenIDConnectImplicit
- OpenIDConnectHybrid
"""
if request.prompt == "login":
False
else:
True

def validate_silent_login(self, request):
"""Ensure session user has authorized silent OpenID login.

If no user is logged in or has not authorized silent login, this
method should return False.

If the user is logged in but associated with multiple accounts and
not selected which one to link to the token then this method should
raise an oauthlib.oauth2.AccountSelectionRequired error.

:param request: The HTTP Request (oauthlib.common.Request)
:rtype: True or False

Method is used by:
- OpenIDConnectAuthCode
- OpenIDConnectImplicit
- OpenIDConnectHybrid
"""
if frappe.session.user == "Guest" or request.prompt.lower() == "login":
return False
else:
return True

def validate_user_match(self, id_token_hint, scopes, claims, request):
"""Ensure client supplied user id hint matches session user.

If the sub claim or id_token_hint is supplied then the session
user must match the given ID.

:param id_token_hint: User identifier string.
:param scopes: List of OAuth 2 scopes and OpenID claims (strings).
:param claims: OpenID Connect claims dict.
:param request: The HTTP Request (oauthlib.common.Request)
:rtype: True or False

Method is used by:
- OpenIDConnectAuthCode
- OpenIDConnectImplicit
- OpenIDConnectHybrid
"""
if id_token_hint and id_token_hint == frappe.get_value("User", frappe.session.user, "frappe_userid"):
return True
else:
return False


def get_cookie_dict_from_headers(r): def get_cookie_dict_from_headers(r):
if r.headers.get('Cookie'): if r.headers.get('Cookie'):
@@ -280,3 +390,39 @@ def get_cookie_dict_from_headers(r):
return cookie_dict return cookie_dict
else: else:
return {} return {}

def calculate_at_hash(access_token, hash_alg):
"""Helper method for calculating an access token
hash, as described in http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken
Its value is the base64url encoding of the left-most half of the hash of the octets
of the ASCII representation of the access_token value, where the hash algorithm
used is the hash algorithm used in the alg Header Parameter of the ID Token's JOSE
Header. For instance, if the alg is RS256, hash the access_token value with SHA-256,
then take the left-most 128 bits and base64url encode them. The at_hash value is a
case sensitive string.
Args:
access_token (str): An access token string.
hash_alg (callable): A callable returning a hash object, e.g. hashlib.sha256
"""
hash_digest = hash_alg(access_token.encode('utf-8')).digest()
cut_at = int(len(hash_digest) / 2)
truncated = hash_digest[:cut_at]
from jwt.utils import base64url_encode
at_hash = base64url_encode(truncated)
return at_hash.decode('utf-8')

def delete_oauth2_data():
# Delete Invalid Authorization Code and Revoked Token
commit_code, commit_token = False, False
code_list = frappe.get_all("OAuth Authorization Code", filters={"validity":"Invalid"})
token_list = frappe.get_all("OAuth Bearer Token", filters={"status":"Revoked"})
if len(code_list) > 0:
commit_code = True
if len(token_list) > 0:
commit_token = True
for code in code_list:
frappe.delete_doc("OAuth Authorization Code", code["name"])
for token in token_list:
frappe.delete_doc("OAuth Bearer Token", token["name"])
if commit_code or commit_token:
frappe.db.commit()

+ 27
- 1
frappe/utils/oauth.py 查看文件

@@ -10,6 +10,9 @@ from frappe import _
class SignupDisabledError(frappe.PermissionError): pass class SignupDisabledError(frappe.PermissionError): pass


def get_oauth2_providers(): def get_oauth2_providers():
frappe_server_url = frappe.db.get_value("Social Login Keys", None, "frappe_server_url") or None
if not frappe_server_url:
frappe.throw(_("Define Frappe Server URL in Social Login Keys"))
return { return {
"google": { "google": {
"flow_params": { "flow_params": {
@@ -64,7 +67,26 @@ def get_oauth2_providers():
"api_endpoint": "/v2.5/me", "api_endpoint": "/v2.5/me",
"api_endpoint_args": { "api_endpoint_args": {
"fields": "first_name,last_name,email,gender,location,verified,picture" "fields": "first_name,last_name,email,gender,location,verified,picture"
}
},
},

"frappe": {
"flow_params": {
"name": "frappe",
"authorize_url": frappe_server_url + "/api/method/frappe.integration_broker.oauth2.authorize",
"access_token_url": frappe_server_url + "/api/method/frappe.integration_broker.oauth2.get_token",
"base_url": frappe_server_url
},

"redirect_uri": "/api/method/frappe.www.login.login_via_frappe",

"auth_url_data": {
"response_type": "code",
"scope": "openid"
},

# relative to base_url
"api_endpoint": "/api/method/frappe.integration_broker.oauth2.openid_profile"
} }
} }


@@ -282,6 +304,10 @@ def update_oauth_user(user, data, provider):
user.github_userid = data["id"] user.github_userid = data["id"]
user.github_username = data["login"] user.github_username = data["login"]


elif provider=="frappe" and not user.get("frappe_userid"):
save = True
user.frappe_userid = data["sub"]

if save: if save:
user.flags.ignore_permissions = True user.flags.ignore_permissions = True
user.flags.no_welcome_mail = True user.flags.no_welcome_mail = True


+ 5
- 0
frappe/www/login.html 查看文件

@@ -47,6 +47,11 @@
<a href="{{ github_login }}" class="no-decoration btn-social btn-github"> <a href="{{ github_login }}" class="no-decoration btn-social btn-github">
<i class="icon-github-sign icon-2x"></i></a> <i class="icon-github-sign icon-2x"></i></a>
{%- endif -%} {%- endif -%}

{%- if frappe_login is defined %}
<a href="{{ frappe_login }}" class="no-decoration">
<img src="/assets/frappe/images/favicon.png"></a>
{%- endif -%}
</p> </p>
{%- endif -%} {%- endif -%}
{% if ldap_settings.enabled %} {% if ldap_settings.enabled %}


+ 5
- 1
frappe/www/login.py 查看文件

@@ -23,7 +23,7 @@ def get_context(context):
context["title"] = "Login" context["title"] = "Login"
context["disable_signup"] = frappe.utils.cint(frappe.db.get_value("Website Settings", "Website Settings", "disable_signup")) context["disable_signup"] = frappe.utils.cint(frappe.db.get_value("Website Settings", "Website Settings", "disable_signup"))


for provider in ("google", "github", "facebook"):
for provider in ("google", "github", "facebook", "frappe"):
if get_oauth_keys(provider): if get_oauth_keys(provider):
context["{provider}_login".format(provider=provider)] = get_oauth2_authorize_url(provider) context["{provider}_login".format(provider=provider)] = get_oauth2_authorize_url(provider)
context["social_login"] = True context["social_login"] = True
@@ -45,6 +45,10 @@ def login_via_github(code, state):
def login_via_facebook(code, state): def login_via_facebook(code, state):
login_via_oauth2("facebook", code, state) login_via_oauth2("facebook", code, state)


@frappe.whitelist(allow_guest=True)
def login_via_frappe(code, state):
login_via_oauth2("frappe", code, state, decoder=json.loads)

@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
def login_oauth_user(data=None, provider=None, state=None, email_id=None, key=None, generate_login_token=False): def login_oauth_user(data=None, provider=None, state=None, email_id=None, key=None, generate_login_token=False):
if not ((data and provider and state) or (email_id and key)): if not ((data and provider and state) or (email_id and key)):


+ 1
- 0
requirements.txt 查看文件

@@ -39,3 +39,4 @@ psutil
unittest-xml-reporting unittest-xml-reporting
xlwt xlwt
oauthlib oauthlib
PyJWT

正在加载...
取消
保存