diff --git a/APIDOCS.md b/APIDOCS.md index 326d645..162158a 100644 --- a/APIDOCS.md +++ b/APIDOCS.md @@ -25,6 +25,8 @@ You'll need to store two things in local storage: ## OAuth2 +RDIR - /login - start the oauth2 login process, see OAuth2.md + POST - /api/auth - interface directly with the burgerauth system, provide "secretKey, appId, code and codemethod" Code and Codemethod are only used for PKCE, if PKCE is not enabled set the value of both to "none" diff --git a/Oauth2.md b/Oauth2.md new file mode 100644 index 0000000..c4545ea --- /dev/null +++ b/Oauth2.md @@ -0,0 +1,152 @@ +# Burgerauth as an oauth2 provider + +## Endpoints + +GET - /.well-known/openid-configuration - OpenID Connect Discovery + +GET - /login - Authorization Endpoint + +POST - /login/oauth/access_token - Token Exchange Endpoint + +POST - /userinfo - OpenID Connect UserInfo + +## Supported OAuth2 Grants + +Burgerauth supports the authorization code grant standard with support for: + +- Proof Key for Code Exchange (PKCE) +- OpenID Connect (OCID) + +To use the Authorization Code Grant as a third party application it is required to register a new application in the dashboard (/dashboard) + +## Scopes + +Burgerauth does not currently support scopes, and by default grants all scopes regarding user infomation + +## Client types + +Burgerauth supports both confidential and public client types, as defined by RFC 6749. + +You can view this spec here: https://datatracker.ietf.org/doc/html/rfc6749#section-2.1 + +## Examples + +### Confidential client + +This example does not use PKCE. + +1. Redirect the user to the authorization endpoint in order to get their consent for accessing the resources: + +``` +/login?client_id=CLIENT_ID&redirect_uri=REDIRECT_URI&response_type=code&state=STATE +``` + +The CLIENT_ID can be obtained by registering an application in the dashboard. The STATE is a random string that will be sent back to your application after the user authorizes. The state parameter is optional, but should be used to prevent CSRF attacks. + +The user will now be asked to authorize your application. If they authorize it, the user will be redirected to the REDIRECT_URL, for example: + +``` +https://[REDIRECT_URI]?code=RETURNED_CODE&state=STATE +``` + +2. Using the provided code from the redirect, you can request a new application and refresh token. The access token endpoint accepts POST requests with application/json and application/x-www-form-urlencoded body, for example: + +POST - /api/tokenauth + +``` +{ + "client_id": "YOUR_CLIENT_ID", + "client_secret": "YOUR_CLIENT_SECRET", + "code": "RETURNED_CODE", + "grant_type": "authorization_code", + "redirect_uri": "REDIRECT_URI" (optional, since redirect_uri is not verified on the server) +} +``` +Response: +``` +{ + "access_token": "(random string)", + "token_type": "bearer", + "expires_in": 3600, + "refresh_token": "(random string)", + "id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJleGFtcGxlIiwiaXNzIjoiaHR0cHM6Ly9hdXRoLmhlY3RhYml0Lm9yZyIsIm5hbWUiOiJleGFtcGxlIiwiYXVkIjoibXlhcHBpZCIsImV4cCI6MCwiaWF0IjozNjAwLCJhdXRoX3RpbWUiOjAsIm5vbmNlIjoiMDYyMGM2NGNjZmE5MzQ0NTczMTQ5NTM5MmNiMzQzYjM2MGJkMjAwYmIzM2QyNTRjNzQ3YWZjYmY1YzlhNDkzYjM3MWE0MWI2Zjk3YzM5ZWI0ZjU0YmJmNDBjYjNkNjhmNTBmNmFhNjMwMmY2ZjRkM2I0NTI4MWVjYzI2ZTQ5MjNiNDFjYTFhYTIzNjIwOTMzNmU4MGZkZjk1ZGUxMGRiMzNhZWJjY2M3MmM2NjU2ZjI4MmUzZTY0ZTFkZDVlMzkxOGJlMmM2MDA4ZmFlZGZiNGVjOGE3NDk5Y2JkMzI1NGNjM2Q0ZGI0ZTMyNjQ4NDgxZDY2Mjg0YmNkMjgyZDVkNGQ3NDg2NjU2NWZjNjhlYTM0ZTIxYmE3MTgzZDU1NWM2ZjNiOTc3ODk2Zjk4NjE5MTEwMjlkYWVmZTA2MWIyMzY3ODE1MWY3Mjk3Y2E2N2M0OWQ1MjIyYjBhZWYxNDYzZTE5ZDU2MGFlYWQyMzZkOTc4YTUwNDRlNTE4NjQwYjExZTA2Zjk1OTNiOTIyNjc4Njk5YWU4MWE3MTM3ODhjYjk0MjdkZDI4ZjA4ZGRjMGIzNmE1Mjk4MDQwMmU5MmU3ZWI0MjllMWZmN2U5NTU3NmJlNmJkNmE4NzJhMDZkYjkzNmNjNGE1MTQ3ZWY1NDRhNmJiNTljMjQ3Mjc2OTcwNTRmMzEzNTc2MmRiNDg5NmZlNjcwNDEwZTA5MjMyMDdmOWNjZmNiZmE3ZWU2NThkNDI3MjI2YWZkZjJlN2RlNzMyYTEyYTg1NGQ4OTg5MWE5OTJmNDE0MWY5Nzg4ZTE2OGQ2YWFkMjc1OGExYjM3NjZkZTVlNmJkZjMyODdlZmQ3MWU4ZTkwMjY3NjMzYmM4YTA3Y2I0ZTYxODRiZDE0MWY1ZmE1MDU0YTE0NWMyZTMxMWQ1ZDNiYWY2MjkwZDIwNTljMDQyN2U2MjAxYWIzYjM5Zjc3ZTA1ZTU0NzVkM2FhYjg5NjcxMjQ2MmZlYTBjNmRjMzJlNDcwZjIxZDliOGZhZmMxMWEzYTUzNGM5NzkzY2ZhZjE2NWI5NTAwNDBhYmE4YmI2MzJkY2I5MzhiYjBhNmNiMDhlYWQ2NmJmMDgxN2E4ODUwZTMwODFlNmQ5NTYzNzJkYzQwOTU3NmQ0ZGY5ZjY4MjM2MTZlMjI5Y2VjNTk5NjQxNjlmOTEyOTBhODUyNjk1MDg2ZGMzYjQ3OTMzOTE1M2Q0MGMzYzM2YTFiZGRjN2IyMTRlM2YzMDY3ODhiOGQwOGVjZWNkMmFlMjk2NzY5MTdlNWU3MjM5NzQyYzlkOTliYTAwZDUyMWQ5NDM2NjRiMGNiYTEyZjBhNTNhMjhjYzg4NGE5NWVhNzdjMSJ9.C_M48hKf9Ccj-lRV6X0co2vwUee5coxYEwoYfSL0ZlM" +} +``` + +ID Token is a jwt encoded string in this format: + +``` +{ + "sub": "(username)", + "iss": "(issuer URL)", + "name": "(username)", + "aud": "(appid)", + "exp": (expiration time), + "iat": (time of issue), + "auth_time": (time of auth), + "nonce": "(random string)" +} +``` + +3. Use id_token to contact the /userinfo endpoint, with the "Authorization" HTTP header structured like this: + +``` +Bearer (id_token) +``` + +Use access_token to access other scopes, defined in scopes.md + +### Public Client (PKCE) + +1. PKCE (Proof Key for Code Exchange) is an extension to the OAuth flow which allows for a secure credential exchange without the requirement to provide a client secret. To achieve this, you have to provide a code_verifier for every authorization request. A code_verifier has to be a random string with a minimum length of 43 characters and a maximum length of 128 characters. It can contain alphanumeric characters as well as the characters -, ., _ and ~. + +Using this code_verifier string, a new one called code_challenge is created by using one of two methods: + +- If you have the required functionality on your client, set code_challenge to be a URL-safe base64-encoded string of the SHA256 hash of code_verifier. In that case, your code_challenge_method becomes S256. +- If you are unable to do so, you can provide your code_verifier as a plain string to code_challenge. Then you have to set your code_challenge_method as plain. + +2. Redirect the user to the authorization endpoint in order to get their consent for accessing the resources: + +``` +/login?client_id=CLIENT_ID&redirect_uri=REDIRECT_URI&response_type=code&code_challenge_method=CODE_CHALLENGE_METHOD&code_challenge=CODE_CHALLENGE&state=STATE +``` + +The CLIENT_ID can be obtained by registering an application in the dashboard. The STATE is a random string that will be sent back to your application after the user authorizes. The state parameter is optional, but should be used to prevent CSRF attacks. + +The user will now be asked to authorize your application. If they authorize it, the user will be redirected to the REDIRECT_URL, for example: + +``` +https://[REDIRECT_URI]?code=RETURNED_CODE&state=STATE +``` + +3. Using the provided code from the redirect, you can request a new application and refresh token. The access token endpoint accepts POST requests with application/json and application/x-www-form-urlencoded body, for example: + +POST - /api/tokenauth + +``` +{ + "client_id": "YOUR_CLIENT_ID", + "code": "RETURNED_CODE", + "grant_type": "authorization_code", + "redirect_uri": "REDIRECT_URI", (optional, since redirect_uri is not verified on the server) + "code_verifier": "CODE_VERIFIER" +} +``` +Response: +``` +{ + "access_token": "(random string)", + "token_type": "bearer", + "expires_in": 3600, + "refresh_token": "(random string)", + "id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJleGFtcGxlIiwiaXNzIjoiaHR0cHM6Ly9hdXRoLmhlY3RhYml0Lm9yZyIsIm5hbWUiOiJleGFtcGxlIiwiYXVkIjoibXlhcHBpZCIsImV4cCI6MCwiaWF0IjozNjAwLCJhdXRoX3RpbWUiOjAsIm5vbmNlIjoiMDYyMGM2NGNjZmE5MzQ0NTczMTQ5NTM5MmNiMzQzYjM2MGJkMjAwYmIzM2QyNTRjNzQ3YWZjYmY1YzlhNDkzYjM3MWE0MWI2Zjk3YzM5ZWI0ZjU0YmJmNDBjYjNkNjhmNTBmNmFhNjMwMmY2ZjRkM2I0NTI4MWVjYzI2ZTQ5MjNiNDFjYTFhYTIzNjIwOTMzNmU4MGZkZjk1ZGUxMGRiMzNhZWJjY2M3MmM2NjU2ZjI4MmUzZTY0ZTFkZDVlMzkxOGJlMmM2MDA4ZmFlZGZiNGVjOGE3NDk5Y2JkMzI1NGNjM2Q0ZGI0ZTMyNjQ4NDgxZDY2Mjg0YmNkMjgyZDVkNGQ3NDg2NjU2NWZjNjhlYTM0ZTIxYmE3MTgzZDU1NWM2ZjNiOTc3ODk2Zjk4NjE5MTEwMjlkYWVmZTA2MWIyMzY3ODE1MWY3Mjk3Y2E2N2M0OWQ1MjIyYjBhZWYxNDYzZTE5ZDU2MGFlYWQyMzZkOTc4YTUwNDRlNTE4NjQwYjExZTA2Zjk1OTNiOTIyNjc4Njk5YWU4MWE3MTM3ODhjYjk0MjdkZDI4ZjA4ZGRjMGIzNmE1Mjk4MDQwMmU5MmU3ZWI0MjllMWZmN2U5NTU3NmJlNmJkNmE4NzJhMDZkYjkzNmNjNGE1MTQ3ZWY1NDRhNmJiNTljMjQ3Mjc2OTcwNTRmMzEzNTc2MmRiNDg5NmZlNjcwNDEwZTA5MjMyMDdmOWNjZmNiZmE3ZWU2NThkNDI3MjI2YWZkZjJlN2RlNzMyYTEyYTg1NGQ4OTg5MWE5OTJmNDE0MWY5Nzg4ZTE2OGQ2YWFkMjc1OGExYjM3NjZkZTVlNmJkZjMyODdlZmQ3MWU4ZTkwMjY3NjMzYmM4YTA3Y2I0ZTYxODRiZDE0MWY1ZmE1MDU0YTE0NWMyZTMxMWQ1ZDNiYWY2MjkwZDIwNTljMDQyN2U2MjAxYWIzYjM5Zjc3ZTA1ZTU0NzVkM2FhYjg5NjcxMjQ2MmZlYTBjNmRjMzJlNDcwZjIxZDliOGZhZmMxMWEzYTUzNGM5NzkzY2ZhZjE2NWI5NTAwNDBhYmE4YmI2MzJkY2I5MzhiYjBhNmNiMDhlYWQ2NmJmMDgxN2E4ODUwZTMwODFlNmQ5NTYzNzJkYzQwOTU3NmQ0ZGY5ZjY4MjM2MTZlMjI5Y2VjNTk5NjQxNjlmOTEyOTBhODUyNjk1MDg2ZGMzYjQ3OTMzOTE1M2Q0MGMzYzM2YTFiZGRjN2IyMTRlM2YzMDY3ODhiOGQwOGVjZWNkMmFlMjk2NzY5MTdlNWU3MjM5NzQyYzlkOTliYTAwZDUyMWQ5NDM2NjRiMGNiYTEyZjBhNTNhMjhjYzg4NGE5NWVhNzdjMSJ9.C_M48hKf9Ccj-lRV6X0co2vwUee5coxYEwoYfSL0ZlM" +} +``` + +3. Use id_token to contact the /userinfo endpoint, with the "Authorization" HTTP header structured like this: + +``` +Bearer (id_token) +``` + +Use access_token to access other scopes, defined in scopes.md diff --git a/main.save b/main.save new file mode 100644 index 0000000..6cd2192 --- /dev/null +++ b/main.save @@ -0,0 +1,598 @@ +#!/usr/bin/python3 +import hashlib +import base64 +import jwt +import os +import sqlite3 +import time +import secrets +import configparser +import asyncio +from hypercorn.config import Config +from hypercorn.asyncio import serve +from werkzeug.security import generate_password_hash, check_password_hash +from quart import Quart, render_template, request, url_for, flash, redirect, session, make_response, send_from_directory, stream_with_context, Response, request + +# Parse configuration file, and check if anything is wrong with it +if not os.path.exists("config.ini"): + print("config.ini does not exist") + quit(1) + +config = configparser.ConfigParser() +config.read("config.ini") + +HOST = config["config"]["HOST"] +PORT = config["config"]["PORT"] +SECRET_KEY = config["config"]["SECRET_KEY"] +MAX_STORAGE = config["config"]["MAX_STORAGE"] + +if SECRET_KEY == "supersecretkey" or SECRET_KEY == "placeholder": + print("[WARNING] Secret key not set") + +# Define Quart +app = Quart(__name__) +app.config["SECRET_KEY"] = SECRET_KEY + +# Hash creation function +def sha256_base64(s: str) -> str: + hashed = hashlib.sha256(s.encode()).digest() + encoded = base64.urlsafe_b64encode(hashed).rstrip(b'=').decode() + return encoded + +# Database functions +def get_db_connection(): + conn = sqlite3.connect("database.db") + conn.row_factory = sqlite3.Row + return conn + +def get_user(id): + conn = get_db_connection() + post = conn.execute("SELECT * FROM users WHERE id = ?", + (id,)).fetchone() + conn.close() + if post is None: + return None + return post + +def get_session(id): + conn = get_db_connection() + post = conn.execute("SELECT * FROM sessions WHERE session = ?", + (id,)).fetchone() + conn.close() + if post is None: + return None + return post + +def get_session_from_sessionid(id): + conn = get_db_connection() + post = conn.execute("SELECT * FROM sessions WHERE sessionid = ?", + (id,)).fetchone() + conn.close() + if post is None: + return None + return post + +def check_username_taken(username): + conn = get_db_connection() + post = conn.execute("SELECT * FROM users WHERE lower(username) = ?", + (username.lower(),)).fetchone() + conn.close() + if post is None: + return None + return post["id"] + +async def oauth2_token_refresh(openid, appId): + while True: + await asyncio.sleep(3600) + conn = get_db_connection() + + # Fetch required data in a single query + login_data = conn.execute("SELECT nextcode, nextsecret, nextopenid, creator FROM logins WHERE appId = ? AND openid = ?", (str(appId), str(openid))).fetchone() + + user = get_user(int(login_data[3])) + + datatemplate = { + "sub": user["username"], + "iss": "https://auth.hectabit.org", + "name": user["username"], + "aud": appId, + "exp": time.time() + 3600, + "iat": time.time(), + "auth_time": time.time(), + "nonce": str(secrets.token_hex(512)) + } + + jwt_token = jwt.encode(datatemplate, SECRET_KEY, algorithm='HS256') + + if login_data: + nextcode = login_data[0] + nextsecret = login_data[1] + nextopenid = login_data[2] + conn.execute("UPDATE logins SET code = ?, nextcode = ?, secret = ?, nextsecret = ?, openid = ?, nextopenid = ? WHERE appId = ? AND openid = ?", (nextcode, str(secrets.token_hex(512)), nextsecret, str(secrets.token_hex(512)), nextopenid, str(jwt_token), str(appId), str(openid))) + conn.commit() + conn.close() + else: + conn.close() + return + +# Disable CORS +@app.after_request +async def add_cors_headers(response): + response.headers.add("Access-Control-Allow-Origin", "*") + response.headers.add("Access-Control-Allow-Headers", "*") + response.headers.add("Access-Control-Allow-Methods", "*") + return response + +@app.route("/api/version", methods=("GET", "POST")) +async def apiversion(): + return "Burgerauth Version 1.2" + +@app.route("/api/signup", methods=("GET", "POST")) +async def apisignup(): + if request.method == "POST": + data = await request.get_json() + username = data["username"] + password = data["password"] + + if username == "": + return {}, 422 + + if len(username) > 20: + return {}, 422 + + if not username.isalnum(): + return {}, 422 + + if password == "": + return {}, 422 + + if len(password) < 14: + return {}, 422 + + if not check_username_taken(username) == None: + return {}, 409 + + hashedpassword = generate_password_hash(password) + + conn = get_db_connection() + conn.execute("INSERT INTO users (username, password, created) VALUES (?, ?, ?)", + (username, hashedpassword, str(time.time()))) + conn.commit() + conn.close() + + userID = check_username_taken(username) + user = get_user(userID) + + randomCharacters = secrets.token_hex(512) + + conn = get_db_connection() + conn.execute("INSERT INTO sessions (session, id, device) VALUES (?, ?, ?)", + (randomCharacters, userID, request.headers.get("user-agent"))) + conn.commit() + conn.close() + + return { + "key": randomCharacters + }, 200 + +@app.route("/api/login", methods=("GET", "POST")) +async def apilogin(): + if request.method == "POST": + data = await request.get_json() + username = data["username"] + password = data["password"] + passwordchange = data["passwordchange"] + newpass = data["newpass"] + + check_username_thing = check_username_taken(username) + + if check_username_thing == None: + return {}, 401 + + userID = check_username_taken(username) + user = get_user(userID) + + if not check_password_hash(user["password"], (password)): + return {}, 401 + + randomCharacters = secrets.token_hex(512) + + conn = get_db_connection() + conn.execute("INSERT INTO sessions (session, id, device) VALUES (?, ?, ?)", + (randomCharacters, userID, request.headers.get("user-agent"))) + conn.commit() + conn.close() + + if passwordchange == "yes": + hashedpassword = generate_password_hash(newpass) + conn = get_db_connection() + conn.execute("UPDATE users SET password = ? WHERE username = ?", (hashedpassword, username)) + conn.commit() + conn.close() + + return { + "key": randomCharacters, + }, 200 + + +@app.route("/api/userinfo", methods=("GET", "POST")) +async def apiuserinfo(): + if request.method == "POST": + data = await request.get_json() + secretKey = data["secretKey"] + + userCookie = get_session(secretKey) + user = get_user(userCookie["id"]) + datatemplate = { + "username": user["username"], + "id": user["id"], + "created": user["created"] + } + return datatemplate + +@app.route("/userinfo", methods=("GET", "POST")) +async def apiopeniduserinfo(): + if request.method == "GET": + access_token = request.headers.get('Authorization').split(' ')[1] + + conn = get_db_connection() + userid = int(conn.execute("SELECT creator FROM logins WHERE code = ?", (str(access_token),)).fetchone()[0]) + user = get_user(userid) + + conn.close() + + datatemplate = { + "sub": user["username"], + "name": user["username"] + } + + return datatemplate + +@app.route("/api/auth", methods=("GET", "POST")) +async def apiauthenticate(): + if request.method == "POST": + data = await request.get_json() + secretKey = data["secretKey"] + appId = data["appId"] + code = data["code"] + codemethod = data["codemethod"] + + userCookie = get_session(secretKey) + user = get_user(userCookie["id"]) + conn = get_db_connection() + secretkey = str(secrets.token_hex(512)) + + appidcheck = str(conn.execute("SELECT appId FROM oauth WHERE appId = ?", (str(appId),)).fetchone()[0]) + if not str(appidcheck) == str(appId): + return {}, 401 + + datatemplate = { + "sub": user["username"], + "iss": "https://auth.hectabit.org", + "name": user["username"], + "aud": appId, + "exp": time.time() + 3600, + "iat": time.time(), + "auth_time": time.time(), + "nonce": str(secrets.token_hex(512)) + } + + jwt_token = jwt.encode(datatemplate, SECRET_KEY, algorithm='HS256') + + datatemplate2 = { + "sub": user["username"], + "iss": "https://auth.hectabit.org", + "name": user["username"], + "aud": appId, + "exp": time.time() + 7200, + "iat": time.time() + 3600, + "auth_time": time.time(), + "nonce": str(secrets.token_hex(512)) + } + + nextjwt_token = jwt.encode(datatemplate2, SECRET_KEY, algorithm='HS256') + + conn.execute("INSERT INTO logins (appId, secret, nextsecret, code, nextcode, creator, openid, nextopenid, pkce, pkcemethod) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + (str(appId), str(secretkey), str(secrets.token_hex(512)), str(secrets.token_hex(512)), str(secrets.token_hex(512)), int(user["id"]), str(jwt_token), str(nextjwt_token), str(code), str(codemethod))) + + conn.commit() + conn.close() + + if secretkey: + return secretkey, 200 + else: + return {}, 400 + +@app.route("/api/tokenauth", methods=("GET", "POST")) +async def apitokenexchange(): + if request.method == "POST": + data = await request.form + appId = data["client_id"] + code = data["code"] + + if "code_verifier" in data: + code_verify = data["code_verifier"] + verifycode = True + else: + secret = data["client_secret"] + verifycode = False + + conn = get_db_connection() + + # Fetch required data in a single query + oauth_data = conn.execute("SELECT appId, secret FROM oauth WHERE appId = ?", (str(appId),)).fetchone() + if not oauth_data or oauth_data["appId"] != appId: + return {}, 401 + + login_data = conn.execute("SELECT openid, code, pkce, pkcemethod FROM logins WHERE appId = ? AND secret = ?", (str(appId), str(code))).fetchone() + + if verifycode: + if str(login_data["pkce"]) == "none": + return {}, 400 + else: + if str(login_data["pkcemethod"]) == "S256": + if str(sha256_base64(code_verify)) != str(login_data["pkce"]): + return {}, 403 + elif str(login_data["pkcemethod"]) == "plain": + if str(code_verify) != str(login_data["pkce"]): + return {}, 403 + else: + return {}, 501 + else: + if not oauth_data["secret"] == secret: + return {}, 401 + + newkey = str(secrets.token_hex(512)) + conn.execute("UPDATE logins SET secret = ?, nextsecret = ? WHERE appId = ? AND secret = ?", (str(newkey), str(secrets.token_hex(512)), str(appId), str(code))) + + conn.close() + + if login_data: + access_token = { + "access_token": str(login_data["code"]), + "token_type": "bearer", + "expires_in": 3600, + "refresh_token": newkey, + "id_token": str(login_data["openid"]) + } + asyncio.create_task(oauth2_token_refresh(login_data["openid"], appId)) + return access_token, 200 + else: + return {}, 400 + +@app.route("/api/deleteauth", methods=("GET", "POST")) +async def apideleteauth(): + if request.method == "POST": + data = await request.get_json() + appId = data["appId"] + secretKey = data["secretKey"] + + userCookie = get_session(secretKey) + user = get_user(userCookie["id"]) + + conn = get_db_connection() + + try: + conn.execute("DELETE FROM oauth WHERE appId = ? AND creator = ?", (str(appId), int(user["id"]))) + conn.commit() + conn.close() + except: + return {}, 400 + else: + return {}, 200 + +@app.route("/api/newauth", methods=("GET", "POST")) +async def apicreateauth(): + if request.method == "POST": + data = await request.get_json() + appId = data["appId"] + secretKey = data["secretKey"] + secret = str(secrets.token_hex(512)) + conn = get_db_connection() + while True: + try: + conn.execute("SELECT secret FROM oauth WHERE secret = ?", (str(secret),)).fetchone()[0] + except: + break + else: + secret = str(secrets.token_hex(512)) + continue + + try: + conn.execute("SELECT secret FROM oauth WHERE appId = ?", (str(appId),)).fetchone()[0] + except: + print("New Oauth added with ID", appId) + else: + return {}, 401 + + userCookie = get_session(secretKey) + user = get_user(userCookie["id"]) + + conn.execute("INSERT INTO oauth (appId, creator, secret) VALUES (?, ?, ?)", + (str(appId),int(user["id"]),str(secret))) + conn.commit() + conn.close() + secretkey = { + "key": secret + } + return secretkey, 200 + +@app.route("/api/listauth", methods=("GET", "POST")) +async def apiauthlist(): + if request.method == "POST": + data = await request.get_json() + secretKey = data["secretKey"] + + userCookie = get_session(secretKey) + user = get_user(userCookie["id"]) + + conn = get_db_connection() + oauths = conn.execute("SELECT * FROM oauth WHERE creator = ? ORDER BY creator DESC;", (user["id"],)).fetchall() + conn.close() + + datatemplate = [] + + for i in oauths: + template = { + "appId": i["appId"] + } + datatemplate.append(template) + + return datatemplate, 200 + +@app.route("/api/deleteaccount", methods=("GET", "POST")) +async def apideleteaccount(): + if request.method == "POST": + data = await request.get_json() + secretKey = data["secretKey"] + + userCookie = get_session(secretKey) + user = get_user(userCookie["id"]) + + conn = get_db_connection() + try: + conn.execute("DELETE FROM userdata WHERE creator = ?", (userCookie["id"],)) + except: + pass + else: + pass + + try: + conn.execute("DELETE FROM logins WHERE creator = ?", (userCookie["id"],)) + except: + pass + else: + pass + + try: + conn.execute("DELETE FROM oauth WHERE creator = ?", (userCookie["id"],)) + except: + pass + else: + pass + + try: + conn.execute("DELETE FROM users WHERE id = ?", (userCookie["id"],)) + except: + return {}, 400 + else: + pass + + conn.commit() + conn.close() + + return {}, 200 + +@app.route("/api/sessions/list", methods=("GET", "POST")) +async def apisessionslist(): + if request.method == "POST": + data = await request.get_json() + secretKey = data["secretKey"] + + userCookie = get_session(secretKey) + user = get_user(userCookie["id"]) + + conn = get_db_connection() + sessions = conn.execute("SELECT * FROM sessions WHERE id = ? ORDER BY id DESC;", (user["id"],)).fetchall() + conn.close() + + datatemplate = [] + + for x in sessions: + device = x["device"] + thisSession = False + if (x["session"] == secretKey): + thisSession = True + sessiontemplate = { + "id": x["sessionid"], + "thisSession": thisSession, + "device": device + } + datatemplate.append(sessiontemplate) + + return datatemplate, 200 + +@app.route("/api/sessions/remove", methods=("GET", "POST")) +async def apisessionsremove(): + if request.method == "POST": + data = await request.get_json() + secretKey = data["secretKey"] + sessionId = data["sessionId"] + + userCookie = get_session(secretKey) + user = get_user(userCookie["id"]) + + session = get_session_from_sessionid(sessionId) + + if (session != None): + if (user["id"] == session["id"]): + conn = get_db_connection() + conn.execute("DELETE FROM sessions WHERE sessionid = ?", (session["sessionid"],)) + conn.commit() + conn.close() + + return {}, 200 + else: + return {}, 403 + else: + return {}, 422 + + +@app.route("/listusers/", methods=("GET", "POST")) +def listusers(secretkey): + if secretkey == SECRET_KEY: + conn = get_db_connection() + users = conn.execute("SELECT * FROM users").fetchall() + conn.close() + thing = "" + for x in users: + thing = str(x["id"]) + " - " + x["username"] + " - " + str(get_space(x["id"])) + "
" + thing + + return thing + else: + return redirect("/") + +@app.errorhandler(500) +async def burger(e): + return {}, 500 + +@app.errorhandler(404) +async def burger(e): + return {}, 404 + +@app.route("/") +async def index(): + return redirect("/login", code=302) + +@app.route("/login") +async def login(): + return await render_template("login.html") + +@app.route("/signup") +async def signup(): + return await render_template("signup.html") + +@app.route("/logout") +async def logout(): + return await render_template("logout.html") + +@app.route("/app") +async def mainapp(): + return await render_template("main.html") + +@app.route("/dashboard") +async def dashboard(): + return await render_template("dashboard.html") + +@app.route("/.well-known/openid-configuration") +async def openid(): + return await render_template("openid.json") + +# Start server +hypercornconfig = Config() +hypercornconfig.bind = (HOST + ":" + PORT) + +if __name__ == "__main__": + print("[INFO] Server started") + asyncio.run(serve(app, hypercornconfig)) + print("[INFO] Server stopped") diff --git a/scopes.md b/scopes.md new file mode 100644 index 0000000..ffb3c56 --- /dev/null +++ b/scopes.md @@ -0,0 +1,39 @@ +# Scopes + +Burgerauth comes with predefined scopes used for syncing data across applications. + +However, Burgerauth assumes all clients request all scopes in OAuth2 mode, as to keep compatibility with the base Burgerauth system. + +## Endpoints + +### RDIR - /rsakeyshare + +This one requires more explanation. This is used for clients who wish to have a RSA key unique to the account. To get this RSA Key, create a redirect like this in JS: + +``` +window.postMessage("JSON.stringify({"access_token": "ACCESS_TOKEN", "redirect_uri": "REDIRECT_URI"})", https://auth.hectabit.org/rsakeyshare); +``` +REDIRECT_URI is the correspondding URI where a Burgerauth LocalStorage acceptor is set up. Here is some JS code for a basic acceptor: + +``` +window.addEventListener( + "message", + (event) => { + if (event.origin !== "https://auth.hectabit.org/rsakeyshare") return; + localStorage.setItem("DONOTSHARE-rsakey", message) + }, + false, +); +``` +Please use this on client-side only, to maintain security and E2EE best practices. + +### POST - /api/uniqueid +Provide "access_token" + +Returns a ID unique to the user in this format: + +``` +{ + "uniqueid": "(random string)" +} +```