From: Michael Tremer Date: Mon, 16 Jun 2025 16:35:23 +0000 (+0000) Subject: API: Add some simple authentication endpoint X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=85ea8e8a0cccbb69154e626af0df01adb05542de;p=pbs.git API: Add some simple authentication endpoint This will now respond with JWT tokens. Signed-off-by: Michael Tremer --- diff --git a/Makefile.am b/Makefile.am index 9b715146..094522aa 100644 --- a/Makefile.am +++ b/Makefile.am @@ -122,6 +122,7 @@ CLEANFILES += \ api_PYTHON = \ src/api/__init__.py \ + src/api/auth.py \ src/api/builds.py \ src/api/uploads.py diff --git a/configure.ac b/configure.ac index 3c79a7ac..92d7660a 100644 --- a/configure.ac +++ b/configure.ac @@ -85,6 +85,7 @@ AX_PYTHON_MODULE([babel], [fatal]) AX_PYTHON_MODULE([boto3], [fatal]) AX_PYTHON_MODULE([cryptography], [fatal]) AX_PYTHON_MODULE([jinja2], [fatal]) +AX_PYTHON_MODULE([jwt], [fatal]) AX_PYTHON_MODULE([kerberos], [fatal]) AX_PYTHON_MODULE([ldap], [fatal]) AX_PYTHON_MODULE([location], [fatal]) diff --git a/src/api/__init__.py b/src/api/__init__.py index c5dfacce..7fab99e6 100644 --- a/src/api/__init__.py +++ b/src/api/__init__.py @@ -46,5 +46,6 @@ app.add_middleware( ) # Load all further modules +from . import auth from . import builds from . import uploads diff --git a/src/api/auth.py b/src/api/auth.py new file mode 100644 index 00000000..3bd8eb59 --- /dev/null +++ b/src/api/auth.py @@ -0,0 +1,110 @@ +############################################################################### +# # +# Pakfire - The IPFire package management system # +# Copyright (C) 2025 Pakfire development team # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program 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 General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +############################################################################### + +import datetime +import fastapi.security +import jwt +import kerberos +import os +import pydantic +import socket + +from . import app +from . import backend + +# Fetch Kerberos configuration +KERBEROS_KEYTAB = backend.config.get("krb5", "keytab") +KERBEROS_REALM = backend.config.get("krb5", "realm", fallback="IPFIRE.ORG") +KERBEROS_PRINCIPAL = backend.config.get("krb5", "principal", + fallback="pakfire/%s" % socket.getfqdn()) + +TOKEN_SECRET = backend.config.get("jwt", "secret") +TOKEN_ALGO = "HS256" + +# Expiry times for the tokens +ACCESS_TOKEN_EXPIRY_TIME = datetime.timedelta(minutes=60) +REFRESH_TOKEN_EXPIRY_TIME = datetime.timedelta(days=7) + +class AuthResponse(pydantic.BaseModel): + # Token Type + type: str + + # Access Token + access_token: str + + # Refresh Token + refresh_token: str + + +def create_token(subject, type, expires_after, **kwargs): + issued_at = datetime.datetime.utcnow() + + # Compute the absolute expiry time + expires_at = issued_at + expires_after + + # Compute the payload + payload = { + "sub" : subject, + "typ" : type, + + # Issue & Expiry Time + "iat" : issued_at, + "exp" : expires_at, + } | kwargs + + return jwt.encode(payload, TOKEN_SECRET, algorithm=TOKEN_ALGO) + + +@app.post("/auth/user") +async def auth_user(credentials: fastapi.security.OAuth2PasswordRequestForm = + fastapi.Depends()) -> AuthResponse: + # Set keytab to use + os.environ["KRB5_KTNAME"] = KERBEROS_KEYTAB + + # Check the credentials against the Kerberos database + try: + kerberos.checkPassword( + credentials.username, + credentials.password, + KERBEROS_PRINCIPAL, + KERBEROS_REALM, + ) + + # Catch any authentication errors + except kerberos.BasicAuthError as e: + raise fastapi.HTTPException(401, "Invalid username or password") + + # Create user principal name + principal = "%s@%s" % (credentials.username, KERBEROS_REALM) + + # Generate the access token + access_token = create_token(principal, + type="access", expires_after=ACCESS_TOKEN_EXPIRY_TIME) + + # Generate the refresh token + refresh_token = create_token(principal, + type="refresh", expires_after=REFRESH_TOKEN_EXPIRY_TIME) + + # Send the response + return AuthResponse( + type = "Bearer", + access_token = access_token, + refresh_token = refresh_token, + )