Skip to content
This repository has been archived by the owner on Mar 13, 2022. It is now read-only.

Add oidc auth #48

Merged
merged 2 commits into from
Mar 20, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 102 additions & 1 deletion config/kube_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -169,14 +173,17 @@ def _load_authentication(self):
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
if self._load_gcp_token():
return
if self._load_user_token():
return
if self._load_oid_token():
return
self._load_user_pass_token()

def _load_gcp_token(self):
Expand Down Expand Up @@ -208,6 +215,100 @@ def _refresh_gcp_token(self):
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',
Expand Down
87 changes: 87 additions & 0 deletions config/kube_config_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -67,6 +69,17 @@ def _raise_exception(st):
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):
Expand Down Expand Up @@ -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": {
Expand Down Expand Up @@ -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": {
Expand Down Expand Up @@ -537,6 +591,39 @@ def cred(): return None
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()
Expand Down