|
|
@@ -1,65 +1,16 @@ |
|
|
|
from __future__ import print_function, unicode_literals |
|
|
|
import frappe |
|
|
|
import pytz |
|
|
|
import jwt |
|
|
|
import hashlib |
|
|
|
import re |
|
|
|
import base64 |
|
|
|
import datetime |
|
|
|
|
|
|
|
from frappe import _ |
|
|
|
from frappe.auth import LoginManager |
|
|
|
from http import cookies |
|
|
|
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 |
|
|
|
from six.moves.urllib.parse import unquote |
|
|
|
|
|
|
|
def get_url_delimiter(separator_character=" "): |
|
|
|
return separator_character |
|
|
|
from oauthlib.openid import RequestValidator |
|
|
|
from urllib.parse import urlparse, unquote |
|
|
|
|
|
|
|
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. |
|
|
|
""" |
|
|
|
implicit_grant = ImplicitGrant(request_validator) |
|
|
|
auth_grant = AuthorizationCodeGrant(request_validator) |
|
|
|
refresh_grant = RefreshTokenGrant(request_validator) |
|
|
|
resource_owner_password_credentials_grant = ResourceOwnerPasswordCredentialsGrant(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, |
|
|
|
'token': implicit_grant |
|
|
|
}, |
|
|
|
default_token_type=bearer) |
|
|
|
TokenEndpoint.__init__(self, default_grant_type='authorization_code', |
|
|
|
grant_types={ |
|
|
|
'authorization_code': auth_grant, |
|
|
|
'refresh_token': refresh_grant, |
|
|
|
'password': resource_owner_password_credentials_grant |
|
|
|
}, |
|
|
|
default_token_type=bearer) |
|
|
|
ResourceEndpoint.__init__(self, default_token='Bearer', |
|
|
|
token_types={'Bearer': bearer}) |
|
|
|
RevocationEndpoint.__init__(self, request_validator) |
|
|
|
import frappe |
|
|
|
from frappe.auth import LoginManager |
|
|
|
|
|
|
|
|
|
|
|
class OAuthWebRequestValidator(RequestValidator): |
|
|
@@ -67,7 +18,7 @@ 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 }) |
|
|
|
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 |
|
|
@@ -78,7 +29,9 @@ class OAuthWebRequestValidator(RequestValidator): |
|
|
|
# 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(get_url_delimiter()) |
|
|
|
redirect_uris = frappe.db.get_value( |
|
|
|
"OAuth Client", client_id, "redirect_uris" |
|
|
|
).split(get_url_delimiter()) |
|
|
|
|
|
|
|
if redirect_uri in redirect_uris: |
|
|
|
return True |
|
|
@@ -89,7 +42,9 @@ class OAuthWebRequestValidator(RequestValidator): |
|
|
|
# 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') |
|
|
|
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): |
|
|
@@ -101,19 +56,23 @@ class OAuthWebRequestValidator(RequestValidator): |
|
|
|
# Scopes a client will authorize for if none are supplied in the |
|
|
|
# authorization request. |
|
|
|
scopes = get_client_scopes(client_id) |
|
|
|
request.scopes = scopes #Apparently this is possible. |
|
|
|
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". |
|
|
|
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 (response_type in allowed_response_types) |
|
|
|
|
|
|
|
def validate_response_type( |
|
|
|
self, client_id, response_type, client, request, *args, **kwargs |
|
|
|
): |
|
|
|
allowed_response_types = [ |
|
|
|
# From OAuth Client response_type field |
|
|
|
client.response_type.lower(), |
|
|
|
# OIDC |
|
|
|
"id_token", |
|
|
|
"id_token token", |
|
|
|
"code id_token", |
|
|
|
"code token id_token", |
|
|
|
] |
|
|
|
|
|
|
|
return response_type in allowed_response_types |
|
|
|
|
|
|
|
# Post-authorization |
|
|
|
|
|
|
@@ -121,38 +80,69 @@ class OAuthWebRequestValidator(RequestValidator): |
|
|
|
|
|
|
|
cookie_dict = get_cookie_dict_from_headers(request) |
|
|
|
|
|
|
|
oac = frappe.new_doc('OAuth Authorization Code') |
|
|
|
oac = frappe.new_doc("OAuth Authorization Code") |
|
|
|
oac.scopes = get_url_delimiter().join(request.scopes) |
|
|
|
oac.redirect_uri_bound_to_authorization_code = request.redirect_uri |
|
|
|
oac.client = client_id |
|
|
|
oac.user = unquote(cookie_dict['user_id'].value) |
|
|
|
oac.authorization_code = code['code'] |
|
|
|
oac.user = unquote(cookie_dict["user_id"].value) |
|
|
|
oac.authorization_code = code["code"] |
|
|
|
|
|
|
|
if request.nonce: |
|
|
|
oac.nonce = request.nonce |
|
|
|
|
|
|
|
if request.code_challenge and request.code_challenge_method: |
|
|
|
oac.code_challenge = request.code_challenge |
|
|
|
oac.code_challenge_method = request.code_challenge_method.lower() |
|
|
|
|
|
|
|
oac.save(ignore_permissions=True) |
|
|
|
frappe.db.commit() |
|
|
|
|
|
|
|
def authenticate_client(self, request, *args, **kwargs): |
|
|
|
#Get ClientID in URL |
|
|
|
# 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. |
|
|
|
# Extract token, instantiate OAuth Bearer Token and use clientid from there. |
|
|
|
if "refresh_token" in frappe.form_dict: |
|
|
|
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", |
|
|
|
), |
|
|
|
) |
|
|
|
elif "token" in frappe.form_dict: |
|
|
|
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')) |
|
|
|
oc = frappe.get_doc( |
|
|
|
"OAuth Client", |
|
|
|
frappe.db.get_value( |
|
|
|
"OAuth Bearer Token", |
|
|
|
frappe.get_request_header("Authorization").split(" ")[1], |
|
|
|
"client", |
|
|
|
), |
|
|
|
) |
|
|
|
try: |
|
|
|
request.client = request.client or oc.as_dict() |
|
|
|
except Exception as e: |
|
|
|
print("Failed body authentication: Application %s does not exist".format(cid=request.client_id)) |
|
|
|
return generate_json_error_response(e) |
|
|
|
|
|
|
|
cookie_dict = get_cookie_dict_from_headers(request) |
|
|
|
user_id = unquote(cookie_dict.get('user_id').value) if 'user_id' in cookie_dict else "Guest" |
|
|
|
user_id = ( |
|
|
|
unquote(cookie_dict.get("user_id").value) |
|
|
|
if "user_id" in cookie_dict |
|
|
|
else "Guest" |
|
|
|
) |
|
|
|
return frappe.session.user == user_id |
|
|
|
|
|
|
|
def authenticate_client_id(self, client_id, request, *args, **kwargs): |
|
|
|
cli_id = frappe.db.get_value('OAuth Client', client_id, 'name') |
|
|
|
cli_id = frappe.db.get_value("OAuth Client", client_id, "name") |
|
|
|
if not cli_id: |
|
|
|
# Don't allow public (non-authenticated) clients |
|
|
|
return False |
|
|
@@ -164,28 +154,66 @@ class OAuthWebRequestValidator(RequestValidator): |
|
|
|
# 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"}) |
|
|
|
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(get_url_delimiter()) |
|
|
|
request.user = frappe.db.get_value("OAuth Authorization Code", code, 'user') |
|
|
|
request.scopes = frappe.db.get_value( |
|
|
|
"OAuth Authorization Code", code, "scopes" |
|
|
|
).split(get_url_delimiter()) |
|
|
|
request.user = frappe.db.get_value("OAuth Authorization Code", code, "user") |
|
|
|
code_challenge_method = frappe.db.get_value( |
|
|
|
"OAuth Authorization Code", code, "code_challenge_method" |
|
|
|
) |
|
|
|
code_challenge = frappe.db.get_value( |
|
|
|
"OAuth Authorization Code", code, "code_challenge" |
|
|
|
) |
|
|
|
|
|
|
|
if code_challenge and not request.code_verifier: |
|
|
|
if frappe.db.exists("OAuth Authorization Code", code): |
|
|
|
frappe.delete_doc( |
|
|
|
"OAuth Authorization Code", code, ignore_permissions=True |
|
|
|
) |
|
|
|
frappe.db.commit() |
|
|
|
return False |
|
|
|
|
|
|
|
if code_challenge_method == "s256": |
|
|
|
m = hashlib.sha256() |
|
|
|
m.update(bytes(request.code_verifier, "utf-8")) |
|
|
|
code_verifier = base64.b64encode(m.digest()).decode("utf-8") |
|
|
|
code_verifier = re.sub(r"\+", "-", code_verifier) |
|
|
|
code_verifier = re.sub(r"\/", "_", code_verifier) |
|
|
|
code_verifier = re.sub(r"=", "", code_verifier) |
|
|
|
return code_challenge == code_verifier |
|
|
|
|
|
|
|
elif code_challenge_method == "plain": |
|
|
|
return code_challenge == request.code_verifier |
|
|
|
|
|
|
|
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 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): |
|
|
|
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", "password"]) |
|
|
|
return grant_type in ["authorization_code", "refresh_token", "password"] |
|
|
|
|
|
|
|
def save_bearer_token(self, token, request, *args, **kwargs): |
|
|
|
# Remember to associate it with request.scopes, request.user and |
|
|
@@ -195,19 +223,29 @@ class OAuthWebRequestValidator(RequestValidator): |
|
|
|
# access_token to now + expires_in seconds. |
|
|
|
|
|
|
|
otoken = frappe.new_doc("OAuth Bearer Token") |
|
|
|
otoken.client = request.client['name'] |
|
|
|
otoken.client = request.client["name"] |
|
|
|
try: |
|
|
|
otoken.user = request.user if request.user else frappe.db.get_value("OAuth Bearer Token", {"refresh_token":request.body.get("refresh_token")}, "user") |
|
|
|
otoken.user = ( |
|
|
|
request.user |
|
|
|
if request.user |
|
|
|
else frappe.db.get_value( |
|
|
|
"OAuth Bearer Token", |
|
|
|
{"refresh_token": request.body.get("refresh_token")}, |
|
|
|
"user", |
|
|
|
) |
|
|
|
) |
|
|
|
except Exception as e: |
|
|
|
otoken.user = frappe.session.user |
|
|
|
otoken.scopes = get_url_delimiter().join(request.scopes) |
|
|
|
otoken.access_token = token['access_token'] |
|
|
|
otoken.refresh_token = token.get('refresh_token') |
|
|
|
otoken.expires_in = token['expires_in'] |
|
|
|
otoken.access_token = token["access_token"] |
|
|
|
otoken.refresh_token = token.get("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") |
|
|
|
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): |
|
|
@@ -222,24 +260,35 @@ class OAuthWebRequestValidator(RequestValidator): |
|
|
|
def validate_bearer_token(self, token, scopes, request): |
|
|
|
# Remember to check expiration and scope membership |
|
|
|
otoken = frappe.get_doc("OAuth Bearer Token", token) |
|
|
|
token_expiration_local = otoken.expiration_time.replace(tzinfo=pytz.timezone(frappe.utils.get_time_zone())) |
|
|
|
token_expiration_local = otoken.expiration_time.replace( |
|
|
|
tzinfo=pytz.timezone(frappe.utils.get_time_zone()) |
|
|
|
) |
|
|
|
token_expiration_utc = token_expiration_local.astimezone(pytz.utc) |
|
|
|
is_token_valid = (frappe.utils.datetime.datetime.utcnow().replace(tzinfo=pytz.utc) < token_expiration_utc) \ |
|
|
|
and otoken.status != "Revoked" |
|
|
|
client_scopes = frappe.db.get_value("OAuth Client", otoken.client, 'scopes').split(get_url_delimiter()) |
|
|
|
is_token_valid = ( |
|
|
|
frappe.utils.datetime.datetime.utcnow().replace(tzinfo=pytz.utc) |
|
|
|
< token_expiration_utc |
|
|
|
) and otoken.status != "Revoked" |
|
|
|
client_scopes = frappe.db.get_value( |
|
|
|
"OAuth Client", otoken.client, "scopes" |
|
|
|
).split(get_url_delimiter()) |
|
|
|
are_scopes_valid = True |
|
|
|
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 |
|
|
|
) |
|
|
|
|
|
|
|
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}) |
|
|
|
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): |
|
|
@@ -250,36 +299,44 @@ class OAuthWebRequestValidator(RequestValidator): |
|
|
|
:param request: The HTTP Request (oauthlib.common.Request) |
|
|
|
|
|
|
|
Method is used by: |
|
|
|
- Revocation Endpoint |
|
|
|
- Revocation Endpoint |
|
|
|
""" |
|
|
|
otoken = None |
|
|
|
|
|
|
|
if token_type_hint == "access_token": |
|
|
|
otoken = frappe.db.set_value("OAuth Bearer Token", token, 'status', 'Revoked') |
|
|
|
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') |
|
|
|
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') |
|
|
|
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. |
|
|
|
"""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. |
|
|
|
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 |
|
|
|
: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 |
|
|
|
# """ |
|
|
|
Method is used by: |
|
|
|
- Authorization Code Grant (indirectly by issuing refresh tokens) |
|
|
|
- Resource Owner Password Credentials Grant (also indirectly) |
|
|
|
- Refresh Token Grant |
|
|
|
""" |
|
|
|
|
|
|
|
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"} |
|
|
|
) |
|
|
|
|
|
|
|
if not otoken: |
|
|
|
return False |
|
|
@@ -287,36 +344,84 @@ class OAuthWebRequestValidator(RequestValidator): |
|
|
|
return True |
|
|
|
|
|
|
|
# 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: |
|
|
|
def finalize_id_token(self, id_token, token, token_handler, request): |
|
|
|
# Check whether frappe server URL is set |
|
|
|
frappe_server_url = ( |
|
|
|
frappe.db.get_value("Social Login Key", "frappe", "base_url") or request.uri |
|
|
|
) |
|
|
|
|
|
|
|
- nonce, if workflow is implicit or hybrid and it was provided |
|
|
|
- claims, if provided to the original Authorization Code request |
|
|
|
id_token_header = {"typ": "jwt", "alg": "HS256"} |
|
|
|
|
|
|
|
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. |
|
|
|
user = frappe.get_doc( |
|
|
|
"User", |
|
|
|
frappe.session.user, |
|
|
|
) |
|
|
|
|
|
|
|
Similarly, when the request parameter has a ``code`` property defined, the ID Token |
|
|
|
*should* include a calculated ``c_hash`` claim. |
|
|
|
if request.nonce: |
|
|
|
id_token["nonce"] = request.nonce |
|
|
|
|
|
|
|
http://openid.net/specs/openid-connect-core-1_0.html (sections `3.1.3.6`_, `3.2.2.10`_, `3.3.2.11`_) |
|
|
|
if "openid" in request.scopes: |
|
|
|
userinfo = get_userinfo(user, request) |
|
|
|
id_token.update(userinfo) |
|
|
|
|
|
|
|
.. _`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 |
|
|
|
id_token_encoded = jwt.encode( |
|
|
|
payload=id_token, |
|
|
|
key=request.client.client_secret, |
|
|
|
algorithm="HS256", |
|
|
|
headers=id_token_header, |
|
|
|
) |
|
|
|
|
|
|
|
: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 |
|
|
|
return frappe.safe_decode(id_token_encoded) |
|
|
|
|
|
|
|
def get_authorization_code_nonce(self, client_id, code, redirect_uri, request): |
|
|
|
if frappe.get_value("OAuth Authorization Code", code, "validity") == "Valid": |
|
|
|
return frappe.get_value("OAuth Authorization Code", code, "nonce") |
|
|
|
|
|
|
|
return None |
|
|
|
|
|
|
|
def get_authorization_code_scopes(self, client_id, code, redirect_uri, request): |
|
|
|
scope = frappe.get_value("OAuth Client", client_id, "scopes") |
|
|
|
if not scope: |
|
|
|
scope = [] |
|
|
|
else: |
|
|
|
scope = scope.split(get_url_delimiter()) |
|
|
|
|
|
|
|
return scope |
|
|
|
|
|
|
|
def get_jwt_bearer_token(self, token, token_handler, request): |
|
|
|
now = datetime.datetime.now() |
|
|
|
id_token = dict( |
|
|
|
aud=token.client_id, |
|
|
|
iat=round(now.timestamp()), |
|
|
|
at_hash=calculate_at_hash(token.access_token, hashlib.sha256), |
|
|
|
) |
|
|
|
return self.finalize_id_token(id_token, token, token_handler, request) |
|
|
|
|
|
|
|
def get_userinfo_claims(self, request): |
|
|
|
user = frappe.get_doc("User", frappe.session.user) |
|
|
|
userinfo = get_userinfo(user, request) |
|
|
|
return userinfo |
|
|
|
|
|
|
|
def validate_id_token(self, token, scopes, request): |
|
|
|
try: |
|
|
|
id_token = frappe.get_doc("OAuth Bearer Token", token) |
|
|
|
if id_token.status == "Active": |
|
|
|
return True |
|
|
|
except Exception: |
|
|
|
return False |
|
|
|
|
|
|
|
return False |
|
|
|
|
|
|
|
def validate_jwt_bearer_token(self, token, scopes, request): |
|
|
|
try: |
|
|
|
jwt = frappe.get_doc("OAuth Bearer Token", token) |
|
|
|
if jwt.status == "Active": |
|
|
|
return True |
|
|
|
except Exception: |
|
|
|
return False |
|
|
|
|
|
|
|
return False |
|
|
|
|
|
|
|
def validate_silent_authorization(self, request): |
|
|
|
"""Ensure the logged in user has authorized silent OpenID authorization. |
|
|
@@ -328,9 +433,9 @@ class OAuthWebRequestValidator(RequestValidator): |
|
|
|
:rtype: True or False |
|
|
|
|
|
|
|
Method is used by: |
|
|
|
- OpenIDConnectAuthCode |
|
|
|
- OpenIDConnectImplicit |
|
|
|
- OpenIDConnectHybrid |
|
|
|
- OpenIDConnectAuthCode |
|
|
|
- OpenIDConnectImplicit |
|
|
|
- OpenIDConnectHybrid |
|
|
|
""" |
|
|
|
if request.prompt == "login": |
|
|
|
False |
|
|
@@ -351,9 +456,9 @@ class OAuthWebRequestValidator(RequestValidator): |
|
|
|
:rtype: True or False |
|
|
|
|
|
|
|
Method is used by: |
|
|
|
- OpenIDConnectAuthCode |
|
|
|
- OpenIDConnectImplicit |
|
|
|
- OpenIDConnectHybrid |
|
|
|
- OpenIDConnectAuthCode |
|
|
|
- OpenIDConnectImplicit |
|
|
|
- OpenIDConnectHybrid |
|
|
|
""" |
|
|
|
if frappe.session.user == "Guest" or request.prompt.lower() == "login": |
|
|
|
return False |
|
|
@@ -373,32 +478,77 @@ class OAuthWebRequestValidator(RequestValidator): |
|
|
|
:rtype: True or False |
|
|
|
|
|
|
|
Method is used by: |
|
|
|
- OpenIDConnectAuthCode |
|
|
|
- OpenIDConnectImplicit |
|
|
|
- OpenIDConnectHybrid |
|
|
|
- OpenIDConnectAuthCode |
|
|
|
- OpenIDConnectImplicit |
|
|
|
- OpenIDConnectHybrid |
|
|
|
""" |
|
|
|
if id_token_hint and id_token_hint == frappe.db.get_value("User Social Login", {"parent":frappe.session.user, "provider": "frappe"}, "userid"): |
|
|
|
if id_token_hint: |
|
|
|
try: |
|
|
|
user = None |
|
|
|
payload = jwt.decode( |
|
|
|
id_token_hint, |
|
|
|
options={ |
|
|
|
"verify_signature": False, |
|
|
|
"verify_aud": False, |
|
|
|
}, |
|
|
|
) |
|
|
|
client_id, client_secret = frappe.get_value( |
|
|
|
"OAuth Client", |
|
|
|
payload.get("aud"), |
|
|
|
["client_id", "client_secret"], |
|
|
|
) |
|
|
|
|
|
|
|
if payload.get("sub") and client_id and client_secret: |
|
|
|
user = frappe.db.get_value( |
|
|
|
"User Social Login", |
|
|
|
{"userid": payload.get("sub"), "provider": "frappe"}, |
|
|
|
"parent", |
|
|
|
) |
|
|
|
user = frappe.get_doc("User", user) |
|
|
|
verified_payload = jwt.decode( |
|
|
|
id_token_hint, |
|
|
|
key=client_secret, |
|
|
|
audience=client_id, |
|
|
|
algorithm="HS256", |
|
|
|
options={ |
|
|
|
"verify_exp": False, |
|
|
|
}, |
|
|
|
) |
|
|
|
|
|
|
|
if verified_payload: |
|
|
|
return user.name == frappe.session.user |
|
|
|
|
|
|
|
except Exception as e: |
|
|
|
return False |
|
|
|
|
|
|
|
elif frappe.session.user != "Guest": |
|
|
|
return True |
|
|
|
else: |
|
|
|
return False |
|
|
|
|
|
|
|
return False |
|
|
|
|
|
|
|
def validate_user(self, username, password, client, request, *args, **kwargs): |
|
|
|
"""Ensure the username and password is valid. |
|
|
|
|
|
|
|
Method is used by: |
|
|
|
- Resource Owner Password Credentials Grant |
|
|
|
""" |
|
|
|
Method is used by: |
|
|
|
- Resource Owner Password Credentials Grant |
|
|
|
""" |
|
|
|
login_manager = LoginManager() |
|
|
|
login_manager.authenticate(username, password) |
|
|
|
|
|
|
|
if login_manager.user == "Guest": |
|
|
|
return False |
|
|
|
|
|
|
|
request.user = login_manager.user |
|
|
|
return True |
|
|
|
|
|
|
|
|
|
|
|
def get_cookie_dict_from_headers(r): |
|
|
|
cookie = cookies.BaseCookie() |
|
|
|
if r.headers.get('Cookie'): |
|
|
|
cookie.load(r.headers.get('Cookie')) |
|
|
|
if r.headers.get("Cookie"): |
|
|
|
cookie.load(r.headers.get("Cookie")) |
|
|
|
return cookie |
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
@@ -409,21 +559,25 @@ def calculate_at_hash(access_token, hash_alg): |
|
|
|
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 |
|
|
|
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() |
|
|
|
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') |
|
|
|
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"}) |
|
|
|
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: |
|
|
@@ -439,3 +593,59 @@ def delete_oauth2_data(): |
|
|
|
def get_client_scopes(client_id): |
|
|
|
scopes_string = frappe.db.get_value("OAuth Client", client_id, "scopes") |
|
|
|
return scopes_string.split() |
|
|
|
|
|
|
|
|
|
|
|
def get_userinfo(user, request): |
|
|
|
picture = None |
|
|
|
frappe_server_url = ( |
|
|
|
frappe.db.get_value("Social Login Key", "frappe", "base_url") or None |
|
|
|
) |
|
|
|
|
|
|
|
request_url = urlparse(request.uri) |
|
|
|
|
|
|
|
if user.user_image: |
|
|
|
if frappe.utils.validate_url(user.user_image): |
|
|
|
picture = user.user_image |
|
|
|
elif frappe_server_url: |
|
|
|
picture = frappe_server_url + "/" + user.user_image |
|
|
|
else: |
|
|
|
picture = request_url.scheme + "://" + request_url.netloc + user.user_image |
|
|
|
|
|
|
|
userinfo = frappe._dict( |
|
|
|
{ |
|
|
|
"sub": frappe.db.get_value( |
|
|
|
"User Social Login", |
|
|
|
{"parent": user.name, "provider": "frappe"}, |
|
|
|
"userid", |
|
|
|
), |
|
|
|
"name": " ".join(filter(None, [user.first_name, user.last_name])), |
|
|
|
"given_name": user.first_name, |
|
|
|
"family_name": user.last_name, |
|
|
|
"email": user.email, |
|
|
|
"picture": picture, |
|
|
|
"roles": frappe.get_roles(user.name), |
|
|
|
} |
|
|
|
) |
|
|
|
|
|
|
|
userinfo["iss"] = frappe_server_url or request.uri |
|
|
|
|
|
|
|
return userinfo |
|
|
|
|
|
|
|
|
|
|
|
def get_url_delimiter(separator_character=" "): |
|
|
|
return separator_character |
|
|
|
|
|
|
|
|
|
|
|
def generate_json_error_response(e): |
|
|
|
if not e: |
|
|
|
e = frappe._dict({}) |
|
|
|
|
|
|
|
frappe.local.response = frappe._dict( |
|
|
|
{ |
|
|
|
"description": getattr(e, "description", "Internal Server Error"), |
|
|
|
"status_code": getattr(e, "status_code", 500), |
|
|
|
"error": getattr(e, "error", "internal_server_error"), |
|
|
|
} |
|
|
|
) |
|
|
|
frappe.local.response["http_status_code"] = getattr(e, "status_code", 500) |
|
|
|
return |