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:
token parameterThe 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:
?token=...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:
Request a trust configuration from Vlaams Toegangsbeheer.
Email integraties@vlaanderen.be with:
CC app.mbp@vlaanderen.be on the email so the Mijn Burgerprofiel team can follow the request.
Implement the token registration endpoint on your backend: POST /auth/v1/token.
Add frontend logic to read the handover token from the URL and exchange it for a session.
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:
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:
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>
token query parameter from the URL.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 |
Note: Your application's client ID will probably differ between TNI and production.
{
"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"
}
}
}
}
}
}
}
}
}