feat: Add HNS.ID domains using ETH signing
All checks were successful
Build Docker / Build Docker (push) Successful in 3m47s

This commit is contained in:
Nathan Woodburn 2024-06-14 15:32:04 +10:00
parent 0bc07e491e
commit 7ea097f916
Signed by: nathanwoodburn
GPG Key ID: 203B000478AD0EF1
5 changed files with 289 additions and 84 deletions

2
.gitignore vendored
View File

@ -4,3 +4,5 @@ __pycache__/
instance/ instance/
website/avatars/ website/avatars/
.env

View File

@ -4,3 +4,6 @@ Authlib
requests requests
dnspython dnspython
requests-doh requests-doh
python-dotenv
web3
eth-account

View File

@ -3,7 +3,10 @@ from flask import Flask
from .models import db from .models import db
from .oauth2 import config_oauth from .oauth2 import config_oauth
from .routes import bp from .routes import bp
from datetime import timedelta
import dotenv
dotenv.load_dotenv()
def create_app(config=None): def create_app(config=None):
app = Flask(__name__) app = Flask(__name__)
@ -15,18 +18,26 @@ def create_app(config=None):
if 'WEBSITE_CONF' in os.environ: if 'WEBSITE_CONF' in os.environ:
app.config.from_envvar('WEBSITE_CONF') app.config.from_envvar('WEBSITE_CONF')
# Set the secret key to a key from the ENV
app.secret_key = os.getenv("FLASK_SECRET_KEY", os.urandom(24).hex())
# Set the session to be permanent
app.config["SESSION_PERMANENT"] = True
# Set it to 6 months
app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(days=180)
# load app specified configuration # load app specified configuration
if config is not None: if config is not None:
if isinstance(config, dict): if isinstance(config, dict):
app.config.update(config) app.config.update(config)
elif config.endswith('.py'): elif config.endswith('.py'):
app.config.from_pyfile(config) app.config.from_pyfile(config)
setup_app(app) setup_app(app)
return app return app
def setup_app(app): def setup_app(app: Flask):
db.init_app(app) db.init_app(app)
# Create tables if they do not exist already # Create tables if they do not exist already

View File

@ -13,9 +13,15 @@ import dns.message
import dns.query import dns.query
import dns.rdatatype import dns.rdatatype
from requests_doh import DNSOverHTTPSSession, add_dns_provider from requests_doh import DNSOverHTTPSSession, add_dns_provider
from datetime import timedelta
from eth_account.messages import encode_defunct
from eth_account import Account
bp = Blueprint("home", __name__) bp = Blueprint("home", __name__)
openSeaAPIKey = os.getenv("OPENSEA_API_KEY")
if not os.path.exists("website/avatars"): if not os.path.exists("website/avatars"):
os.makedirs("website/avatars") os.makedirs("website/avatars")
@ -45,6 +51,8 @@ def home():
db.session.add(user) db.session.add(user)
db.session.commit() db.session.commit()
session["id"] = user.id session["id"] = user.id
# Make sure the session is permanent
session.permanent = True
# if user is not just to log in, but need to head back to the auth page, then go for it # if user is not just to log in, but need to head back to the auth page, then go for it
if next_page: if next_page:
return redirect(next_page) return redirect(next_page)
@ -57,9 +65,69 @@ def home():
else: else:
clients = [] clients = []
return render_template("home.html", user=user, clients=clients) # Check if the user has signed in with HNS ID
hnsid=''
address=''
if "address" in session:
address = session["address"]
openseaInfo = requests.get("https://api.opensea.io/api/v2/chain/optimism/account/{address}/nfts?collection=handshake-slds",
headers={"Accept": "application/json",
"x-api-key":openSeaAPIKey})
if openseaInfo.status_code == 200:
hnsid = openseaInfo.json()
return render_template("home.html", user=user, clients=clients, address=address, hnsid=hnsid)
@bp.route("/hnsid", methods=["POST"])
def hnsid():
# Get address and signature from the request
address = request.json.get("address")
signature = request.json.get("signature")
message = request.json.get("message")
# Verify the signature
msg = encode_defunct(text=message)
signer = Account.recover_message(msg, signature=signature).lower()
if signer != address:
print("Signature verification failed")
print(signer, address)
return jsonify({"success": False})
# Save the address in the session
session["address"] = address
session.permanent = True
return jsonify({"success": True})
@bp.route("/hnsid/<domain>")
def hnsid_domain(domain):
# Get the address from the session
address = session.get("address")
if not address:
return jsonify({"error": "No address found in session"})
# Get domain info from Opensea
openseaInfo = requests.get(f"https://api.opensea.io/api/v2/chain/optimism/account/{address}/nfts?collection=handshake-slds",
headers={"Accept": "application/json",
"x-api-key":openSeaAPIKey})
if openseaInfo.status_code != 200:
return jsonify({"error": "Failed to get domain info from Opensea"})
hnsid = openseaInfo.json()
for nft in hnsid["nfts"]:
if nft["name"] == domain:
# Add domain to the session
user = User.query.filter_by(username=domain).first()
if not user:
user = User(username=domain)
db.session.add(user)
db.session.commit()
session["id"] = user.id
session.permanent = True
return redirect("/")
return jsonify({"success": False, "error": "Domain not found"})
@bp.route("/logout") @bp.route("/logout")
def logout(): def logout():
del session["id"] del session["id"]

