|
|
@ -1,6 +1,7 @@ |
|
|
|
# -*- coding: utf-8 -*- |
|
|
|
|
|
|
|
import json |
|
|
|
import logging |
|
|
|
import time |
|
|
|
import os |
|
|
|
import base64 |
|
|
@ -8,6 +9,7 @@ import base64 |
|
|
|
from odoo import http |
|
|
|
from odoo.http import request |
|
|
|
import werkzeug |
|
|
|
from werkzeug.urls import url_encode |
|
|
|
|
|
|
|
from .. api import resource |
|
|
|
|
|
|
@ -18,6 +20,8 @@ try: |
|
|
|
except ImportError: |
|
|
|
pass |
|
|
|
|
|
|
|
_logger = logging.getLogger(__name__) |
|
|
|
|
|
|
|
def jwk_from_json(json_key): |
|
|
|
key = jwk.JWK() |
|
|
|
key.import_key(**json.loads(json_key)) |
|
|
@ -54,24 +58,24 @@ class OAuthException(Exception): |
|
|
|
self.type = type |
|
|
|
|
|
|
|
class Main(http.Controller): |
|
|
|
def __get_authorization_code_jwk(self, req): |
|
|
|
return jwk_from_json(req.env['ir.config_parameter'].sudo().get_param( |
|
|
|
def __get_authorization_code_jwk(self): |
|
|
|
return jwk_from_json(request.env['ir.config_parameter'].sudo().get_param( |
|
|
|
'galicea_openid_connect.authorization_code_jwk' |
|
|
|
)) |
|
|
|
|
|
|
|
def __get_id_token_jwk(self, req): |
|
|
|
return jwk_from_json(req.env['ir.config_parameter'].sudo().get_param( |
|
|
|
def __get_id_token_jwk(self): |
|
|
|
return jwk_from_json(request.env['ir.config_parameter'].sudo().get_param( |
|
|
|
'galicea_openid_connect.id_token_jwk' |
|
|
|
)) |
|
|
|
|
|
|
|
def __validate_client(self, req, **query): |
|
|
|
def __validate_client(self, **query): |
|
|
|
if 'client_id' not in query: |
|
|
|
raise OAuthException( |
|
|
|
'client_id param is missing', |
|
|
|
OAuthException.INVALID_CLIENT, |
|
|
|
) |
|
|
|
client_id = query['client_id'] |
|
|
|
client = req.env['galicea_openid_connect.client'].sudo().search( |
|
|
|
client = request.env['galicea_openid_connect.client'].sudo().search( |
|
|
|
[('client_id', '=', client_id)] |
|
|
|
) |
|
|
|
if not client: |
|
|
@ -81,7 +85,7 @@ class Main(http.Controller): |
|
|
|
) |
|
|
|
return client |
|
|
|
|
|
|
|
def __validate_redirect_uri(self, client, req, **query): |
|
|
|
def __validate_redirect_uri(self, client, **query): |
|
|
|
if 'redirect_uri' not in query: |
|
|
|
raise OAuthException( |
|
|
|
'redirect_uri param is missing', |
|
|
@ -97,7 +101,7 @@ class Main(http.Controller): |
|
|
|
|
|
|
|
return redirect_uri |
|
|
|
|
|
|
|
def __validate_client_secret(self, client, req, **query): |
|
|
|
def __validate_client_secret(self, client, **query): |
|
|
|
if 'client_secret' not in query or query['client_secret'] != client.secret: |
|
|
|
raise OAuthException( |
|
|
|
'client_secret param is not valid', |
|
|
@ -125,7 +129,7 @@ class Main(http.Controller): |
|
|
|
@http.route('/oauth/jwks', auth='public', type='http') |
|
|
|
def jwks(self, **query): |
|
|
|
keyset = jwk.JWKSet() |
|
|
|
keyset.add(self.__get_id_token_jwk(request)) |
|
|
|
keyset.add(self.__get_id_token_jwk()) |
|
|
|
return keyset.export(private_keys=False) |
|
|
|
|
|
|
|
@resource('/oauth/userinfo', method='GET') |
|
|
@ -152,8 +156,8 @@ class Main(http.Controller): |
|
|
|
def authorize(self, **query): |
|
|
|
# First, validate client_id and redirect_uri params. |
|
|
|
try: |
|
|
|
client = self.__validate_client(request, **query) |
|
|
|
redirect_uri = self.__validate_redirect_uri(client, request, **query) |
|
|
|
client = self.__validate_client(**query) |
|
|
|
redirect_uri = self.__validate_redirect_uri(client, **query) |
|
|
|
except OAuthException as e: |
|
|
|
# If those are not valid, we must not redirect back to the client |
|
|
|
# - instead, we display a message to the user |
|
|
@ -203,14 +207,14 @@ class Main(http.Controller): |
|
|
|
needs_login = True |
|
|
|
if needs_login: |
|
|
|
params = { |
|
|
|
'force_auth_and_redirect': '/oauth/authorize?{}'.format(werkzeug.url_encode(query)) |
|
|
|
'force_auth_and_redirect': '/oauth/authorize?{}'.format(url_encode(query)) |
|
|
|
} |
|
|
|
return self.__redirect('/web/login', params, 'query') |
|
|
|
|
|
|
|
response_types = response_type.split() |
|
|
|
|
|
|
|
extra_claims = { |
|
|
|
'sid': http.request.httprequest.session.sid, |
|
|
|
'sid': http.request.session.sid, |
|
|
|
} |
|
|
|
if 'nonce' in query: |
|
|
|
extra_claims['nonce'] = query['nonce'] |
|
|
@ -228,7 +232,7 @@ class Main(http.Controller): |
|
|
|
'exp': int(time.time()) + 60 |
|
|
|
} |
|
|
|
payload.update(extra_claims) |
|
|
|
key = self.__get_authorization_code_jwk(request) |
|
|
|
key = self.__get_authorization_code_jwk() |
|
|
|
response_params['code'] = jwt_encode(payload, key) |
|
|
|
if 'token' in response_types: |
|
|
|
access_token = request.env['galicea_openid_connect.access_token'].sudo().retrieve_or_create( |
|
|
@ -244,23 +248,24 @@ class Main(http.Controller): |
|
|
|
#extra_claims['at_hash'] = base64.urlsafe_b64encode(at_hash[:16]).strip('=') |
|
|
|
extra_claims['at_hash'] = base64.urlsafe_b64encode(at_hash[:16]) |
|
|
|
if 'id_token' in response_types: |
|
|
|
response_params['id_token'] = self.__create_id_token(request, user.id, client, extra_claims) |
|
|
|
response_params['id_token'] = self.__create_id_token(user.id, client, extra_claims) |
|
|
|
|
|
|
|
return self.__redirect(redirect_uri, response_params, response_mode) |
|
|
|
|
|
|
|
@http.route('/oauth/token', auth='public', type='http', methods=['POST', 'OPTIONS'], csrf=False) |
|
|
|
def token(self, req, **query): |
|
|
|
def token(self, **query): |
|
|
|
cors_headers = { |
|
|
|
'Access-Control-Allow-Origin': '*', |
|
|
|
'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept, X-Debug-Mode, Authorization', |
|
|
|
'Access-Control-Max-Age': 60 * 60 * 24, |
|
|
|
} |
|
|
|
if req.httprequest.method == 'OPTIONS': |
|
|
|
_logger.info("Test1 %s" % request.httprequest.method) |
|
|
|
if request.httprequest.method == 'OPTIONS': |
|
|
|
return http.Response( |
|
|
|
status=200, |
|
|
|
headers=cors_headers |
|
|
|
) |
|
|
|
|
|
|
|
_logger.info("Test2 %s" % query) |
|
|
|
try: |
|
|
|
if 'grant_type' not in query: |
|
|
|
raise OAuthException( |
|
|
@ -268,12 +273,15 @@ class Main(http.Controller): |
|
|
|
OAuthException.INVALID_REQUEST, |
|
|
|
) |
|
|
|
if query['grant_type'] == 'authorization_code': |
|
|
|
return json.dumps(self.__handle_grant_type_authorization_code(req, **query)) |
|
|
|
_logger.info("Test3") |
|
|
|
return json.dumps(self.__handle_grant_type_authorization_code(**query)) |
|
|
|
elif query['grant_type'] == 'client_credentials': |
|
|
|
return json.dumps(self.__handle_grant_type_client_credentials(req, **query)) |
|
|
|
_logger.info("Test4") |
|
|
|
return json.dumps(self.__handle_grant_type_client_credentials(**query)) |
|
|
|
elif query['grant_type'] == 'password': |
|
|
|
_logger.info("Test5") |
|
|
|
return werkzeug.Response( |
|
|
|
response=json.dumps(self.__handle_grant_type_password(req, **query)), |
|
|
|
response=json.dumps(self.__handle_grant_type_password(**query)), |
|
|
|
headers=cors_headers |
|
|
|
) |
|
|
|
else: |
|
|
@ -285,10 +293,10 @@ class Main(http.Controller): |
|
|
|
body = json.dumps({'error': e.type, 'error_description': e}) |
|
|
|
return werkzeug.Response(response=body, status=400, headers=cors_headers) |
|
|
|
|
|
|
|
def __handle_grant_type_authorization_code(self, req, **query): |
|
|
|
client = self.__validate_client(req, **query) |
|
|
|
redirect_uri = self.__validate_redirect_uri(client, req, **query) |
|
|
|
self.__validate_client_secret(client, req, **query) |
|
|
|
def __handle_grant_type_authorization_code(self, **query): |
|
|
|
client = self.__validate_client(**query) |
|
|
|
redirect_uri = self.__validate_redirect_uri(client, **query) |
|
|
|
self.__validate_client_secret(client, **query) |
|
|
|
|
|
|
|
if 'code' not in query: |
|
|
|
raise OAuthException( |
|
|
@ -296,7 +304,7 @@ class Main(http.Controller): |
|
|
|
OAuthException.INVALID_GRANT, |
|
|
|
) |
|
|
|
try: |
|
|
|
payload = jwt_decode(query['code'], self.__get_authorization_code_jwk(req)) |
|
|
|
payload = jwt_decode(query['code'], self.__get_authorization_code_jwk()) |
|
|
|
except jwt.JWTExpired: |
|
|
|
raise OAuthException( |
|
|
|
'Code expired', |
|
|
@ -319,7 +327,7 @@ class Main(http.Controller): |
|
|
|
) |
|
|
|
|
|
|
|
# Retrieve/generate access token. We currently only store one per user/client |
|
|
|
token = req.env['galicea_openid_connect.access_token'].sudo().retrieve_or_create( |
|
|
|
token = request.env['galicea_openid_connect.access_token'].sudo().retrieve_or_create( |
|
|
|
payload['user_id'], |
|
|
|
client.id |
|
|
|
) |
|
|
@ -329,11 +337,11 @@ class Main(http.Controller): |
|
|
|
} |
|
|
|
if 'openid' in payload['scopes']: |
|
|
|
extra_claims = { name: payload[name] for name in payload if name in ['sid', 'nonce'] } |
|
|
|
response['id_token'] = self.__create_id_token(req, payload['user_id'], client, extra_claims) |
|
|
|
response['id_token'] = self.__create_id_token(payload['user_id'], client, extra_claims) |
|
|
|
return response |
|
|
|
|
|
|
|
def __handle_grant_type_password(self, req, **query): |
|
|
|
client = self.__validate_client(req, **query) |
|
|
|
def __handle_grant_type_password(self, **query): |
|
|
|
client = self.__validate_client(**query) |
|
|
|
if not client.allow_password_grant: |
|
|
|
raise OAuthException( |
|
|
|
'This client is not allowed to perform password flow', |
|
|
@ -346,8 +354,8 @@ class Main(http.Controller): |
|
|
|
'{} is required'.format(param), |
|
|
|
OAuthException.INVALID_REQUEST |
|
|
|
) |
|
|
|
user_id = req.env['res.users'].authenticate( |
|
|
|
req.env.cr.dbname, |
|
|
|
user_id = request.env['res.users'].authenticate( |
|
|
|
request.env.cr.dbname, |
|
|
|
query['username'], |
|
|
|
query['password'], |
|
|
|
None |
|
|
@ -360,7 +368,7 @@ class Main(http.Controller): |
|
|
|
|
|
|
|
scopes = query['scope'].split(' ') if query.get('scope') else [] |
|
|
|
# Retrieve/generate access token. We currently only store one per user/client |
|
|
|
token = req.env['galicea_openid_connect.access_token'].sudo().retrieve_or_create( |
|
|
|
token = request.env['galicea_openid_connect.access_token'].sudo().retrieve_or_create( |
|
|
|
user_id, |
|
|
|
client.id |
|
|
|
) |
|
|
@ -369,19 +377,19 @@ class Main(http.Controller): |
|
|
|
'token_type': 'bearer' |
|
|
|
} |
|
|
|
if 'openid' in scopes: |
|
|
|
response['id_token'] = self.__create_id_token(req, user_id, client, {}) |
|
|
|
response['id_token'] = self.__create_id_token(user_id, client, {}) |
|
|
|
return response |
|
|
|
|
|
|
|
def __handle_grant_type_client_credentials(self, req, **query): |
|
|
|
client = self.__validate_client(req, **query) |
|
|
|
self.__validate_client_secret(client, req, **query) |
|
|
|
token = req.env['galicea_openid_connect.client_access_token'].sudo().retrieve_or_create(client.id) |
|
|
|
def __handle_grant_type_client_credentials(self, **query): |
|
|
|
client = self.__validate_client(**query) |
|
|
|
self.__validate_client_secret(client, **query) |
|
|
|
token = request.env['galicea_openid_connect.client_access_token'].sudo().retrieve_or_create(client.id) |
|
|
|
return { |
|
|
|
'access_token': token.token, |
|
|
|
'token_type': 'bearer' |
|
|
|
} |
|
|
|
|
|
|
|
def __create_id_token(self, req, user_id, client, extra_claims): |
|
|
|
def __create_id_token(self, user_id, client, extra_claims): |
|
|
|
claims = { |
|
|
|
'iss': http.request.httprequest.host_url, |
|
|
|
'sub': str(user_id), |
|
|
@ -397,14 +405,14 @@ class Main(http.Controller): |
|
|
|
if 'at_hash' in extra_claims: |
|
|
|
claims['at_hash'] = extra_claims['at_hash'] |
|
|
|
|
|
|
|
key = self.__get_id_token_jwk(req) |
|
|
|
key = self.__get_id_token_jwk() |
|
|
|
return jwt_encode(claims, key) |
|
|
|
|
|
|
|
def __redirect(self, url, params, response_mode): |
|
|
|
location = '{}{}{}'.format( |
|
|
|
url, |
|
|
|
'?' if response_mode == 'query' else '#', |
|
|
|
werkzeug.url_encode(params) |
|
|
|
url_encode(params) |
|
|
|
) |
|
|
|
return werkzeug.Response( |
|
|
|
headers={'Location': location}, |
|
|
|