Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

openid-connect #180

Merged
merged 7 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,32 @@ You need to mount the file [`env.js`](frontend/public/environment/env.js) in t
* If the username does not contain `@` characters this postfix (plus `@` character) is appended to the username and used to construct the email address
* Defaults to: `None`

### OpenID Connect environment variables
* `OIDC_CLIENT_ID`
* The client id of the openid connect client
* Defaults to: `None`
* `OIDC_CLIENT_SECRET`
* The secret of the openid connect client
* Defaults to: `None`
* `OIDC_REDIRECT_MAIN_PAGE`
* The domain on which the drinklist is hosten **including `http(s)://`**
* Defaults to: `http://127.0.0.1:3000`
* `OIDC_AUTH_PATH`
* The url where the login redirects to e.g. `<server>/oauth2/auth`
* Defaults to: `None`
* `OIDC_AUTH_TOKEN`
* The url where the token is retreived to e.g. `<server>/oauth2/token`
* Defaults to: `None`
* `OIDC_AUTH_REDIRECT`
* The url of the backend where the login provider sends the data to
* Defaults to: `http://127.0.0.1:5000/api/oidc-redirect`
* `OIDC_USER_INFO`
* The url under which the backend can retreive the information of the user with the token
* Defaults to: `None`
* `OIDC_USER_NEEDS_VERIFICATION`
* Flag if users need to be activated by an admin when using openid connect for the first time
* Defaults to: `true`

### Importing from old Drinklist
* `X_AUTH_TOKEN`
* If you need to import data from an old drinklist provide an admin api token and this drinklist will fetch all data from the old drinklist
Expand Down
15 changes: 11 additions & 4 deletions backend/database/Queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def get_user_history(self, member_id):

output.sort(key=lambda transaction: transaction['id'])
output.reverse()

return output

def add_user_favorite(self, member_id, drink_id):
Expand Down Expand Up @@ -90,9 +90,11 @@ def change_user_visibility(self, member_id, visibility=None):

def add_user(self, name, money, password, alias="", hidden=False):
pw_hash, salt = TokenManager.hashPassword(password)
self.session.add(
Member(name=name.lower(), balance=money, password=pw_hash, salt=salt, alias=alias, hidden=hidden))
new_member = Member(name=name.lower(), balance=money,
password=pw_hash, salt=salt, alias=alias, hidden=hidden)
self.session.add(new_member)
self.session.commit()
return new_member

def get_drinks(self):
drinks = self.session.query(Drink).all()
Expand Down Expand Up @@ -329,6 +331,12 @@ def checkPassword(self, name, password):
else:
return None

def check_user(self, name):
member: Member = self.session.query(
Member).filter_by(name=name.lower()).first()

return member

def get_most_bought_drink_name(self, member_id: int, timestamp: datetime):
# Round timestamp down to nearest 15-minute interval
now = datetime.now()
Expand Down Expand Up @@ -703,7 +711,6 @@ def restore_database(self, imported_data):
except:
self.session.rollback()
print("Failed to import drink", d['name'])


print("Added drinks")

Expand Down
68 changes: 66 additions & 2 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from flask_restx import reqparse
import flask
import mail
import secrets

api_bp = flask.Blueprint("api", __name__, url_prefix="/api/")
api = Api(api_bp, doc='/docu/', base_url='/api')
Expand Down Expand Up @@ -313,9 +314,9 @@ def post(self):
else:
db.add_user(request.json["name"],
request.json["money"], request.json["password"])

mail.send_welcome_mail(request.json["name"])

return util.build_response("User added")


Expand Down Expand Up @@ -663,6 +664,69 @@ def get(self):
return util.build_response("OK")


@api.route('/start-oidc')
class start_oidc(Resource):
def get(self):
"""
Start oidc flow
"""

return util.build_response(f"{util.OIDC_AUTH_PATH}?client_id={util.OIDC_CLIENT_ID}&response_type=code&scope=email%20profile%20openid&state={secrets.token_urlsafe(32)}")


@api.route('/oidc-redirect')
class oidc_redirect(Resource):
def get(self):
"""
Handle redirect of auth provider
"""

# get query parameters
code = request.args.get('code')
# state = request.args.get('state')
# scope = request.args.get('scope')

token_endpoint = util.OIDC_AUTH_TOKEN
redirect_uri = util.OIDC_AUTH_REDIRECT

