Custom Identity Providers #

Pragma Engine offers the flexibility to create multiple custom identity providers. To create a custom identity provider, there are several steps that need to be taken:

  1. Add a new enum value.
  2. Create a config class.
  3. Create an Identity Provider Plugin class.
  4. Configure the Identity Provider Plugin in Pragma Engine.
  5. [optional] Set up Portal login.

Add a new enum value #

  1. In the AccountRpcExt.proto file under 5-ext, update the ExtIdProvider to have a custom enum value (DEMO):
enum ExtIdProvider {
    EXT_UNUSED = 0;
    reserved 1 to 100; // Pragma supported Identity Providers

    //  === EXTENSIONS (101+) ===
    DEMO = 101; // Add this
}
  1. Run the following command to build the protos: make ext-protos.

Create a config class #

Create the DemoIdentityProviderConfig.kt file under platform/5-ext/ext/src/main/kotlin/ext/account.

Below is an example of a plugin config:

// DemoIdentityProviderConfig.kt
package ext.account

import pragma.account.config.IdentityProviderPluginConfig
import pragma.config.ConfigBackendModeFactory
import pragma.settings.BackendType

class DemoIdentityProviderConfig private constructor(type: BackendType) :
    IdentityProviderPluginConfig<DemoIdentityProviderConfig>(type) {
    override val description = "Demo Identity Provider configuration."

    var clientSecret by types.encryptedString("your app's secret key")
    var clientId by types.string("your app's public id")

    companion object : ConfigBackendModeFactory<DemoIdentityProviderConfig> {
        override fun getFor(type: BackendType) : DemoIdentityProviderConfig {
            return DemoIdentityProviderConfig(type)
        }
    }
}

Any configurable settings for this plugin can be added in the config class.

You can find a list of the supported types for configurable settings in the inner class Types which is found in the file platform/2-pragma/core/src/main/kotlin/pragma/config/ConfigObject.kt.

Create an Identity Provider Plugin class #

To create a custom Identity Provider Plugin implement this interface:

interface IdentityProviderPlugin: IdentityProvider, PragmaServicePlugin

Below is an example class implementing this interface:

// DemoIdentityProviderPlugin.kt
package ext.account

import pragma.account.ExtIdProvider
import pragma.account.IdentityProviderPlugin
import pragma.auth.IdProviderAccount
import pragma.content.ContentDataNodeService
import pragma.http.TimedHttpClient
import pragma.plugins.ConfigurablePlugin
import pragma.services.Service

class DemoIdentityProviderPlugin(
    override val service: Service,
    override val contentDataNodeService: ContentDataNodeService
) : ConfigurablePlugin<DemoIdentityProviderConfig>, IdentityProviderPlugin {
    override fun getType() = ExtIdProvider.DEMO_VALUE

    override var canAuthenticate: Boolean = true
    override var separator: String = ""

    override lateinit var config: DemoIdentityProviderConfig
    private var httpClient:
            TimedHttpClient = TimedHttpClient("demo.outgoing.http", service)

    private var accountNumber = 0

    constructor(
        service: Service,
        contentDataNodeService: ContentDataNodeService,
        timedHttpClient: TimedHttpClient
    ) : this(service, contentDataNodeService) {
        this.httpClient = timedHttpClient
    }

    override suspend fun onConfigChanged(config: DemoIdentityProviderConfig) {
        this.config = config
    }

    // Code to always validate providerToken
    override suspend fun validate(providerToken: String): IdProviderAccount {
        accountNumber++
        return IdProviderAccount(
            ExtIdProvider.DEMO_VALUE,
            "demo-user-account-id-$accountNumber",
            "demo-user-display-name-$accountNumber",
            "demo-user-discrimator-$accountNumber",
        )
    }

    // Code to return information Portal needs to use as Identity Provider
    override fun getPublicInfo(): Map<String, String> {
        return mapOf(
            "clientId" to config.clientId,
            "showPortalLoginButton" to config.showPortalLoginButton.toString(),
            "playerLoginEnabled" to config.playerLoginEnabled.toString(),
            "operatorLoginEnabled" to config.operatorLoginEnabled.toString(),
            "accountLinkingEnabled" to config.accountLinkingEnabled.toString()
            // any additional required fields
        )
    }
}
Method overview

