Merge pull request #48 from ltamaster/add-oidc-auth-support
Add oidc auth
This commit is contained in:
commit
2010e2d1ee
@ -15,13 +15,17 @@
|
||||
import atexit
|
||||
import base64
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import google.auth
|
||||
import google.auth.transport.requests
|
||||
import oauthlib.oauth2
|
||||
import urllib3
|
||||
import yaml
|
||||
from requests_oauthlib import OAuth2Session
|
||||
from six import PY3
|
||||
|
||||
from kubernetes.client import ApiClient, Configuration
|
||||
|
||||
@ -169,7 +173,8 @@ class KubeConfigLoader(object):
|
||||
1. GCP auth-provider
|
||||
2. token_data
|
||||
3. token field (point to a token file)
|
||||
4. username/password
|
||||
4. oidc auth-provider
|
||||
5. username/password
|
||||
"""
|
||||
if not self._user:
|
||||
return
|
||||
@ -177,6 +182,8 @@ class KubeConfigLoader(object):
|
||||
return
|
||||
if self._load_user_token():
|
||||
return
|
||||
if self._load_oid_token():
|
||||
return
|
||||
self._load_user_pass_token()
|
||||
|
||||
def _load_gcp_token(self):
|
||||
@ -208,6 +215,100 @@ class KubeConfigLoader(object):
|
||||
if self._config_persister:
|
||||
self._config_persister(self._config.value)
|
||||
|
||||
def _load_oid_token(self):
|
||||
if 'auth-provider' not in self._user:
|
||||
return
|
||||
provider = self._user['auth-provider']
|
||||
|
||||
if 'name' not in provider or 'config' not in provider:
|
||||
return
|
||||
|
||||
if provider['name'] != 'oidc':
|
||||
return
|
||||
|
||||
parts = provider['config']['id-token'].split('.')
|
||||
|
||||
if len(parts) != 3: # Not a valid JWT
|
||||
return None
|
||||
|
||||
if PY3:
|
||||
jwt_attributes = json.loads(
|
||||
base64.b64decode(parts[1]).decode('utf-8')
|
||||
)
|
||||
else:
|
||||
jwt_attributes = json.loads(
|
||||
base64.b64decode(parts[1] + "==")
|
||||
)
|
||||
|
||||
expire = jwt_attributes.get('exp')
|
||||
|
||||
if ((expire is not None) and
|
||||
(_is_expired(datetime.datetime.fromtimestamp(expire,
|
||||
tz=UTC)))):
|
||||
self._refresh_oidc(provider)
|
||||
|
||||
if self._config_persister:
|
||||
self._config_persister(self._config.value)
|
||||
|
||||
self.token = "Bearer %s" % provider['config']['id-token']
|
||||
|
||||
return self.token
|
||||
|
||||
def _refresh_oidc(self, provider):
|
||||
ca_cert = tempfile.NamedTemporaryFile(delete=True)
|
||||
|
||||
if PY3:
|
||||
cert = base64.b64decode(
|
||||
provider['config']['idp-certificate-authority-data']
|
||||
).decode('utf-8')
|
||||
else:
|
||||
cert = base64.b64decode(
|
||||
provider['config']['idp-certificate-authority-data'] + "=="
|
||||
)
|
||||
|
||||
with open(ca_cert.name, 'w') as fh:
|
||||
fh.write(cert)
|
||||
|
||||
config = Configuration()
|
||||
config.ssl_ca_cert = ca_cert.name
|
||||
|
||||
client = ApiClient(configuration=config)
|
||||
|
||||
response = client.request(
|
||||
method="GET",
|
||||
url="%s/.well-known/openid-configuration"
|
||||
% provider['config']['idp-issuer-url']
|
||||
)
|
||||
|
||||
if response.status != 200:
|
||||
return
|
||||
|
||||
response = json.loads(response.data)
|
||||
|
||||
request = OAuth2Session(
|
||||
client_id=provider['config']['client-id'],
|
||||
token=provider['config']['refresh-token'],
|
||||
auto_refresh_kwargs={
|
||||
'client_id': provider['config']['client-id'],
|
||||
'client_secret': provider['config']['client-secret']
|
||||
},
|
||||
auto_refresh_url=response['token_endpoint']
|
||||
)
|
||||
|
||||
try:
|
||||
refresh = request.refresh_token(
|
||||
token_url=response['token_endpoint'],
|
||||
refresh_token=provider['config']['refresh-token'],
|
||||
auth=(provider['config']['client-id'],
|
||||
provider['config']['client-secret']),
|
||||
verify=ca_cert.name
|
||||
)
|
||||
except oauthlib.oauth2.rfc6749.errors.InvalidClientIdError:
|
||||
return
|
||||
|
||||
provider['config'].value['id-token'] = refresh['id_token']
|
||||
provider['config'].value['refresh-token'] = refresh['refresh_token']
|
||||
|
||||
def _load_user_token(self):
|
||||
token = FileOrData(
|
||||
self._user, 'tokenFile', 'token',
|
||||
|
||||
@ -14,11 +14,13 @@
|
||||
|
||||
import base64
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
import yaml
|
||||
from six import PY3
|
||||
|
||||
@ -67,6 +69,17 @@ TEST_CLIENT_CERT = "client-cert"
|
||||
TEST_CLIENT_CERT_BASE64 = _base64(TEST_CLIENT_CERT)
|
||||
|
||||
|
||||
TEST_OIDC_TOKEN = "test-oidc-token"
|
||||
TEST_OIDC_INFO = "{\"name\": \"test\"}"
|
||||
TEST_OIDC_BASE = _base64(TEST_OIDC_TOKEN) + "." + _base64(TEST_OIDC_INFO)
|
||||
TEST_OIDC_LOGIN = TEST_OIDC_BASE + "." + TEST_CLIENT_CERT_BASE64
|
||||
TEST_OIDC_TOKEN = "Bearer %s" % TEST_OIDC_LOGIN
|
||||
TEST_OIDC_EXP = "{\"name\": \"test\",\"exp\": 536457600}"
|
||||
TEST_OIDC_EXP_BASE = _base64(TEST_OIDC_TOKEN) + "." + _base64(TEST_OIDC_EXP)
|
||||
TEST_OIDC_EXPIRED_LOGIN = TEST_OIDC_EXP_BASE + "." + TEST_CLIENT_CERT_BASE64
|
||||
TEST_OIDC_CA = _base64(TEST_CERTIFICATE_AUTH)
|
||||
|
||||
|
||||
class BaseTestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
@ -326,6 +339,20 @@ class TestKubeConfigLoader(BaseTestCase):
|
||||
"user": "expired_gcp"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "oidc",
|
||||
"context": {
|
||||
"cluster": "default",
|
||||
"user": "oidc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "expired_oidc",
|
||||
"context": {
|
||||
"cluster": "default",
|
||||
"user": "expired_oidc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "user_pass",
|
||||
"context": {
|
||||
@ -443,6 +470,33 @@ class TestKubeConfigLoader(BaseTestCase):
|
||||
"password": TEST_PASSWORD, # should be ignored
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "oidc",
|
||||
"user": {
|
||||
"auth-provider": {
|
||||
"name": "oidc",
|
||||
"config": {
|
||||
"id-token": TEST_OIDC_LOGIN
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "expired_oidc",
|
||||
"user": {
|
||||
"auth-provider": {
|
||||
"name": "oidc",
|
||||
"config": {
|
||||
"client-id": "tectonic-kubectl",
|
||||
"client-secret": "FAKE_SECRET",
|
||||
"id-token": TEST_OIDC_EXPIRED_LOGIN,
|
||||
"idp-certificate-authority-data": TEST_OIDC_CA,
|
||||
"idp-issuer-url": "https://example.org/identity",
|
||||
"refresh-token": "lucWJjEhlxZW01cXI3YmVlcYnpxNGhzk"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "user_pass",
|
||||
"user": {
|
||||
@ -537,6 +591,39 @@ class TestKubeConfigLoader(BaseTestCase):
|
||||
self.assertEqual(BEARER_TOKEN_FORMAT % TEST_ANOTHER_DATA_BASE64,
|
||||
loader.token)
|
||||
|
||||
def test_oidc_no_refresh(self):
|
||||
loader = KubeConfigLoader(
|
||||
config_dict=self.TEST_KUBE_CONFIG,
|
||||
active_context="oidc",
|
||||
)
|
||||
self.assertTrue(loader._load_oid_token())
|
||||
self.assertEqual(TEST_OIDC_TOKEN, loader.token)
|
||||
|
||||
@mock.patch('kubernetes.config.kube_config.OAuth2Session.refresh_token')
|
||||
@mock.patch('kubernetes.config.kube_config.ApiClient.request')
|
||||
def test_oidc_with_refresh(self, mock_ApiClient, mock_OAuth2Session):
|
||||
mock_response = mock.MagicMock()
|
||||
type(mock_response).status = mock.PropertyMock(
|
||||
return_value=200
|
||||
)
|
||||
type(mock_response).data = mock.PropertyMock(
|
||||
return_value=json.dumps({
|
||||
"token_endpoint": "https://example.org/identity/token"
|
||||
})
|
||||
)
|
||||
|
||||
mock_ApiClient.return_value = mock_response
|
||||
|
||||
mock_OAuth2Session.return_value = {"id_token": "abc123",
|
||||
"refresh_token": "newtoken123"}
|
||||
|
||||
loader = KubeConfigLoader(
|
||||
config_dict=self.TEST_KUBE_CONFIG,
|
||||
active_context="expired_oidc",
|
||||
)
|
||||
self.assertTrue(loader._load_oid_token())
|
||||
self.assertEqual("Bearer abc123", loader.token)
|
||||
|
||||
def test_user_pass(self):
|
||||
expected = FakeConfig(host=TEST_HOST, token=TEST_BASIC_TOKEN)
|
||||
actual = FakeConfig()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user