Skip to content
View in the app

A better way to browse. Learn more.

ernestdefoe.online

A full-screen app on your home screen with push notifications, badges and more.

To install this app on iOS and iPadOS
  1. Tap the Share icon in Safari
  2. Scroll the menu and tap Add to Home Screen.
  3. Tap Add in the top-right corner.
To install this app on Android
  1. Tap the 3-dot menu (⋮) in the top-right corner of the browser.
  2. Tap Add to Home screen or Install app.
  3. Confirm by tapping Install.
ernestdefoe.online

Extensions, themes & support for Flarum and Invision Community

Vibe coding for the community web. Report a bug, request a feature, or dig into the source — this is where the tools you use get built, in the open.

We do custom Bespoke Invision Community apps. If you have an idea for something you want then use the contact form to get in touch with us.
Knowledge base

Things that cost me a day, so they cost you none

Working notes from building Invision Community and Flarum applications. Mostly the failures that give no error at all — the ones where everything installs cleanly and quietly does the wrong thing.

92 articles

Invision Community 5

86 articles

Extensions and contracts

39

What each extension point is for, what it must declare, and what happens when it is wrong — which is usually nothing visible.

Languages and text

5

The string table, translation, and the places where text does not appear where you expected it to.

Theming, templates and forms

9

Theme hooks, CSS that survives both colour schemes, and building forms that do not throw on render.

Background work and scheduled tasks

5

The queue system, work that has to happen after the response, and jobs that finish without doing anything.

Data, settings and storage

11

The database layer, settings, tags, file storage, and backing up a live site.

AI features and expectations

5

What these features do, what they cost, and what buyers reasonably but wrongly assume they do.

Application structure and releases

11

The JSON files an application is made of, versioning and upgrade steps, and testing from the command line.

Realtime, chat and calls

1

WebSocket gateways, relays and the server-side pieces live features depend on — where "it works when I test it" and "it works for your members" are different claims.

Nothing matches that.

Invision Community already runs an OAuth 2.1 server — do not write another one

If you are building anything that needs a third party to act on a member's behalf — an AI connector, a mobile app, a desktop client, an integration a customer authorises — the instinct is to invent a token scheme. A long secret in a URL, an API key in a settings field, a bearer token you mint yourself.

Do not. Invision Community 5 ships a complete OAuth 2.1 authorization server, and almost nobody building on the platform seems to know it is there. It is not a stub. It supports PKCE, refresh tokens, per-client scopes, brute-force lockout and revocation, and it already has an AdminCP screen.

What is actually there

Four tables, all created by the core installer:

core_oauth_clients                      ← the registered clients
core_oauth_server_access_tokens         ← issued tokens, with status and scope
core_oauth_server_authorization_codes   ← codes, with code_challenge
core_oauth_authorize_prompts            ← in-flight consent screens

Two physical endpoints in the site root:

/oauth/authorize/    ← the consent screen
/oauth/token/        ← the token exchange

And the AdminCP screen that manages it all, at System → Site Features → API → OAuth Clients.

PKCE is real

The client row has an oauth_pkce column taking S256, plain or none, and the authorization-codes table carries code_challenge and code_challenge_method. The token endpoint verifies it:

if ( $authorizationCode['code_challenge'] or $this->client->pkce !== 'none' )
{
    if ( !$codeVerifier or !$authorizationCode['code_challenge'] )
    {
        return;
    }
    // ... S256 hash, then Login::compareHashes()
}

It also treats a re-used authorization code as an attack and revokes every token that code produced, which is the behaviour the specification asks for and the part people most often get wrong when writing their own.

Validating a bearer token in your own code

One public static call:

use IPS\Api\OAuthClient;

$details = OAuthClient::accessTokenDetails( $token );

$member = \IPS\Member::load( $details['member_id'] );
$scopes = json_decode( $details['scope'], TRUE );

It throws UnderflowException if the token is unknown, and IPS\Api\Exception if it has expired or been revoked.

Four traps

1. It does not check whether the member is banned. accessTokenDetails() and validateAccessToken() check expiry and revocation and stop there — the REST dispatcher does the standing check separately. Somebody banned after they authorised keeps a working token until it expires. Check it yourself:

if ( $member->isBanned() or $member->members_bitoptions['validating'] )
{
    // refuse
}

2. A client_credentials token has no member behind it. member_id is NULL. If your code assumes a member and calls Member::load( NULL ), you get a guest — so a permission check that should have failed quietly passes as "guest can see public content", or worse, your code falls back to the currently logged-in member. Decide explicitly what a memberless token may do, and if the answer is "nothing", refuse it.

3. The token format is <client_id>_<token> and it is split on the first underscore. accessTokenDetails() does explode( '_', $accessToken ) and takes elements 0 and 1. Client IDs generated by core are 32 hex characters, so this is safe — but if you ever create a client row programmatically, keep the ID hex. An underscore in a client ID silently breaks every token it issues.

4. Scopes are free text you define per client, and an empty endpoints array means no REST access. The scope definition stored on the client looks like this:

{
  "myapp.read": { "description": "Read …", "endpoints": {} },
  "myapp.write": { "description": "Post …", "endpoints": {} }
}

That endpoints map is what scopesCanAccess() consults to decide whether a scope may reach a core REST endpoint. Leaving it empty is a feature, not an oversight: the token then works for your endpoint and has no access to the REST API at all. That is least privilege for free.

Create the client for your users

Every field is on core's own OAuth screen, but it is six settings to get exactly right, and a wrong PKCE dropdown fails with a symptom ("it would not connect") that points nowhere. Insert the row yourself from your application's settings page and give the administrator a button:

Db::i()->insert( 'core_oauth_clients', array(
    'oauth_client_id'     => $clientId,                          // hex, 32 chars
    'oauth_client_secret' => password_hash( $secret, PASSWORD_DEFAULT ),
    'oauth_grant_types'   => 'authorization_code',
    'oauth_redirect_uris' => json_encode( $redirectUris ),
    'oauth_pkce'          => 'S256',
    'oauth_choose_scopes' => 1,       // the member ticks which scopes to grant
    'oauth_ucp'           => 1,       // and can revoke it from their own settings
    'oauth_scopes'        => json_encode( $scopes ),
    'oauth_enabled'       => 1,
    'oauth_prompt'        => 'reauthorize',
) );

// The display name is a language string, not a column:
Lang::saveCustom( 'core', "core_oauth_client_{$clientId}", 'My Integration' );

oauth_choose_scopes is the one worth understanding. With it off, the member grants whatever the client asked for. With it on, they see a checklist and decide — which is the difference between "authorise this application" and "authorise this application to do these specific things".

Why this beats rolling your own

The security argument is the obvious one, and it is real: you are not going to write a better authorization-code flow in an afternoon than the one that already ships.

But the better argument is that it respects the site. The member authorising is a real member, signing in through the community's own login — so two-factor authentication applies, bans apply, group permissions apply, and their existing session is used. The administrator revokes access on a screen they already know, in a list alongside every other connected application, rather than on one you invented. And the member can cut it off themselves from their own account settings.

None of that is something you can bolt onto an API key.

User Feedback

Recommended Comments

There are no comments to display.

Account

Navigation

Search

Search

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.