The getType function needs to return the new enum value(AccountRpcExt.ExtIdProvider.DEMO_VALUE) for your custom identity provider.

The validate function takes in the providerToken and makes the appropriate third party calls to your custom identity provider. If valid, this function returns IdProviderAccount. If invalid, this function should throw a Pragma exception.

if (!validFromThirdParty) {
    throw PragmaException(
      PragmaError.AccountService_Unverified,
      "Account not verified.")
}

The getPublicInfo function should return all information necessary for a third party to use your custom identity provider. The Pragma Engine Portal uses this information to allow login.

getPublicInfo should not return any secret or sensitive information.

Configure the identity provider #

To enable and configure your custom Identity Provider Plugin, you’ll need to add the plugin config to your chosen Pragma Engine yaml. For local development this would be in platform/5-ext/config/local-dev.yml.

Below is an example of enabling your custom identity provider:

social:
  pluginConfigs:
    AccountService.identityProviderPlugins:
      plugins:
        Demo:
          class: "ext.account.DemoIdentityProviderPlugin"
          config:
            clientId: "your-client-id"
            clientSecret: "encrypted-string-for-your-client-secret"

Test with Postman #

To test that your custom identity is properly hooked up to Pragma Engine, you’ll need to first enable playerLoginEnabled:

social:
  pluginConfigs:
    AccountService.identityProviderPlugins:
      plugins:
        Demo:
          class: "ext.account.DemoIdentityProviderPlugin"
          config:
            clientId: "your-client-id"
            clientSecret: "encrypted-string-for-your-client-secret"
            playerLoginEnabled: true

Run Pragma Engine via one of the following methods.

Running via Make
Run make run to start the platform. Run this in a terminal with platform as the working directory.
Running in IntelliJ

From the IntelliJ toolbar in the upper right, ensure 5-ext - LocalConfigured is selected, then click the play button.

If 5-ext - LocalConfigured isn’t available, you will need to configure it. In the IntelliJ toolbar, click the dropdown next to the run button, then click Edit Configurations…. In the Run/Debug Configurations window that appears, expand Kotlin in the left hand side, then select 5-ext - LocalConfigured. Click OK. Click the play button in the IntelliJ toolbar to start Pragma Engine.

Once the engine has started successfully, it prints the message [main] INFO main - Pragma server startup complete.

Send a request to v1/account/authenticateorcreatev2 with the following data:

{
    "providerId": 101, // your custom identity provider ID
    "providerToken": "demo-token",
    "gameShardId":"00000000-0000-0000-0000-000000000001",
    "loginQueuePassToken": "{{loginQueueToken}}"
}

See Login queue for more details.

[optional] Set up Portal login #

To enable Portal login you’ll need to do the following:

  1. Go to the platform/web/portal/src/authProviders directory.
  2. Create a demo folder.
  3. In the demo directory, create a file named demo.js.
import SignInDemo from './SignInDemo.vue.js'
import LinkDemo from './LinkDemo.vue.js'
import { Demo, demoAuthStep1_InitAndRedirect } from './demoAuth.js'
const utils = pragma.utils
const { getRedirectUri } = pragma.auth

export default {
    id: 'DEMO',
    name: 'Demo',
    iconName: 'demo',
    onSignInButtonClick: ({ router }) => {
        demoAuthStep1_InitAndRedirect(getRedirectUri(Demo.SIGNIN_REDIRECT_PATH))
    },
    onLinkButtonClick: ({ router }) => {
        demoAuthStep1_InitAndRedirect(getRedirectUri(Demo.LINK_REDIRECT_PATH))
    },
    // these components will be rendered inside the usual Signin box
    registeredRoutes: [
        {
            name: 'SignInDemo',
            path: '/signin-demo',
            component: SignInDemo
        },
    ],
    registeredLinkRoutes: [
        {
            name: 'LinkDemo',
            path: '/link-demo',
            component: LinkDemo
        },
    ],
    // should return true if the url is forcibly changed
    routerBeforeHook: ({ router }) => {
        let url = utils.getWindowLocation().href
        if (url.search('#/access_token=') >= 0) {
            url = url.replace('#/access_token=', '&access_token=')
            utils.goToUrl(url)
            return true
        }
    }
}
  1. Create a file named demoAuth.js. This is where you define logic to communicate with your custom identity provider.
