Skip to content

Commit

Permalink
Passwordless (#120)
Browse files Browse the repository at this point in the history
* added support to Passkey with Passwordless

* added envs for Passkey with Passwordless
  • Loading branch information
olegbilovus committed Sep 30, 2023
1 parent 64ebaa7 commit 2ffa960
Show file tree
Hide file tree
Showing 15 changed files with 338 additions and 42 deletions.
4 changes: 4 additions & 0 deletions .idea/ch-system.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .idea/codestream.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions .idea/inspectionProfiles/Project_Default.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .idea/jsLibraryMappings.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions .idea/sqldialects.xml
100755 → 100644

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 8 additions & 7 deletions chsystem/database/SQLscripts/postgresql_ch.sql
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ CREATE TABLE clan
DROP TABLE IF EXISTS userProfile CASCADE;
CREATE TABLE userProfile
(
ID BIGSERIAL PRIMARY KEY,
ID VARCHAR(100) PRIMARY KEY DEFAULT md5(random()::text),
name VARCHAR(30),
serverID SMALLSERIAL,
clanID SERIAL,
Expand All @@ -35,7 +35,7 @@ CREATE TABLE userProfile
DROP TABLE IF EXISTS apiKey CASCADE;
CREATE TABLE apiKey
(
userProfileID BIGSERIAL PRIMARY KEY,
userProfileID VARCHAR(100) PRIMARY KEY,
key VARCHAR(50) UNIQUE,
FOREIGN KEY (userProfileID)
REFERENCES userProfile (ID)
Expand Down Expand Up @@ -71,7 +71,7 @@ CREATE TABLE timer
DROP TABLE IF EXISTS discordID CASCADE;
CREATE TABLE discordID
(
userProfileID BIGSERIAL PRIMARY KEY,
userProfileID VARCHAR(100) PRIMARY KEY,
discordID BIGINT UNIQUE,
discordTag VARCHAR(50),
FOREIGN KEY (userProfileID)
Expand All @@ -82,7 +82,7 @@ CREATE TABLE discordID
DROP TABLE IF EXISTS webProfile CASCADE;
CREATE TABLE webProfile
(
userProfileID BIGSERIAL PRIMARY KEY,
userProfileID VARCHAR(100) PRIMARY KEY,
username VARCHAR(50) UNIQUE,
hash_pw VARCHAR(150),
change_pw BOOLEAN DEFAULT TRUE,
Expand All @@ -95,7 +95,7 @@ DROP TABLE IF EXISTS webSession CASCADE;
CREATE TABLE webSession
(
id VARCHAR(200) UNIQUE,
userProfileID BIGSERIAL,
userProfileID VARCHAR(100),
sessionID VARCHAR(200) UNIQUE,
host VARCHAR(50),
creation timestamp DEFAULT now(),
Expand All @@ -108,7 +108,7 @@ CREATE TABLE webSession
DROP TABLE IF EXISTS subscriber CASCADE;
CREATE TABLE subscriber
(
userProfileID BIGSERIAL,
userProfileID VARCHAR(100),
timerID BIGSERIAL,
PRIMARY KEY (timerID, userProfileID),
FOREIGN KEY (userProfileID)
Expand All @@ -126,5 +126,6 @@ AS
$$
DELETE
FROM websession
WHERE creation < now() - interval '3 days' or creation = lastuse;
WHERE creation < now() - interval '3 days'
or creation = lastuse;
$$;
55 changes: 49 additions & 6 deletions chsystem/web/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import requests
from utils import get_current_time_minutes, TIMER_OFFSET

from models import User
from models import User, PWLCredential

_HOST = os.getenv('HOST')
MAX_NUM_TIMERS = 50
Expand Down Expand Up @@ -56,15 +56,21 @@ def get_servers_names(self):
return self.session.get(f'{self.url}/server?select=id,name&order=name').json()

@postgrest_sanitize
def login(self, username, password, serverid, clan) -> User | None:
def get_username_by_userid(self, userid):
res = self.session.get(f'{self.url}/webprofile?userprofileid=eq.{userid}&select=username').json()
if res:
return res[0]['username']

@postgrest_sanitize
def login(self, username, password=None, serverid=None, clan=None, skip=False) -> User | None:
# The login is divided in multiple steps to avoid making complex query when the user does not exist
user = self.session.get(f'{self.url}/webprofile?username=eq.{username}').json()
if user and bcrypt.checkpw(bytes(password, 'utf-8'), bytes(user[0]['hash_pw'], 'utf-8')):
if user and (skip or bcrypt.checkpw(bytes(password, 'utf-8'), bytes(user[0]['hash_pw'], 'utf-8'))):
user = user[0]
user_data = self.session.get(f'{self.url}/userprofile?id=eq.{user["userprofileid"]}').json()[0]
if user_data['serverid'] == serverid:
if user_data['serverid'] == serverid or skip:
clan_data = self.session.get(f'{self.url}/clan?id=eq.{user_data["clanid"]}').json()[0]
if clan_data['name'] == clan:
if clan_data['name'] == clan or skip:
user_session = User(id=token_hex(16), sessionid=token_hex(64), username=username,
userprofileid=user_data['id'],
name=user_data['name'], role=user_data['role'],
Expand Down Expand Up @@ -108,7 +114,8 @@ def get_user_by_sessionid(self, sessionid) -> User | None:
def get_user_sessions(self, userprofileid) -> list[User]:
data = self.session.get(
f'{self.url}/websession?userprofileid=eq.{userprofileid}&select=id,creation,lastuse,host&order=lastuse').json()
return [User(id=d['id'], creation=d['creation'], lastuse=d['lastuse'], host=d['host']) for d in data]
return [User(id=d['id'], creation=datetime.fromisoformat(d['creation']),
lastuse=datetime.fromisoformat(d['lastuse']), host=d['host']) for d in data]

@postgrest_sanitize
def get_session_by_id(self, _id) -> User | None:
Expand Down Expand Up @@ -246,3 +253,39 @@ def patch_timer_by_bossname(self, clanid, bossname, respawn, window):
res = self.session.patch(f'{self.url}/timer?clanid=eq.{clanid}&bossname=eq.{bossname}',
json={'respawntimeminutes': respawn, 'windowminutes': window})
return res.status_code == 204


class ApiPasswordless:
def __init__(self, api_secret_key, url):
self.session = requests.Session()
self.session.headers.update({'ApiSecret': api_secret_key, 'Content-Type': 'application/json'})
self.url = url

def register(self, user: User):
payload = {
"userId": user.userprofileid,
"username": user.username,
"aliases": [user.userprofileid]
}
res = self.session.post(f'{self.url}/register/token', json=payload).json()
return res['token']

def signin_verify(self, verification_token):
res = self.session.post(f'{self.url}/signin/verify', json={'token': verification_token}).json()
if res['success']:
return res['userId']

def credentials(self, user: User) -> list[PWLCredential]:
res = self.session.get(f'{self.url}/credentials/list?userId={user.userprofileid}').json()
data = []
if res:
for credential in res['values']:
data.append(PWLCredential(id=credential['descriptor']['id'],
creation=datetime.fromisoformat(credential['createdAt']),
lastuse=datetime.fromisoformat(credential['lastUsedAt']),
origin=credential['origin']))
return data

def delete_credential(self, credential_id):
res = self.session.post(f'{self.url}/credentials/delete', json={'credentialId': credential_id})
return res.status_code == 200
74 changes: 63 additions & 11 deletions chsystem/web/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from waitress import serve
from models import User

from api import ApiPostgREST
from api import ApiPostgREST, ApiPasswordless

logger = logs.get_logger('Web', token=os.getenv('LOGTAIL_WEB'))
logger.info('Starting Web')
Expand All @@ -35,6 +35,8 @@
cf_client_secret=os.getenv('CF_CLIENT_SECRET'), cert_f=cert_env, key_f=key_env,
api_key=os.getenv('API_KEY'), api_key_name=os.getenv('API_KEY_NAME'))

api_pwl = ApiPasswordless(url=os.getenv('PWL_URL'), api_secret_key=os.getenv('PWL_PRIVATE_KEY'))

app = Flask(__name__, template_folder='templates', static_folder='static')
SESSION_NAME = "SessionID"

Expand Down Expand Up @@ -103,17 +105,23 @@ def home():
return render_template('index.html', servers=api.get_servers_names())


def _login_process(user: User, endpoint):
logger.info(f'{endpoint}:{user}')
resp = make_response(redirect(url_for('dashboard')))
resp.set_cookie(SESSION_NAME, user.sessionid, httponly=True, secure=True, samesite='Lax',
max_age=timedelta(days=3))
return resp


@app.post('/login')
@no_login
def login():
req = request.form
if req['verificationToken']:
return passwordless_signin()
user = api.login(req['username'].lower(), req['password'], int(req['server']), req['clan'].lower())
if user:
logger.info(f'{login.__name__}:{user}')
resp = make_response(redirect(url_for('dashboard')))
resp.set_cookie(SESSION_NAME, user.sessionid, httponly=True, secure=True, samesite='Lax',
max_age=timedelta(days=3))
return resp
return _login_process(user, login.__name__)

cache = {
'username': req['username'],
Expand All @@ -126,6 +134,44 @@ def login():
cache=cache), 401


@app.post('/passwordless/signin')
@no_login
def passwordless_signin():
req = request.form
verification_token = req['verificationToken']
userid = api_pwl.signin_verify(verification_token)
if userid:
username = api.get_username_by_userid(userid)
if username:
user = api.login(username, skip=True)
return _login_process(user, passwordless_signin.__name__)


@app.get('/passwordless/register')
@login_req()
def passwordless_register(user: User):
registration_token = api_pwl.register(user)
return jsonify({'token': registration_token, 'publicKey': os.getenv('PWL_PUBLIC_KEY')})


@app.get('/passwordless/pbk')
@no_login
def passwordless_pbk():
return jsonify({'publicKey': os.getenv('PWL_PUBLIC_KEY')})


@app.get('/passwordless/credentials')
@login_req()
def passwordless_credentials(user: User):
return jsonify(api_pwl.credentials(user))


@app.delete('/passwordless/delete')
@login_req()
def passwordless_delete(user: User):
return jsonify({'deleted': api_pwl.delete_credential(request.json['credential'])})


@app.get('/logout')
@login_req()
def logout(user: User):
Expand All @@ -135,7 +181,8 @@ def logout(user: User):
@app.get('/dashboard')
@login_req()
def dashboard(user: User):
return render_template('dashboard.html', user=user, role_name=ROLES[user.role], role_color=ROLES_COLORS[user.role])
return render_template('dashboard.html', user=user, role_name=ROLES[user.role],
role_color=ROLES_COLORS[user.role])


@app.get('/timers-type')
Expand Down Expand Up @@ -206,8 +253,11 @@ def add_timer(user: User):
@app.get('/profile')
@login_req(change_pw=False)
def profile(user: User):
return render_template('profile.html', user=user, role_name=ROLES[user.role], role_color=ROLES_COLORS[user.role],
msg={'text': 'Please change your password', 'type': 'danger'} if user.change_pw else None)
return render_template('profile.html', user=user, pwl_credentials=api_pwl.credentials(user),
role_name=ROLES[user.role],
role_color=ROLES_COLORS[user.role],
msg={'text': 'Please change your password',
'type': 'danger'} if user.change_pw else None)


@app.post('/change-pw')
Expand All @@ -225,7 +275,8 @@ def change_pwd(user: User):
else:
msg = {'text': 'Try again, there was an error.', 'type': 'danger'}

return render_template('profile.html', user=user, role_name=ROLES[user.role], role_color=ROLES_COLORS[user.role],
return render_template('profile.html', user=user, role_name=ROLES[user.role],
role_color=ROLES_COLORS[user.role],
msg=msg)


Expand Down Expand Up @@ -290,7 +341,8 @@ def change_user_role(user: User):
@app.get('/sessions')
@login_req()
def sessions(user: User):
return render_template('sessions.html', user=user, role_name=ROLES[user.role], role_color=ROLES_COLORS[user.role])
return render_template('sessions.html', user=user, role_name=ROLES[user.role],
role_color=ROLES_COLORS[user.role])


@app.get('/user-sessions')
Expand Down
10 changes: 9 additions & 1 deletion chsystem/web/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class User:
id: str
sessionid: Optional[str] = None
username: Optional[str] = None
userprofileid: Optional[int] = None
userprofileid: Optional[str] = None
host: Optional[str] = None
name: Optional[str] = None
role: Optional[int] = None
Expand All @@ -24,3 +24,11 @@ def __repr__(self):

def get_data_select(self, *args):
return {k: getattr(self, k) for k in args}


@dataclass
class PWLCredential:
id: str
creation: datetime
lastuse: datetime
origin: str
Loading

0 comments on commit 2ffa960

Please sign in to comment.