# Copyright (C) VyOS Inc.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this library. If not, see .
import datetime
import threading
from secrets import token_hex
import jwt
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 .
"""
state = SessionState()
if not state.oidc_issuer:
return None
try:
from jwt import PyJWKClient
# Use explicit jwks-url if set, otherwise use OIDC discovery
if state.oidc_jwks_uri:
jwks_uri = state.oidc_jwks_uri
else:
import urllib.request as _req
import json as _json
disc_url = f"{state.oidc_issuer}/.well-known/openid-configuration"
with _req.urlopen(disc_url, timeout=10) as r:
jwks_uri = _json.loads(r.read())["jwks_uri"]
state.oidc_jwks_uri = jwks_uri
jwks_client = PyJWKClient(jwks_uri, cache_keys=True)
signing_key = jwks_client.get_signing_key_from_jwt(token)
decode_options = {"require": ["exp"]}
decode_kwargs: dict = dict(
algorithms=["RS256", "RS384", "RS512", "ES256", "ES384", "ES512"],
options=decode_options,
issuer=state.oidc_issuer,
leeway=30,
)
if not state.oidc_audience:
# Fail closed: config-time validation in service_https.py's
# verify() requires oidc.audience whenever oidc.issuer is
# set, but never trust that alone for a security-critical
# check. PyJWT also skips aud validation whenever no
# "audience" kwarg is passed at all, independent of
# verify_aud -- so this must refuse outright, not just stop
# forcing verify_aud=False.
return None
decode_kwargs["audience"] = state.oidc_audience
payload = jwt.decode(token, signing_key.key, **decode_kwargs)
return payload.get("sub") or payload.get("client_id")
except Exception:
return None
def init_secret():
state = SessionState()
if state.rest_secret is not None:
return
length = int(state.rest_secret_len or 32)
with _secret_lock:
if state.rest_secret is None:
state.rest_secret = token_hex(length)
def generate_token(key_id: str) -> dict:
state = SessionState()
init_secret()
exp_interval = int(state.rest_token_exp or 3600)
expiration = datetime.datetime.now(tz=datetime.timezone.utc) + datetime.timedelta(
seconds=exp_interval
)
payload = {'iss': 'vyos-rest-api', 'sub': key_id, 'exp': expiration}
token = jwt.encode(payload=payload, key=state.rest_secret, algorithm='HS256')
return {'token': token, 'expires_in': exp_interval}
def verify_token(token: str):
state = SessionState()
if state.rest_secret is None:
return None
try:
payload = jwt.decode(token, state.rest_secret, algorithms=['HS256'])
except jwt.ExpiredSignatureError:
return None
except jwt.PyJWTError:
return None
return payload.get('sub')