Posts

How to connect a Python Flask web app with Hive Keychain by way of Javascript for Hive authentication

avatar of @brianoflondon
25
@brianoflondon
·
0 views
·
3 min read

If you want to see where this is going: login to this site with your Hive Keychain

I'm learning to code. Well I'm re-learning to code at any rate. The first time I learned to code it was by reading, re-reading and reading again the ZX81 BASIC manual. I kept on coding from then until around 1997 when I completed my PhD in computational physics... and then I pretty much stopped coding.

But I'm back and I after a few small projects like recording the level of the Kinneret, also known as the Sea of Galilee in Israel, I decided to start a bit of a monster project to completely revolutionising the way Podcasts are funded.

That project is ongoing but along the way I wanted to get Hive Keychain working as a method to authenticate a user with my Python Flask based server application.

You can try this out on a demo here - there's not much working, I kept it to the bare minimum, but you can log in and log out using Hive KeyChain.

This is based on a terrific set of videos explaining the Python Flask micro web server created by an absolutely brilliant teacher, Corey Schafer.

Full playlist for Corey's Flask Blog

The base code for this comes from Corey's 6th video in the series:

Python Flask Tutorial: Full-Featured Web App Part 6 - User Authentication

And his code is on GitHub

There's one extra step about creating virtual environments which this video is good for https://www.youtube.com/watch?v=ojzNmvkNfqc

The Hive Keychain Parts

The bits I added can all be found in my Github repository: How to use Hive Keychain and Python Flask

The bulk of the changes are in the HTML Jinja2 template called login.html and in the routes.py file.

There was a need to combine Python with Javascript and I suck at Javascript. Thankfully I got some great help and that is what I want to share here. My Javascript is horribly wordy, but I hope it's easy to follow what is going on.

Huge thanks to @rishi556, @stoodkev and @ausbitbank all of whom helped me out.

I really hope putting this up here helps others!

<button class="btn btn-primary" id="Check Keychain" name="check-keychain" onClick="hiveKeychainSign()">Hive KeyChain Login</button> 
 
 
<!-- Hive Keychain javascript part --> 
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> 
<script> 
function hiveKeychainSign(){ 
    let name = document.querySelector("#acc_name").value; 
    console.log(name); 
    const keychain = window.hive_keychain; 
    if (!name) { 
        // need a name 
        console.log('need a name'); 
        return 
    } 
    const signedMessageObj = { type: 'login', address: name, page: window.location.href }; 
    const messageObj = { signed_message: signedMessageObj, timestamp: parseInt(new Date().getTime() / 1000, 10) }; 
    keychain.requestSignBuffer(name, JSON.stringify(messageObj), 'Posting', response => { 
        if (!response.success) { return; } 
            //Successfully logged in 
            console.log(response); 
            //We added stuff here 
            axios.post("/hive/login", response).then((res) => { 
                console.log(res) 
                let data = res.data; 
                //You'd probably want to give the url back in as a json. 
                //Whatever you send back will be save in data. Here' i'm assuming the format 
                //data = {podcaster : "https://google.com"} 
                window.location.href = `${data.loadPage}`; 
            }).catch((err) => { 
                console.log(err); 
                //Deal with any error here 
            }) 
        }); 
    }; 
</script> 
<!-- Hive Keychain javascript part --> 
@app.route("/hive/login", methods=['GET','POST']) 
def hive_login(): 
    """ Handle the answer from the Hive Keychain browser extension """ 
    if current_user.is_authenticated: 
        return redirect(url_for('home')) 
    if request.method == 'POST' and request.data: 
        ans = json.loads(request.data.decode('utf-8')) 
        if ans['success'] and validate_hivekeychain_ans(ans): 
            acc_name = ans['data']['username'] 
            user = User.query.filter_by(username = acc_name).first() 
            if user: 
                login_user(user, remember=True) 
                flash(f'Welcome back - @{user.username}', 'info') 
                app.logger.info(f'{acc_name} logged in successfully') 
                return make_response({'loadPage':url_for('home') }, 200) 
                # return redirect(url_for('podcaster.dashboard')) 
            else: 
                user = User(username=acc_name) 
                db.session.add(user) 
                db.session.commit() 
                result = login_user(user, remember=True) 
                flash(f'Welcome - @{user.username}', 'info') 
                app.logger.info(f'{acc_name} logged in for the first time') 
                return make_response({'loadPage':url_for('home') }, 200) 
                # return redirect(url_for('podcaster.dashboard')) 
        else: 
            flash('Not Authorised','danger') 
            return make_response({'loadPage':url_for('login') }, 401) 
 
 
 
 
 
def validate_hivekeychain_ans(ans): 
    """ takes in the answer from hivekeychain and checks everything """ 
    """ https://bit.ly/keychainpython """ 
 
    acc_name = ans['data']['username'] 
    pubkey = PublicKey(ans['publicKey']) 
    enc_msg = ans['data']['message'] 
    signature = ans['result'] 
 
    msgkey = verify_message(enc_msg, unhexlify(signature)) 
    pk = PublicKey(hexlify(msgkey).decode("ascii")) 
    if str(pk) == str(pubkey): 
        app.logger.info(f'{acc_name} SUCCESS: signature matches given pubkey') 
        acc = Account(acc_name, lazy=True) 
        match = False, 0 
        for key in acc['posting']['key_auths']: 
            match = match or ans['publicKey'] in key 
        if match: 
            app.logger.info(f'{acc_name} Matches public key from Hive') 
            mtime = json.loads(enc_msg)['timestamp'] 
            time_since = time.time() - mtime 
            if time_since < 30: 
                app.logger.info(f'{acc_name} SUCCESS: in {time_since} seconds') 
                return True , time_since 
            else: 
                app.logger.warning(f'{acc_name} ERROR: answer took too long.') 
    else: 
        app.logger.warning(f'{acc_name} ERROR: message was signed with a different key') 
        return False, 0