SSO authentication

This document explains how external service providers can implement Single Sign-On (SSO) for embeds in the Mijn Burgerprofiel mobile app.

Disclaimer: This document changes as the implementation evolves. Contact the Mijn Burgerprofiel team if you have questions or feedback.


When a user opens an embed that requires authentication from the Mijn Burgerprofiel app, the SSO flow passes the app's existing session context to the embed. The user does not need to sign in again.

The flow uses Vlaams Toegangsbeheer (ACM) and a token exchange mechanism. Sensitive tokens stay on the server and are never exposed directly to the frontend.

The following sequence shows the responsibilities of each system:

App          MBP backend         ACM        Embed backend       Embed
 |                |               |               |               |
1. Request SSO    |               |               |               |
 |---------------->               |               |               |
 |                |               |               |               |
 |                2. Token exchange               |               |
 |                |--------------->               |               |
 |                |               |               |               |
 |                3. Access token for the embed backend           |
 |                <---------------|               |               |
 |                |               |               |               |
 |                4. Register the access token    |               |
 |                |------------------------------->               |
 |                |               |               |               |
 |                5. Handover token               |               |
 |                <-------------------------------|               |
 |                |               |               |               |
6. Handover token |               |               |               |
 <----------------|               |               |               |
 |                |               |               |               |
7. Open embed with ?token=<handover-token>        |               |
 |---------------------------------------------------------------->
 |                |               |               |               |
 |                |               |   8. Resolve the handover token
 |                |               |               <---------------|
 |                |               |               |               |
 |                |               |           9. Create the session
 |                |               |               |--------------->
 |                |               |               |               |
Step Description Responsible party
1-3 The MBP backend uses Token Exchange to request an access token from ACM. The token is issued for the embed backend as its audience. MBP
4 The MBP backend sends the access token to the embed backend's token registration endpoint. MBP
5 The embed backend creates a temporary handover token and returns it. Embed provider
6-7 The app receives the handover token and opens the embed with ?token=<handover-token>. MBP
8 The embed frontend reads the token from the URL and sends it to its backend. Embed provider
9 The embed backend validates the handover token and starts a session for the frontend. Embed provider

The normal flow authenticates the user before the embed opens. Step-up authentication starts from an embed that is already open. The embed asks the host for a sign-in handover token when it needs one.

This is useful when:

  • the embed opened without an initial token parameter
  • the user needs to sign in after reaching a protected action
  • the initial handover token expired or was already consumed
  • a newly opened embed needs its own handover token

The step-up flow reuses the same server-side Token Exchange and token registration flow described above. The difference is how the flow starts and how the handover token reaches the embed. The step-up flow returns the token through the SDK instead of adding it to the embed URL.

App        MBP backend       ACM    Embed backend       SDK         Embed
 |              |             |           |              |            |
 |              |             |  1. Frontend calls auth.requestSignIn()
 |              |             |           |              <------------|
 |              |             |           |              |            |
 |              |             | 2. SDK sends requestSignIn            |
 <-------------------------------------------------------|            |
 |              |             |           |              |            |
3. App requests step-up SSO   |           |              |            |
 |-------------->             |           |              |            |
 |              |             |           |              |            |
 |              4. Token Exchange         |              |            |
 |              |------------->           |              |            |
 |              |             |           |              |            |
 |              5. Access token for embed backend        |            |
 |              <-------------|           |              |            |
 |              |             |           |              |            |
 |              6. Register access token  |              |            |
 |              |------------------------->              |            |
 |              |             |           |              |            |
 |              7. Handover token         |              |            |
 |              <-------------------------|              |            |
 |              |             |           |              |            |
8. Handover token             |           |              |            |
 <--------------|             |           |              |            |
 |              |             |           |              |            |
9. SDK receives SignInResult  |           |              |            |
 |------------------------------------------------------->            |
 |              |             |           |              |            |
 |              |             |            10. SDK returns SignInResult
 |              |             |           |              |------------>
 |              |             |           |              |            |
 |              |             |           |  11. Resolve handover token
 |              |             |           <---------------------------|
 |              |             |           |              |            |
 |              |             |           12. Start embed session     |
 |              |             |           |--------------------------->
 |              |             |           |              |            |

