Commit fdc46e81 authored by Fence's avatar Fence 🌈

add Adminlogins & auth backend

parent 1456aa7d
from flask import Flask
from minor.controller import ApiController, MusicController, CoverController
from minor.controller import AuthController
from minor.error_handlers import set_error_handlers
......@@ -7,6 +8,7 @@ class App(object):
def __init__(self, config):
self._flask = Flask(__name__)
self._flask.config['MONGODB_SETTINGS'] = {'db': 'minor'}
self._flask.config['JWT_SEKRIT'] = "sekrit"
self._flask.config.update(config)
set_error_handlers(self._flask)
......@@ -14,6 +16,7 @@ class App(object):
db.init_app(self._flask)
ApiController(self).register(self._flask)
AuthController(self).register(self._flask)
MusicController(
self._flask.config["MUSIC_UPLOAD_DIR"],
self._flask.config["IMAGE_UPLOAD_DIR"]
......
import click
from .rsa import rsa
from .admin import admin
@click.group()
......@@ -9,3 +10,4 @@ def cli(ctx):
cli.add_command(rsa)
cli.add_command(admin)
import sys
import getpass
import click
from mongoengine import connect
from minor.model import AdminLogin
@click.group()
@click.pass_context
def admin(ctx):
ctx.db = ctx.parent.db
connect(ctx.db)
@admin.command("list")
def list_logins():
for login in AdminLogin.objects:
click.echo(login.email)
@admin.command("add")
@click.argument("email", nargs=1)
def add_login(email):
# only add if the issuer does not exist already
try:
AdminLogin.objects.get(email=email)
click.echo(
"A login for the email \"{}\" does already exist".format(email)
)
return sys.exit(1)
except AdminLogin.DoesNotExist:
login = AdminLogin(email=email)
password = getpass.getpass("enter password: ")
password2 = getpass.getpass("re-enter password: ")
if password != password2:
click.echo("passwords do not match")
return sys.exit(1)
login.set_new_password(password)
login.save()
click.echo("login for email \"{}\" added".format(email))
@admin.command("rm")
@click.argument("email", nargs=1)
def rm_login(email):
try:
key = AdminLogin.objects.get(email=email)
key.delete()
click.echo("login for\"{}\" deleted".format(email))
except AdminLogin.DoesNotExist:
click.echo("login for \"{}\" does not exist".format(email))
return sys.exit(1)
from minor.controller.api import ApiController
from minor.controller.music import MusicController
from minor.controller.covers import CoverController
from minor.controller.auth import AuthController
from flask import request, jsonify
from flask_controller import FlaskController, route
from minor.model import AdminLogin
import minor.crypto
@route("/auth")
class AuthController(FlaskController):
def __init__(self, app):
self._app = app
self._secret = app._flask.config["JWT_SEKRIT"]
@route("/login", methods=["POST"])
def index(self):
json = request.get_json()
if json is not None:
email = json.get("email")
password = json.get("password")
if email is not None and password is not None:
try:
login = AdminLogin.objects.get(email=email)
if login.verify_password(password):
token = minor.crypto.issue_token(email, self._secret)
return jsonify({
"code": 200,
"message": "success",
"value": token
}), 200
except AdminLogin.DoesNotExist:
return jsonify({
"code": 403,
"message": "wrong creds"
}), 403
return jsonify({"code": 403, "message": "wrong creds"}), 403
import jwt
from datetime import datetime, timedelta
from minor.model import TrustedRsaKey
def get_asymetric_jwt_payload(self, token):
def get_payload_from_jwt(token, secret):
"""
Returns the payload from a jwt if it can be verified, None otherwise
"""
header = jwt.get_unverified_header(token)
if header["alg"].startswith("HS"):
return get_symetric_jwt_payload(token)
elif header["alg"].startswith("RS"):
return get_asymetric_jwt_payload(token, secret)
else:
return None
def get_symetric_jwt_payload(token, secret):
try:
checked_payload = jwt.decode(token, secret)
# every token issued by this webservice have all privileges
checked_payload["write_permission"] = True
return checked_payload
except Exception:
return None
def get_asymetric_jwt_payload(token):
payload = jwt.decode(token, "", verify=False)
try:
......@@ -14,7 +39,20 @@ def get_asymetric_jwt_payload(self, token):
try:
key = issuer.key
checked_payload = jwt.decode(token, key)
checked_payload["write_permission"] = issuer.can_write
return checked_payload
except Exception:
return None
return None
def issue_token(email, secret):
payload = {}
# TODO add issuer to the config
payload["iss"] = "minor"
payload["aud"] = email
payload["iat"] = datetime.utcnow()
payload["exp"] = datetime.utcnow() + timedelta(minutes=30)
# TODO maybe add jti?
token = jwt.encode(payload, secret, algorithm='HS256')
return str(token)
......@@ -6,12 +6,13 @@ from minor import crypto
def auth_required(f):
@wraps(f)
def decorated_function(self, *args, **kwargs):
auth = request.headers["Authorization"]
auth = request.headers.get("Authorization")
if auth is not None:
auth = auth.split(" ")
if auth[0] == "Bearer":
# print(auth[1])
payload = crypto.get_asymetric_jwt_payload(auth[1])
secret = self.app.config["JWT_SEKRIT"]
payload = crypto.get_payload_from_jwt(auth[1], secret)
if payload is not None:
kwargs["payload"] = payload
return f(self, *args, **kwargs)
......
import bcrypt
from mongoengine import Document, StringField, IntField
from mongoengine import ReferenceField, ListField, CASCADE
from mongoengine import ReferenceField, ListField, CASCADE, BooleanField
from flask_mongoengine import MongoEngine
db = MongoEngine()
......@@ -24,3 +25,22 @@ class Cover(Document):
class TrustedRsaKey(Document):
issuer = StringField(required=True)
key = StringField(required=True)
can_write = BooleanField(default=False)
class AdminLogin(Document):
email = StringField(required=True)
password = StringField(required=True)
def set_new_password(self, new_password):
hash_bytes = bcrypt.hashpw(
new_password.encode("utf-8"),
bcrypt.gensalt()
)
self.password = hash_bytes.decode("utf-8")
def verify_password(self, input_pw):
return bcrypt.checkpw(
input_pw.encode("utf-8"),
self.password.encode("utf-8")
)
......@@ -5,3 +5,4 @@ git+https://github.com/AlexFence/FlaskController.git
mutagen
click
tabulate
bcrypt
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment