Skip to content

Commit 14fd0c4

Browse files
committed
feat(auth): support client-credentials & static token for OIDC client auth
1 parent 9f85892 commit 14fd0c4

File tree

3 files changed

+89
-15
lines changed

3 files changed

+89
-15
lines changed

sdk/python/feast/permissions/auth_model.py

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,29 @@
1-
from typing import Literal
1+
# --------------------------------------------------------------------
2+
# Extends OIDC client auth model with an optional `token` field.
3+
# Works on Pydantic v1 and v2.
4+
#
5+
# Accepted credential sets (exactly **one** of):
6+
# 1 pre-issued `token`
7+
# 2 `client_secret` (client-credentials flow)
8+
# 3 `username` + `password` + `client_secret` (ROPG)
9+
# --------------------------------------------------------------------
10+
from __future__ import annotations
11+
from typing import Literal, Optional
212

313
from feast.repo_config import FeastConfigBaseModel
414

15+
# pick the correct validator decorator for current Pydantic version
16+
try: # Pydantic ≥ 2.0
17+
from pydantic import model_validator as _v2 # type: ignore
18+
19+
def _cred_validator(fn):
20+
return _v2(mode="after")(fn) # run after field validation
21+
except ImportError: # Pydantic 1.x
22+
from pydantic import root_validator as _v1 # type: ignore
23+
24+
def _cred_validator(fn):
25+
return _v1(skip_on_failure=True)(fn)
26+
527

628
class AuthConfig(FeastConfigBaseModel):
729
type: Literal["oidc", "kubernetes", "no_auth"] = "no_auth"
@@ -13,9 +35,40 @@ class OidcAuthConfig(AuthConfig):
1335

1436

1537
class OidcClientAuthConfig(OidcAuthConfig):
16-
username: str
17-
password: str
18-
client_secret: str
38+
# any **one** of the four fields below is sufficient
39+
username: Optional[str] = None
40+
password: Optional[str] = None
41+
client_secret: Optional[str] = None
42+
token: Optional[str] = None # pre-issued `token`
43+
44+
@_cred_validator
45+
def _validate_credentials(cls, values):
46+
"""Enforce exactly one valid credential set."""
47+
d = values.__dict__ if hasattr(values, "__dict__") else values
48+
49+
has_user_pass = bool(d.get("username")) and bool(d.get("password"))
50+
has_secret = bool(d.get("client_secret"))
51+
has_token = bool(d.get("token"))
52+
53+
# 1 static token
54+
if has_token and not (has_user_pass or has_secret):
55+
return values
56+
57+
# 2 client_credentials
58+
if has_secret and not has_user_pass and not has_token:
59+
return values
60+
61+
# 3 ROPG
62+
if has_user_pass and has_secret and not has_token:
63+
return values
64+
65+
raise ValueError(
66+
"Invalid OIDC client auth combination: "
67+
"provide either\n"
68+
" • token\n"
69+
" • client_secret (without username/password)\n"
70+
" • username + password + client_secret"
71+
)
1972

2073

2174
class NoAuthConfig(AuthConfig):

sdk/python/feast/permissions/client/oidc_authentication_client_manager.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,28 @@ def get_token(self):
3030
self.auth_config.auth_discovery_url
3131
).get_token_url()
3232

33-
token_request_body = {
34-
"grant_type": "password",
35-
"client_id": self.auth_config.client_id,
36-
"client_secret": self.auth_config.client_secret,
37-
"username": self.auth_config.username,
38-
"password": self.auth_config.password,
39-
}
33+
# 1) pre-issued JWT supplied in config
34+
if getattr(self.auth_config, "token", None):
35+
return self.auth_config.token
36+
37+
# 2) client_credentials
38+
if self.auth_config.client_secret and not (
39+
self.auth_config.username and self.auth_config.password
40+
):
41+
token_request_body = {
42+
"grant_type": "client_credentials",
43+
"client_id": self.auth_config.client_id,
44+
"client_secret": self.auth_config.client_secret,
45+
}
46+
# 3) ROPG (username + password + client_secret)
47+
else:
48+
token_request_body = {
49+
"grant_type": "password",
50+
"client_id": self.auth_config.client_id,
51+
"client_secret": self.auth_config.client_secret,
52+
"username": self.auth_config.username,
53+
"password": self.auth_config.password,
54+
}
4055
headers = {"Content-Type": "application/x-www-form-urlencoded"}
4156

4257
token_response = requests.post(
@@ -53,4 +68,4 @@ def get_token(self):
5368
else:
5469
raise RuntimeError(
5570
f"""Failed to obtain oidc access token:url=[{token_endpoint}] {token_response.status_code} - {token_response.text}"""
56-
)
71+
)

sdk/python/feast/repo_config.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -308,11 +308,17 @@ def offline_store(self):
308308
def auth_config(self):
309309
if not self._auth:
310310
if isinstance(self.auth, Dict):
311+
# treat this auth block as *client-side* OIDC when it matches
312+
# 1) ROPG – username + password + client_secret
313+
# 2) client-credentials – client_secret only
314+
# 3) static token – token
311315
is_oidc_client = (
312316
self.auth.get("type") == AuthType.OIDC.value
313-
and "username" in self.auth
314-
and "password" in self.auth
315-
and "client_secret" in self.auth
317+
and (
318+
("username" in self.auth and "password" in self.auth and "client_secret" in self.auth) # 1
319+
or ("client_secret" in self.auth and "username" not in self.auth and "password" not in self.auth) # 2
320+
or ("token" in self.auth) # 3
321+
)
316322
)
317323
self._auth = get_auth_config_from_type(
318324
"oidc_client" if is_oidc_client else self.auth.get("type")

0 commit comments

Comments
 (0)