The embed sends no configuration with the request:

const result = await mbpClient.auth.requestSignIn();

if (result.status === 'success') {
// Send this handover token to your backend through the normal
// token-resolution flow. It is not an ACM access token.
await resolveHandoverToken(result.token);
}

The host decides which SSO configuration to use from trusted app state. The embed itself cannot choose the SSO client, ACM audience, scopes, token registration endpoint, or prompt text (these are set up in embed configuration in advance).

Call requestSignIn() when the user starts an action that needs authentication, or when the embed knows it needs a new handover token. Avoid calling it repeatedly in render logic or on every route change. The host can reject duplicate requests.

The SDK waits up to 120 seconds for the host to return a result. A timeout or another messaging failure rejects the promise. Expected outcomes are returned as a SignInResult instead.

Status Meaning
success The host completed SSO and returned a handover token.
cancelled The user cancelled, declined, or abandoned the host-side flow.
rate_limited Another sign-in flow is pending or a recent request is still in its cooldown period.
unavailable SSO is not configured or allowed for this embed.
unsupported The host does not implement step-up SSO.
failed The host attempted SSO but a technical error occurred.

Messages in non-success results are for debugging. Keep them out of user-facing copy.

Treat cancelled as a normal user outcome, not as a technical error. Let the user continue unauthenticated or offer a retry action if that fits your flow.

If the user is not signed in to the host application, the host may show a login UI as part of the step-up flow. If login succeeds, the request can still return success. If login is closed, declined, or fails technically, the result will be cancelled or failed.

The app may ask the user for consent the first time an embed client requests step-up SSO. After the user accepts, later requests from embeds using the same configured client may complete without another prompt while the user remains signed in to the host application.

This only skips the prompt. The host still requests a new handover token for every successful requestSignIn() call. Do not cache and reuse a successful handover token.

If the user logs out of the host application, the remembered step-up consent is cleared. A later login may require consent again.

Multiple embeds that use the same configured client can share the same app-side step-up consent. A newly opened embed can call requestSignIn() and receive a new handover token without another prompt if the user already accepted step-up SSO for that client during the current app login.

Step-up authentication does not replace the initial URL-token flow. A provider can support both:

Both tokens can use the same provider-side token resolution endpoint. Handover tokens are short-lived and must be single-use.

To implement SSO for your embed, you need to:

  1. Request a trust configuration from Vlaams Toegangsbeheer.

    Email integraties@vlaanderen.be with:

    • A description of your use case
    • Any existing ACM clients you already use

    CC app.mbp@vlaanderen.be on the email so the Mijn Burgerprofiel team can follow the request.

  2. Implement the token registration endpoint on your backend: POST /auth/v1/token.

  3. Add frontend logic to read the handover token from the URL and exchange it for a session.

  4. Add step-up logic if needed by calling mbpClient.auth.requestSignIn() when an open embed needs a new handover token.

Your backend needs an endpoint where the MBP backend can register an access token in exchange for a temporary handover token.

POST /auth/v1/token
Content-Type: application/json

The MBP backend sends the access token it received through Token Exchange:

{
"token": "<ACCESS-TOKEN>"
}
Field Type Description
token string OAuth access token received through ACM Token Exchange

Validate the access token with the ACM introspection endpoint. This confirms that the token:

  • Is valid and has not expired
  • Contains the correct audience for your client
  • Comes from a trusted source

The introspection response also contains user information that you can use to create the session.

Important: The userinfo endpoint only works with ID tokens. Use the introspection endpoint for access tokens received through Token Exchange.

See Validate an access token for implementation details.

The introspection endpoint returns information such as:

{
"iss": "https://authenticatie-ti.vlaanderen.be/op",
"active": true,
"exp": 1768862219,
"iat": 1768858619,
"client_id": "111b330d-56f1-47c5-b8a0-ddc171efdd41",
"aud": "<your client ID>",
"scope": "rrn vo_info profile delegation",
"rrn": "85251512381",
"given_name": "First name",
"family_name": "Last name",
"leeftijd": "40",
"jwt": "eyJhbGciOiJSUzI1NiIs..."
}

