Instance ID for Web Portals
This topic describes how a web portal integrates with an ERP.net Instance ID using both the Authorization Code flow and the Client Credentials flow within a single confidential application.
A typical example of such a web portal is an online store, where users sign in to manage their profile or place orders, while all backend operations and API calls are performed by the application itself.
Scenario Overview
The following scenario is covered:
- The external application is a confidential web application (portal)
- The application has a backend component capable of securely storing a secret
- Users sign in interactively (internal and/or external)
- The application uses:
- Authorization Code flow to authenticate users
- Client Credentials flow to access instance APIs
- API calls are executed using a service identity
- A single Trusted Application is used for both flows
The Instance ID is the identity provider of a specific ERP.net instance. Confidential applications use it to authenticate users and obtain tokens against a concrete ERP.net instance.
For example, if your ERP.net instance is located at:
https://mycompany.my.erp.net
then the corresponding Instance ID endpoints are located at:
https://mycompany.my.erp.net/id
Design Goal
The goal of this approach is to clearly separate user authentication from API access.
- Users are authenticated to establish identity and session context
- API access is performed using a service identity, not the user
- External (community) users can sign in without receiving API permissions
- Backend integrations remain stable and centrally controlled
Flow Separation
| Purpose | Flow Used | Token Used | Identity |
|---|---|---|---|
| User sign-in | Authorization Code | ID token | Signed-in user |
| API access | Client Credentials | Access token | Application system user |
In this model, the Authorization Code flow is used only to identify the user, while the Client Credentials flow is used to authorize all API calls.
Prerequisites
Your ERP.net instance must have a trusted application defined with the configuration below.
Note
The values shown below are examples only and must be replaced with values that match your application.
Typical values:
- ApplicationUri:
portal.example.com - ImpersonateLoginUrl:
https://portal.example.com/signin-callback - SystemUser:
<an-internal-erp-user>
Note
The application owner must generate a random client secret, compute its Base64-encoded SHA-256 hash, and submit the hashed value via an internal ticket to erp.net so the Trusted Application can be registered and the configuration activated.
| Attribute | Value | Comment |
|---|---|---|
| Name | My web portal | Used only for user-friendly identification. |
| ApplicationUri | portal.example.com | The unique identifier of the application. |
| IsEnabled | true | Enables the trusted application. |
| SystemUserAllowed | true | Allows the application to authenticate as a service. |
| SystemUser | <an-internal-erp-user> |
The internal user used for service authentication. |
| ImpersonateAsInternalUserAllowed | true | Allows authentication by internal users. |
| ImpersonateAsCommunityUserAllowed | true | Allows authentication by external (community) users. |
| ImpersonateLoginUrl | https://portal.example.com/signin-callback | The callback URL handled by the backend after sign-in. |
| ClientType | Confidential | Indicates that the application can securely store a secret. |
| ApplicationSecretHash | <base64(sha256(your-client-secret))> |
The hashed client secret used during token requests. |
| Scope | read or read update |
Use read for read-only access; include update only if the application must create, modify, or delete data |
All other attributes can keep their default values and are not relevant for this scenario.
Implementation
This section demonstrates how a web portal uses:
- Authorization Code flow to authenticate users
- Client Credential flow to obtain access tokens for calling instance APIs
1. Start user sign-in (Authorization Code)
The portal redirects the user's browser to the Instance ID authorize endpoint.
For each sign-in request, generate:
state–<base64url(random(32 bytes))>nonce–<base64url(random(32 bytes))>code_verifier–<random(64 chars)>code_challenge–<base64url(sha256(code_verifier))>
Authorize request example:
GET /id/connect/authorize?
client_id=portal.example.com&
redirect_uri=https%3A%2F%2Fportal.example.com%2Fsignin-callback&
response_type=code%20id_token&
response_mode=form_post&
scope=openid%20profile&
state=YzQ5ZDc4M2QxN2Q0NDU3YjlhM2Y5OWE1ZTY4OTc0ZGM&
nonce=ZTRjOTM2M2Y0ODFhNGQ0NGE4NmU1N2ExYjY2NjE3ZmQ&
code_challenge=VZ9R6l3kPpJ6m2m9kR9cR9n8s3c7J4GmQ9FZp5mX1kQ&
code_challenge_method=S256
Host: mycompany.my.erp.net
Result:
The user is redirected to the ERP.net login page and signs in.
Note
The openid and profile scopes are requested because this is an OpenID Connect authentication flow that issues an ID token with basic user claims; these scopes are not defined in the Trusted Application because they are protocol-level scopes provided automatically based on the authorization flow.
2. Receive the sign-in response (callback)
After a successful sign-in, the ERP.net Instance ID redirects the user back to the configured callback URL.
Example callback request received by the backend:
GET /signin-callback?
code=SplxlOBeZQQYbYS6WxSbIA&
state=YzQ5ZDc4M2QxN2Q0NDU3YjlhM2Y5OWE1ZTY4OTc0ZGM
Host: portal.example.com
The backend must:
- Validate that the returned
stateequals the original value - Extract the
authorization code - Preserve the original
code_verifierfor the token request
3. Exchange the authorization code (user tokens)
The backend exchanges the authorization code at the Instance ID token endpoint.
Because the application is confidential, the request includes the client secret.
The request must include the original code_verifier.
Note
In this portal model, tokens from this step are used for user identification (ID token validation), not for calling instance APIs.
POST /id/connect/token HTTP/1.1
Host: mycompany.my.erp.net
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
client_id=portal.example.com&
client_secret=<PLAIN_CLIENT_SECRET>&
code=SplxlOBeZQQYbYS6WxSbIA&
redirect_uri=https%3A%2F%2Fportal.example.com%2Fsignin-callback&
code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
Example token response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjVCMjc5MjBFNjUzREQ3QUM2N0QyRjY0QjMyQTE3OTkyIiwidHlwIjoiSldUIn0.eyJuYmYiOjE3NjY0OTM3ODEsImV4cCI6MTc2NjQ5NDA4MSwiaXNzIjoiaHR0cHM6Ly9lMS1kZXYubG9jYWwvaWQiLCJhdWQiOiJQSyIsIm5vbmNlIjoiNjUwMTAwMWU0NGUzNGFlY2FiMTQzMTRiMDI4YzM0YzQiLCJpYXQiOjE3NjY0OTM3ODEsImF0X2hhc2giOiIwYWpYNkhYODNtbmJVNVhxVDUtUUZ3Iiwic19oYXNoIjoib1RRalZZUXdBRDZEMWV2TzlBTVNrUSIsInNpZCI6IkQxRkZFREZFNENCMzIzMDBEMjZDMjY1QjlCRDM3NEZCIiwic3ViIjoiYWRtaW4iLCJhdXRoX3RpbWUiOjE3NjY0OTM0OTUsImlkcCI6ImxvY2FsIiwiYW1yIjpbInB3ZCJdfQ.hL4SpH9yoMx5j0e2KMmd6Xttw3M5ktRNbs39m2cJ0-8MvCIoeTkkNtasXS7burHX9mh1vxAow1kYYdcedOQNRWERyGsNQM3jy1yal5jCixBRTPkC86bkAeS_h9Q7gWWLFSrSeaHvi_xJGoRFBLymS44dM20kZxFI9o1qZfOX79hnNnIoF_j7BEV0u77lj7Bb7OE3Xh2cyBcn0-yQB9SmGhesvt5B2IqZ0odn6il_qMabnAK_sm80b-4HtA4w8uLCCpD4JBg6g4O2zmhm_jVUQtYsqS9VjPMA4F1-pTB3ayrP40Dvq_cGP5dPspenR2GCxJqAg0bQu8AcBqLJkVcD-w",
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjVCMjc5MjBFNjUzREQ3QUM2N0QyRjY0QjMyQTE3OTkyIiwidHlwIjoiYXQrand0In0.eyJuYmYiOjE3NjY0OTM3ODEsImV4cCI6MTc2NjQ5NzM4MSwiaXNzIjoiaHR0cHM6Ly9lMS1kZXYubG9jYWwvaWQiLCJhdWQiOiJodHRwczovL2UxLWRldi5sb2NhbC9pZC9yZXNvdXJjZXMiLCJjbGllbnRfaWQiOiJQSyIsInN1YiI6ImFkbWluIiwiYXV0aF90aW1lIjoxNzY2NDkzNDk1LCJpZHAiOiJsb2NhbCIsImlkIjoiOWRhNjQ4MzktYThkMC00OTFkLWFlYmItNGQxOGZhNDJiMDE0IiwibmFtZSI6IkpvaG4gRG9lIiwiZW1haWwiOiJhZG1pbkBtYWlsLmNvbSIsInVzZXJfdHlwZSI6IkludGVybmFsVXNlciIsImlzX2FkbWluIjoidHJ1ZSIsImVtYWlsX3ZlcmlmaWVkIjoiZmFsc2UiLCJkYiI6IkUxX0RFVi0xIFRlc3QiLCJsb2NhbGUiOiJiZyIsImp0aSI6IkM4MTcxRjlFQ0FEN0U2OThEMjBFN0Q2Qjc5ODQyMjYwIiwic2lkIjoiRDFGRkVERkU0Q0IzMjMwMEQyNkMyNjVCOUJEMzc0RkIiLCJpYXQiOjE3NjY0OTM3ODEsInNjb3BlIjpbIm9wZW5pZCIsInByb2ZpbGUiXSwiYW1yIjpbInB3ZCJdfQ.TWePn6yinGcAaItZzfmNM9en3ESfGFhcLulTR7U7wfVK86RW9s8LlsgqoAc2SkigH2cXrovnADCe8uwp7BFSKbQBsN18vH5GmidFpILEr-M_WnOWfFZt941SA04vXpBzDJaiunzRl4FAEMd4RKXJ2L_wCZW8DNaKmsZfS9SH8N6QAYABrNoHZbrRV83pUh7suUnsBF_Ps-jdFNTnuxoj4UooRghAp7dn6LYw3MBcTb3GNdrJosb63DXpQY-Kq58gbYrov0Of4H_RD6lEaKcFbRVmIpg5S1UoM2DFaS-PWxKN_DF9_4A8zazBHM8BgGN4hA7JPOv6W7waODLvNqhnzQ",
"expires_in": 3600,
"scope": "openid profile"
}
The application can now validate the ID token to identify the user.
4. Request an access token for API calls (Client Credentials)
The portal backend requests an access token using the Client Credentials flow.
POST /id/connect/token HTTP/1.1
Host: mycompany.my.erp.net
Content-Type: application/x-www-form-urlencoded
client_id=portal.example.com&
client_secret=<PLAIN_CLIENT_SECRET>&
grant_type=client_credentials&
scope=read%20update
Note
The client_secret is sent as a plain (unhashed) value and must exactly match the secret configured for the trusted application.
Result:
If the request is successful, the Instance ID returns an access token.
{
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjVCMjc5MjBFNjUzREQ3QUM2N0QyRjY0QjMyQTE3OTkyIiwidHlwIjoiYXQrand0In0.eyJuYmYiOjE3NjY1MDUxNDcsImV4cCI6MTc2NjUwODc0NywiaXNzIjoiaHR0cHM6Ly9lMS1kZXYubG9jYWwvaWQiLCJhdWQiOlsiRG9tYWluQVBJIiwiVGFibGVBUEkiLCJPTEFQIiwiQXBwU2VydmVyIiwiaHR0cHM6Ly9lMS1kZXYubG9jYWwvaWQvcmVzb3VyY2VzIl0sImNsaWVudF9pZCI6IlBLIiwiY2xpZW50X3N5c3RlbV91c2VyIjoiYWRtaW4iLCJjbGllbnRfc3lzdGVtX3VzZXJfdHlwZSI6IkludGVybmFsVXNlciIsImNsaWVudF9kYiI6IkUxX0RFVi0xIFRlc3QiLCJqdGkiOiI4RDBCNzA1NTczQTFBOThGREJBREZGMENEN0Y3RUVCNCIsImlhdCI6MTc2NjUwNTE0Nywic2NvcGUiOlsicmVhZCIsInVwZGF0ZSJdfQ.MRvH-EtWu-PWDhjDDn73OVQwH29DZ_RRu6XheFsoRxImyjLQRIU7-S1GuyTnnqPyXEEkGkKTu_s3IEwxGORgY48jLH3l1juJDt8_JvcyJlIdhVZSZNC1Bpft_K1NJswJ6QmJ6bWgev7cqHaxM3p7AEEPjkSmnAjdBCz7ItMV93Yio5kCRBmP9DQUoxtL0webG7zV_f5uOkt8xhbUVHpdU9FQY-XLf_heLJv_81vvpf39kPxD4WTZRly8X_mNdlqi0DxiFXK3TFOnbdoLKxeljke8jV0t-agaHWcZ4B-SMQ77falwtFaxrEzXDY4g-iUg2kl_tABOUxzoqyFkGZm3DA",
"expires_in": 3600,
"token_type": "Bearer",
"scope": "read update"
}
5. Call instance APIs using the service access token
The following example uses the Domain API.
GET /api/domain/odata/Crm_Customers?$top=10 HTTP/1.1
Host: mycompany.my.erp.net
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjVCMjc5MjBFNjUzREQ3QUM2N0QyRjY0QjMyQTE3OTkyIiwidHlwIjoiYXQrand0In0.eyJuYmYiOjE3NjY1MDUxNDcsImV4cCI6MTc2NjUwODc0NywiaXNzIjoiaHR0cHM6Ly9lMS1kZXYubG9jYWwvaWQiLCJhdWQiOlsiRG9tYWluQVBJIiwiVGFibGVBUEkiLCJPTEFQIiwiQXBwU2VydmVyIiwiaHR0cHM6Ly9lMS1kZXYubG9jYWwvaWQvcmVzb3VyY2VzIl0sImNsaWVudF9pZCI6IlBLIiwiY2xpZW50X3N5c3RlbV91c2VyIjoiYWRtaW4iLCJjbGllbnRfc3lzdGVtX3VzZXJfdHlwZSI6IkludGVybmFsVXNlciIsImNsaWVudF9kYiI6IkUxX0RFVi0xIFRlc3QiLCJqdGkiOiI4RDBCNzA1NTczQTFBOThGREJBREZGMENEN0Y3RUVCNCIsImlhdCI6MTc2NjUwNTE0Nywic2NvcGUiOlsicmVhZCIsInVwZGF0ZSJdfQ.MRvH-EtWu-PWDhjDDn73OVQwH29DZ_RRu6XheFsoRxImyjLQRIU7-S1GuyTnnqPyXEEkGkKTu_s3IEwxGORgY48jLH3l1juJDt8_JvcyJlIdhVZSZNC1Bpft_K1NJswJ6QmJ6bWgev7cqHaxM3p7AEEPjkSmnAjdBCz7ItMV93Yio5kCRBmP9DQUoxtL0webG7zV_f5uOkt8xhbUVHpdU9FQY-XLf_heLJv_81vvpf39kPxD4WTZRly8X_mNdlqi0DxiFXK3TFOnbdoLKxeljke8jV0t-agaHWcZ4B-SMQ77falwtFaxrEzXDY4g-iUg2kl_tABOUxzoqyFkGZm3DA
Security and implementation considerations
- The client secret must never be exposed to frontend code.
- Use Authorization Code flow tokens for identity and session context only.
- Use Client Credentials flow tokens for all API calls.
- Restrict scopes to the minimum required by the portal backend.
- The
stateandnoncevalues must be generated per request and validated. - Redirect URIs must exactly match the configured value.
- The Instance ID does not allow embedding. If authentication is initiated from within an iframe, the sign-in flow must be opened in a popup window or top-level navigation.