Skip to main content

Linking a user account to an external phpBB forum

Sidebar

This article will show you how to verify a user's identity by letting them associate their account with an external third party phpBB account. I used Python and Flask to achieve this, however any language and framework should work, and shouldn't be too hard to port to.

This article will show you how to verify a user’s identity by letting them associate their account with an external third party phpBB account. I used Python and Flask to achieve this, however any language and framework should work, and shouldn’t be too hard to port to.

I wrote a Python library to parse phpBB profiles which I will be using throughout this tutorial.

First, you’ll need to install some packages:

pip install beautifulsoup4 phpbb-parser

I’m using Flask, Flask-User and Flask-SQLAlchemy.

pip install flask flask-user Flask-SQLAlchemy

You need to add a forum field to the user model:

class User(db.Model, UserMixin):
    # ...

    # Forum
    forum = db.Column(db.String(100), nullable=True)

Basic Idea: Forum signature linking #

  1. Server generates random token.
  2. User adds token to signature.
  3. User gives forum username.
  4. Server looks up signature from forum username.
  5. If match, link accounts

This is a page that gives the user the option to link to a forum.


<div class="box box_grey">
    <h2>Link forum account</h2>

    <form method="post" action="{{ url_for('link_forum_page') }}">
        <p>
            Enter your forum username here:
        </p>

        <input type="text" name="forum_username"
            value="{{ forum_username }}"
            required placeholder="Forum username">

        <p>
            Go to
            <a href="{{ forum_url }}/ucp.php?i=profile&mode=signature">
                User Control Panel > Profile > Edit signature
            </a>
        </p>
        <p>
            Paste this into your signature:
        </p>

        <input type="text" value="{{ key }}" readonly size=32>

        <p>
            Click next so we can check it.
        </p>
        <p>
            Don't worry, you can remove it after this is done.
        </p>

        <input type="submit" value="Next">
    </form>
</div>

Token cache #

I used a memcache in order to temporarily associate an IP with a random token

from werkzeug.contrib.cache import SimpleCache
cache = SimpleCache()
# Project components
from app import app
from models import *

# Dependencies
from flask import *
from flask_user import *
import phpbb_parser as parser

# GET link_forum_page
def link_forum_get():
    # Used to automatically fill out the forms in the template with a forum username
    forum_username = request.args.get("user") or ""

    # Create random token
    import uuid
    key = uuid.uuid4().hex
    cache.set("forum_claim_key_" + request.remote_addr, key, 5*60)

    return render_template('link_forum.html', key=key,
        forum_username=forum_username, forum_url="https://example.com")

# POST link_forum_page
def link_forum_post():
    forum_username = request.form["forum_username"]

    # Get profile
    profile = parser.get_profile("https://example.com", forum_username)
    if not profile:
        flash("Unable to find your forum user.", "error")
        return redirect(url_for("link_forum_page", user=forum_username))

    # Get stored key
    stored_key = cache.get("forum_claim_key_" + request.remote_addr)
    if not stored_key:
        flash("Failed to get key", "error")
        return redirect(url_for("link_forum_page", user=forum_username))

    # Get token in signature
    if not stored_key in profile.signature.text:
        flash("Could not find the key in your signature!", "error")
        return redirect(url_for("link_forum_page", user=forum_username))

    # Get user from database
    user = User.query.filter_by(username=current_user.username).first()
    if not user:
        flash("Could not find a user of that name!")
        return redirect(url_for("link_forum_page", user=forum_username))

    user.forum = forum_username
    db.session.commit()

    # SUCCESS! Redirect to index page
    return redirect(url_for("index_page"))

# Register link_forum_page
@app.route("/user/claim/", methods=["POST", "GET"])
@login_required
def link_forum_page():
    if request.method == "GET":
        return link_forum_get()
    else:
        return link_forum_post()

Function split up to make it easier to read.

Signing in with a forum account #

The above is only good for linking, and not for actually signing in. To allow signing in, you could read public profile data off of the forum profile and then use OAuth to sign in with those.

For example, if the user has their Github listed on their forum profile, you can sign them in using that Github account.

  1. User gives forum username
  2. Server looks up Github field in forum username
  3. User signs in using Github to confirm identity
    1. Server redirects user to github authentication.
    2. Github redirects user to github callback with access token.
    3. Server looks up Github username using access token.
    4. Access token added to user model and accounts linked.
    5. If not logged in, server logs user into the account associated with that Github username.
rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source maintainer, and a graduate from the University of Bristol. I’m a core developer for Luanti, an open source voxel game engine.

Comments

Leave comment

Shown publicly next to your comment. Leave blank to show as "Anonymous".
Optional, to notify you if rubenwardy replies. Not shown publicly.
Max 1800 characters. You may use plain text, HTML, or Markdown.