summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authoromnom62 <omnom62@outlook.com>2026-08-14 16:06:57 +1000
committerJohn Estabrook <jestabro@vyos.io>2026-09-01 19:35:25 -0500
commitb8136290e326ba8a1879a3c55eea9e0a1303fedd (patch)
treebec0c69fc2d1fab98484796bf14f7b4207de13d7 /src
parent1832b2bb3e9948f8bf6dfb3be50fd10363bc3a38 (diff)
downloadvyos-1x-b8136290e326ba8a1879a3c55eea9e0a1303fedd.tar.gz
vyos-1x-b8136290e326ba8a1879a3c55eea9e0a1303fedd.zip
http-api: T8989: add OAuth2/OIDC authentication for REST API
Add support for validating OIDC Bearer tokens issued by external identity providers (Keycloak, Azure AD, Okta, Vault, etc.) against the configured JWKS endpoint. Configuration: set service https api rest authentication oidc issuer <url> set service https api rest authentication oidc jwks-url <url> If jwks-url is not set, it is derived from the issuer URL using the standard OpenID Connect discovery path. Auth priority: mTLS -> VyOS JWT -> OIDC -> API key
Diffstat (limited to 'src')
-rw-r--r--src/services/api/rest/libs/token_auth.py31
-rw-r--r--src/services/api/rest/routers.py6
-rw-r--r--src/services/api/session.py2
-rwxr-xr-xsrc/services/vyos-http-api-server9
4 files changed, 48 insertions, 0 deletions
diff --git a/src/services/api/rest/libs/token_auth.py b/src/services/api/rest/libs/token_auth.py
index 87464eb0f..b7c2c450a 100644
--- a/src/services/api/rest/libs/token_auth.py
+++ b/src/services/api/rest/libs/token_auth.py
@@ -24,6 +24,37 @@ from ...session import SessionState
_secret_lock = threading.Lock()
+def verify_oidc_token(token: str):
+ """Validate an OIDC Bearer token against the configured OIDC issuer JWKS.
+
+ Returns the subject identifier on success, None on failure.
+ Requires set service https api rest authentication oidc issuer <url>.
+ """
+ state = SessionState()
+ if not state.oidc_issuer:
+ return None
+ try:
+ from jwt import PyJWKClient
+
+ jwks_uri = (
+ state.oidc_jwks_uri or f"{state.oidc_issuer}/protocol/openid-connect/certs"
+ )
+ jwks_client = PyJWKClient(jwks_uri, cache_keys=True)
+ signing_key = jwks_client.get_signing_key_from_jwt(token)
+ payload = jwt.decode(
+ token,
+ signing_key.key,
+ algorithms=["RS256", "RS384", "RS512", "ES256", "ES384", "ES512"],
+ options={"verify_aud": False},
+ issuer=state.oidc_issuer,
+ leeway=30,
+ )
+ return payload.get("sub") or payload.get("client_id") or "oidc-client"
+ except Exception as e:
+ print(f"OIDC token validation failed: {e}", flush=True)
+ return None
+
+
def init_secret():
state = SessionState()
if state.rest_secret is not None:
diff --git a/src/services/api/rest/routers.py b/src/services/api/rest/routers.py
index 0973ad145..0f509caad 100644
--- a/src/services/api/rest/routers.py
+++ b/src/services/api/rest/routers.py
@@ -80,6 +80,7 @@ from .models import PingModel
from .models import PoweroffModel
from .models import TracerouteModel
from .libs.token_auth import generate_token
+from .libs.token_auth import verify_oidc_token
from .libs.token_auth import verify_token
@@ -120,6 +121,11 @@ def auth_required(
if key_id:
session.id = key_id
return
+ # Try OIDC token validation
+ oidc_sub = verify_oidc_token(token)
+ if oidc_sub:
+ session.id = f'oidc:{oidc_sub}'
+ return
raise HTTPException(status_code=401, detail='Invalid or expired token')
key = data.key or x_api_key
diff --git a/src/services/api/session.py b/src/services/api/session.py
index 0b1c935c7..a50b692f0 100644
--- a/src/services/api/session.py
+++ b/src/services/api/session.py
@@ -42,3 +42,5 @@ class SessionState:
self.rest_secret = None
self.rest_secret_len = None
self.rest_token_exp = None
+ self.oidc_issuer = None
+ self.oidc_jwks_uri = None
diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server
index bdcf24e39..00fd42586 100755
--- a/src/services/vyos-http-api-server
+++ b/src/services/vyos-http-api-server
@@ -191,6 +191,15 @@ def initialization(session: SessionState, app: FastAPI = app):
session.rest_token_exp = int(auth['expiration'])
if 'secret_length' in auth:
session.rest_secret_len = int(auth['secret_length'])
+ oidc = auth.get('oidc', {})
+ if isinstance(oidc, dict):
+ if 'issuer' in oidc:
+ session.oidc_issuer = oidc['issuer']
+ if 'jwks_url' in oidc:
+ session.oidc_jwks_uri = oidc['jwks_url']
+ elif 'issuer' in oidc:
+ # Derive JWKS URI from issuer if not explicitly set
+ session.oidc_jwks_uri = None # will be resolved in token_auth
graphql_config = server_config.get('graphql', {})
session.origins = graphql_config.get('cors', {}).get('allow_origin', [])