--- /dev/null
+###############################################################################
+# #
+# 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 <http://www.gnu.org/licenses/>. #
+# #
+###############################################################################
+
+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,
+ )