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

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

image.png

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

image.png

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




brianoflondon hive footer.png