Merge pull request #2695 from coreos-inc/oidc-internal-auth
OIDC internal auth support
This commit is contained in:
commit
3bef21253d
29 changed files with 341 additions and 38 deletions
|
@ -10,6 +10,7 @@ from data.users.database import DatabaseUsers
|
|||
from data.users.externalldap import LDAPUsers
|
||||
from data.users.externaljwt import ExternalJWTAuthN
|
||||
from data.users.keystone import get_keystone_users
|
||||
from data.users.oidc import OIDCInternalAuth
|
||||
from util.security.aes import AESCipher
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -24,6 +25,12 @@ def get_federated_service_name(authentication_type):
|
|||
if authentication_type == 'Keystone':
|
||||
return 'keystone'
|
||||
|
||||
if authentication_type == 'OIDC':
|
||||
return None
|
||||
|
||||
if authentication_type == 'Database':
|
||||
return None
|
||||
|
||||
raise Exception('Unknown auth type: %s' % authentication_type)
|
||||
|
||||
|
||||
|
@ -74,8 +81,15 @@ def get_users_handler(config, _, override_config_dir):
|
|||
keystone_admin_password = config.get('KEYSTONE_ADMIN_PASSWORD')
|
||||
keystone_admin_tenant = config.get('KEYSTONE_ADMIN_TENANT')
|
||||
return get_keystone_users(auth_version, auth_url, keystone_admin_username,
|
||||
keystone_admin_password, keystone_admin_tenant, timeout,
|
||||
requires_email=features.MAILING)
|
||||
keystone_admin_password, keystone_admin_tenant, timeout,
|
||||
requires_email=features.MAILING)
|
||||
|
||||
if authentication_type == 'OIDC':
|
||||
if features.DIRECT_LOGIN:
|
||||
raise Exception('Direct login feature must be disabled to use OIDC internal auth')
|
||||
|
||||
login_service = config.get('INTERNAL_OIDC_SERVICE_ID')
|
||||
return OIDCInternalAuth(config, login_service, requires_email=features.MAILING)
|
||||
|
||||
raise RuntimeError('Unknown authentication type: %s' % authentication_type)
|
||||
|
||||
|
@ -160,6 +174,11 @@ class UserAuthentication(object):
|
|||
"""
|
||||
return self.state.federated_service
|
||||
|
||||
@property
|
||||
def supports_encrypted_credentials(self):
|
||||
""" Returns whether this auth system supports using encrypted credentials. """
|
||||
return self.state.supports_encrypted_credentials
|
||||
|
||||
def query_users(self, query, limit=20):
|
||||
""" Performs a lookup against the user system for the specified query. The returned tuple
|
||||
will be of the form (results, federated_login_id, err_msg). If the method is unsupported,
|
||||
|
|
|
@ -9,6 +9,10 @@ class DatabaseUsers(object):
|
|||
""" Always assumed to be working. If the DB is broken, other checks will handle it. """
|
||||
return (True, None)
|
||||
|
||||
@property
|
||||
def supports_encrypted_credentials(self):
|
||||
return True
|
||||
|
||||
def verify_credentials(self, username_or_email, password):
|
||||
""" Simply delegate to the model implementation. """
|
||||
result = model.user.verify_user(username_or_email, password)
|
||||
|
|
|
@ -24,6 +24,10 @@ class FederatedUsers(object):
|
|||
def federated_service(self):
|
||||
return self._federated_service
|
||||
|
||||
@property
|
||||
def supports_encrypted_credentials(self):
|
||||
return True
|
||||
|
||||
def get_user(self, username_or_email):
|
||||
""" Retrieves the user with the given username or email, returning a tuple containing
|
||||
a UserInformation (if success) and the error message (on failure).
|
||||
|
|
83
data/users/oidc.py
Normal file
83
data/users/oidc.py
Normal file
|
@ -0,0 +1,83 @@
|
|||
import logging
|
||||
|
||||
from data import model
|
||||
from oauth.loginmanager import OAuthLoginManager
|
||||
from oauth.oidc import PublicKeyLoadException
|
||||
from util.security.jwtutil import InvalidTokenError
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UnknownServiceException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class OIDCInternalAuth(object):
|
||||
""" Handles authentication by delegating authentication to a signed OIDC JWT produced by the
|
||||
configured OIDC service.
|
||||
"""
|
||||
def __init__(self, config, login_service_id, requires_email):
|
||||
login_manager = OAuthLoginManager(config)
|
||||
|
||||
self.login_service_id = login_service_id
|
||||
self.login_service = login_manager.get_service(login_service_id)
|
||||
if self.login_service is None:
|
||||
raise UnknownServiceException('Unknown OIDC login service %s' % login_service_id)
|
||||
|
||||
@property
|
||||
def federated_service(self):
|
||||
return None
|
||||
|
||||
@property
|
||||
def supports_encrypted_credentials(self):
|
||||
# Since the "password" is already a signed JWT.
|
||||
return False
|
||||
|
||||
def verify_credentials(self, username_or_email, id_token):
|
||||
# Parse the ID token.
|
||||
try:
|
||||
payload = self.login_service.decode_user_jwt(id_token)
|
||||
except InvalidTokenError as ite:
|
||||
logger.exception('Got invalid token error on OIDC decode: %s. Token: %s', ite.message, id_token)
|
||||
return (None, 'Could not validate OIDC token')
|
||||
except PublicKeyLoadException as pke:
|
||||
logger.exception('Could not load public key during OIDC decode: %s. Token: %s', pke.message, id_token)
|
||||
return (None, 'Could not validate OIDC token')
|
||||
|
||||
# Find the user ID.
|
||||
user_id = payload['sub']
|
||||
|
||||
# Lookup the federated login and user record with that matching ID and service.
|
||||
user_found = model.user.verify_federated_login(self.login_service_id, user_id)
|
||||
if user_found is None:
|
||||
return (None, 'User does not exist')
|
||||
|
||||
if not user_found.enabled:
|
||||
return (None, 'User account is disabled. Please contact your administrator.')
|
||||
|
||||
return (user_found, None)
|
||||
|
||||
def verify_and_link_user(self, username_or_email, password):
|
||||
return self.verify_credentials(username_or_email, password)
|
||||
|
||||
def confirm_existing_user(self, username, password):
|
||||
return self.verify_credentials(username, password)
|
||||
|
||||
def link_user(self, username_or_email):
|
||||
return (None, 'Unsupported for this authentication system')
|
||||
|
||||
def get_and_link_federated_user_info(self, user_info):
|
||||
return (None, 'Unsupported for this authentication system')
|
||||
|
||||
def query_users(self, query, limit):
|
||||
return (None, '', '')
|
||||
|
||||
def check_group_lookup_args(self, group_lookup_args):
|
||||
return (False, 'Not supported')
|
||||
|
||||
def iterate_group_members(self, group_lookup_args, page_size=None, disable_pagination=False):
|
||||
return (None, 'Not supported')
|
||||
|
||||
def service_metadata(self):
|
||||
return {}
|
38
data/users/test/test_oidc.py
Normal file
38
data/users/test/test_oidc.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
import pytest
|
||||
|
||||
from httmock import HTTMock
|
||||
|
||||
from data import model
|
||||
from data.users.oidc import OIDCInternalAuth
|
||||
from oauth.test.test_oidc import *
|
||||
from test.fixtures import *
|
||||
|
||||
@pytest.mark.parametrize('username, expect_success', [
|
||||
('devtable', True),
|
||||
('disabled', False)
|
||||
])
|
||||
def test_oidc_login(username, expect_success, app_config, id_token, jwks_handler,
|
||||
discovery_handler, app):
|
||||
internal_auth = OIDCInternalAuth(app_config, 'someoidc', False)
|
||||
with HTTMock(jwks_handler, discovery_handler):
|
||||
# Try an invalid token.
|
||||
(user, err) = internal_auth.verify_credentials('someusername', 'invalidtoken')
|
||||
assert err is not None
|
||||
assert user is None
|
||||
|
||||
# Try a valid token for an unlinked user.
|
||||
(user, err) = internal_auth.verify_credentials('someusername', id_token)
|
||||
assert err is not None
|
||||
assert user is None
|
||||
|
||||
# Link the user to the service.
|
||||
model.user.attach_federated_login(model.user.get_user(username), 'someoidc', 'cooluser')
|
||||
|
||||
# Try a valid token for a linked user.
|
||||
(user, err) = internal_auth.verify_credentials('someusername', id_token)
|
||||
if expect_success:
|
||||
assert err is None
|
||||
assert user.username == username
|
||||
else:
|
||||
assert err is not None
|
||||
assert user is None
|
Reference in a new issue