const { store } = pragma
const { utils } = pragma
const { signIn, link, getIdProviderConfig, getAuthenticateBaseUri, getRedirectUri } = pragma.auth

export const Demo = {
    SIGNIN_REDIRECT_PATH: '/redirect/SignInDemo',
    LINK_REDIRECT_PATH: '/redirect/LinkDemo',
    PROVIDER_ID: 'DEMO',
    BACKEND_REDIRECT: '/v1/account/oauth-redirect/demo',
}

export const authenticateDemo = async token =>
    await signIn({
        providerId: Demo.PROVIDER_ID,
        providerToken: token
    })

export const linkDemo = async token =>
    await link({
        providerId: Demo.PROVIDER_ID,
        providerToken: token
    })

export const demoAuthStep1_InitAndRedirect = (redirectUri) => {
    const config = getIdProviderConfig(Demo.PROVIDER_ID)

    // Code to request auth page of provider

    utils.goToUrl(`${redirectUri}?code=demo-code`) // Replace this with auth page of provider
}

export const demoAuthStep2_GetTokenAndAuthenticate = async code => {
    const token = await getDemoAuthToken(code, getRedirectUri(Demo.SIGNIN_REDIRECT_PATH))
    if (!token) return { ok: false, error: 'Invalid code.' }

    return await authenticateDemo(token)
}

export const demoAuthStep2_GetTokenAndLink = async code => {
    const token = await getDemoAuthToken(code, getRedirectUri(Demo.SIGNIN_REDIRECT_PATH))
    if (!token) return { ok: false, error: 'Invalid code.' }

    return await linkDemo(token)
}

const getDemoAuthToken = async (code, redirectUri) => {

    // Code to request auth token using auth code

    return 'demo-token'
}

export default {
    Demo,
    demoAuthStep1_InitAndRedirect,
    demoAuthStep2_GetTokenAndAuthenticate,
}
  1. Create a file named LinkDemo.vue.js. This is the intermediate redirect page used for the link account flow.
import { demoAuthStep2_GetTokenAndLink } from './demoAuth.js'

const eventEmitter = pragma.ui.helpers.eventEmitter
const initiateLinkAndRedirectWithCode = pragma.auth.initiateLinkAndRedirectWithCode

const LinkDemo = {
  name: 'LinkDemo',
  components: {},
  emits: ['linkResult'],
  setup(_props, { emit }) {
    initiateLinkAndRedirectWithCode(
      demoAuthStep2_GetTokenAndLink,
      'Demo',
      emit,
      eventEmitter
    )
    return {}
  },
  template: `<div/>`
}

export default LinkDemo
  1. Create a file named SignInDemo.vue.js. This is the intermediate redirect page used for the sign in flow.
import { Demo, demoAuthStep2_GetTokenAndAuthenticate } from './demoAuth.js'

const { Icon, IdProviderAuthResultHandler } = pragma.ui.components
const { initiateAuthenticateAndRedirectWithCode } = pragma.auth

const SignInDemo = {
  name: 'SignInDemo',
  components: { Icon, IdProviderAuthResultHandler },
  emits: ['success'],
  setup(_props, { emit }) {
    const authResultRef = Vue.ref()

    initiateAuthenticateAndRedirectWithCode(
      demoAuthStep2_GetTokenAndAuthenticate,
      authResultRef,
    )
    return {
      Demo,
      authResultRef,
      emitSuccess: () => emit('success')
    }
  },
  template: `
    <div>
      <IdProviderAuthResultHandler
        v-if='authResultRef'
        :idProviderType='Demo.PROVIDER_ID'
        :authResult='authResultRef'
        @success='emitSuccess'
      />
      <div v-else class='loading'></div>
    </div>
  `
}

export default SignInDemo

Test with Portal #

To test your custom identity provider with Portal, add the following configurations:

social:
  pluginConfigs:
    AccountService.identityProviderPlugins:
      plugins:
        Demo:
          class: "demo.account.DemoIdentityProviderPlugin"
          config:
            clientId: "your-client-id"
            clientSecret: "encrypted-string-for-your-client-secret"
            showPortalLoginButton: true 
            accountLinkingEnabled: true
            operatorLoginEnabled: true
            playerLoginEnabled: true

Run the following command: make portal-package

Open the Social Player Portal (this is generally localhost:11000) and log into your newly created custom identity provider.