Relevant claims for identifying the user:

Claim Description
rrn The user's National Register number
given_name The user's first name
family_name The user's family name
leeftijd The user's age
jwt The complete JWT, if you need it for further validation

After validation:

  1. Store the access token on the server for the embed session.
  2. Extract the user information from the introspection response.
  3. Associate the information with the handover token you generate.

Important: Never send the access token to the frontend. Store it securely on your backend and associate it with the handover token.

Generate a temporary handover token and return it.

Success (200 OK):

{
"token": "<HANDOVER-TOKEN>"
}
Field Type Description
token string Temporary handover token for the embed
HTTP status Description Use it when
400 Bad Request Invalid request Required fields are missing or invalid in the request body
401 Unauthorized Invalid token The access token is invalid, expired, or has the wrong audience
500 Internal Server Error Server error An unexpected error occurs while processing the request

Example error response:

{
"error": "Invalid or expired token"
}

When the embed opens, the frontend receives the handover token as a query parameter:

https://example.com/embed?token=<HANDOVER-TOKEN>
  1. Read the token query parameter from the URL.
  2. Send the handover token to your backend.
  3. Let your backend validate the token and start a session.

Important: Never request the original access token from the frontend. The handover token only starts the session. Your backend handles all further authentication through the server-side session.

Scenario Recommended action
The URL has no token This may be expected when SSO is optional. Offer an alternative sign-in method if needed.
The token has expired, usually after about two minutes Tell the user that the session has expired and ask them to open the embed again.
The token has already been used Show an error. Handover tokens are single-use.
The backend is unavailable Show a technical error and offer a retry option.
Recommendation Description
Short lifetime Set the lifetime to no more than about two minutes.
Single use Mark the token as used after the first successful validation.
Unpredictable value Generate the token with a cryptographically secure random generator.
HTTPS required Accept requests only over HTTPS.
Requirement Description
Server-side storage Store the access token only on your backend.
Never send it to the frontend Never expose the access token to client-side code.
Associate it with the session Link the token to the user's server-side session.

Request a trust relationship for the relevant client ID from Vlaams Toegangsbeheer:

Environment Client ID
TNI (test) 111b330d-56f1-47c5-b8a0-ddc171efdd41
Production 444daeb0-9c24-4a7c-8781-78ab526cd17b
  1. TNI environment: ACM creates a client for the TNI test environment.
  2. Validation: Test your integration thoroughly in TNI.
  3. Production: After successful validation, ask ACM to move the configuration to production.

Note: Your application's client ID will probably differ between TNI and production.

Show the OpenAPI specification

{
  "openapi": "3.0.2",
  "info": {
    "title": "Mijn Burgerprofiel - SSO Token",
    "version": "1.0"
  },
  "components": {
    "schemas": {
      "RequestToken": {
        "type": "object",
        "required": [
          "token"
        ],
        "properties": {
          "token": {
            "type": "string",
            "description": "Token received through IdP Token Exchange"
          }
        }
      },
      "ResponseTempToken": {
        "type": "object",
        "required": [
          "token"
        ],
        "properties": {
          "token": {
            "type": "string",
            "description": "Temporary SSO handover token"
          }
        }
      },
      "ClientError": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Error message"
          }
        }
      },
      "ServerError": {
        "type": "string"
      }
    }
  },
  "servers": [
    {
      "description": "Your embed backend",
      "url": "https://embed.example.be"
    }
  ],
  "paths": {
    "/auth/v1/token": {
      "post": {
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RequestToken"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The request was accepted and a temporary token was generated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResponseTempToken"
                }
              }
            }
          },
          "400": {
            "description": "The request is missing required fields",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientError"
                }
              }
            }
          },
          "401": {
            "description": "The token in the request is invalid or expired",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientError"
                }
              }
            }
          },
          "500": {
            "description": "The application encountered an unexpected error while processing the token",
            "content": {
              "text/html": {
                "schema": {
                  "$ref": "#/components/schemas/ServerError"
                }
              }
            }
          }
        }
      }
    }
  }
}