mirror of
https://github.com/offen/website.git
synced 2024-11-22 17:10:29 +01:00
Merge pull request #53 from offen/token-auth
Move authentication to jwt based token system handled by account app
This commit is contained in:
commit
b2f3c5cf04
@ -94,6 +94,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- checkout:
|
- checkout:
|
||||||
path: ~/offen
|
path: ~/offen
|
||||||
|
- run:
|
||||||
|
name: Install dependencies
|
||||||
|
command: go get ./...
|
||||||
- run:
|
- run:
|
||||||
name: Run tests
|
name: Run tests
|
||||||
command: make test
|
command: make test
|
||||||
@ -150,6 +153,9 @@ jobs:
|
|||||||
path: ~/offen
|
path: ~/offen
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
key: offen-auditorium-{{ checksum "package.json" }}
|
key: offen-auditorium-{{ checksum "package.json" }}
|
||||||
|
- run:
|
||||||
|
name: Install lsof
|
||||||
|
command: sudo apt-get install lsof
|
||||||
- run:
|
- run:
|
||||||
name: Install dependencies
|
name: Install dependencies
|
||||||
command: npm install
|
command: npm install
|
||||||
@ -187,6 +193,48 @@ jobs:
|
|||||||
accounts:
|
accounts:
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/python:3.6
|
- image: circleci/python:3.6
|
||||||
|
environment:
|
||||||
|
HASHED_PASSWORD: JDJhJDEwJGpFRXJMOVVSQndZQlFQNjkxallkZi53aGp1cDMvRW5maGUvakZleG1pWFlnWEVXcU93ODBp
|
||||||
|
JWT_PRIVATE_KEY: |-
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCzgU18PnRrpbVK
|
||||||
|
LU4EewU476arjLeMAXoxQAvrufvnwAGlrvnuh+TE7z7R3KslOyP0m3bTMaNyn2la
|
||||||
|
DFz0ERR8KFA3rbUUDLG8QAUomm6X2WNbZJqFG/ORBWyEWy/bvNEbCTtF8K09v6HT
|
||||||
|
PcNzpSrzN8MK/xsIXnMTa/acftbfD3jnb1KFNWaSUKgEM9OL8NSW8PpqTf+le/+R
|
||||||
|
0Dwhd4vNnELpobKY3tvhxjvlQmbZA6vRrOoWfWbKXuS/7B7CWXWEhXPTbcJPcKPt
|
||||||
|
zlkpJGlrcHgvTBorM/lBZw7zrmfUcJROdX6ziU1ymEENkKtLBORedWdBZU8Ivuyy
|
||||||
|
hDIzS7cdAgMBAAECggEAVReXfq0wjRMJhHdDg5Y5nIrmbG4RWFoe7ZfZzs3kXzDC
|
||||||
|
1yLCMdPTm5N6KQu9SbHmUn8b7fOa8qwkyd4QdlZeapjFpg8/RpjZ7E5A48WJZYxU
|
||||||
|
sC9ZnH3qkTWMApYjcrvoODPBGF+GED52XOfrbje+y3sEh4L08purW2qThg4Ol8A2
|
||||||
|
+lqh7W0DCAYV9BG4jFo0QyJRsXa88JIxVS2gSbuegDyHwRTtPq0ZH+vqUEsJti0c
|
||||||
|
G60hwcl0v9eRbJk/e9lzhKFKnf4/ReRAX9pUyyIb7za3vhYwDmjglgZ+Ax4HhkPK
|
||||||
|
SR8KjqX5hQ2nOtDmxkxEO1QF5Qm99VLlZyo55vdJYQKBgQDomtQ7jwc41JM9naZC
|
||||||
|
1agGjJamZAtfDf/00KQILWplaDCDEyue9A1iSnu7yNLX4VOjsKhfvugARwz2pOrw
|
||||||
|
PkOYasurM+P16qNkyfyVUilC/QoOcm5UjsCy/wAViwYFG8hZGBj+jPELqPglSdr4
|
||||||
|
m2FPTFOmtQDzB7EOk9Mzaic26QKBgQDFjz96/3OAC2dBdjQz7zXKHYPPo2gXqS76
|
||||||
|
3UOX2u+S43jJGEEKLdWa34qO6KrnnUsivyvEyF2/5H6n8Dc2Xj0LBdO67wyzbtFM
|
||||||
|
dI9RiQ3DqTBbebmrkSdIaTBAGu1VtSCzBKUqckkM3lLSHhJBg0XrURGdqro28nhZ
|
||||||
|
uSQ8EzzGFQKBgQCCiVlvrz3jU9Dp9E45FcR9IGrvKBgFmUq6bliPykT6cfU/qgOB
|
||||||
|
6f6U2a4E3ZgN1QNmSp7DVNTISxdoV3cNqjOvFsgD5VQaTzqxNnXMqtZDJNR+9RMb
|
||||||
|
2x0jlt3KOUIAne3aqh5kxF4GKCZSbtc3S6PZp8EOPmgw+3EO+EC/iuRE+QKBgHIW
|
||||||
|
uqs2UKY2b6ffMmB3mVGiX9eOX3OikW3wT7OnjMkAMmW3awAM3hl1VNgYx3HAZX6o
|
||||||
|
dgdLStChjP9A+zGblJcEA3Ulzejla1tCyO1mP5up3jJFhpLs3Ym0rVen9T2Uv1CC
|
||||||
|
sztjCoqy7ZNIKHTK8Zrmk0zBJo7K0fPGtoU2+tbNAoGAc2x7cW8y9ERRJg8FSWj0
|
||||||
|
ADPUPzsW3Bu76U0oVEXg66d3+D9z8LBVhBYjL/+tJWADapePnye+gpqzd4kGFeV4
|
||||||
|
q49Qz5hhZUzsUl2iOURQLUnI2g0AINiMIVL5EkZtdD+Au65+AaYL7YEsXmKcLXHX
|
||||||
|
coEomAoy102k4A6WsM2bKf4=
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
|
JWT_PUBLIC_KEY: |-
|
||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs4FNfD50a6W1Si1OBHsF
|
||||||
|
OO+mq4y3jAF6MUAL67n758ABpa757ofkxO8+0dyrJTsj9Jt20zGjcp9pWgxc9BEU
|
||||||
|
fChQN621FAyxvEAFKJpul9ljW2SahRvzkQVshFsv27zRGwk7RfCtPb+h0z3Dc6Uq
|
||||||
|
8zfDCv8bCF5zE2v2nH7W3w94529ShTVmklCoBDPTi/DUlvD6ak3/pXv/kdA8IXeL
|
||||||
|
zZxC6aGymN7b4cY75UJm2QOr0azqFn1myl7kv+wewll1hIVz023CT3Cj7c5ZKSRp
|
||||||
|
a3B4L0waKzP5QWcO865n1HCUTnV+s4lNcphBDZCrSwTkXnVnQWVPCL7ssoQyM0u3
|
||||||
|
HQIDAQAB
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
|
|
||||||
working_directory: ~/offen/accounts
|
working_directory: ~/offen/accounts
|
||||||
steps:
|
steps:
|
||||||
- checkout:
|
- checkout:
|
||||||
@ -229,10 +277,10 @@ jobs:
|
|||||||
key: offen-packages-{{ checksum "package.json" }}
|
key: offen-packages-{{ checksum "package.json" }}
|
||||||
- run:
|
- run:
|
||||||
name: Deploy
|
name: Deploy
|
||||||
working_directory: ~/offen
|
working_directory: ~/offen/accounts
|
||||||
command: |
|
command: |
|
||||||
echo "Deploying accounts ..."
|
echo "Deploying accounts ..."
|
||||||
$(npm bin)/sls deploy --config accounts/serverless.yml
|
$(npm bin)/sls deploy
|
||||||
|
|
||||||
deploy_golang:
|
deploy_golang:
|
||||||
docker:
|
docker:
|
||||||
|
@ -13,3 +13,7 @@ indent_style = tab
|
|||||||
[{*.js,*.yml,*.md,Gopkg.toml,package.json,*.html,Dockerfile.*}]
|
[{*.js,*.yml,*.md,Gopkg.toml,package.json,*.html,Dockerfile.*}]
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.py]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
4
accounts/.gitignore
vendored
4
accounts/.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
|
*.pem
|
||||||
*.dot
|
*.dot
|
||||||
__pycache__
|
__pycache__
|
||||||
*.log
|
*.log
|
||||||
@ -5,6 +6,3 @@ __pycache__
|
|||||||
.pytest_cache
|
.pytest_cache
|
||||||
venv/
|
venv/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
|
||||||
models/
|
|
||||||
|
|
||||||
|
@ -1,13 +1,5 @@
|
|||||||
from flask import Flask, jsonify, render_template
|
from flask import Flask
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
import accounts.views
|
||||||
@app.route("/")
|
|
||||||
def home():
|
|
||||||
return render_template("index.html")
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/status")
|
|
||||||
def status():
|
|
||||||
return jsonify({"ok": True})
|
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
import unittest
|
|
||||||
|
|
||||||
from accounts import app
|
|
||||||
|
|
||||||
|
|
||||||
class TestStatus(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.app = app.test_client()
|
|
||||||
|
|
||||||
def test_get(self):
|
|
||||||
rv = self.app.get("/status")
|
|
||||||
assert rv.status.startswith("200")
|
|
||||||
assert b"ok" in rv.data
|
|
69
accounts/accounts/views.py
Normal file
69
accounts/accounts/views.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
from datetime import datetime, timedelta
|
||||||
|
from os import environ
|
||||||
|
import base64
|
||||||
|
|
||||||
|
from flask import jsonify, render_template, make_response, request
|
||||||
|
from flask_cors import cross_origin
|
||||||
|
from passlib.hash import bcrypt
|
||||||
|
import jwt
|
||||||
|
|
||||||
|
from accounts import app
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def home():
|
||||||
|
return render_template("index.html")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/login", methods=["POST"])
|
||||||
|
@cross_origin(origins=[environ.get("CORS_ORIGIN", "*")], supports_credentials=True)
|
||||||
|
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
|
||||||
|
|
||||||
|
private_key = environ.get("JWT_PRIVATE_KEY", "")
|
||||||
|
expiry = datetime.utcnow() + timedelta(hours=24)
|
||||||
|
try:
|
||||||
|
encoded = jwt.encode(
|
||||||
|
{"ok": True, "exp": expiry}, private_key.encode(), algorithm="RS256"
|
||||||
|
).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}))
|
||||||
|
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)
|
||||||
|
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"])
|
||||||
|
def key():
|
||||||
|
public_key = environ.get("JWT_PUBLIC_KEY", "").strip()
|
||||||
|
return jsonify({"key": public_key})
|
@ -1,2 +1,6 @@
|
|||||||
Flask==1.0.2
|
Flask==1.0.2
|
||||||
|
Flask-Cors==3.0.8
|
||||||
werkzeug==0.15.4
|
werkzeug==0.15.4
|
||||||
|
pyjwt[crypto]==1.7.1
|
||||||
|
passlib==1.7.1
|
||||||
|
bcrypt==3.1.7
|
||||||
|
@ -14,9 +14,6 @@ provider:
|
|||||||
|
|
||||||
package:
|
package:
|
||||||
individually: true
|
individually: true
|
||||||
excludeDevDependencies: false
|
|
||||||
exclude:
|
|
||||||
- '**/*'
|
|
||||||
|
|
||||||
plugins:
|
plugins:
|
||||||
- serverless-domain-manager
|
- serverless-domain-manager
|
||||||
@ -25,10 +22,18 @@ plugins:
|
|||||||
|
|
||||||
custom:
|
custom:
|
||||||
stage: ${opt:stage, self:provider.stage}
|
stage: ${opt:stage, self:provider.stage}
|
||||||
|
origin:
|
||||||
|
production: vault.offen.dev
|
||||||
|
staging: vault-staging.offen.dev
|
||||||
|
alpha: vault-alpha.offen.dev
|
||||||
domain:
|
domain:
|
||||||
production: accounts.offen.dev
|
production: accounts.offen.dev
|
||||||
staging: accounts-staging.offen.dev
|
staging: accounts-staging.offen.dev
|
||||||
alpha: accounts-alpha.offen.dev
|
alpha: accounts-alpha.offen.dev
|
||||||
|
cookieDomain:
|
||||||
|
production: .offen.dev
|
||||||
|
staging: .offen.dev
|
||||||
|
alpha: .offen.dev
|
||||||
customDomain:
|
customDomain:
|
||||||
basePath: ''
|
basePath: ''
|
||||||
certificateName: '*.offen.dev'
|
certificateName: '*.offen.dev'
|
||||||
@ -37,18 +42,15 @@ custom:
|
|||||||
endpointType: regional
|
endpointType: regional
|
||||||
createRoute53Record: false
|
createRoute53Record: false
|
||||||
wsgi:
|
wsgi:
|
||||||
app: accounts.accounts.app
|
app: accounts.app
|
||||||
packRequirements: false
|
packRequirements: false
|
||||||
pythonRequirements:
|
pythonRequirements:
|
||||||
slim: true
|
slim: true
|
||||||
dockerizePip: non-linux
|
dockerizePip: non-linux
|
||||||
fileName: accounts/requirements.txt
|
fileName: requirements.txt
|
||||||
|
|
||||||
functions:
|
functions:
|
||||||
app:
|
app:
|
||||||
package:
|
|
||||||
include:
|
|
||||||
- accounts/**/*
|
|
||||||
handler: wsgi_handler.handler
|
handler: wsgi_handler.handler
|
||||||
events:
|
events:
|
||||||
- http:
|
- http:
|
||||||
@ -57,3 +59,10 @@ functions:
|
|||||||
- http:
|
- http:
|
||||||
path: '{proxy+}'
|
path: '{proxy+}'
|
||||||
method: any
|
method: any
|
||||||
|
environment:
|
||||||
|
USER: offen
|
||||||
|
CORS_ORIGIN: https://${self:custom.origin.${self:custom.stage}}
|
||||||
|
COOKIE_DOMAIN: ${self:custom.origin.${self:custom.stage}}
|
||||||
|
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}'
|
||||||
|
HASHED_PASSWORD: ${ssm:/aws/reference/secretsmanager/${self:custom.stage}/accounts/hashedPassword~true}
|
||||||
|
21
accounts/tests/test_jwt.py
Normal file
21
accounts/tests/test_jwt.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import unittest
|
||||||
|
import json
|
||||||
|
|
||||||
|
from accounts import app
|
||||||
|
|
||||||
|
|
||||||
|
class TestJWT(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.app = app.test_client()
|
||||||
|
|
||||||
|
def test_jwt_flow(self):
|
||||||
|
rv = self.app.get("/api/login")
|
||||||
|
assert rv.status.startswith("401")
|
||||||
|
|
||||||
|
rv = self.app.post(
|
||||||
|
"/api/login", data=json.dumps({"username": "offen", "password": "develop"})
|
||||||
|
)
|
||||||
|
assert rv.status.startswith("200")
|
||||||
|
|
||||||
|
rv = self.app.get("/api/login")
|
||||||
|
assert rv.status.startswith("200")
|
@ -10,16 +10,19 @@ services:
|
|||||||
- .:/offen
|
- .:/offen
|
||||||
- kmsdeps:/go/pkg/mod
|
- kmsdeps:/go/pkg/mod
|
||||||
environment:
|
environment:
|
||||||
- KEY_FILE=key.txt
|
KEY_FILE: key.txt
|
||||||
- PORT=8081
|
PORT: 8081
|
||||||
|
JWT_PUBLIC_KEY: http://accounts:5000/api/key
|
||||||
ports:
|
ports:
|
||||||
- 8081:8081
|
- 8081:8081
|
||||||
command: refresh run
|
command: refresh run
|
||||||
|
links:
|
||||||
|
- accounts
|
||||||
|
|
||||||
server_database:
|
server_database:
|
||||||
image: postgres:11.2
|
image: postgres:11.2
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_PASSWORD=develop
|
POSTGRES_PASSWORD: develop
|
||||||
|
|
||||||
server:
|
server:
|
||||||
build:
|
build:
|
||||||
@ -30,9 +33,10 @@ services:
|
|||||||
- .:/offen
|
- .:/offen
|
||||||
- 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
|
||||||
- KMS_ENCRYPTION_ENDPOINT=http://kms:8081/encrypt
|
KMS_ENCRYPTION_ENDPOINT: http://kms:8081/encrypt
|
||||||
- PORT=8080
|
PORT: 8080
|
||||||
|
JWT_PUBLIC_KEY: http://accounts:5000/api/key
|
||||||
ports:
|
ports:
|
||||||
- 8080:8080
|
- 8080:8080
|
||||||
command: refresh run
|
command: refresh run
|
||||||
@ -57,6 +61,7 @@ services:
|
|||||||
- KMS_HOST=http://localhost:8081
|
- KMS_HOST=http://localhost:8081
|
||||||
- SCRIPT_HOST=http://localhost:9977
|
- SCRIPT_HOST=http://localhost:9977
|
||||||
- AUDITORIUM_HOST=http://localhost:9955
|
- AUDITORIUM_HOST=http://localhost:9955
|
||||||
|
- ACCOUNTS_HOST=http://localhost:5000
|
||||||
|
|
||||||
script:
|
script:
|
||||||
build:
|
build:
|
||||||
@ -98,8 +103,50 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 5000:5000
|
- 5000:5000
|
||||||
environment:
|
environment:
|
||||||
- FLASK_APP=accounts
|
FLASK_APP: accounts
|
||||||
- FLASK_ENV=development
|
FLASK_ENV: development
|
||||||
|
CORS_ORIGIN: http://localhost:9977
|
||||||
|
# local password is `develop`
|
||||||
|
HASHED_PASSWORD: JDJhJDEwJGpFRXJMOVVSQndZQlFQNjkxallkZi53aGp1cDMvRW5maGUvakZleG1pWFlnWEVXcU93ODBp
|
||||||
|
JWT_PRIVATE_KEY: |-
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCa6AEl0RUW43YS
|
||||||
|
6cfbYkEDoSxQV8WoEQPuwM9OPdrJjTHYwX9+J9dPhvdnuSIGca86jgYwg45xG5MC
|
||||||
|
7arOIIGOI8rBuXWyPF/iZliWxUmzEMIsmZYFNTjezRl95ymy43ZH9JCS2pw2gTTq
|
||||||
|
Z4ln/hPkl2KMs+Two7iux52sgFRAxWYPNgqCpkbzEAL+zCHIPaZqsZjRX/nES7/y
|
||||||
|
JZWyvSSDQ/CsdeR+xssx2F1JXjQHl68dBEJJc7hAU4xNqs9aP5vaaL6PbwWOh6S7
|
||||||
|
bzieDvankyZSrCxVBbMy8+JHXDLLQTp8Lv3y38HL/Ez0gefm2kRnGNTU19kwoFT+
|
||||||
|
ceeuYE6RAgMBAAECggEAcZ2g2d/UnAkRXSXi1GHoVYUtP3BhJLf2LnN0mWp8wj+x
|
||||||
|
Q84IeLs4DLhtVcJP1nIjl8r7dzHGk+cpmIhBMxZcb6iI2jXwwV3O5fszFsJ1H8U2
|
||||||
|
5gdwJTm4EJJWFCYsS2zSIEycjVmSIdf6u8Jc4c1VQeBXA+QeEvHCT09RsmgdY7NA
|
||||||
|
zEZtpIf4EUXtQMmZdKwYVRJUJlBccAP2IR23lhAV6pS5n0pwOodnZD7lf/XAIrOm
|
||||||
|
Shy0WE+8DAxmg1DXzFLC+vw45kGJTdhHKc+ZFj86Nv2nUx0u12mb1hF3XlC66ojc
|
||||||
|
jmS0i5NbJVCqTSSXlC81V/16wCJmWIF0+O6y2EkZwQKBgQDLcgORV1QhUAz6QfiP
|
||||||
|
wR9OIWCJyxaGbaBe1FZvx8T9ofri8z/ycwxkzlEDCLYu7CZ2ZCpZLMwE5njzfmTq
|
||||||
|
ixhiVvu/fCPgo4bXRt6H/BaggBs04p8UB3ZJKl0ib67kKMXQjH2qFhuCS2Jpy3ru
|
||||||
|
REmRoukY4iAqHRdIrP6zW0y55QKBgQDC7BDIZrSbZNwkdbj+UilKEit+plBAvdan
|
||||||
|
7CGLg4XhwiW7XYr9gHM3RKCAlUFRIsAaFClxiCvy21p3UpbO0XC5aIC5ReWYK9zh
|
||||||
|
THqwjxgDCev4FIe0md2fQCP7TP4wTYwvKjSqzI4v/XW86VYynegDBQSqeI8heuY4
|
||||||
|
YbTBYMvHPQKBgQCy89wdiVJwZwizTTpFwNs3j3ZqXmC26FEreN17P56Qd13HKa6z
|
||||||
|
Je3d8fkikRQnnAONGjiB7jybhtsXW7OK98UAI4EYAytP2qeuTyFJPj3s+iJ0V28U
|
||||||
|
YCf03bXEp7aP7Slrc1jKNt4Fsyei5aCBW0HXQBSHlcgzIxmrDLiRrZqE3QKBgBPu
|
||||||
|
KUUkY0EkTfIYa2LtqbUeKH5ZqQkFoCYpWcC3IQBVZqBCz0xeTumOxc5/9F7Ea9n+
|
||||||
|
x8IJB11cmmJq+mqJNbpvegH3qKMnkP0kYcMdznm5EPybtMh9lxCKcWNnmvH7a+MC
|
||||||
|
sMHqCnvTsa8wOJUSWj+8yp5Xl2L3+wQ20VGYgR2NAoGAE8/BCLHcescaA9PGr4Kx
|
||||||
|
EfVfoULTXOJp8a8j+N3C8cXhnroXGT6IYnETb34Z2J713m/5Ko/74PBWXnRvvnu4
|
||||||
|
ATPZbyXlmtsorV5E5lRs9x6U9bRUzDEWLp1yITfaDHflYUWkrtAKDZ6GslCMbvQz
|
||||||
|
9/CbsOaHzDf73JYos55E78E=
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
|
JWT_PUBLIC_KEY: |-
|
||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmugBJdEVFuN2EunH22JB
|
||||||
|
A6EsUFfFqBED7sDPTj3ayY0x2MF/fifXT4b3Z7kiBnGvOo4GMIOOcRuTAu2qziCB
|
||||||
|
jiPKwbl1sjxf4mZYlsVJsxDCLJmWBTU43s0ZfecpsuN2R/SQktqcNoE06meJZ/4T
|
||||||
|
5JdijLPk8KO4rsedrIBUQMVmDzYKgqZG8xAC/swhyD2marGY0V/5xEu/8iWVsr0k
|
||||||
|
g0PwrHXkfsbLMdhdSV40B5evHQRCSXO4QFOMTarPWj+b2mi+j28Fjoeku284ng72
|
||||||
|
p5MmUqwsVQWzMvPiR1wyy0E6fC798t/By/xM9IHn5tpEZxjU1NfZMKBU/nHnrmBO
|
||||||
|
kQIDAQAB
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
kmsdeps:
|
kmsdeps:
|
||||||
|
93
shared/http/jwt.go
Normal file
93
shared/http/jwt.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/lestrrat-go/jwx/jwa"
|
||||||
|
"github.com/lestrrat-go/jwx/jwt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type contextKey string
|
||||||
|
|
||||||
|
// ClaimsContextKey will be used to attach a JWT claim to a request context
|
||||||
|
const ClaimsContextKey contextKey = "claims"
|
||||||
|
|
||||||
|
// JWTProtect uses the public key located at the given URL to check if the
|
||||||
|
// cookie value is signed properly. In case yes, the JWT claims will be added
|
||||||
|
// to the request context
|
||||||
|
func JWTProtect(keyURL, cookieName string) func(http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
authCookie, err := r.Cookie(cookieName)
|
||||||
|
if err != nil {
|
||||||
|
RespondWithJSONError(w, fmt.Errorf("jwt: error reading cookie: %s", err), http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
keyRes, keyErr := fetchKey(keyURL)
|
||||||
|
if keyErr != nil {
|
||||||
|
RespondWithJSONError(w, fmt.Errorf("jwt: error fetching key: %v", keyErr), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
keyBytes, _ := pem.Decode([]byte(keyRes))
|
||||||
|
if keyBytes == nil {
|
||||||
|
RespondWithJSONError(w, errors.New("jwt: no PEM block found in given key"), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parseResult, parseErr := x509.ParsePKIXPublicKey(keyBytes.Bytes)
|
||||||
|
if parseErr != nil {
|
||||||
|
RespondWithJSONError(w, fmt.Errorf("jwt: error parsing key: %v", parseErr), http.StatusBadGateway)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pubKey, pubKeyOk := parseResult.(*rsa.PublicKey)
|
||||||
|
if !pubKeyOk {
|
||||||
|
RespondWithJSONError(w, errors.New("jwt: given key is not of type RSA public key"), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token, jwtErr := jwt.ParseVerify(strings.NewReader(authCookie.Value), jwa.RS256, pubKey)
|
||||||
|
if jwtErr != nil {
|
||||||
|
RespondWithJSONError(w, fmt.Errorf("jwt: error parsing token: %v", jwtErr), http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := token.Verify(jwt.WithAcceptableSkew(0)); err != nil {
|
||||||
|
RespondWithJSONError(w, fmt.Errorf("jwt: error verifying token: %v", err), http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
privateClaims, _ := token.Get("priv")
|
||||||
|
r = r.WithContext(context.WithValue(r.Context(), ClaimsContextKey, privateClaims))
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type keyResponse struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchKey(keyURL string) ([]byte, error) {
|
||||||
|
fetchRes, fetchErr := http.Get(keyURL)
|
||||||
|
if fetchErr != nil {
|
||||||
|
return nil, fetchErr
|
||||||
|
}
|
||||||
|
defer fetchRes.Body.Close()
|
||||||
|
payload := keyResponse{}
|
||||||
|
if err := json.NewDecoder(fetchRes.Body).Decode(&payload); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return []byte(payload.Key), nil
|
||||||
|
}
|
191
shared/http/jwt_test.go
Normal file
191
shared/http/jwt_test.go
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/lestrrat-go/jwx/jwa"
|
||||||
|
|
||||||
|
"github.com/lestrrat-go/jwx/jwt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const publicKey = `
|
||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2yUfHH6SRYKvBTemrefi
|
||||||
|
Hk4L4qkcc4skl4QCaHOkfgA4VcGKG2nXysYuZK7AzNOcHQVi+e4BwN+BfIZtwEU5
|
||||||
|
7Ogctb5eg8ksxxLjS7eSRfQIvPGfAbJ12R9OoOWcue/CdUy/YMec4R/o4+tZ45S6
|
||||||
|
QQWIMhLqYljw+s1Runda3K8Q8lOdJ4yEZckXaZr1waNJikC7oGpT7ClAgdbvWIbo
|
||||||
|
N18G1OluRn+3WNdcN6V+vIj8c9dGs92bgTPX4cn3RmB/80BDfzeFiPMRw5xaq66F
|
||||||
|
42zXzllkTqukQPk2wmO5m9pFy0ciRve+awfgbTtZRZOEpTSWLbbpOfd4RQ5YqDWJ
|
||||||
|
mQIDAQAB
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
|
`
|
||||||
|
|
||||||
|
const privateKey = `
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDbJR8cfpJFgq8F
|
||||||
|
N6at5+IeTgviqRxziySXhAJoc6R+ADhVwYobadfKxi5krsDM05wdBWL57gHA34F8
|
||||||
|
hm3ARTns6By1vl6DySzHEuNLt5JF9Ai88Z8BsnXZH06g5Zy578J1TL9gx5zhH+jj
|
||||||
|
61njlLpBBYgyEupiWPD6zVG6d1rcrxDyU50njIRlyRdpmvXBo0mKQLugalPsKUCB
|
||||||
|
1u9Yhug3XwbU6W5Gf7dY11w3pX68iPxz10az3ZuBM9fhyfdGYH/zQEN/N4WI8xHD
|
||||||
|
nFqrroXjbNfOWWROq6RA+TbCY7mb2kXLRyJG975rB+BtO1lFk4SlNJYttuk593hF
|
||||||
|
DlioNYmZAgMBAAECggEADvr6pXgBh77nN/QV8M1pJ6kuJtBooX1hgvoDMCC3neVl
|
||||||
|
9HbGehlCJxplEXzgsR/GDDXSDkO22vhsYZbO6dXRn+A+Fi5tR5T4+qLP5t0loqKL
|
||||||
|
9l6OAA+y/qSlO1p23D8Hi/0zF+qNTtZflTUBcA06rjcymDmyzAZIctyWOajvDSbK
|
||||||
|
Df0ZvKYPnwG5gjF01hPS2VJicv/O0HXLN7elq/jio1dwvLa2JjPyXhWBkHqnJBcq
|
||||||
|
ncWP9IEJQmhQ8ijNEg78uLtiNZQ4+GcXNBlwhM7JER6X/AxSxEZ/7fjZog685yUH
|
||||||
|
3iF820SnStOJQQci/RMMPOsK6cM7BiJxGp2W12EOAQKBgQD85UdCDro6zpblpAw7
|
||||||
|
Gw82SkWGksJXuGlTX+nj3/3iIiEb4ATCvZufYXALGNtiG0tPHDMBQCKLYrbLE1pt
|
||||||
|
9uIU/IbDFPeQk8rR/b7IHu0gv3463p6r7WVhzY2/JCororKYQk4zbuk3cNYtlV76
|
||||||
|
ojnNY1EFDLK/1nGT6QDxDA7Z5wKBgQDd1chB2qlbljRzYFwKrWXZ0COtbnEGPnUz
|
||||||
|
rLvSlAvYlZSKuB/vXkHGepxdlAjDGgX6xkKSl1TKb8UWQ9JSv0MPGBcMPukuwCAL
|
||||||
|
BOobyvd1mln6f/C7FrATkRbrG+r8RAQTwR+eknwYYOPAS/PpXm8gZvVntiahihFd
|
||||||
|
NqQtud8QfwKBgQDGV+xzWqmkxbKDmQ4erTJZGhc9XI0fz3qL8YW3O04btTjSa/hP
|
||||||
|
4/XSItGFYpFteIqwGSXHrU1qlJlY3GzoIeFfJE9tYVxpAADqgWDIA7lnHcka0s8P
|
||||||
|
eLky48xwRSTt5ES+NgKvRCWVXeIdDjHX0LQU6ff5ReRLoRyjLPOYGiTrsQKBgAmq
|
||||||
|
z1dPWCINoauFf31XoSCk2Wktbu9+uUzPMkAzA3Ek05xX+cxMp0EnBrltQhR+hdQv
|
||||||
|
36bTwXYw+L3HptrESv/VZOu7sh2/caYJSMp9RdtyJomsGamNi47Ou9jzFoJ31FWo
|
||||||
|
DOC0MYQ+dK5koPSCkQUwd3FVlsljYu5U+0Ki3v2xAoGASIMhNHOvz+Ay2otovVFN
|
||||||
|
gfRGTnepw8znHbkr10IG97BWd4VbFnHRdpYbtk8fH0UOyUVMrcY0B2/d73Rzqze3
|
||||||
|
iZ//FXIDTtmKnVS/ZhC2w0AH8Piziy3NW3G6jRZN6+9NpOf/BIc4pfzgUJ3RqHz/
|
||||||
|
IeONX+52k6gz1SCjPgSUlTs=
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
|
`
|
||||||
|
|
||||||
|
func TestJWTProtect(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
cookie *http.Cookie
|
||||||
|
server *httptest.Server
|
||||||
|
expectedStatusCode int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"no cookie",
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
http.StatusForbidden,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bad server",
|
||||||
|
&http.Cookie{
|
||||||
|
Name: "auth",
|
||||||
|
Value: "irrelevantgibberish",
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
http.StatusInternalServerError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"non-json response",
|
||||||
|
&http.Cookie{
|
||||||
|
Name: "auth",
|
||||||
|
Value: "irrelevantgibberish",
|
||||||
|
},
|
||||||
|
httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write([]byte("here's some bytes 4 y'all"))
|
||||||
|
})),
|
||||||
|
http.StatusInternalServerError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bad key value",
|
||||||
|
&http.Cookie{
|
||||||
|
Name: "auth",
|
||||||
|
Value: "irrelevantgibberish",
|
||||||
|
},
|
||||||
|
httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write([]byte(`{"key":"not really a key"}`))
|
||||||
|
})),
|
||||||
|
http.StatusInternalServerError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"invalid key",
|
||||||
|
&http.Cookie{
|
||||||
|
Name: "auth",
|
||||||
|
Value: "irrelevantgibberish",
|
||||||
|
},
|
||||||
|
httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write([]byte(`{"key":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCATZAMIIBCgKCAQEA2yUfHH6SRYKvBTemrefi\nHk4L4qkcc4skl4QCaHOkfgA4VcGKG2nXysYuZK7AzNOcHQVi+e4BwN+BfIZtwEU5\n7Ogctb5eg8ksxxLjS7eSRfQIvPGfAbJ12R9OoOWcue/CdUy/YMec4R/o4+tZ45S6\nQQWIMhLqYljw+s1Runda3K8Q8lOdJ4yEZckXaZr1waNJikC7oGpT7ClAgdbvWIbo\nN18G1OluRn+3WNdcN6V+vIj8c9dGs92bgTPX4cn3RmB/80BDfzeFiPMRw5xaq66F\n42zXzllkTqukQPk2wmO5m9pFy0ciRve+awfgbTtZRZOEpTSWLbbpOfd4RQ5YqDWJ\nmQIDAQAB\n-----END PUBLIC KEY-----"}`))
|
||||||
|
})),
|
||||||
|
http.StatusBadGateway,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"valid key, bad auth token",
|
||||||
|
&http.Cookie{
|
||||||
|
Name: "auth",
|
||||||
|
Value: "irrelevantgibberish",
|
||||||
|
},
|
||||||
|
httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write([]byte(
|
||||||
|
fmt.Sprintf(`{"key":"%s"}`, strings.ReplaceAll(publicKey, "\n", `\n`)),
|
||||||
|
))
|
||||||
|
})),
|
||||||
|
http.StatusForbidden,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"valid key, valid token",
|
||||||
|
&http.Cookie{
|
||||||
|
Name: "auth",
|
||||||
|
Value: (func() string {
|
||||||
|
token := jwt.New()
|
||||||
|
token.Set("exp", time.Now().Add(time.Hour))
|
||||||
|
keyBytes, _ := pem.Decode([]byte(privateKey))
|
||||||
|
privKey, _ := x509.ParsePKCS8PrivateKey(keyBytes.Bytes)
|
||||||
|
b, _ := token.Sign(jwa.RS256, privKey)
|
||||||
|
return string(b)
|
||||||
|
})(),
|
||||||
|
},
|
||||||
|
httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write([]byte(
|
||||||
|
fmt.Sprintf(`{"key":"%s"}`, strings.ReplaceAll(publicKey, "\n", `\n`)),
|
||||||
|
))
|
||||||
|
})),
|
||||||
|
http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"valid key, expired token",
|
||||||
|
&http.Cookie{
|
||||||
|
Name: "auth",
|
||||||
|
Value: (func() string {
|
||||||
|
token := jwt.New()
|
||||||
|
token.Set("exp", time.Now().Add(-time.Hour))
|
||||||
|
keyBytes, _ := pem.Decode([]byte(privateKey))
|
||||||
|
privKey, _ := x509.ParsePKCS8PrivateKey(keyBytes.Bytes)
|
||||||
|
b, _ := token.Sign(jwa.RS256, privKey)
|
||||||
|
return string(b)
|
||||||
|
})(),
|
||||||
|
},
|
||||||
|
httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write([]byte(
|
||||||
|
fmt.Sprintf(`{"key":"%s"}`, strings.ReplaceAll(publicKey, "\n", `\n`)),
|
||||||
|
))
|
||||||
|
})),
|
||||||
|
http.StatusForbidden,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
var url string
|
||||||
|
if test.server != nil {
|
||||||
|
url = test.server.URL
|
||||||
|
}
|
||||||
|
wrappedHandler := JWTProtect(url, "auth")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write([]byte("OK"))
|
||||||
|
}))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
if test.cookie != nil {
|
||||||
|
r.AddCookie(test.cookie)
|
||||||
|
}
|
||||||
|
wrappedHandler.ServeHTTP(w, r)
|
||||||
|
if w.Code != test.expectedStatusCode {
|
||||||
|
t.Errorf("Unexpected status code %v", w.Code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -52,7 +52,7 @@ func UserCookieMiddleware(cookieKey string, contextKey interface{}) func(http.Ha
|
|||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
c, err := r.Cookie(cookieKey)
|
c, err := r.Cookie(cookieKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
RespondWithJSONError(w, errors.New("received no or blank user identifier"), http.StatusBadRequest)
|
RespondWithJSONError(w, errors.New("user cookie: received no or blank identifier"), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
r = r.WithContext(
|
r = r.WithContext(
|
||||||
|
@ -83,7 +83,7 @@ func TestUserCookieMiddleware(t *testing.T) {
|
|||||||
if w.Code != http.StatusBadRequest {
|
if w.Code != http.StatusBadRequest {
|
||||||
t.Errorf("Unexpected status code %v", w.Code)
|
t.Errorf("Unexpected status code %v", w.Code)
|
||||||
}
|
}
|
||||||
if !strings.Contains(w.Body.String(), "received no or blank user identifier") {
|
if !strings.Contains(w.Body.String(), "received no or blank identifier") {
|
||||||
t.Errorf("Unexpected body %s", w.Body.String())
|
t.Errorf("Unexpected body %s", w.Body.String())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -99,7 +99,7 @@ func TestUserCookieMiddleware(t *testing.T) {
|
|||||||
if w.Code != http.StatusBadRequest {
|
if w.Code != http.StatusBadRequest {
|
||||||
t.Errorf("Unexpected status code %v", w.Code)
|
t.Errorf("Unexpected status code %v", w.Code)
|
||||||
}
|
}
|
||||||
if !strings.Contains(w.Body.String(), "received no or blank user identifier") {
|
if !strings.Contains(w.Body.String(), "received no or blank identifier") {
|
||||||
t.Errorf("Unexpected body %s", w.Body.String())
|
t.Errorf("Unexpected body %s", w.Body.String())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user