mirror of
https://github.com/offen/website.git
synced 2024-11-22 09:00:28 +01:00
add accounts and user CRUD and connect to auth layer
This commit is contained in:
parent
670032ee92
commit
d5be3feb7e
@ -196,6 +196,7 @@ jobs:
|
|||||||
docker:
|
docker:
|
||||||
- image: circleci/python:3.6
|
- image: circleci/python:3.6
|
||||||
environment:
|
environment:
|
||||||
|
POSTGRES_CONNECTION_STRING: postgres://circle:test@localhost:5432/circle_test?sslmode=disable
|
||||||
HASHED_PASSWORD: JDJhJDEwJGpFRXJMOVVSQndZQlFQNjkxallkZi53aGp1cDMvRW5maGUvakZleG1pWFlnWEVXcU93ODBp
|
HASHED_PASSWORD: JDJhJDEwJGpFRXJMOVVSQndZQlFQNjkxallkZi53aGp1cDMvRW5maGUvakZleG1pWFlnWEVXcU93ODBp
|
||||||
JWT_PRIVATE_KEY: |-
|
JWT_PRIVATE_KEY: |-
|
||||||
-----BEGIN PRIVATE KEY-----
|
-----BEGIN PRIVATE KEY-----
|
||||||
@ -236,7 +237,10 @@ jobs:
|
|||||||
a3B4L0waKzP5QWcO865n1HCUTnV+s4lNcphBDZCrSwTkXnVnQWVPCL7ssoQyM0u3
|
a3B4L0waKzP5QWcO865n1HCUTnV+s4lNcphBDZCrSwTkXnVnQWVPCL7ssoQyM0u3
|
||||||
HQIDAQAB
|
HQIDAQAB
|
||||||
-----END PUBLIC KEY-----
|
-----END PUBLIC KEY-----
|
||||||
|
- image: circleci/postgres:11.2-alpine
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=circle
|
||||||
|
- POSTGRES_PASSWORD=test
|
||||||
working_directory: ~/offen/accounts
|
working_directory: ~/offen/accounts
|
||||||
steps:
|
steps:
|
||||||
- checkout:
|
- checkout:
|
||||||
@ -254,6 +258,16 @@ jobs:
|
|||||||
paths:
|
paths:
|
||||||
- ~/offen/accounts/venv
|
- ~/offen/accounts/venv
|
||||||
key: offen-accounts-{{ checksum "requirements.txt" }}-{{ checksum "requirements-dev.txt" }}
|
key: offen-accounts-{{ checksum "requirements.txt" }}-{{ checksum "requirements-dev.txt" }}
|
||||||
|
- run:
|
||||||
|
name: Waiting for Postgres to be ready
|
||||||
|
command: |
|
||||||
|
for i in `seq 1 10`;
|
||||||
|
do
|
||||||
|
nc -z localhost 5432 && echo Success && exit 0
|
||||||
|
echo -n .
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
echo Failed waiting for Postgres && exit 1
|
||||||
- run:
|
- run:
|
||||||
name: Run tests
|
name: Run tests
|
||||||
command: |
|
command: |
|
||||||
|
1
Makefile
1
Makefile
@ -20,5 +20,6 @@ setup:
|
|||||||
bootstrap:
|
bootstrap:
|
||||||
@docker-compose run kms make bootstrap
|
@docker-compose run kms make bootstrap
|
||||||
@docker-compose run server make bootstrap
|
@docker-compose run server make bootstrap
|
||||||
|
@docker-compose run accounts make bootstrap
|
||||||
|
|
||||||
.PHONY: setup bootstrap
|
.PHONY: setup bootstrap
|
||||||
|
@ -4,4 +4,7 @@ test:
|
|||||||
fmt:
|
fmt:
|
||||||
@black .
|
@black .
|
||||||
|
|
||||||
.PHONY: test fmt
|
bootstrap:
|
||||||
|
@python -m scripts.bootstrap
|
||||||
|
|
||||||
|
.PHONY: test fmt bootstrap
|
||||||
|
@ -1,5 +1,23 @@
|
|||||||
|
from os import environ
|
||||||
|
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from flask_admin import Admin
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
app.secret_key = environ.get("SESSION_SECRET")
|
||||||
|
app.config["SQLALCHEMY_DATABASE_URI"] = environ.get("POSTGRES_CONNECTION_STRING")
|
||||||
|
db = SQLAlchemy(app)
|
||||||
|
|
||||||
import accounts.views
|
from accounts.models import Account, User
|
||||||
|
from accounts.views import AccountView, UserView
|
||||||
|
import accounts.api
|
||||||
|
|
||||||
|
# set optional bootswatch theme
|
||||||
|
app.config["FLASK_ADMIN_SWATCH"] = "flatly"
|
||||||
|
|
||||||
|
admin = Admin(app, name="offen admin", template_mode="bootstrap3")
|
||||||
|
# Add administrative views here
|
||||||
|
|
||||||
|
admin.add_view(AccountView(Account, db.session))
|
||||||
|
admin.add_view(UserView(User, db.session))
|
||||||
|
93
accounts/accounts/api.py
Normal file
93
accounts/accounts/api.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
from datetime import datetime, timedelta
|
||||||
|
from os import environ
|
||||||
|
import base64
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
from flask import jsonify, make_response, request
|
||||||
|
from flask_cors import cross_origin
|
||||||
|
from passlib.hash import bcrypt
|
||||||
|
import jwt
|
||||||
|
|
||||||
|
from accounts import app
|
||||||
|
from accounts.models import User
|
||||||
|
|
||||||
|
|
||||||
|
def json_error(handler):
|
||||||
|
@wraps(handler)
|
||||||
|
def wrapped_handler(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
return handler(*args, **kwargs)
|
||||||
|
except Exception as server_error:
|
||||||
|
return (
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"error": "Internal server error: {}".format(str(server_error)),
|
||||||
|
"status": 500,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
500,
|
||||||
|
)
|
||||||
|
|
||||||
|
return wrapped_handler
|
||||||
|
|
||||||
|
|
||||||
|
class UnauthorizedError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@app.route("/api/login", methods=["POST"])
|
||||||
|
@cross_origin(origins=[environ.get("CORS_ORIGIN", "*")], supports_credentials=True)
|
||||||
|
@json_error
|
||||||
|
def post_login():
|
||||||
|
credentials = request.get_json(force=True)
|
||||||
|
try:
|
||||||
|
match = User.query.filter_by(email=credentials["username"]).first()
|
||||||
|
if not match:
|
||||||
|
raise UnauthorizedError("bad username")
|
||||||
|
if not bcrypt.verify(credentials["password"], match.hashed_password):
|
||||||
|
raise UnauthorizedError("bad password")
|
||||||
|
except UnauthorizedError as unauthorized_error:
|
||||||
|
resp = make_response(jsonify({"error": str(unauthorized_error), "status": 401}))
|
||||||
|
resp.set_cookie("auth", "", expires=0)
|
||||||
|
resp.status_code = 401
|
||||||
|
return resp
|
||||||
|
|
||||||
|
private_key = environ.get("JWT_PRIVATE_KEY", "")
|
||||||
|
expiry = datetime.utcnow() + timedelta(hours=24)
|
||||||
|
encoded = jwt.encode(
|
||||||
|
{"ok": True, "exp": expiry}, private_key.encode(), algorithm="RS256"
|
||||||
|
).decode("utf-8")
|
||||||
|
|
||||||
|
resp = make_response(jsonify({"match": match}))
|
||||||
|
resp.set_cookie(
|
||||||
|
"auth",
|
||||||
|
encoded,
|
||||||
|
httponly=True,
|
||||||
|
expires=expiry,
|
||||||
|
path="/",
|
||||||
|
domain=environ.get("COOKIE_DOMAIN"),
|
||||||
|
samesite="strict",
|
||||||
|
)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/login", methods=["GET"])
|
||||||
|
@cross_origin(origins=[environ.get("CORS_ORIGIN", "*")], supports_credentials=True)
|
||||||
|
@json_error
|
||||||
|
def get_login():
|
||||||
|
auth_cookie = request.cookies.get("auth")
|
||||||
|
public_key = environ.get("JWT_PUBLIC_KEY", "")
|
||||||
|
try:
|
||||||
|
jwt.decode(auth_cookie, public_key)
|
||||||
|
except jwt.exceptions.PyJWTError as unauthorized_error:
|
||||||
|
return jsonify({"error": str(unauthorized_error), "status": 401}), 401
|
||||||
|
|
||||||
|
return jsonify({"ok": True})
|
||||||
|
|
||||||
|
|
||||||
|
# This route is not supposed to be called by client-side applications, so
|
||||||
|
# no CORS configuration is added
|
||||||
|
@app.route("/api/key", methods=["GET"])
|
||||||
|
@json_error
|
||||||
|
def key():
|
||||||
|
public_key = environ.get("JWT_PUBLIC_KEY", "").strip()
|
||||||
|
return jsonify({"key": public_key})
|
39
accounts/accounts/models.py
Normal file
39
accounts/accounts/models.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from accounts import db
|
||||||
|
|
||||||
|
|
||||||
|
def generate_key():
|
||||||
|
return str(uuid4())
|
||||||
|
|
||||||
|
|
||||||
|
class Account(db.Model):
|
||||||
|
__tablename__ = "accounts"
|
||||||
|
account_id = db.Column(db.String, primary_key=True, default=generate_key)
|
||||||
|
name = db.Column(db.String, nullable=False, unique=True)
|
||||||
|
users = db.relationship("AccountUserAssociation", back_populates="account")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class User(db.Model):
|
||||||
|
__tablename__ = "users"
|
||||||
|
user_id = db.Column(db.String, primary_key=True, default=generate_key)
|
||||||
|
email = db.Column(db.String, nullable=False, unique=True)
|
||||||
|
hashed_password = db.Column(db.String, nullable=False)
|
||||||
|
accounts = db.relationship("AccountUserAssociation", back_populates="user")
|
||||||
|
|
||||||
|
|
||||||
|
class AccountUserAssociation(db.Model):
|
||||||
|
__tablename__ = "account_to_user"
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
|
||||||
|
user_id = db.Column(db.String, db.ForeignKey("users.user_id"), nullable=False)
|
||||||
|
account_id = db.Column(
|
||||||
|
db.String, db.ForeignKey("accounts.account_id"), nullable=False
|
||||||
|
)
|
||||||
|
|
||||||
|
user = db.relationship("User", back_populates="accounts")
|
||||||
|
account = db.relationship("Account", back_populates="users")
|
@ -1,69 +1,97 @@
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from os import environ
|
from os import environ
|
||||||
import base64
|
|
||||||
|
|
||||||
from flask import jsonify, render_template, make_response, request
|
import requests
|
||||||
from flask_cors import cross_origin
|
from flask_admin.contrib.sqla import ModelView
|
||||||
|
from wtforms import PasswordField, StringField, Form
|
||||||
|
from wtforms.validators import InputRequired, EqualTo
|
||||||
from passlib.hash import bcrypt
|
from passlib.hash import bcrypt
|
||||||
import jwt
|
import jwt
|
||||||
|
|
||||||
from accounts import app
|
from accounts import db
|
||||||
|
from accounts.models import AccountUserAssociation
|
||||||
@app.route("/")
|
|
||||||
def home():
|
|
||||||
return render_template("index.html")
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/login", methods=["POST"])
|
class RemoteServerException(Exception):
|
||||||
@cross_origin(origins=[environ.get("CORS_ORIGIN", "*")], supports_credentials=True)
|
status = 0
|
||||||
def post_login():
|
|
||||||
credentials = request.get_json(force=True)
|
|
||||||
|
|
||||||
if credentials["username"] != environ.get("USER", "offen"):
|
|
||||||
return jsonify({"error": "bad username", "status": 401}), 401
|
|
||||||
|
|
||||||
hashed_password = base64.standard_b64decode(environ.get("HASHED_PASSWORD", ""))
|
|
||||||
if not bcrypt.verify(credentials["password"], hashed_password):
|
|
||||||
return jsonify({"error": "bad password", "status": 401}), 401
|
|
||||||
|
|
||||||
|
def create_remote_account(name, account_id):
|
||||||
private_key = environ.get("JWT_PRIVATE_KEY", "")
|
private_key = environ.get("JWT_PRIVATE_KEY", "")
|
||||||
expiry = datetime.utcnow() + timedelta(hours=24)
|
expiry = datetime.utcnow() + timedelta(seconds=10)
|
||||||
try:
|
|
||||||
encoded = jwt.encode(
|
encoded = jwt.encode(
|
||||||
{"ok": True, "exp": expiry}, private_key.encode(), algorithm="RS256"
|
{"ok": True, "exp": expiry, "priv": {"rpc": "1"}},
|
||||||
|
private_key.encode(),
|
||||||
|
algorithm="RS256",
|
||||||
).decode("utf-8")
|
).decode("utf-8")
|
||||||
except jwt.exceptions.PyJWTError as encode_error:
|
|
||||||
return jsonify({"error": str(encode_error), "status": 500}), 500
|
|
||||||
|
|
||||||
resp = make_response(jsonify({"ok": True}))
|
r = requests.post(
|
||||||
resp.set_cookie(
|
"{}/accounts".format(environ.get("SERVER_HOST")),
|
||||||
"auth",
|
json={"name": name, "account_id": account_id},
|
||||||
encoded,
|
headers={"X-RPC-Authentication": encoded},
|
||||||
httponly=True,
|
|
||||||
expires=expiry,
|
|
||||||
path="/",
|
|
||||||
domain=environ.get("COOKIE_DOMAIN"),
|
|
||||||
samesite="strict"
|
|
||||||
)
|
)
|
||||||
return resp
|
|
||||||
|
if r.status_code > 299:
|
||||||
|
err = r.json()
|
||||||
|
remote_err = RemoteServerException(err["error"])
|
||||||
|
remote_err.status = err["status"]
|
||||||
|
raise remote_err
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/login", methods=["GET"])
|
class AccountForm(Form):
|
||||||
@cross_origin(origins=[environ.get("CORS_ORIGIN", "*")], supports_credentials=True)
|
name = StringField(
|
||||||
def get_login():
|
"Account Name",
|
||||||
auth_cookie = request.cookies.get("auth")
|
validators=[InputRequired()],
|
||||||
public_key = environ.get("JWT_PUBLIC_KEY", "")
|
description="This is the account name visible to users",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AccountView(ModelView):
|
||||||
|
form = AccountForm
|
||||||
|
column_display_all_relations = True
|
||||||
|
column_list = ("account_id", "name")
|
||||||
|
|
||||||
|
def after_model_change(self, form, model, is_created):
|
||||||
|
if is_created:
|
||||||
try:
|
try:
|
||||||
jwt.decode(auth_cookie, public_key)
|
create_remote_account(model.name, model.account_id)
|
||||||
except jwt.exceptions.PyJWTError as unauthorized_error:
|
except RemoteServerException as server_error:
|
||||||
return jsonify({"error": str(unauthorized_error), "status": 401}), 401
|
db.session.delete(model)
|
||||||
|
db.session.commit()
|
||||||
return jsonify({"ok": True})
|
raise server_error
|
||||||
|
|
||||||
|
|
||||||
# This route is not supposed to be called by client-side applications, so
|
class UserView(ModelView):
|
||||||
# no CORS configuration is added
|
inline_models = [(AccountUserAssociation, dict(form_columns=["id", "account"]))]
|
||||||
@app.route("/api/key", methods=["GET"])
|
column_auto_select_related = True
|
||||||
def key():
|
column_display_all_relations = True
|
||||||
public_key = environ.get("JWT_PUBLIC_KEY", "").strip()
|
column_list = ("user_id", "email")
|
||||||
return jsonify({"key": public_key})
|
form_columns = ("email", "accounts")
|
||||||
|
|
||||||
|
def on_model_change(self, form, model, is_created):
|
||||||
|
if form.password.data:
|
||||||
|
model.hashed_password = bcrypt.hash(form.password.data)
|
||||||
|
|
||||||
|
def get_create_form(self):
|
||||||
|
form = super().get_create_form()
|
||||||
|
form.password = PasswordField(
|
||||||
|
"Password",
|
||||||
|
validators=[
|
||||||
|
InputRequired(),
|
||||||
|
EqualTo("confirm", message="Passwords must match"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
form.confirm = PasswordField("Repeat Password", validators=[InputRequired()])
|
||||||
|
return form
|
||||||
|
|
||||||
|
def get_edit_form(self):
|
||||||
|
form = super().get_edit_form()
|
||||||
|
form.password = PasswordField(
|
||||||
|
"Password",
|
||||||
|
description="When left blank, the password will remain unchanged on update",
|
||||||
|
validators=[
|
||||||
|
EqualTo("confirm", message="Passwords must match"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
form.confirm = PasswordField("Repeat Password", validators=[])
|
||||||
|
return form
|
||||||
|
0
accounts/bootstrap.yml
Executable file
0
accounts/bootstrap.yml
Executable file
@ -1,2 +1,3 @@
|
|||||||
pytest
|
pytest
|
||||||
black
|
black
|
||||||
|
pyyaml
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
Flask==1.0.2
|
Flask==1.0.2
|
||||||
|
Flask-Admin==1.5.3
|
||||||
Flask-Cors==3.0.8
|
Flask-Cors==3.0.8
|
||||||
|
Flask-SQLAlchemy==2.4.0
|
||||||
werkzeug==0.15.4
|
werkzeug==0.15.4
|
||||||
pyjwt[crypto]==1.7.1
|
pyjwt[crypto]==1.7.1
|
||||||
passlib==1.7.1
|
passlib==1.7.1
|
||||||
bcrypt==3.1.7
|
bcrypt==3.1.7
|
||||||
|
psycopg2==2.8.3
|
||||||
|
requests==2.22.0
|
||||||
|
0
accounts/scripts/__init__.py
Normal file
0
accounts/scripts/__init__.py
Normal file
23
accounts/scripts/bootstrap.py
Executable file
23
accounts/scripts/bootstrap.py
Executable file
@ -0,0 +1,23 @@
|
|||||||
|
import yaml
|
||||||
|
from passlib.hash import bcrypt
|
||||||
|
|
||||||
|
from accounts import db
|
||||||
|
from accounts.models import Account
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
db.drop_all()
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
|
with open("./bootstrap.yml", "r") as stream:
|
||||||
|
data = yaml.safe_load(stream)
|
||||||
|
|
||||||
|
for account in data["accounts"]:
|
||||||
|
record = Account(
|
||||||
|
name=account["name"],
|
||||||
|
account_id=account["id"],
|
||||||
|
)
|
||||||
|
db.session.add(record)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
print("Successfully bootstrapped accounts database")
|
@ -26,6 +26,10 @@ custom:
|
|||||||
production: vault.offen.dev
|
production: vault.offen.dev
|
||||||
staging: vault-staging.offen.dev
|
staging: vault-staging.offen.dev
|
||||||
alpha: vault-alpha.offen.dev
|
alpha: vault-alpha.offen.dev
|
||||||
|
serverHost:
|
||||||
|
production: server.offen.dev
|
||||||
|
staging: server-staging.offen.dev
|
||||||
|
alpha: server-alpha.offen.dev
|
||||||
domain:
|
domain:
|
||||||
production: accounts.offen.dev
|
production: accounts.offen.dev
|
||||||
staging: accounts-staging.offen.dev
|
staging: accounts-staging.offen.dev
|
||||||
@ -60,9 +64,10 @@ functions:
|
|||||||
path: '{proxy+}'
|
path: '{proxy+}'
|
||||||
method: any
|
method: any
|
||||||
environment:
|
environment:
|
||||||
USER: offen
|
|
||||||
CORS_ORIGIN: https://${self:custom.origin.${self:custom.stage}}
|
CORS_ORIGIN: https://${self:custom.origin.${self:custom.stage}}
|
||||||
COOKIE_DOMAIN: ${self:custom.cookieDomain.${self:custom.stage}}
|
COOKIE_DOMAIN: ${self:custom.cookieDomain.${self:custom.stage}}
|
||||||
JWT_PRIVATE_KEY: '${ssm:/aws/reference/secretsmanager/${self:custom.stage}/accounts/jwtPrivateKey~true}'
|
JWT_PRIVATE_KEY: '${ssm:/aws/reference/secretsmanager/${self:custom.stage}/accounts/jwtPrivateKey~true}'
|
||||||
JWT_PUBLIC_KEY: '${ssm:/aws/reference/secretsmanager/${self:custom.stage}/accounts/jwtPublicKey~true}'
|
JWT_PUBLIC_KEY: '${ssm:/aws/reference/secretsmanager/${self:custom.stage}/accounts/jwtPublicKey~true}'
|
||||||
HASHED_PASSWORD: ${ssm:/aws/reference/secretsmanager/${self:custom.stage}/accounts/hashedBasicAuthPassword~true}
|
HASHED_PASSWORD: ${ssm:/aws/reference/secretsmanager/${self:custom.stage}/accounts/hashedBasicAuthPassword~true}
|
||||||
|
SESSION_SECRET: '${ssm:/aws/reference/secretsmanager/${self:custom.stage}/accounts/sessionSecret~true}'
|
||||||
|
SERVER_URL: ${self:custom.serverHost.${self:custom.stage}}
|
||||||
|
@ -24,6 +24,11 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
POSTGRES_PASSWORD: develop
|
POSTGRES_PASSWORD: develop
|
||||||
|
|
||||||
|
accounts_database:
|
||||||
|
image: postgres:11.2
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: develop
|
||||||
|
|
||||||
server:
|
server:
|
||||||
build:
|
build:
|
||||||
context: '.'
|
context: '.'
|
||||||
@ -31,6 +36,7 @@ services:
|
|||||||
working_dir: /offen/server
|
working_dir: /offen/server
|
||||||
volumes:
|
volumes:
|
||||||
- .:/offen
|
- .:/offen
|
||||||
|
- ./bootstrap.yml:/offen/server/bootstrap.yml
|
||||||
- serverdeps:/go/pkg/mod
|
- serverdeps:/go/pkg/mod
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_CONNECTION_STRING: postgres://postgres:develop@server_database:5432/postgres?sslmode=disable
|
POSTGRES_CONNECTION_STRING: postgres://postgres:develop@server_database:5432/postgres?sslmode=disable
|
||||||
@ -100,16 +106,20 @@ services:
|
|||||||
working_dir: /offen/accounts
|
working_dir: /offen/accounts
|
||||||
volumes:
|
volumes:
|
||||||
- .:/offen
|
- .:/offen
|
||||||
|
- ./bootstrap.yml:/offen/accounts/bootstrap.yml
|
||||||
- accountdeps:/root/.local
|
- accountdeps:/root/.local
|
||||||
command: flask run --host 0.0.0.0
|
command: flask run --host 0.0.0.0
|
||||||
ports:
|
ports:
|
||||||
- 5000:5000
|
- 5000:5000
|
||||||
|
links:
|
||||||
|
- accounts_database
|
||||||
environment:
|
environment:
|
||||||
FLASK_APP: accounts
|
FLASK_APP: accounts:app
|
||||||
FLASK_ENV: development
|
FLASK_ENV: development
|
||||||
|
POSTGRES_CONNECTION_STRING: postgres://postgres:develop@accounts_database:5432/postgres?sslmode=disable
|
||||||
CORS_ORIGIN: http://localhost:9977
|
CORS_ORIGIN: http://localhost:9977
|
||||||
# local password is `develop`
|
SERVER_HOST: http://server:8080
|
||||||
HASHED_PASSWORD: JDJhJDEwJGpFRXJMOVVSQndZQlFQNjkxallkZi53aGp1cDMvRW5maGUvakZleG1pWFlnWEVXcU93ODBp
|
SESSION_SECRET: vndJRFJTiyjfgtTF
|
||||||
JWT_PRIVATE_KEY: |-
|
JWT_PRIVATE_KEY: |-
|
||||||
-----BEGIN PRIVATE KEY-----
|
-----BEGIN PRIVATE KEY-----
|
||||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCa6AEl0RUW43YS
|
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCa6AEl0RUW43YS
|
||||||
|
@ -26,9 +26,18 @@ const ClaimsContextKey contextKey = "claims"
|
|||||||
func JWTProtect(keyURL, cookieName string) func(http.Handler) http.Handler {
|
func JWTProtect(keyURL, cookieName string) func(http.Handler) http.Handler {
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
authCookie, err := r.Cookie(cookieName)
|
var jwtValue string
|
||||||
if err != nil {
|
var isRPC bool
|
||||||
RespondWithJSONError(w, fmt.Errorf("jwt: error reading cookie: %s", err), http.StatusForbidden)
|
if authCookie, err := r.Cookie(cookieName); err == nil {
|
||||||
|
jwtValue = authCookie.Value
|
||||||
|
} else {
|
||||||
|
if header := r.Header.Get("X-RPC-Authentication"); header != "" {
|
||||||
|
jwtValue = header
|
||||||
|
isRPC = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if jwtValue == "" {
|
||||||
|
RespondWithJSONError(w, errors.New("jwt: could not infer JWT value from cookie or header"), http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,7 +65,7 @@ func JWTProtect(keyURL, cookieName string) func(http.Handler) http.Handler {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
token, jwtErr := jwt.ParseVerify(strings.NewReader(authCookie.Value), jwa.RS256, pubKey)
|
token, jwtErr := jwt.ParseVerify(strings.NewReader(jwtValue), jwa.RS256, pubKey)
|
||||||
if jwtErr != nil {
|
if jwtErr != nil {
|
||||||
RespondWithJSONError(w, fmt.Errorf("jwt: error parsing token: %v", jwtErr), http.StatusForbidden)
|
RespondWithJSONError(w, fmt.Errorf("jwt: error parsing token: %v", jwtErr), http.StatusForbidden)
|
||||||
return
|
return
|
||||||
@ -68,8 +77,19 @@ func JWTProtect(keyURL, cookieName string) func(http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
privateClaims, _ := token.Get("priv")
|
privateClaims, _ := token.Get("priv")
|
||||||
r = r.WithContext(context.WithValue(r.Context(), ClaimsContextKey, privateClaims))
|
if isRPC {
|
||||||
|
cast, ok := privateClaims.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
RespondWithJSONError(w, fmt.Errorf("jwt: malformed private claims section in token: %v", privateClaims), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if cast["rpc"] != "1" {
|
||||||
|
RespondWithJSONError(w, errors.New("jwt: token claims do not allow the requested operation"), http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r = r.WithContext(context.WithValue(r.Context(), ClaimsContextKey, privateClaims))
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user