From 6f195c86a1a61dd3c84e628f06bccb69946f8f87 Mon Sep 17 00:00:00 2001 From: Nathan Woodburn Date: Fri, 9 Feb 2024 23:04:20 +1100 Subject: [PATCH] feat: Push initial version --- .gitea/workflows/build.yml | 28 ++++++ .gitignore | 4 + Dockerfile | 7 ++ README.md | 9 ++ app.py | 9 ++ requrements.txt | 4 + website/app.py | 36 +++++++ website/models.py | 56 +++++++++++ website/oauth2.py | 101 +++++++++++++++++++ website/routes.py | 144 +++++++++++++++++++++++++++ website/settings.py | 0 website/templates/authorize.html | 62 ++++++++++++ website/templates/create_client.html | 101 +++++++++++++++++++ website/templates/favicon.png | Bin 0 -> 29332 bytes website/templates/home.html | 119 ++++++++++++++++++++++ website/varo_auth.py | 22 ++++ 16 files changed, 702 insertions(+) create mode 100644 .gitea/workflows/build.yml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 app.py create mode 100644 requrements.txt create mode 100644 website/app.py create mode 100644 website/models.py create mode 100644 website/oauth2.py create mode 100644 website/routes.py create mode 100644 website/settings.py create mode 100644 website/templates/authorize.html create mode 100644 website/templates/create_client.html create mode 100644 website/templates/favicon.png create mode 100644 website/templates/home.html create mode 100644 website/varo_auth.py diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..d95d32f --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,28 @@ +name: Build Docker +run-name: Build Docker Image +on: [push] + +jobs: + Build Docker: + runs-on: [ubuntu-latest, amd] + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Install Docker + run : | + apt-get install ca-certificates curl gnupg + install -m 0755 -d /etc/apt/keyrings + curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg + chmod a+r /etc/apt/keyrings/docker.gpg + echo "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null + apt-get update + apt-get install docker-ce-cli -y + - name: Build Docker image + run : | + echo "${{ secrets.DOCKERGIT_TOKEN }}" | docker login git.woodburn.au -u nathanwoodburn --password-stdin + tag_num=$(git rev-parse --short HEAD) + docker build -t hns-login:$tag_num . + docker tag hns-login:$tag_num git.woodburn.au/nathanwoodburn/hns-login:$tag_num + docker push git.woodburn.au/nathanwoodburn/hns-login:$tag_num + docker tag hns-login:$tag_num git.woodburn.au/nathanwoodburn/hns-login:latest + docker push git.woodburn.au/nathanwoodburn/hns-login:latest \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..040812d --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ + +__pycache__/ + +instance/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1522c9b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM python:3.10-bullseye +COPY requirements.txt /app/ +WORKDIR /app +RUN pip install -r requirements.txt +COPY . . +VOLUME /app/instance +CMD ["flask", "run", "-p", "9090", "-h", "0.0.0.0"] \ No newline at end of file diff --git a/README.md b/README.md index e69501f..d75c05e 100644 --- a/README.md +++ b/README.md @@ -1 +1,10 @@ # varo-openid + +## Add a client +Set the following parameters: + +Allowed Scope: `profile` +Allowed Grant Types: `authorization_code` +Allowed Response Types: `code` +Token Endpoint Authentication Method: `client_secret_post` + diff --git a/app.py b/app.py new file mode 100644 index 0000000..72a7d05 --- /dev/null +++ b/app.py @@ -0,0 +1,9 @@ +from website.app import create_app + + +app = create_app({ + 'SECRET_KEY': 'secret', + 'OAUTH2_REFRESH_TOKEN_GENERATOR': True, + 'SQLALCHEMY_TRACK_MODIFICATIONS': False, + 'SQLALCHEMY_DATABASE_URI': 'sqlite:///db.sqlite', +}) \ No newline at end of file diff --git a/requrements.txt b/requrements.txt new file mode 100644 index 0000000..fca4a56 --- /dev/null +++ b/requrements.txt @@ -0,0 +1,4 @@ +Flask +Flask-SQLAlchemy +Authlib +requests \ No newline at end of file diff --git a/website/app.py b/website/app.py new file mode 100644 index 0000000..ded960c --- /dev/null +++ b/website/app.py @@ -0,0 +1,36 @@ +import os +from flask import Flask +from .models import db +from .oauth2 import config_oauth +from .routes import bp + + +def create_app(config=None): + app = Flask(__name__) + + # load default configuration + app.config.from_object('website.settings') + + # load environment configuration + if 'WEBSITE_CONF' in os.environ: + app.config.from_envvar('WEBSITE_CONF') + + # load app specified configuration + if config is not None: + if isinstance(config, dict): + app.config.update(config) + elif config.endswith('.py'): + app.config.from_pyfile(config) + + setup_app(app) + return app + + +def setup_app(app): + + db.init_app(app) + # Create tables if they do not exist already + with app.app_context(): + db.create_all() + config_oauth(app) + app.register_blueprint(bp, url_prefix='') \ No newline at end of file diff --git a/website/models.py b/website/models.py new file mode 100644 index 0000000..ef66d8e --- /dev/null +++ b/website/models.py @@ -0,0 +1,56 @@ +import time +from flask_sqlalchemy import SQLAlchemy +from authlib.integrations.sqla_oauth2 import ( + OAuth2ClientMixin, + OAuth2AuthorizationCodeMixin, + OAuth2TokenMixin, +) + +db = SQLAlchemy() + + +class User(db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(40), unique=True) + + def __str__(self): + return self.username + + def get_user_id(self): + return self.id + + def check_password(self, password): + return password == 'valid' + + +class OAuth2Client(db.Model, OAuth2ClientMixin): + __tablename__ = 'oauth2_client' + + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column( + db.Integer, db.ForeignKey('user.id', ondelete='CASCADE')) + user = db.relationship('User') + + +class OAuth2AuthorizationCode(db.Model, OAuth2AuthorizationCodeMixin): + __tablename__ = 'oauth2_code' + + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column( + db.Integer, db.ForeignKey('user.id', ondelete='CASCADE')) + user = db.relationship('User') + + +class OAuth2Token(db.Model, OAuth2TokenMixin): + __tablename__ = 'oauth2_token' + + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column( + db.Integer, db.ForeignKey('user.id', ondelete='CASCADE')) + user = db.relationship('User') + + def is_refresh_token_active(self): + if self.revoked: + return False + expires_at = self.issued_at + self.expires_in * 2 + return expires_at >= time.time() \ No newline at end of file diff --git a/website/oauth2.py b/website/oauth2.py new file mode 100644 index 0000000..4ab250e --- /dev/null +++ b/website/oauth2.py @@ -0,0 +1,101 @@ +from authlib.integrations.flask_oauth2 import ( + AuthorizationServer, + ResourceProtector, +) +from authlib.integrations.sqla_oauth2 import ( + create_query_client_func, + create_save_token_func, + create_revocation_endpoint, + create_bearer_token_validator, +) +from authlib.oauth2.rfc6749 import grants +from authlib.oauth2.rfc7636 import CodeChallenge +from .models import db, User +from .models import OAuth2Client, OAuth2AuthorizationCode, OAuth2Token + + +class AuthorizationCodeGrant(grants.AuthorizationCodeGrant): + TOKEN_ENDPOINT_AUTH_METHODS = [ + 'client_secret_basic', + 'client_secret_post', + 'none', + ] + + def save_authorization_code(self, code, request): + code_challenge = request.data.get('code_challenge') + code_challenge_method = request.data.get('code_challenge_method') + auth_code = OAuth2AuthorizationCode( + code=code, + client_id=request.client.client_id, + redirect_uri=request.redirect_uri, + scope=request.scope, + user_id=request.user.id, + code_challenge=code_challenge, + code_challenge_method=code_challenge_method, + ) + db.session.add(auth_code) + db.session.commit() + return auth_code + + def query_authorization_code(self, code, client): + auth_code = OAuth2AuthorizationCode.query.filter_by( + code=code, client_id=client.client_id).first() + if auth_code and not auth_code.is_expired(): + return auth_code + + def delete_authorization_code(self, authorization_code): + db.session.delete(authorization_code) + db.session.commit() + + def authenticate_user(self, authorization_code): + return User.query.get(authorization_code.user_id) + + +class PasswordGrant(grants.ResourceOwnerPasswordCredentialsGrant): + def authenticate_user(self, username, password): + user = User.query.filter_by(username=username).first() + if user is not None and user.check_password(password): + return user + + +class RefreshTokenGrant(grants.RefreshTokenGrant): + def authenticate_refresh_token(self, refresh_token): + token = OAuth2Token.query.filter_by(refresh_token=refresh_token).first() + if token and token.is_refresh_token_active(): + return token + + def authenticate_user(self, credential): + return User.query.get(credential.user_id) + + def revoke_old_credential(self, credential): + credential.revoked = True + db.session.add(credential) + db.session.commit() + + +query_client = create_query_client_func(db.session, OAuth2Client) +save_token = create_save_token_func(db.session, OAuth2Token) +authorization = AuthorizationServer( + query_client=query_client, + save_token=save_token, +) +require_oauth = ResourceProtector() + + +def config_oauth(app): + authorization.init_app(app) + + # support all grants + authorization.register_grant(grants.ImplicitGrant) + authorization.register_grant(grants.ClientCredentialsGrant) + authorization.register_grant(AuthorizationCodeGrant, [CodeChallenge(required=True)]) + authorization.register_grant(PasswordGrant) + authorization.register_grant(RefreshTokenGrant) + + # support revocation + revocation_cls = create_revocation_endpoint(db.session, OAuth2Token) + authorization.register_endpoint(revocation_cls) + + # protect resource + bearer_cls = create_bearer_token_validator(db.session, OAuth2Token) + require_oauth.register_token_validator(bearer_cls()) \ No newline at end of file diff --git a/website/routes.py b/website/routes.py new file mode 100644 index 0000000..e091e6b --- /dev/null +++ b/website/routes.py @@ -0,0 +1,144 @@ +import time +from .varo_auth import flask_login as varo_auth_flask_login +from flask import Blueprint, request, session, url_for +from flask import render_template, redirect, jsonify, send_from_directory +from werkzeug.security import gen_salt +from authlib.integrations.flask_oauth2 import current_token +from authlib.oauth2 import OAuth2Error +from .models import db, User, OAuth2Client +from .oauth2 import authorization, require_oauth + + +bp = Blueprint('home', __name__) + + +def current_user(): + if 'id' in session: + uid = session['id'] + return User.query.get(uid) + return None + + +def split_by_crlf(s): + return [v for v in s.splitlines() if v] + + +@bp.route('/', methods=('GET', 'POST')) +def home(): + next_page = request.args.get('next') + if request.method == 'POST': + auth = varo_auth_flask_login(request) + if auth == False: + return redirect('/?error=login_failed') + print(auth) + user = User.query.filter_by(username=auth).first() + if not user: + user = User(username=auth) + db.session.add(user) + db.session.commit() + session['id'] = user.id + # if user is not just to log in, but need to head back to the auth page, then go for it + if next_page: + return redirect(next_page) + return redirect('/') + user = current_user() + if user: + clients = OAuth2Client.query.filter_by(user_id=user.id).all() + if next_page: + return redirect(next_page) + else: + clients = [] + + return render_template('home.html', user=user, clients=clients) + + +@bp.route('/logout') +def logout(): + del session['id'] + next = request.args.get('next') + if next: + return redirect(url_for('home.home', next=next)) + + return redirect('/') + + +@bp.route('/create_client', methods=('GET', 'POST')) +def create_client(): + user = current_user() + if not user: + return redirect('/') + if request.method == 'GET': + return render_template('create_client.html') + + client_id = gen_salt(24) + client_id_issued_at = int(time.time()) + client = OAuth2Client( + client_id=client_id, + client_id_issued_at=client_id_issued_at, + user_id=user.id, + ) + + form = request.form + client_metadata = { + "client_name": form["client_name"], + "client_uri": form["client_uri"], + "grant_types": split_by_crlf(form["grant_type"]), + "redirect_uris": split_by_crlf(form["redirect_uri"]), + "response_types": split_by_crlf(form["response_type"]), + "scope": form["scope"], + "token_endpoint_auth_method": form["token_endpoint_auth_method"] + } + client.set_client_metadata(client_metadata) + + if form['token_endpoint_auth_method'] == 'none': + client.client_secret = '' + else: + client.client_secret = gen_salt(48) + + db.session.add(client) + db.session.commit() + return redirect('/') + + +@bp.route('/oauth/authorize', methods=['GET', 'POST']) +def authorize(): + user = current_user() + # if user log status is not true (Auth server), then to log it in + if not user: + return redirect(url_for('home.home', next=request.url)) + if request.method == 'GET': + try: + grant = authorization.get_consent_grant(end_user=user) + except OAuth2Error as error: + return error.error + return render_template('authorize.html', user=user, grant=grant) + + grant_user = user + + return authorization.create_authorization_response(grant_user=grant_user) + + +@bp.route('/oauth/token', methods=['POST']) +def issue_token(): + return authorization.create_token_response() + + +@bp.route('/oauth/revoke', methods=['POST']) +def revoke_token(): + return authorization.create_endpoint_response('revocation') + + +@bp.route('/api/me') +@require_oauth('profile') +def api_me(): + user = current_token.user + print(user.id, user.username) + return jsonify(id=user.id, username=user.username, + email="auth+" + user.username + "@hnshosting.au", + displayName=user.username+"/") + + + +@bp.route('/favicon.png') +def favicon(): + return send_from_directory('templates', 'favicon.png', mimetype='image/png') \ No newline at end of file diff --git a/website/settings.py b/website/settings.py new file mode 100644 index 0000000..e69de29 diff --git a/website/templates/authorize.html b/website/templates/authorize.html new file mode 100644 index 0000000..e0369b4 --- /dev/null +++ b/website/templates/authorize.html @@ -0,0 +1,62 @@ + + + + + + HNS Login + + + + +

