Skip to content

Commit

Permalink
Implement bearer tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
frezik committed Jun 1, 2024
1 parent 3c3845a commit 45149dd
Show file tree
Hide file tree
Showing 16 changed files with 350 additions and 68 deletions.
32 changes: 28 additions & 4 deletions Doorbot/API.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ def set_error(
response.set_data( json_data )
return response

def error_response( msg, status = 400 ):
response = flask.make_response()
response = set_error( response, msg, status )
return response

# From https://stackoverflow.com/questions/2546207/does-sqlalchemy-have-an-equivalent-of-djangos-get-or-create
def get_or_create(
session,
Expand Down Expand Up @@ -144,11 +149,30 @@ def search_tag_list(

def auth_required( func ):
def check( *args, **kwargs ):
# For now, we only allow these endpoints for testing
if 'is_testing' in app.config and app.config[ 'is_testing' ]:
#if 'is_testing' in app.config and app.config[ 'is_testing' ]:
# return func( *args, **kwargs )

auth_header = flask.request.headers.get( 'authorization' )
if not auth_header:
return error_response( "Invalid authorization", 401 )
auth_match = re.search( r"^Bearer\s+(.*)$", auth_header )
bearer_str = auth_match.group( 1 ) if auth_match else None
if not bearer_str:
return error_response( "Invalid authorization", 401 )

session = get_session()
stmt = select( Doorbot.SQLAlchemy.OauthToken ).where(
Doorbot.SQLAlchemy.OauthToken.token == bearer_str
)
token = session.scalars( stmt ).one_or_none()
session.close()

# TODO store token somewhere so we can check permissions on an
# endpoint later
if token:
return func( *args, **kwargs )
else:
return redirect_home()

return error_response( "Invalid authorization", 401 )

# Avoid error of "View function mapping is overwriting an existing endpoint
# function"
Expand Down
66 changes: 65 additions & 1 deletion Doorbot/Pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@
from Doorbot.SQLAlchemy import Role
from Doorbot.SQLAlchemy import get_engine
from Doorbot.SQLAlchemy import get_session
from datetime import datetime
from datetime import datetime, timedelta, timezone
from flask_stache import render_template
from sqlalchemy import select
from sqlalchemy.sql import text
from urllib.parse import urlparse
import pathlib
import secrets


def error_page(
Expand Down Expand Up @@ -803,3 +804,66 @@ def mp_rfid_report():
page_text = mp_rfid_rpt
)

@app.route( "/create-oauth", methods = [ "GET" ] )
@require_logged_in
def create_oauth_form():
username = flask.session.get( 'username' )

return render_tmpl(
'create_oauth',
page_name = "Create OAuth2 Token"
)

@app.route( "/create-oauth", methods = [ "POST" ] )
@require_logged_in
def create_oauth():
username = flask.session.get( 'username' )

request = flask.request
name = request.form[ 'name' ]

session = get_session()

errors = []
if not Doorbot.API.MATCH_NAME.match( name ):
errors.append( "Token Name should be a string" )

response = flask.make_response()
if errors:
session.close()
return error_page(
response,
msgs = errors,
tmpl = "create_oauth",
page_name = "Create OAuth Token",
status = 400,
)
else:
token_config = Doorbot.Config.get( 'oauth' )

token_str = secrets.token_hex( token_config[ 'token_hex_length' ] )
now = datetime.now( timezone.utc )
expires_delta = timedelta( days = token_config[ 'expires_days' ] )
expires = now + expires_delta
member = Member.get_by_username( username, session )

token = Doorbot.SQLAlchemy.OauthToken(
name = name,
token = token_str,
expiration_date = expires,
member = member
)

session.add( token )
session.commit()
session.close()

return render_tmpl(
'create_oauth_submit',
page_name = "Create OAuth Token",
msg = "Added token",
token = token_str,
token_name = name,
token_expires = expires.isoformat(),
)

29 changes: 29 additions & 0 deletions Doorbot/SQLAlchemy.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,10 @@ class Member( Base ):
back_populates = "members",
)

tokens: Mapped[ List[ "OauthToken" ] ] = relationship(
back_populates = "member"
)


def get_by_tag( tag, session ):
"""Fetch a single member by RFID tag"""
Expand Down Expand Up @@ -575,3 +579,28 @@ def all_members_with_permission(
result = query.all()
session.close()
return result

class OauthToken( Base ):
"""Represents an OAuth2 bearer token"""
__tablename__ = "oauth_tokens"

id: Mapped[ int ] = mapped_column( primary_key = True )
name: Mapped[ str ] = mapped_column(
String(),
nullable = False,
)
token: Mapped[ str ] = mapped_column(
String(),
nullable = False,
)
expiration_date: Mapped[ str ] = mapped_column(
DateTime(),
nullable = False
)
member_id: Mapped[ int ] = mapped_column(
ForeignKey( "members.id" ),
nullable = False,
)
member: Mapped[ "Member" ] = relationship(
back_populates = "tokens"
)
1 change: 0 additions & 1 deletion TODO
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# Future Version
* Rebuild flask directory layout
* HTML Tests
* Oauth2 on API
* Page to change password for the logged in user
* Page to change password for any user
** Force them to change their password at next login
Expand Down
4 changes: 4 additions & 0 deletions config.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ session:
key:
life_minutes: 60

oauth:
expires_days: 180
token_hex_length: 64

build_id:
build_branch:
build_date:
10 changes: 10 additions & 0 deletions rfid_app/templates/create_oauth.mustache
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{{> head }}

{{> top_nav }}

<form action="/create-oauth" method="POST">
<p>Name: <input type="text" id="name" name="name"></p>
<p><input type="submit" value="Create OAuth2 Token"></p>
</form>

{{> foot }}
19 changes: 19 additions & 0 deletions rfid_app/templates/create_oauth_submit.mustache
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{{> head }}

{{> top_nav }}

<p>Copy token into your application. <strong>Token will not be shown again</strong>.</p>

<ul>
<li>Name: {{{token_name}}}</li>
<li>Expires: {{token_expires}}</li>
<li>Token: <input
id="token"
name="token"
value="{{token}}"
readonly="1"
type="text"
></li>
</ul>

{{> foot }}
19 changes: 14 additions & 5 deletions rfid_app/templates/home.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,20 @@
{{> top_nav }}

<ul>
<li><a href="/add-tag">Add New RFID</a></li>
<li><a href="/view-tag-list">View RFID List</a></li>
<li><a href="/search-scan-logs">View Entry Log</a></li>
<li><a href="/device-list">View Devices</a></li>
<li><a href="/mp-rfid-report">View MemberPress vs RFID Report</a></li>
<li>Member Tags
<ul>
<li><a href="/add-tag">Add New RFID</a></li>
<li><a href="/view-tag-list">View RFID List</a></li>
<li><a href="/search-scan-logs">View Entry Log</a></li>
<li><a href="/device-list">View Devices</a></li>
<li><a href="/mp-rfid-report">View MemberPress vs RFID Report</a></li>
</ul>
</li>
<li>Controllers
<ul>
<li><a href="/create-oauth">Create OAuth Token</a></li>
</ul>
</li>
</ul>

{{> foot }}
36 changes: 6 additions & 30 deletions sql/changes_pg.sql
Original file line number Diff line number Diff line change
@@ -1,32 +1,8 @@
ALTER TABLE members ADD COLUMN password_type TEXT;
ALTER TABLE members ADD COLUMN encoded_password TEXT;
ALTER TABLE members ADD COLUMN username TEXT UNIQUE;


CREATE TABLE roles (
CREATE TABLE oauth_tokens (
id SERIAL PRIMARY KEY NOT NULL,
name TEXT NOT NULL UNIQUE
name TEXT NOT NULL,
token TEXT NOT NULL UNIQUE,
expiration_date TIMESTAMP WITH TIME ZONE NOT NULL,
member_id INT NOT NULL REFERENCES members (id)
);

CREATE TABLE role_members (
member_id INT REFERENCES members (id),
role_id INT REFERENCES roles (id),

PRIMARY KEY( member_id, role_id )
);
CREATE INDEX ON role_members (member_id);
CREATE INDEX ON role_members (role_id);

CREATE TABLE permissions (
id SERIAL PRIMARY KEY NOT NULL,
name TEXT NOT NULL UNIQUE
);

CREATE TABLE role_permissions (
role_id INT REFERENCES roles (id),
permission_id INT REFERENCES permissions(id),

PRIMARY KEY( role_id, permission_id )
);
CREATE INDEX ON role_permissions (role_id);
CREATE INDEX ON role_permissions (permission_id);
CREATE INDEX ON oauth_tokens (member_id);
9 changes: 9 additions & 0 deletions sql/pg.sql
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,12 @@ CREATE TABLE role_permissions (
);
CREATE INDEX ON role_permissions (role_id);
CREATE INDEX ON role_permissions (permission_id);

CREATE TABLE oauth_tokens (
id SERIAL PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
token TEXT NOT NULL UNIQUE,
expiration_date TIMESTAMP WITH TIME ZONE NOT NULL,
member_id INT NOT NULL REFERENCES members (id)
);
CREATE INDEX ON oauth_tokens (member_id);
9 changes: 9 additions & 0 deletions sql/sqlite.sql
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,12 @@ CREATE TABLE entry_log (
location INT REFERENCES locations (id)
);
CREATE INDEX ON entry_log (entry_time DESC);

CREATE TABLE oauth_tokens (
id INT PRIMARY KEY AUTOINCREMENT NOT NULL,
name TEXT NOT NULL,
token TEXT NOT NULL UNIQUE,
expiration_date DATETIME NOT NULL,
member_id INT NOT NULL REFERENCES members (id)
);
CREATE INDEX ON oauth_tokens (member_id);
Loading

0 comments on commit 45149dd

Please sign in to comment.