Merge pull request #48 from ltamaster/add-oidc-auth-support

Add oidc auth
This commit is contained in:
Yu Liao 2018-03-20 14:52:39 -07:00 committed by GitHub
commit 2010e2d1ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 189 additions and 1 deletions

View File

@ -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',

View File

@ -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()