Authorize Access

+

{{grant.client.client_name}} is requesting to access your account.

+

+ +

+You are currently logged in as {{ user.username }}/ (Change Account) +

+ +
+
+ +
+ +
+ Powered by Varo Auth and Nathan.Woodburn/ +
+ + + + diff --git a/website/templates/create_client.html b/website/templates/create_client.html new file mode 100644 index 0000000..2c8ff4f --- /dev/null +++ b/website/templates/create_client.html @@ -0,0 +1,101 @@ + + + + + + HNS Login + + + + + + + + + + +

Home

+

Create OAuth Client

+ +
+ + + + + + + + +
+ +
+
+ Powered by Varo Auth and Nathan.Woodburn/ +
+ + \ No newline at end of file diff --git a/website/templates/favicon.png b/website/templates/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..88ddd853bf4df98e47beaa14c0dd2b18155619e6 GIT binary patch literal 29332 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 literal 0 HcmV?d00001 diff --git a/website/templates/home.html b/website/templates/home.html new file mode 100644 index 0000000..fa151cb --- /dev/null +++ b/website/templates/home.html @@ -0,0 +1,119 @@ + + + + + + HNS Login + + + + +

HNS Login

+ +{% if user %} + +

You are currently logged in as {{ user }}/

+ + +Log Out + +{% for client in clients %} +
+Client Info
+  {%- for key in client.client_info %}
+  {{ key }}: {{ client.client_info[key] }}
+  {%- endfor %}
+Client Metadata
+  {%- for key in client.client_metadata %}
+  {{ key }}: {{ client.client_metadata[key] }}
+  {%- endfor %}
+
+
+{% endfor %} + +

+

Want to implement OAuth?
+Contact Nathan.Woodburn/ on any social media platform

+ +{% else %} +

Login with your Handshake domain

+

If you have already used Varo Auth, you can just select the domain you want to login with.

+ + + + + + + +{% endif %} + + + +
+ Powered by Varo Auth and Nathan.Woodburn/ +
+ + \ No newline at end of file diff --git a/website/varo_auth.py b/website/varo_auth.py new file mode 100644 index 0000000..738d07d --- /dev/null +++ b/website/varo_auth.py @@ -0,0 +1,22 @@ +import requests +import json + +def flask_login(request): + dict = request.form.to_dict() + keys = dict.keys() + keys = list(keys)[0] + keys = json.loads(keys) + auth_request = keys['request'] + return login(auth_request) + +def login(request): + r = requests.get(f'https://auth.varo.domains/verify/{request}') + r = r.json() + if r['success'] == False: + return False + + if 'data' in r: + data = r['data'] + if 'name' in data: + return data['name'] + return False \ No newline at end of file