View File

@ -1,80 +1,104 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HNS Login</title> <title>HNS Login</title>
<link rel="icon" href="favicon.png" type="image/png"> <link rel="icon" href="favicon.png" type="image/png">
<style> <style>
body { body {
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
margin: 0; margin: 0;
padding: 0; padding: 0;
/* Dark theme*/ /* Dark theme*/
background-color: #222; background-color: #222;
color: #fff; color: #fff;
} }
h1 {
margin: 0;
padding: 20px;
background-color: #333;
color: #fff;
text-align: center;
}
h2 {
margin: 0;
padding: 20px;
text-align: center;
}
p {
margin: 0;
padding: 20px;
text-align: center;
}
form {
text-align: center;
}
button {
padding: 10px 20px;
font-size: 16px;
background-color: #333;
color: #fff;
border: none;
cursor: pointer;
}
a.button {
display: block;
width: 200px;
margin: 20px auto;
padding: 10px 20px;
font-size: 16px;
background-color: #333;
color: #fff;
border: none;
cursor: pointer;
text-align: center;
text-decoration: none;
}
button.loginbutton {
/* Put in the centre of the screen */
margin-left: 50%; h1 {
transform: translateX(-50%); margin: 0;
} padding: 20px;
</style> background-color: #333;
color: #fff;
text-align: center;
}
h2 {
margin: 0;
padding: 20px;
text-align: center;
}
p {
margin: 0;
padding: 20px;
text-align: center;
}
form {
text-align: center;
}
button {
padding: 10px 20px;
font-size: 16px;
background-color: #333;
color: #fff;
border: none;
cursor: pointer;
}
a.button {
display: block;
width: 200px;
margin: 20px auto;
padding: 10px 20px;
font-size: 16px;
background-color: #333;
color: #fff;
border: none;
cursor: pointer;
text-align: center;
text-decoration: none;
}
button.loginbutton {
/* Put in the centre of the screen */
margin-left: 50%;
margin-top: 20px;
transform: translateX(-50%);
}
.login-option {
margin-top: 20px;
}
select {
padding: 10px 20px;
font-size: 16px;
background-color: #333;
color: #fff;
border: none;
cursor: pointer;
margin-right: 25px;
}
</style>
</head> </head>
<body> <body>
<h1>HNS Login</h1> <h1>HNS Login</h1>
{% if user %} {% if user %}
<h2>You are currently logged in as <strong>{{ user }}/</strong></h2> <h2>You are currently logged in as <strong>{{ user }}/</strong></h2>
<a href="{{ url_for('.logout') }}" class="button">Log Out</a> <a href="{{ url_for('.logout') }}" class="button">Log Out</a>
{% for client in clients %} {% for client in clients %}
<pre> <pre>
<strong>Client Info</strong> <strong>Client Info</strong>
{%- for key in client.client_info %} {%- for key in client.client_info %}
<strong>{{ key }}: </strong>{{ client.client_info[key] }} <strong>{{ key }}: </strong>{{ client.client_info[key] }}
@ -84,36 +108,133 @@
<strong>{{ key }}: </strong>{{ client.client_metadata[key] }} <strong>{{ key }}: </strong>{{ client.client_metadata[key] }}
{%- endfor %} {%- endfor %}
</pre> </pre>
<hr> <hr>
{% endfor %} {% endfor %}
<br><br> <br><br>
<p>Want to implement OAuth?<br> <p>Want to implement OAuth?<br>
Contact Nathan.Woodburn/ on any social media platform</p> Contact Nathan.Woodburn/ on any social media platform</p>
{% else %} {% else %}
<h2>Login with your Handshake domain</h2> <h2>Login with your Handshake domain</h2>
<p>If you have already used Varo Auth, you can just select the domain you want to login with.</p>
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
<script type="text/javascript" src="https://auth.varo.domains/v1"></script> <script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
<script>var varo = new Varo();</script>
<button class="loginbutton" onclick='varo.auth().then(auth => { <div class="login-option">
<script type="text/javascript" src="https://auth.varo.domains/v1"></script>
<script>var varo = new Varo();</script>
<button class="loginbutton" onclick='varo.auth().then(auth => {
if (auth.success) { if (auth.success) {
// handle success by calling your api to update the users session // handle success by calling your api to update the users session
$.post("/", JSON.stringify(auth.data), (response) => { $.post("/", JSON.stringify(auth.data), (response) => {
window.location.reload(); window.location.reload();
}); });
} }
});'>Login</button> });'>Login with Varo</button>
{% endif %} </div>
<div class="login-option">
<!-- Login for HNS.ID domains -->
<script>
async function loginETH() {
if (typeof window.ethereum === 'undefined') {
alert('Please install MetaMask to use this feature');
return;
}
try {
// Check if the user is already connected
const accounts = await ethereum.request({ method: 'eth_accounts' });
var address = '';
if (accounts.length > 0) {
console.log('Already connected', accounts[0]);
address = accounts[0];
} else {
console.log('Not connected yet');
// Request connection
const accounts = await ethereum.request({ method: 'eth_requestAccounts' });
if (accounts.length > 0) {
address = accounts[0];
console.log('Connected', accounts[0]);
}
}
if (address != '') {
// Send the address to the server
console.log('Sending address to server', address);
// Sign a message
const message = 'I am signing my one-time nonce: ' + Math.floor(Math.random() * 1000000) + ' to log in to HNS Login as ' + address;
const signature = await ethereum.request({
method: 'personal_sign',
params: [message, address],
});
console.log('Signature', signature);
// Redirect user to choose a domain to log in with
$.ajax({
url: '/hnsid',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ address: address, signature: signature, message: message }),
dataType: 'json',
success: function (response) {
console.log('Response', response);
if (response.success) {
window.location.reload();
}
}
});
}
<div style="position: fixed; bottom: 0; width: 100%; text-align: center; background-color: #333; padding: 10px;"> } catch (error) {
Powered by <a href="https://auth.varo.domains/implement" target="_blank">Varo Auth</a> and <a href="https://nathan.woodburn.au" target="_blank">Nathan.Woodburn/</a> console.error('Error checking connection status', error);
}
}
</script>
{% if address %}
<h4 style="text-align: center;">Logged in as {{address}}</h4>
<span style="text-align: center;display: block;">Select a HNS.ID domain to log in with</span><br>
<div style="text-align: center;">
<select id="nftDropdown">
{% for nft in hnsid.nfts %}
<option value="{{nft.name}}">{{nft.name}}</option>
{% endfor %}
</select>
<button onclick="HNSIDLoginSelect()">Login</button>
</div> </div>
<script>
function HNSIDLoginSelect() {
var selectedNFT = document.getElementById("nftDropdown").value;
window.location.href = "/hnsid/" + selectedNFT;
}
</script>
<button class="loginbutton" onclick='javascript:loginETH();'>Login with another ETH address</button>
{% else %}
<button class="loginbutton" onclick='javascript:loginETH();'>Login with HNS.ID</button>
{% endif %}
</div>
{% endif %}
<div style="position: fixed; bottom: 0; width: 100%; text-align: center; background-color: #333; padding: 10px;">
Powered by <a href="https://auth.varo.domains/implement" target="_blank">Varo Auth</a> and <a
href="https://nathan.woodburn.au" target="_blank">Nathan.Woodburn/</a>
</div>
</body> </body>
</html> </html>