* OAuth 2.0 Classes Added Added oauthlib in requirement.txt Added class WebApplicationServer for Authorization code grant and Bearer tokens. Added class OAuthWebRequestValidator for Validating Oauth Request for Web Application * copied code from mnt_oauth needs review * [New] OAuth2 Doctypes and endpoints added Integrations > OAuth Provider Settings Integration Broker > OAuth Bearer Token Integration Broker > OAuth Authorization Code Integration Broker > OAuth Client endpoints for authorize, approve, get_token, revoke_token and test_resource * oauth2.py: renamed skipauth to skip_auth * [Fix] Parse URL instead of storing it in settings * [Fix] get skip_auth from OAuth Provider Settings * Success URL format. Failure URL added. Confirmation dialog layout changed. * Validate access token if passed during use of REST API * OAuth Confirmation colours fixed * Multiple Changes Added links for OAuth under Integrations in Module list. Updated permissions on OAuth doctypes. Updated layout of OAuth Client doctype. * [Docs] Integrations > How to setup OAuth * [Docs] Integration > Using OAuth * [Fix] get_token endpoint must to handle POST request * [Fix] http verbs and responses for OAuth 2.0 Endpoints * [Fix] accept oauth2 access_token from headers * Removed unused imports from api.pyversion-14
@@ -8,6 +8,9 @@ import frappe.handler | |||||
import frappe.client | import frappe.client | ||||
from frappe.utils.response import build_response | from frappe.utils.response import build_response | ||||
from frappe import _ | from frappe import _ | ||||
from urlparse import urlparse | |||||
from urllib import urlencode | |||||
from frappe.integration_broker.oauth2 import oauth_server | |||||
def handle(): | def handle(): | ||||
""" | """ | ||||
@@ -32,6 +35,27 @@ def handle(): | |||||
`/api/resource/{doctype}/{name}?run_method={method}` will run a whitelisted controller method | `/api/resource/{doctype}/{name}?run_method={method}` will run a whitelisted controller method | ||||
""" | """ | ||||
form_dict = frappe.local.form_dict | |||||
authorization_header = frappe.get_request_header("Authorization").split(" ") if frappe.get_request_header("Authorization") else None | |||||
if authorization_header and authorization_header[0].lower() == "bearer": | |||||
token = authorization_header[1] | |||||
r = frappe.request | |||||
parsed_url = urlparse(r.url) | |||||
access_token = { "access_token": token} | |||||
uri = parsed_url.scheme + "://" + parsed_url.netloc + parsed_url.path + "?" + urlencode(access_token) | |||||
http_method = r.method | |||||
body = r.get_data() | |||||
headers = r.headers | |||||
required_scopes = frappe.db.get_value("OAuth Bearer Token", token, "scopes").split(";") | |||||
valid, oauthlib_request = oauth_server.verify_request(uri, http_method, body, headers, required_scopes) | |||||
if valid: | |||||
frappe.set_user(frappe.db.get_value("OAuth Bearer Token", token, "user")) | |||||
frappe.local.form_dict = form_dict | |||||
parts = frappe.request.path[1:].split("/",3) | parts = frappe.request.path[1:].split("/",3) | ||||
call = doctype = name = None | call = doctype = name = None | ||||
@@ -224,7 +224,17 @@ def get_data(): | |||||
"type": "doctype", | "type": "doctype", | ||||
"name": "Integration Service", | "name": "Integration Service", | ||||
"description": _("Centralize access to Integrations"), | "description": _("Centralize access to Integrations"), | ||||
} | |||||
}, | |||||
{ | |||||
"type": "doctype", | |||||
"name": "OAuth Client", | |||||
"description": _("Register OAuth Client App"), | |||||
}, | |||||
{ | |||||
"type": "doctype", | |||||
"name": "OAuth Provider Settings", | |||||
"description": _("Settings for OAuth Provider"), | |||||
}, | |||||
] | ] | ||||
}, | }, | ||||
{ | { | ||||
@@ -0,0 +1,47 @@ | |||||
# How to setup oauth? | |||||
<a href="https://tools.ietf.org/html/rfc6749">OAuth 2.0</a> provider based on <a href="https://github.com/idan/oauthlib">oauthlib</a> is built into frappe. Third party apps can now access resources of users based on Frappe Role and User permission system. To setup an app to access | |||||
## OAuth defines four roles | |||||
#### resource owner | |||||
An entity capable of granting access to a protected resource. When the resource owner is a person, it is referred to as an end-user. | |||||
#### resource server | |||||
The server hosting the protected resources, capable of accepting and responding to protected resource requests using access tokens. | |||||
#### client | |||||
An application making protected resource requests on behalf of the resource owner and with its authorization. The term "client" does not imply any particular implementation characteristics (e.g., | |||||
whether the application executes on a server, a desktop, or other devices). | |||||
#### authorization server | |||||
The server issuing access tokens to the client after successfully authenticating the resource owner and obtaining authorization. | |||||
## Setup OAuth Provider | |||||
System Managers can setup behavior of confirmation message as `Force` or `Auto` in OAuth Provider Settings. | |||||
If Force is selected the system will always ask for user's confirmation. If Auto is selected system asks for the confirmation only if there are no active tokens for the user. | |||||
Go to | |||||
> Setup > Integrations > OAuth Provider Settings | |||||
<img class="screenshot" src="assets/frappe/img/oauth_provider_settings.png"> | |||||
## Add a Client App | |||||
As a System Manager go to | |||||
> Setup > Integrations > OAuth Client | |||||
<img class="screenshot" src="assets/frappe/img/oauth2_client_app.png"> | |||||
To add a client fill in the following details | |||||
1. **App Name** : Enter App Name e.g. CAVS | |||||
2. **Skip Authorization** : If this is checked, during authentication there won't be me any confirmation message | |||||
3. **Scopes** : List of scopes shown to user along with confirmation message. scopes are separated by semicolon ';' | |||||
4. **Redirect URIs** : List of Redirect URIs separated by semicolon ';' | |||||
5. **Default Redirect URIs** : Default Redirect URI from list of Redirect URIs | |||||
6. **Grant Type**: select `Authorization Code` | |||||
7. **Response Type**: select `Code` |
@@ -1 +1,3 @@ | |||||
rest_api | rest_api | ||||
how_to_setup_oauth | |||||
using_oauth |
@@ -0,0 +1,109 @@ | |||||
# Using OAuth | |||||
Once the client and provider settings are entered, following steps can be used to start using OAuth 2.0 | |||||
### Authorization Code Endpoint | |||||
#### Authorization Request | |||||
URL: | |||||
``` | |||||
[GET] 0.0.0.0:8000/api/method/frappe.integration_broker.oauth2.authorize | |||||
``` | |||||
Params: | |||||
``` | |||||
client_id = <client ID of registered app> | |||||
scope = <access scope, e.g. scope=project will allow you to access project doctypes.> | |||||
response_type = "code" | |||||
redirect_uri = <redirect uri from OAuth Client> | |||||
``` | |||||
#### Confirmation Dialog | |||||
<img class="screenshot" src="assets/frappe/img/oauth_confirmation_page.png"> | |||||
Click 'Allow' to receive authorization code in redirect uri. | |||||
``` | |||||
http://localhost:3000/oauth_code?code=plkj2mqDLwaLJAgDBAkyR1W8Co08Ud | |||||
``` | |||||
If user clicks 'Deny' receive error | |||||
``` | |||||
http://localhost:3000/oauth_code?error=access_denied | |||||
``` | |||||
### Token Endpoints | |||||
#### Get Access Token | |||||
URL: | |||||
``` | |||||
[POST] 0.0.0.0:8000/api/method/frappe.integration_broker.oauth2.get_token | |||||
``` | |||||
Params: | |||||
``` | |||||
grant_type = "authorization_code" | |||||
code = <code received in redirect uri after confirmation> | |||||
redirect_uri = <valid redirect uri> | |||||
client_id = <client ID of app from OAuth Client> | |||||
``` | |||||
Response: | |||||
``` | |||||
{ | |||||
"access_token": "pNO2DpTMHTcFHYUXwzs74k6idQBmnI", | |||||
"token_type": "Bearer", | |||||
"expires_in": 3600, | |||||
"refresh_token": "cp74cxbbDgaxFuUZ8Usc7egYlhKbH1", | |||||
"scope": "project" | |||||
} | |||||
``` | |||||
#### Refresh Access Token | |||||
URL: | |||||
``` | |||||
[POST] 0.0.0.0:8000/api/method/frappe.integration_broker.oauth2.get_token | |||||
``` | |||||
Params: | |||||
``` | |||||
grant_type = "refresh_token" | |||||
refresh_token = <refresh token from the response of get_token call with grant_type=authorization_code> | |||||
redirect_uri = <valid redirect uri> | |||||
client_id = <client ID of app from OAuth Client> | |||||
``` | |||||
Response: | |||||
``` | |||||
{ | |||||
"access_token": "Ywz1iNk0b21iAmjWAYnFWT4CuudHD5", | |||||
"token_type": "Bearer", | |||||
"expires_in": 3600, | |||||
"refresh_token": "PNux3Q8Citr3s9rl2zEsKuU1l8bSN5", | |||||
"scope": "project" | |||||
} | |||||
``` | |||||
#### Revoke Token Endpoint | |||||
URL: | |||||
``` | |||||
[POST] 0.0.0.0:8000/api/method/frappe.integration_broker.oauth2.revoke_token | |||||
``` | |||||
Params: | |||||
``` | |||||
token = <access token to be revoked> | |||||
``` | |||||
Success Response | |||||
``` | |||||
status : 200 | |||||
{"message": "success"} | |||||
``` | |||||
Error Response: | |||||
``` | |||||
status : 400 | |||||
{"message": "bad request"} | |||||
``` | |||||
### Accessing Resource | |||||
Add header `Authorizaton: Bearer <valid_bearer_token>` to Frappe's REST API endpoints to access user's resource |
@@ -0,0 +1,8 @@ | |||||
// Copyright (c) 2016, Frappe Technologies and contributors | |||||
// For license information, please see license.txt | |||||
frappe.ui.form.on('OAuth Authorization Code', { | |||||
refresh: function(frm) { | |||||
} | |||||
}); |
@@ -0,0 +1,241 @@ | |||||
{ | |||||
"allow_copy": 0, | |||||
"allow_import": 0, | |||||
"allow_rename": 0, | |||||
"autoname": "field:authorization_code", | |||||
"beta": 0, | |||||
"creation": "2016-08-24 14:12:13.647159", | |||||
"custom": 0, | |||||
"docstatus": 0, | |||||
"doctype": "DocType", | |||||
"document_type": "Document", | |||||
"editable_grid": 1, | |||||
"engine": "InnoDB", | |||||
"fields": [ | |||||
{ | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "client", | |||||
"fieldtype": "Link", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 1, | |||||
"label": "Client", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"options": "OAuth Client", | |||||
"permlevel": 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": "user", | |||||
"fieldtype": "Link", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 0, | |||||
"label": "User", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"options": "User", | |||||
"permlevel": 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": "scopes", | |||||
"fieldtype": "Text", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 0, | |||||
"label": "Scopes", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 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": "authorization_code", | |||||
"fieldtype": "Text", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 1, | |||||
"label": "Authorization Code", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 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": "expiration_time", | |||||
"fieldtype": "Datetime", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 0, | |||||
"label": "Expiration time", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 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": "redirect_uri_bound_to_authorization_code", | |||||
"fieldtype": "Data", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 0, | |||||
"label": "Redirect URI Bound To Auth Code", | |||||
"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": "validity", | |||||
"fieldtype": "Select", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 1, | |||||
"label": "Validity", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"options": "Valid\nInvalid", | |||||
"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 | |||||
} | |||||
], | |||||
"hide_heading": 0, | |||||
"hide_toolbar": 0, | |||||
"idx": 0, | |||||
"image_view": 0, | |||||
"in_create": 0, | |||||
"in_dialog": 0, | |||||
"is_submittable": 0, | |||||
"issingle": 0, | |||||
"istable": 0, | |||||
"max_attachments": 0, | |||||
"modified": "2016-10-20 00:13:53.224437", | |||||
"modified_by": "Administrator", | |||||
"module": "Integration Broker", | |||||
"name": "OAuth Authorization Code", | |||||
"name_case": "", | |||||
"owner": "Administrator", | |||||
"permissions": [ | |||||
{ | |||||
"amend": 0, | |||||
"apply_user_permissions": 0, | |||||
"cancel": 0, | |||||
"create": 1, | |||||
"delete": 1, | |||||
"email": 1, | |||||
"export": 1, | |||||
"if_owner": 0, | |||||
"import": 0, | |||||
"is_custom": 0, | |||||
"permlevel": 0, | |||||
"print": 1, | |||||
"read": 1, | |||||
"report": 1, | |||||
"role": "System Manager", | |||||
"set_user_permissions": 0, | |||||
"share": 1, | |||||
"submit": 0, | |||||
"write": 1 | |||||
} | |||||
], | |||||
"quick_entry": 0, | |||||
"read_only": 0, | |||||
"read_only_onload": 0, | |||||
"sort_field": "modified", | |||||
"sort_order": "DESC", | |||||
"track_seen": 0 | |||||
} |
@@ -0,0 +1,10 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# Copyright (c) 2015, Frappe Technologies and contributors | |||||
# For license information, please see license.txt | |||||
from __future__ import unicode_literals | |||||
import frappe | |||||
from frappe.model.document import Document | |||||
class OAuthAuthorizationCode(Document): | |||||
pass |
@@ -0,0 +1,12 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# Copyright (c) 2015, Frappe Technologies and Contributors | |||||
# See license.txt | |||||
from __future__ import unicode_literals | |||||
import frappe | |||||
import unittest | |||||
# test_records = frappe.get_test_records('OAuth Authorization Code') | |||||
class TestOAuthAuthorizationCode(unittest.TestCase): | |||||
pass |
@@ -0,0 +1,8 @@ | |||||
// Copyright (c) 2016, Frappe Technologies and contributors | |||||
// For license information, please see license.txt | |||||
frappe.ui.form.on('OAuth Bearer Token', { | |||||
refresh: function(frm) { | |||||
} | |||||
}); |
@@ -0,0 +1,266 @@ | |||||
{ | |||||
"allow_copy": 0, | |||||
"allow_import": 0, | |||||
"allow_rename": 0, | |||||
"autoname": "field:access_token", | |||||
"beta": 0, | |||||
"creation": "2016-08-24 14:10:17.471264", | |||||
"custom": 0, | |||||
"docstatus": 0, | |||||
"doctype": "DocType", | |||||
"document_type": "Document", | |||||
"editable_grid": 1, | |||||
"engine": "InnoDB", | |||||
"fields": [ | |||||
{ | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "client", | |||||
"fieldtype": "Link", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 1, | |||||
"label": "Client", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"options": "OAuth Client", | |||||
"permlevel": 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": "user", | |||||
"fieldtype": "Link", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 0, | |||||
"label": "User", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"options": "User", | |||||
"permlevel": 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": "scopes", | |||||
"fieldtype": "Text", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 0, | |||||
"label": "Scopes", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 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": "access_token", | |||||
"fieldtype": "Data", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 0, | |||||
"label": "Access Token", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 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": "refresh_token", | |||||
"fieldtype": "Data", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 0, | |||||
"label": "Refresh Token", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 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": "expiration_time", | |||||
"fieldtype": "Datetime", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 0, | |||||
"label": "Expiration time", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 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": "expires_in", | |||||
"fieldtype": "Int", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 0, | |||||
"label": "Expires In", | |||||
"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": "status", | |||||
"fieldtype": "Select", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 0, | |||||
"label": "Status", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"options": "Active\nRevoked", | |||||
"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 | |||||
} | |||||
], | |||||
"hide_heading": 0, | |||||
"hide_toolbar": 0, | |||||
"idx": 0, | |||||
"image_view": 0, | |||||
"in_create": 0, | |||||
"in_dialog": 0, | |||||
"is_submittable": 0, | |||||
"issingle": 0, | |||||
"istable": 0, | |||||
"max_attachments": 0, | |||||
"modified": "2016-10-20 00:13:41.747141", | |||||
"modified_by": "Administrator", | |||||
"module": "Integration Broker", | |||||
"name": "OAuth Bearer Token", | |||||
"name_case": "", | |||||
"owner": "Administrator", | |||||
"permissions": [ | |||||
{ | |||||
"amend": 0, | |||||
"apply_user_permissions": 0, | |||||
"cancel": 0, | |||||
"create": 1, | |||||
"delete": 1, | |||||
"email": 1, | |||||
"export": 1, | |||||
"if_owner": 0, | |||||
"import": 0, | |||||
"is_custom": 0, | |||||
"permlevel": 0, | |||||
"print": 1, | |||||
"read": 1, | |||||
"report": 1, | |||||
"role": "System Manager", | |||||
"set_user_permissions": 0, | |||||
"share": 1, | |||||
"submit": 0, | |||||
"write": 1 | |||||
} | |||||
], | |||||
"quick_entry": 0, | |||||
"read_only": 0, | |||||
"read_only_onload": 0, | |||||
"sort_field": "modified", | |||||
"sort_order": "DESC", | |||||
"track_seen": 0 | |||||
} |
@@ -0,0 +1,13 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# Copyright (c) 2015, Frappe Technologies and contributors | |||||
# For license information, please see license.txt | |||||
from __future__ import unicode_literals | |||||
import frappe | |||||
from frappe.model.document import Document | |||||
class OAuthBearerToken(Document): | |||||
def validate(self): | |||||
if not self.expiration_time: | |||||
self.expiration_time = frappe.utils.datetime.datetime.strptime(self.creation, "%Y-%m-%d %H:%M:%S.%f") + frappe.utils.datetime.timedelta(seconds=self.expires_in) | |||||
@@ -0,0 +1,12 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# Copyright (c) 2015, Frappe Technologies and Contributors | |||||
# See license.txt | |||||
from __future__ import unicode_literals | |||||
import frappe | |||||
import unittest | |||||
# test_records = frappe.get_test_records('OAuth Bearer Token') | |||||
class TestOAuthBearerToken(unittest.TestCase): | |||||
pass |
@@ -0,0 +1,8 @@ | |||||
// Copyright (c) 2016, Frappe Technologies and contributors | |||||
// For license information, please see license.txt | |||||
frappe.ui.form.on('OAuth Client', { | |||||
refresh: function(frm) { | |||||
} | |||||
}); |
@@ -0,0 +1,428 @@ | |||||
{ | |||||
"allow_copy": 0, | |||||
"allow_import": 0, | |||||
"allow_rename": 0, | |||||
"autoname": "", | |||||
"beta": 0, | |||||
"creation": "2016-08-24 14:07:21.955052", | |||||
"custom": 0, | |||||
"docstatus": 0, | |||||
"doctype": "DocType", | |||||
"document_type": "Document", | |||||
"editable_grid": 1, | |||||
"engine": "InnoDB", | |||||
"fields": [ | |||||
{ | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "app_name", | |||||
"fieldtype": "Data", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 0, | |||||
"label": "App Name", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"report_hide": 0, | |||||
"reqd": 1, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "user", | |||||
"fieldtype": "Link", | |||||
"hidden": 1, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 0, | |||||
"label": "User", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"options": "User", | |||||
"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": "cb_1", | |||||
"fieldtype": "Column Break", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 0, | |||||
"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, | |||||
"default": "", | |||||
"fieldname": "client_id", | |||||
"fieldtype": "Data", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 0, | |||||
"label": "App Client ID", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 1, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"description": "If checked, users will not see the Confirm Access dialog.", | |||||
"fieldname": "skip_authorization", | |||||
"fieldtype": "Check", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 0, | |||||
"label": "Skip Authorization", | |||||
"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, | |||||
"description": "", | |||||
"fieldname": "sb_1", | |||||
"fieldtype": "Section Break", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 0, | |||||
"label": "", | |||||
"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, | |||||
"description": "A list of resources which the Client App will have access to after the user allows it.<br> e.g. project", | |||||
"fieldname": "scopes", | |||||
"fieldtype": "Text", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 0, | |||||
"label": "Scopes", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"report_hide": 0, | |||||
"reqd": 1, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "cb_3", | |||||
"fieldtype": "Column Break", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 0, | |||||
"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, | |||||
"description": "URIs for receiving authorization code once the user allows access, as well as failure responses. Typically a REST endpoint exposed by the Client App.\n<br>e.g. http://hostname//api/method/frappe.www.login.login_via_facebook", | |||||
"fieldname": "redirect_uris", | |||||
"fieldtype": "Text", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 0, | |||||
"label": "Redirect URIs", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 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": "default_redirect_uri", | |||||
"fieldtype": "Data", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 0, | |||||
"label": "Default Redirect URI", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"report_hide": 0, | |||||
"reqd": 1, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 1, | |||||
"collapsible_depends_on": "1", | |||||
"columns": 0, | |||||
"fieldname": "sb_advanced", | |||||
"fieldtype": "Section Break", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 0, | |||||
"label": " Advanced Settings", | |||||
"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": "grant_type", | |||||
"fieldtype": "Select", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 1, | |||||
"label": "Grant Type", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"options": "Authorization Code\nImplicit\nResource Owner Password Credentials\nClient Credentials", | |||||
"permlevel": 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": "cb_2", | |||||
"fieldtype": "Column Break", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 0, | |||||
"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, | |||||
"default": "Code", | |||||
"fieldname": "response_type", | |||||
"fieldtype": "Select", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 1, | |||||
"label": "Response Type", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"options": "Code\nToken", | |||||
"permlevel": 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 | |||||
} | |||||
], | |||||
"hide_heading": 0, | |||||
"hide_toolbar": 0, | |||||
"idx": 0, | |||||
"image_view": 0, | |||||
"in_create": 0, | |||||
"in_dialog": 0, | |||||
"is_submittable": 0, | |||||
"issingle": 0, | |||||
"istable": 0, | |||||
"max_attachments": 0, | |||||
"modified": "2016-10-20 00:32:11.993940", | |||||
"modified_by": "Administrator", | |||||
"module": "Integration Broker", | |||||
"name": "OAuth Client", | |||||
"name_case": "", | |||||
"owner": "Administrator", | |||||
"permissions": [ | |||||
{ | |||||
"amend": 0, | |||||
"apply_user_permissions": 0, | |||||
"cancel": 0, | |||||
"create": 1, | |||||
"delete": 1, | |||||
"email": 1, | |||||
"export": 1, | |||||
"if_owner": 0, | |||||
"import": 0, | |||||
"is_custom": 0, | |||||
"permlevel": 0, | |||||
"print": 1, | |||||
"read": 1, | |||||
"report": 1, | |||||
"role": "System Manager", | |||||
"set_user_permissions": 0, | |||||
"share": 1, | |||||
"submit": 0, | |||||
"write": 1 | |||||
} | |||||
], | |||||
"quick_entry": 0, | |||||
"read_only": 0, | |||||
"read_only_onload": 0, | |||||
"sort_field": "modified", | |||||
"sort_order": "DESC", | |||||
"title_field": "app_name", | |||||
"track_seen": 0 | |||||
} |
@@ -0,0 +1,11 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# Copyright (c) 2015, Frappe Technologies and contributors | |||||
# For license information, please see license.txt | |||||
from __future__ import unicode_literals | |||||
import frappe | |||||
from frappe.model.document import Document | |||||
class OAuthClient(Document): | |||||
def validate(self): | |||||
self.client_id = self.name |
@@ -0,0 +1,12 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# Copyright (c) 2015, Frappe Technologies and Contributors | |||||
# See license.txt | |||||
from __future__ import unicode_literals | |||||
import frappe | |||||
import unittest | |||||
# test_records = frappe.get_test_records('OAuth Client') | |||||
class TestOAuthClient(unittest.TestCase): | |||||
pass |
@@ -0,0 +1,123 @@ | |||||
from __future__ import unicode_literals | |||||
import frappe, json | |||||
from frappe.oauth import OAuthWebRequestValidator, WebApplicationServer | |||||
from oauthlib.oauth2 import FatalClientError, OAuth2Error | |||||
from urllib import quote, urlencode | |||||
from urlparse import urlparse | |||||
from frappe.integrations.doctype.oauth_provider_settings.oauth_provider_settings import get_oauth_settings | |||||
#Variables required across requests | |||||
oauth_validator = OAuthWebRequestValidator() | |||||
oauth_server = WebApplicationServer(oauth_validator) | |||||
credentials = None | |||||
def get_urlparams_from_kwargs(param_kwargs): | |||||
arguments = param_kwargs | |||||
if arguments.get("data"): | |||||
arguments.pop("data") | |||||
if arguments.get("cmd"): | |||||
arguments.pop("cmd") | |||||
return urlencode(arguments) | |||||
@frappe.whitelist() | |||||
def approve(*args, **kwargs): | |||||
r = frappe.request | |||||
uri = r.url | |||||
http_method = r.method | |||||
body = r.get_data() | |||||
headers = r.headers | |||||
try: | |||||
scopes, credentials = oauth_server.validate_authorization_request(uri, http_method, body, headers) | |||||
headers, body, status = oauth_server.create_authorization_response(uri=credentials['redirect_uri'], \ | |||||
body=body, headers=headers, scopes=scopes, credentials=credentials) | |||||
uri = headers.get('Location', None) | |||||
frappe.local.response["type"] = "redirect" | |||||
frappe.local.response["location"] = uri | |||||
except FatalClientError as e: | |||||
return e | |||||
except OAuth2Error as e: | |||||
return e | |||||
@frappe.whitelist(allow_guest=True) | |||||
def authorize(*args, **kwargs): | |||||
#Fetch provider URL from settings | |||||
oauth_settings = get_oauth_settings() | |||||
params = get_urlparams_from_kwargs(kwargs) | |||||
request_url = urlparse(frappe.request.url) | |||||
success_url = request_url.scheme + "://" + request_url.netloc + "/api/method/frappe.integration_broker.oauth2.approve?" + params | |||||
failure_url = frappe.form_dict["redirect_uri"] + "?error=access_denied" | |||||
if frappe.session['user']=='Guest': | |||||
#Force login, redirect to preauth again. | |||||
frappe.local.response["type"] = "redirect" | |||||
frappe.local.response["location"] = "/login?redirect-to=/api/method/frappe.integration_broker.oauth2.authorize?" + quote(params) | |||||
elif frappe.session['user']!='Guest': | |||||
try: | |||||
r = frappe.request | |||||
uri = r.url | |||||
http_method = r.method | |||||
body = r.get_data() | |||||
headers = r.headers | |||||
scopes, credentials = oauth_server.validate_authorization_request(uri, http_method, body, headers) | |||||
skip_auth = frappe.db.get_value("OAuth Client", credentials['client_id'], "skip_authorization") | |||||
unrevoked_tokens = frappe.get_all("OAuth Bearer Token", filters={"status":"Active"}) | |||||
if skip_auth or (oauth_settings["skip_authorization"] == "Auto" and len(unrevoked_tokens)): | |||||
frappe.local.response["type"] = "redirect" | |||||
frappe.local.response["location"] = success_url | |||||
else: | |||||
#Show Allow/Deny screen. | |||||
response_html_params = frappe._dict({ | |||||
"client_id": frappe.db.get_value("OAuth Client", kwargs['client_id'], "app_name"), | |||||
"success_url": success_url, | |||||
"failure_url": failure_url, | |||||
"details": scopes | |||||
}) | |||||
resp_html = frappe.render_template("templates/includes/oauth_confirmation.html", response_html_params) | |||||
frappe.respond_as_web_page("Confirm Access", resp_html) | |||||
except FatalClientError as e: | |||||
return e | |||||
except OAuth2Error as e: | |||||
return e | |||||
@frappe.whitelist(allow_guest=True) | |||||
def get_token(*args, **kwargs): | |||||
r = frappe.request | |||||
uri = r.url | |||||
http_method = r.method | |||||
body = r.form | |||||
headers = r.headers | |||||
try: | |||||
headers, body, status = oauth_server.create_token_response(uri, http_method, body, headers, credentials) | |||||
frappe.local.response = frappe._dict(json.loads(body)) | |||||
except FatalClientError as e: | |||||
return e | |||||
@frappe.whitelist(allow_guest=True) | |||||
def revoke_token(*args, **kwargs): | |||||
r = frappe.request | |||||
uri = r.url | |||||
http_method = r.method | |||||
body = r.form | |||||
headers = r.headers | |||||
headers, body, status = oauth_server.create_revocation_response(uri, headers=headers, body=body, http_method=http_method) | |||||
frappe.local.response['http_status_code'] = status | |||||
if status == 200: | |||||
return "success" | |||||
else: | |||||
return "bad request" |
@@ -0,0 +1,8 @@ | |||||
// Copyright (c) 2016, Frappe Technologies and contributors | |||||
// For license information, please see license.txt | |||||
frappe.ui.form.on('OAuth Provider Settings', { | |||||
refresh: function(frm) { | |||||
} | |||||
}); |
@@ -0,0 +1,87 @@ | |||||
{ | |||||
"allow_copy": 0, | |||||
"allow_import": 0, | |||||
"allow_rename": 0, | |||||
"beta": 0, | |||||
"creation": "2016-09-03 11:42:42.575525", | |||||
"custom": 0, | |||||
"docstatus": 0, | |||||
"doctype": "DocType", | |||||
"document_type": "Setup", | |||||
"editable_grid": 1, | |||||
"engine": "InnoDB", | |||||
"fields": [ | |||||
{ | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "skip_authorization", | |||||
"fieldtype": "Select", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 0, | |||||
"label": "Skip Authorization", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"options": "Force\nAuto", | |||||
"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 | |||||
} | |||||
], | |||||
"hide_heading": 0, | |||||
"hide_toolbar": 0, | |||||
"idx": 0, | |||||
"image_view": 0, | |||||
"in_create": 0, | |||||
"in_dialog": 0, | |||||
"is_submittable": 0, | |||||
"issingle": 1, | |||||
"istable": 0, | |||||
"max_attachments": 0, | |||||
"modified": "2016-10-20 00:13:21.988739", | |||||
"modified_by": "Administrator", | |||||
"module": "Integrations", | |||||
"name": "OAuth Provider Settings", | |||||
"name_case": "", | |||||
"owner": "Administrator", | |||||
"permissions": [ | |||||
{ | |||||
"amend": 0, | |||||
"apply_user_permissions": 0, | |||||
"cancel": 0, | |||||
"create": 1, | |||||
"delete": 1, | |||||
"email": 1, | |||||
"export": 0, | |||||
"if_owner": 0, | |||||
"import": 0, | |||||
"is_custom": 0, | |||||
"permlevel": 0, | |||||
"print": 1, | |||||
"read": 1, | |||||
"report": 0, | |||||
"role": "System Manager", | |||||
"set_user_permissions": 0, | |||||
"share": 1, | |||||
"submit": 0, | |||||
"write": 1 | |||||
} | |||||
], | |||||
"quick_entry": 1, | |||||
"read_only": 0, | |||||
"read_only_onload": 0, | |||||
"sort_field": "modified", | |||||
"sort_order": "DESC", | |||||
"track_seen": 0 | |||||
} |
@@ -0,0 +1,19 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# Copyright (c) 2015, Frappe Technologies and contributors | |||||
# For license information, please see license.txt | |||||
from __future__ import unicode_literals | |||||
import frappe | |||||
from frappe.model.document import Document | |||||
from frappe import _ | |||||
class OAuthProviderSettings(Document): | |||||
pass | |||||
def get_oauth_settings(): | |||||
"""Returns oauth settings""" | |||||
out = frappe._dict({ | |||||
"skip_authorization" : frappe.db.get_value("OAuth Provider Settings", None, "skip_authorization") | |||||
}) | |||||
return out |
@@ -0,0 +1,282 @@ | |||||
import frappe, urllib | |||||
from urlparse import parse_qs, urlparse | |||||
from oauthlib.oauth2.rfc6749.tokens import BearerToken | |||||
from oauthlib.oauth2.rfc6749.grant_types import AuthorizationCodeGrant, ImplicitGrant, ResourceOwnerPasswordCredentialsGrant, ClientCredentialsGrant, RefreshTokenGrant | |||||
from oauthlib.oauth2 import RequestValidator | |||||
from oauthlib.oauth2.rfc6749.endpoints.authorization import AuthorizationEndpoint | |||||
from oauthlib.oauth2.rfc6749.endpoints.token import TokenEndpoint | |||||
from oauthlib.oauth2.rfc6749.endpoints.resource import ResourceEndpoint | |||||
from oauthlib.oauth2.rfc6749.endpoints.revocation import RevocationEndpoint | |||||
from oauthlib.common import Request | |||||
class WebApplicationServer(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint, | |||||
RevocationEndpoint): | |||||
"""An all-in-one endpoint featuring Authorization code grant and Bearer tokens.""" | |||||
def __init__(self, request_validator, token_generator=None, | |||||
token_expires_in=None, refresh_token_generator=None, **kwargs): | |||||
"""Construct a new web application server. | |||||
:param request_validator: An implementation of | |||||
oauthlib.oauth2.RequestValidator. | |||||
:param token_expires_in: An int or a function to generate a token | |||||
expiration offset (in seconds) given a | |||||
oauthlib.common.Request object. | |||||
:param token_generator: A function to generate a token from a request. | |||||
:param refresh_token_generator: A function to generate a token from a | |||||
request for the refresh token. | |||||
:param kwargs: Extra parameters to pass to authorization-, | |||||
token-, resource-, and revocation-endpoint constructors. | |||||
""" | |||||
auth_grant = AuthorizationCodeGrant(request_validator) | |||||
refresh_grant = RefreshTokenGrant(request_validator) | |||||
bearer = BearerToken(request_validator, token_generator, | |||||
token_expires_in, refresh_token_generator) | |||||
AuthorizationEndpoint.__init__(self, default_response_type='code', | |||||
response_types={'code': auth_grant}, | |||||
default_token_type=bearer) | |||||
TokenEndpoint.__init__(self, default_grant_type='authorization_code', | |||||
grant_types={ | |||||
'authorization_code': auth_grant, | |||||
'refresh_token': refresh_grant, | |||||
}, | |||||
default_token_type=bearer) | |||||
ResourceEndpoint.__init__(self, default_token='Bearer', | |||||
token_types={'Bearer': bearer}) | |||||
RevocationEndpoint.__init__(self, request_validator) | |||||
class OAuthWebRequestValidator(RequestValidator): | |||||
# Pre- and post-authorization. | |||||
def validate_client_id(self, client_id, request, *args, **kwargs): | |||||
# Simple validity check, does client exist? Not banned? | |||||
cli_id = frappe.db.get_value("OAuth Client",{ "name":client_id }) | |||||
if cli_id: | |||||
request.client = frappe.get_doc("OAuth Client", client_id).as_dict() | |||||
return True | |||||
else: | |||||
return False | |||||
def validate_redirect_uri(self, client_id, redirect_uri, request, *args, **kwargs): | |||||
# Is the client allowed to use the supplied redirect_uri? i.e. has | |||||
# the client previously registered this EXACT redirect uri. | |||||
redirect_uris = frappe.db.get_value("OAuth Client", client_id, 'redirect_uris').split(';') | |||||
if redirect_uri in redirect_uris: | |||||
return True | |||||
else: | |||||
return False | |||||
def get_default_redirect_uri(self, client_id, request, *args, **kwargs): | |||||
# The redirect used if none has been supplied. | |||||
# Prefer your clients to pre register a redirect uri rather than | |||||
# supplying one on each authorization request. | |||||
redirect_uri = frappe.db.get_value("OAuth Client", client_id, 'default_redirect_uri') | |||||
return redirect_uri | |||||
def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs): | |||||
# Is the client allowed to access the requested scopes? | |||||
client_scopes = frappe.db.get_value("OAuth Client", client_id, 'scopes').split(';') | |||||
are_scopes_valid = True | |||||
for scp in scopes: | |||||
are_scopes_valid = are_scopes_valid and True if scp in client_scopes else False | |||||
return are_scopes_valid | |||||
def get_default_scopes(self, client_id, request, *args, **kwargs): | |||||
# Scopes a client will authorize for if none are supplied in the | |||||
# authorization request. | |||||
scopes = frappe.db.get_value("OAuth Client", client_id, 'scopes').split(';') | |||||
request.scopes = scopes #Apparently this is possible. | |||||
return scopes | |||||
def validate_response_type(self, client_id, response_type, client, request, *args, **kwargs): | |||||
# Clients should only be allowed to use one type of response type, the | |||||
# one associated with their one allowed grant type. | |||||
# In this case it must be "code". | |||||
return (client.response_type.lower() == response_type) | |||||
# Post-authorization | |||||
def save_authorization_code(self, client_id, code, request, *args, **kwargs): | |||||
cookie_dict = get_cookie_dict_from_headers(request) | |||||
oac = frappe.new_doc('OAuth Authorization Code') | |||||
oac.scopes = ';'.join(request.scopes) | |||||
oac.redirect_uri_bound_to_authorization_code = request.redirect_uri | |||||
oac.client = client_id | |||||
oac.user = urllib.unquote(cookie_dict['user_id']) | |||||
oac.authorization_code = code['code'] | |||||
oac.save(ignore_permissions=True) | |||||
frappe.db.commit() | |||||
def authenticate_client(self, request, *args, **kwargs): | |||||
cookie_dict = get_cookie_dict_from_headers(request) | |||||
#Get ClientID in URL | |||||
if request.client_id: | |||||
oc = frappe.get_doc("OAuth Client", request.client_id) | |||||
else: | |||||
#Extract token, instantiate OAuth Bearer Token and use clientid from there. | |||||
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')) | |||||
else: | |||||
oc = frappe.get_doc("OAuth Client", frappe.db.get_value("OAuth Bearer Token", frappe.form_dict["token"], 'client')) | |||||
try: | |||||
request.client = request.client or oc.as_dict() | |||||
except Exception, e: | |||||
print "Failed body authentication: Application %s does not exist".format(cid=request.client_id) | |||||
return frappe.session.user == urllib.unquote(cookie_dict.get('user_id', "Guest")) | |||||
def authenticate_client_id(self, client_id, request, *args, **kwargs): | |||||
cli_id = frappe.db.get_value('OAuth Client', client_id, 'name') | |||||
if not cli_id: | |||||
# Don't allow public (non-authenticated) clients | |||||
return False | |||||
else: | |||||
request["client"] = frappe.get_doc("OAuth Client", cli_id) | |||||
return True | |||||
def validate_code(self, client_id, code, client, request, *args, **kwargs): | |||||
# Validate the code belongs to the client. Add associated scopes, | |||||
# state and user to request.scopes and request.user. | |||||
validcodes = frappe.get_all("OAuth Authorization Code", filters={"client": client_id, "validity": "Valid"}) | |||||
checkcodes = [] | |||||
for vcode in validcodes: | |||||
checkcodes.append(vcode["name"]) | |||||
if code in checkcodes: | |||||
request.scopes = frappe.db.get_value("OAuth Authorization Code", code, 'scopes').split(';') | |||||
request.user = frappe.db.get_value("OAuth Authorization Code", code, 'user') | |||||
return True | |||||
else: | |||||
return False | |||||
def confirm_redirect_uri(self, client_id, code, redirect_uri, client, *args, **kwargs): | |||||
saved_redirect_uri = frappe.db.get_value('OAuth Client', client_id, 'default_redirect_uri') | |||||
return saved_redirect_uri == redirect_uri | |||||
def validate_grant_type(self, client_id, grant_type, client, request, *args, **kwargs): | |||||
# Clients should only be allowed to use one type of grant. | |||||
# In this case, it must be "authorization_code" or "refresh_token" | |||||
return (grant_type in ["authorization_code", "refresh_token"]) | |||||
def save_bearer_token(self, token, request, *args, **kwargs): | |||||
# Remember to associate it with request.scopes, request.user and | |||||
# request.client. The two former will be set when you validate | |||||
# the authorization code. Don't forget to save both the | |||||
# access_token and the refresh_token and set expiration for the | |||||
# access_token to now + expires_in seconds. | |||||
otoken = frappe.new_doc("OAuth Bearer Token") | |||||
otoken.client = request.client['name'] | |||||
otoken.user = request.user | |||||
otoken.scopes = ";".join(request.scopes) | |||||
otoken.access_token = token['access_token'] | |||||
otoken.refresh_token = token['refresh_token'] | |||||
otoken.expires_in = token['expires_in'] | |||||
otoken.save(ignore_permissions=True) | |||||
frappe.db.commit() | |||||
default_redirect_uri = frappe.db.get_value("OAuth Client", request.client['name'], "default_redirect_uri") | |||||
return default_redirect_uri | |||||
def invalidate_authorization_code(self, client_id, code, request, *args, **kwargs): | |||||
# Authorization codes are use once, invalidate it when a Bearer token | |||||
# has been acquired. | |||||
frappe.db.set_value("OAuth Authorization Code", code, "validity", "Invalid") | |||||
frappe.db.commit() | |||||
# Protected resource request | |||||
def validate_bearer_token(self, token, scopes, request): | |||||
# Remember to check expiration and scope membership | |||||
otoken = frappe.get_doc("OAuth Bearer Token", token) #{"access_token": str(token)}) | |||||
is_token_valid = (frappe.utils.datetime.datetime.now() < otoken.expiration_time) \ | |||||
and otoken.status != "Revoked" | |||||
client_scopes = frappe.db.get_value("OAuth Client", otoken.client, 'scopes').split(';') | |||||
are_scopes_valid = True | |||||
for scp in scopes: | |||||
are_scopes_valid = are_scopes_valid and True if scp in client_scopes else False | |||||
return is_token_valid and are_scopes_valid | |||||
# Token refresh request | |||||
def get_original_scopes(self, refresh_token, request, *args, **kwargs): | |||||
# Obtain the token associated with the given refresh_token and | |||||
# return its scopes, these will be passed on to the refreshed | |||||
# access token if the client did not specify a scope during the | |||||
# request. | |||||
obearer_token = frappe.get_doc("OAuth Bearer Token", {"refresh_token": refresh_token}) | |||||
return obearer_token.scopes | |||||
def revoke_token(self, token, token_type_hint, request, *args, **kwargs): | |||||
"""Revoke an access or refresh token. | |||||
:param token: The token string. | |||||
:param token_type_hint: access_token or refresh_token. | |||||
:param request: The HTTP Request (oauthlib.common.Request) | |||||
Method is used by: | |||||
- Revocation Endpoint | |||||
""" | |||||
otoken = None | |||||
if token_type_hint == "access_token": | |||||
otoken = frappe.db.set_value("OAuth Bearer Token", token, 'status', 'Revoked') | |||||
elif token_type_hint == "refresh_token": | |||||
otoken = frappe.db.set_value("OAuth Bearer Token", {"refresh_token": token}, 'status', 'Revoked') | |||||
else: | |||||
otoken = frappe.db.set_value("OAuth Bearer Token", token, 'status', 'Revoked') | |||||
frappe.db.commit() | |||||
def validate_refresh_token(self, refresh_token, client, request, *args, **kwargs): | |||||
# """Ensure the Bearer token is valid and authorized access to scopes. | |||||
# OBS! The request.user attribute should be set to the resource owner | |||||
# associated with this refresh token. | |||||
# :param refresh_token: Unicode refresh token | |||||
# :param client: Client object set by you, see authenticate_client. | |||||
# :param request: The HTTP Request (oauthlib.common.Request) | |||||
# :rtype: True or False | |||||
# Method is used by: | |||||
# - Authorization Code Grant (indirectly by issuing refresh tokens) | |||||
# - Resource Owner Password Credentials Grant (also indirectly) | |||||
# - Refresh Token Grant | |||||
# """ | |||||
# raise NotImplementedError('Subclasses must implement this method.') | |||||
otoken = frappe.get_doc("OAuth Bearer Token", {"refresh_token": refresh_token, "status": "Active"}) | |||||
if not otoken: | |||||
return False | |||||
else: | |||||
return True | |||||
#TODO: Validate scopes. | |||||
def get_cookie_dict_from_headers(r): | |||||
if r.headers.get('Cookie'): | |||||
cookie = r.headers.get('Cookie') | |||||
cookie = cookie.split("; ") | |||||
cookie_dict = {k:v for k,v in (x.split('=') for x in cookie)} | |||||
return cookie_dict | |||||
else: | |||||
return {} |
@@ -0,0 +1,47 @@ | |||||
{% if not error %} | |||||
<div class="panel panel-default"> | |||||
<div class="panel-heading"> | |||||
<h3 class="panel-title">{{ client_id }} wants to access the following details from your account</h3> | |||||
</div> | |||||
<div class="panel-body"> | |||||
<ul class="list-group"> | |||||
{% for dtl in details %} | |||||
<li class="list-group-item">{{ dtl.title() }}</li> | |||||
{% endfor %} | |||||
</ul> | |||||
<ul class="list-inline"> | |||||
<li> | |||||
<button id="allow" class="btn btn-sm btn-primary">Allow</button> | |||||
</li> | |||||
<li> | |||||
<button id="deny" class="btn btn-sm btn-default">Deny</button> | |||||
</li> | |||||
</ul> | |||||
</div> | |||||
</div> | |||||
<script type="text/javascript"> | |||||
frappe.ready(function() { | |||||
$('#allow').on('click', function(event) { | |||||
window.location.replace("{{ success_url|string }}"); | |||||
}); | |||||
$('#deny').on('click', function(event) { | |||||
window.location.replace("{{ failure_url|string }}"); | |||||
}); | |||||
}); | |||||
</script> | |||||
{% else %} | |||||
<div class="panel panel-danger"> | |||||
<div class="panel-heading"> | |||||
<h3 class="panel-title">Authorization error for {{ client_id }}</h3> | |||||
</div> | |||||
<div class="panel-body"> | |||||
<p>An unexpected error occurred while authorizing {{ client_id }}.</p> | |||||
<h4>{{ error }}</h4> | |||||
<ul class="list-inline"> | |||||
<li> | |||||
<button class="btn btn-sm btn-default">OK</button> | |||||
</li> | |||||
</ul> | |||||
</div> | |||||
</div> | |||||
{% endif %} |
@@ -38,3 +38,4 @@ zxcvbn | |||||
psutil | psutil | ||||
unittest-xml-reporting | unittest-xml-reporting | ||||
xlwt | xlwt | ||||
oauthlib |