token = util.get_oidc_token(token_endpoint, code, redirect_uri)
userinfo = util.get_user_info(access_token=token,
resource_url=util.OIDC_USER_INFO)

# check if user exists
user = db.check_user(userinfo["username"])
login_token = None
user_id = None

if user is None:
# create user
new_member = db.add_user(userinfo["username"],
0, util.standard_user_password, alias=userinfo["name"], hidden=util.OIDC_USER_NEEDS_VERIFICATION)
mail.send_welcome_mail(new_member.name)

if util.OIDC_USER_NEEDS_VERIFICATION:
return flask.redirect(util.OIDC_REDIRECT_MAIN_PAGE+"/message/new-user", code=302)

user_id = new_member.id
login_token = token_manager.create_token(user_id)

else:
if user.hidden:
return flask.redirect(util.OIDC_REDIRECT_MAIN_PAGE+"/message/activate", code=302)

# log user in
user_id = user.id
login_token = token_manager.create_token(user_id)

r = flask.redirect(util.OIDC_REDIRECT_MAIN_PAGE, code=302)
r.set_cookie("memberID", str(user_id),
domain=util.domain, max_age=util.cookie_expire, samesite='Strict')
r.set_cookie("token", login_token,
domain=util.domain, max_age=util.cookie_expire, samesite='Strict')

return r


@api.route('/login/admin/check')
class login_Check_Admin(Resource):
@admin
Expand Down
36 changes: 36 additions & 0 deletions backend/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import os
import datetime
import time
import requests

cookie_expire = int(os.environ.get("COOKIE_EXPIRE_TIME")) * \
60*60 if os.environ.get("COOKIE_EXPIRE_TIME") else 60**3
domain = os.environ.get("DOMAIN") if os.environ.get(
Expand Down Expand Up @@ -49,6 +51,23 @@
mail_postfix = os.environ.get(
"MAIL_POSTFIX") if os.environ.get("MAIL_POSTFIX") else None

OIDC_CLIENT_ID = os.environ.get(
"OIDC_CLIENT_ID") if os.environ.get("OIDC_CLIENT_ID") else None
OIDC_CLIENT_SECRET = os.environ.get(
"OIDC_CLIENT_SECRET") if os.environ.get("OIDC_CLIENT_SECRET") else None
OIDC_REDIRECT_MAIN_PAGE = os.environ.get(
"OIDC_REDIRECT_MAIN_PAGE") if os.environ.get("OIDC_REDIRECT_MAIN_PAGE") else "http://127.0.0.1:3000"
OIDC_AUTH_PATH = os.environ.get(
"OIDC_AUTH_PATH") if os.environ.get("OIDC_AUTH_PATH") else None
OIDC_AUTH_TOKEN = os.environ.get(
"OIDC_AUTH_TOKEN") if os.environ.get("OIDC_AUTH_TOKEN") else None
OIDC_AUTH_REDIRECT = os.environ.get(
"OIDC_AUTH_REDIRECT") if os.environ.get("OIDC_AUTH_REDIRECT") else "http://127.0.0.1:5000/api/oidc-redirect"
OIDC_USER_INFO = os.environ.get(
"OIDC_USER_INFO") if os.environ.get("OIDC_USER_INFO") else None
OIDC_USER_NEEDS_VERIFICATION = os.environ.get(
"OIDC_USER_NEEDS_VERIFICATION")=="true" if os.environ.get("OIDC_USER_NEEDS_VERIFICATION") else True

tempfile_path = "tempfiles"
backup_file_name = "backup.json"

Expand Down Expand Up @@ -78,6 +97,23 @@ def log(prefix, message):
f.write(f"{output_string}\n")


def get_oidc_token(token_url, code, redirect_uri):
client_auth = requests.auth.HTTPBasicAuth(OIDC_CLIENT_ID, OIDC_CLIENT_SECRET)
post_data = {
"grant_type": "authorization_code",
"code": code,
"redirect_uri": redirect_uri
}
response = requests.post(token_url, auth=client_auth, data=post_data)
token_json = response.json()
return token_json["access_token"]


def get_user_info(access_token, resource_url):
headers = {'Authorization': 'Bearer ' + access_token}
response = requests.get(resource_url, headers=headers)
return response.json()

checkout_mail_text = """Hallo {name},
eine Getränkelisten abrechnung wurde durchgeführt, wir möchten dich hiermit über deinen aktuellen Kontostand informieren.
Aktuell hast du ein Guthaben von {balance}€.
Expand Down
Loading
Loading