If your application connects members to an outside service — Discord, Telegram, anything with OAuth — the temptation is to add a table: myapp_links, member id, remote id, done. Do not. Invision Community already stores that relationship, and keeping a second copy creates a class of bug you cannot fix later.
core_login_links
When a member signs in with, or connects, an OAuth login handler, the link is recorded in core_login_links:
token_login_method— the id of the login handlertoken_member— the membertoken_identifier— their id on the remote servicetoken_linked— whether the link is currently activetoken_access_token— the OAuth token, which you will need if you intend to act on the member's behalf
Read it through one accessor and use that everywhere:
public static function idFor( Member $member ): ?string
{
if ( !$member->member_id )
{
return NULL;
}
try
{
$handler = static::instance();
return (string) Db::i()->select(
'token_identifier',
'core_login_links',
array( 'token_login_method=? AND token_member=? AND token_linked=1',
$handler->id, $member->member_id )
)->first() ?: NULL;
}
catch ( \Throwable $e )
{
return NULL;
}
}
Why a second copy goes wrong
A member who disconnects the service in Account Settings is unlinked in core_login_links and nowhere else. Your table still says they are connected, so your application keeps acting on an account the member deliberately disconnected — still assigning them roles, still treating them as verified. There is no event you can subscribe to that reliably repairs this, and the member has already done the one thing the interface offered them.
The same applies in reverse to merges, deletions and re-links. Every one of them is handled for you if you have no second copy to keep in step.
Requesting the scopes you will actually need
Scopes are granted at authorisation time and cannot be widened later using a token that was issued without them. If your application intends to act on the member's behalf, ask up front:
protected function scopesToRequest( array $additional = NULL ): array
{
return array( 'identify', 'email', 'guilds.join' );
}
Discovering afterwards that every existing member must re-authorise is an expensive mistake, because most of them never will.
Two things worth checking
Do not trust an unverified email. Services will hand over an address the account holder has never proved they own. Accepting it lets somebody register on the remote service against another person's address and land on their forum account:
protected function authenticatedEmail( string $accessToken ): ?string
{
$data = $this->_userData( $accessToken );
if ( empty( $data['verified'] ) )
{
return NULL;
}
return $data['email'] ?: NULL;
}
Set $allowMultiple = FALSE on the handler extension if only one connection per community makes sense. Two configured handlers means two sets of credentials and two sets of links, and every lookup that assumes one answer starts returning whichever it found first.
The migration payoff
There is a further reason to use the suite's table rather than your own. If somebody ever moves to your application from a competing one, the expensive thing to carry across is not settings — those take minutes to re-enter — it is the linked accounts. Every member who has to reconnect is a member who mostly will not, and the community quietly loses the feature it was paying for. Writing rows into core_login_links is a well-defined migration target. Writing them into a private table nobody else knows about is not.
Related application: Discord Integration — Discord Integration stores member connections in Invision Community's own login-link table, so disconnecting Discord in Account Settings really does disconnect it everywhere.
Recommended Comments