]> git.ipfire.org Git - pbs.git/commitdiff
API: Add some simple authentication endpoint
authorMichael Tremer <michael.tremer@ipfire.org>
Mon, 16 Jun 2025 16:35:23 +0000 (16:35 +0000)
committerMichael Tremer <michael.tremer@ipfire.org>
Mon, 16 Jun 2025 16:35:23 +0000 (16:35 +0000)
This will now respond with JWT tokens.

Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
Makefile.am
configure.ac
src/api/__init__.py
src/api/auth.py [new file with mode: 0644]

index 9b7151469fe4a629301d9f8bd673a21c1a82400c..094522aa68c243e1fe8a4c8847384a0cf88df726 100644 (file)
@@ -122,6 +122,7 @@ CLEANFILES += \
 
 api_PYTHON = \
        src/api/__init__.py \
+       src/api/auth.py \
        src/api/builds.py \
        src/api/uploads.py
 
index 3c79a7ac5c7fab7e5f1b1fd445ddb0ea4232d818..92d7660a329e248cf302de995958350a1b1bbefd 100644 (file)
@@ -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])
index c5dfacce9ce2d6cb6e82e6aa85915137fa501442..7fab99e6f54f54cff8f31a58fb2d91b8fdee712e 100644 (file)
@@ -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 (file)
index 0000000..3bd8eb5
--- /dev/null
@@ -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 <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,
+       )