Browse Source

[FIX] new request management in V16.0

16.0
default 2 years ago
parent
commit
b4c50708de
  1. 23
      galicea_openid_connect/api.py
  2. 7
      galicea_openid_connect/controllers/ext_web_login.py
  3. 90
      galicea_openid_connect/controllers/main.py

23
galicea_openid_connect/api.py

@ -5,6 +5,7 @@ import logging
from functools import wraps from functools import wraps
from odoo import http from odoo import http
from odoo.http import request
import werkzeug import werkzeug
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@ -22,13 +23,13 @@ def resource(path, method, auth='user'):
def endpoint_decorator(func): def endpoint_decorator(func):
@http.route(path, auth='public', type='http', methods=[method, 'OPTIONS'], csrf=False) @http.route(path, auth='public', type='http', methods=[method, 'OPTIONS'], csrf=False)
@wraps(func) @wraps(func)
def func_wrapper(self, req, **query):
def func_wrapper(self, **query):
cors_headers = { cors_headers = {
'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept, X-Debug-Mode, Authorization', 'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept, X-Debug-Mode, Authorization',
'Access-Control-Max-Age': 60 * 60 * 24, 'Access-Control-Max-Age': 60 * 60 * 24,
} }
if req.httprequest.method == 'OPTIONS':
if request.httprequest.method == 'OPTIONS':
return http.Response( return http.Response(
status=200, status=200,
headers=cors_headers headers=cors_headers
@ -36,8 +37,8 @@ def resource(path, method, auth='user'):
try: try:
access_token = None access_token = None
if 'Authorization' in req.httprequest.headers:
authorization_header = req.httprequest.headers['Authorization']
if 'Authorization' in request.httprequest.headers:
authorization_header = request.httprequest.headers['Authorization']
if authorization_header[:7] == 'Bearer ': if authorization_header[:7] == 'Bearer ':
access_token = authorization_header.split(' ', 1)[1] access_token = authorization_header.split(' ', 1)[1]
if access_token is None: if access_token is None:
@ -48,7 +49,7 @@ def resource(path, method, auth='user'):
'invalid_request', 'invalid_request',
) )
if auth == 'user': if auth == 'user':
token = req.env['galicea_openid_connect.access_token'].sudo().search(
token = request.env['galicea_openid_connect.access_token'].sudo().search(
[('token', '=', access_token)] [('token', '=', access_token)]
) )
if not token: if not token:
@ -56,9 +57,9 @@ def resource(path, method, auth='user'):
'access_token is invalid', 'access_token is invalid',
'invalid_request', 'invalid_request',
) )
req.uid = token.user_id.id
request.update_env(user=token.user_id.id)
elif auth == 'client': elif auth == 'client':
token = req.env['galicea_openid_connect.client_access_token'].sudo().search(
token = request.env['galicea_openid_connect.client_access_token'].sudo().search(
[('token', '=', access_token)] [('token', '=', access_token)]
) )
if not token: if not token:
@ -66,13 +67,11 @@ def resource(path, method, auth='user'):
'access_token is invalid', 'access_token is invalid',
'invalid_request', 'invalid_request',
) )
req.uid = token.client_id.system_user_id.id
request.update_env(user=token.client_id.system_user_id.id)
ctx = req.context.copy()
ctx.update({'client_id': token.client_id.id})
req.context = ctx
request.update_env(context={'client_id': token.client_id.id})
response = func(self, req, **query)
response = func(self, **query)
return werkzeug.Response( return werkzeug.Response(
response=json.dumps(response), response=json.dumps(response),
headers=cors_headers, headers=cors_headers,

7
galicea_openid_connect/controllers/ext_web_login.py

@ -3,15 +3,16 @@
import time import time
from odoo import http from odoo import http
from odoo.http import request
from odoo.addons import web from odoo.addons import web
class Home(web.controllers.main.Home):
class Home(web.controllers.home.Home):
@http.route('/web/login', type='http', auth="none") @http.route('/web/login', type='http', auth="none")
def web_login(self, redirect=None, **kw): def web_login(self, redirect=None, **kw):
result = super(Home, self).web_login(redirect, **kw) result = super(Home, self).web_login(redirect, **kw)
if result.is_qweb and 'force_auth_and_redirect' in kw: if result.is_qweb and 'force_auth_and_redirect' in kw:
result.qcontext['redirect'] = kw['force_auth_and_redirect'] result.qcontext['redirect'] = kw['force_auth_and_redirect']
if http.request.params.get('login_success'):
http.request.session['auth_time'] = int(time.time())
if request.params.get('login_success'):
request.session['auth_time'] = int(time.time())
return result return result

90
galicea_openid_connect/controllers/main.py

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

Loading…
Cancel
Save