{"id":15419897,"url":"https://github.com/navzam/user-direct-line-token-sample","last_synced_at":"2026-02-04T01:09:49.082Z","repository":{"id":40713934,"uuid":"275189139","full_name":"navzam/user-direct-line-token-sample","owner":"navzam","description":"Sample for getting consistent and trustworthy user IDs in WebChat bots","archived":false,"fork":false,"pushed_at":"2022-12-22T17:26:42.000Z","size":325,"stargazers_count":0,"open_issues_count":15,"forks_count":2,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-02-02T05:14:03.975Z","etag":null,"topics":["aad","bot-framework","webchat"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/navzam.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-06-26T15:32:38.000Z","updated_at":"2020-07-23T18:15:46.000Z","dependencies_parsed_at":"2023-01-30T15:01:35.454Z","dependency_job_id":null,"html_url":"https://github.com/navzam/user-direct-line-token-sample","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/navzam%2Fuser-direct-line-token-sample","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/navzam%2Fuser-direct-line-token-sample/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/navzam%2Fuser-direct-line-token-sample/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/navzam%2Fuser-direct-line-token-sample/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/navzam","download_url":"https://codeload.github.com/navzam/user-direct-line-token-sample/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245966929,"owners_count":20701759,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["aad","bot-framework","webchat"],"created_at":"2024-10-01T17:27:01.533Z","updated_at":"2026-02-04T01:09:49.012Z","avatar_url":"https://github.com/navzam.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# User-specific Direct Line token sample\n\nThis sample demonstrates how to implement Web Chat in a way that (a) does not expose your Direct Line secret to the browser, and (b) ensures your bot will receive a consistent and trustworthy user ID across sessions. Specifically, it shows how to retrieve a user-specific Direct Line token for a user who has been verified by an identity provider.\n\n## Motivation\n\nIn the [Direct Line token sample](https://github.com/navzam/direct-line-token-sample), in order to hide the Web Chat secret and avoid user impersonation, we bound a random user ID to the Direct Line token. The downside of that approach is that users will have a different ID every time they talk to the bot. We could improve on this by storing a user ID in client-side storage (cookie, `localStorage`, etc.) and sending it to the token API, but there would still be two issues:\n- The user ID would be tied to the browser and wouldn't be consistent across browsers, devices, etc.\n- A malicious user could modify their user ID to attempt to impersonate a different user, so the bot wouldn't be able to trust the user ID.\n\nA better approach is to leverage a user's existing identity from a true identity provider. The user must first sign in to the site before talking to the bot. Then, if the user signs in using the same identity on a different browser or device, the user ID will be the same. This also prevents user impersonation because we can verify the user's identity with the identity provider instead of blindly trusting the user ID.\n\n## Architecture\n\nThis sample contains three components:\n- **The backend API** performs the Direct Line token acquisition. It verifies the user's identity and then acquires a Direct Line token that is bound to that identity.\n- **The UI** is static HTML/JS that could be hosted using any web server. It requires the user to sign in, then makes a request to the backend API with proof of the user's identity. It uses the resulting Direct Line token to render Web Chat.\n- **The bot** is a bare-bones bot that responds to every activity by sending the user's ID.  \n\nThe interesting component is the backend API, which goes through the following steps:\n\n1. In the body of the POST request to the API, receive an OpenID Connect (OIDC) JWT (called an ID token) that identifies the user.\n1. Validate the ID token against the chosen identity provider (AAD in this sample).\n1. Build a user ID using claims from the validated token. We chose to use the `sub` (subject) claim because it's a standard OIDC claim that uniquely identifies the user, and it doesn't require any additional scopes.\n    - In AAD, the `sub` claim is only consistent per user *per application*. This means our user ID wouldn't be sufficient for looking up the user in other systems (such as Microsoft Graph). If we needed a user ID that identifies the user across applications, we could use the `oid` (object ID) claim, but it requires the `profile` scope. See [AAD ID token claims](https://docs.microsoft.com/en-us/azure/active-directory/develop/id-tokens#claims-in-an-id_token) for more details.\n    - Since the user identity is verified, it is okay for this user ID to be a \"guessable\" value.\n1. Retrieve a user-specific Direct Line token using the Direct Line API.\n1. Respond with the user-specific Direct Line token.\n\nDepending on the scenario, the backend API could be called from a client (such as a single-page application) or a server (such as a more traditional web app, where tokens are handled server-side). The only requirement is that the caller can provide an ID token from the expected identity provider. If you are embedding the bot in an authenticated site, then you may already have an ID token that you can use.\n\nAfter receiving the Direct Line token, the caller can then use it to render Web Chat, and the bot will receive a consistent user ID that it can rely on.\n\n## Code highlights\n\n### Receiving the ID token\n\nThe API expects the ID token to be passed in the request body:\n\n\u003cdetails\u003e\u003csummary\u003eJavaScript\u003c/summary\u003e\n\n```js\n// server.js\n\nconst idToken = req.body['id_token'];\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003eC#\u003c/summary\u003e\n\n```csharp\n// DirectLineTokenController.cs\n\npublic class TokenRequest\n{\n    [JsonPropertyName(\"id_token\")]\n    public string idToken { get; set; }\n}\n...\npublic async Task\u003cIActionResult\u003e Post([FromBody] TokenRequest request)\n{\n    ...\n}\n```\n\n\u003c/details\u003e\n\nTokens are typically sent as bearer tokens in the `Authorization` header. However, we aren't using the user's ID token to protect the API. Rather, it is a parameter of the request itself. Although the API isn't protected in this sample, you could protect the API using a different token (such as an OAuth access token) which *would* go in the `Authorization` header.\n\n### Validating the ID token\n\n\u003cdetails\u003e\u003csummary\u003eJavaScript\u003c/summary\u003e\n\nWe use two libraries to achieve token validation: [jwks-rsa](https://www.npmjs.com/package/jwks-rsa) and [jsonwebtoken](https://www.npmjs.com/package/jsonwebtoken). We first use `jwks-rsa` to retrieve the AAD public keys used to sign the token:\n\n```js\n// validateToken.js\n\nconst decodedToken = jwt.decode(token, { complete: true });\n...\nconst tokenHeader = decodedToken['header'];\n...\nconst keyResult = await getSigningKeyAsync(tokenHeader.kid);\nconst pubKey = keyResult.getPublicKey();\n```\n\nand then use `jsonwebtoken` to validate the token:\n\n```js\n// validateToken.js\n\nreturn jwt.verify(token, pubKey, validationOptions);\n```\n\n`validationOptions` define certain parameters of the validation, such as the expected audience.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003eC#\u003c/summary\u003e\n\nWe use two packages to achieve token validation: [Microsoft.IdentityModel.Protocols.OpenIdConnect](https://www.nuget.org/packages/Microsoft.IdentityModel.Protocols.OpenIdConnect/) and [System.IdentityModel.Tokens.Jwt](https://www.nuget.org/packages/System.IdentityModel.Tokens.Jwt/). We first retrieve the AAD public keys used to sign the token:\n\n```csharp\n// DirectLineTokenController.cs\n\nvar configurationManager = new ConfigurationManager\u003cOpenIdConnectConfiguration\u003e(\n    \"https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration\",\n    new OpenIdConnectConfigurationRetriever(),\n    new HttpDocumentRetriever());\n...\nvar discoveryDocument = await configurationManager.GetConfigurationAsync(ct);\nvar signingKeys = discoveryDocument.SigningKeys;\n```\n\nand then validate the token:\n\n```csharp\n// DirectLineTokenController.cs\n\nvar principal = new JwtSecurityTokenHandler()\n    .ValidateToken(token, validationParameters, out var rawValidatedToken);\n```\n\n`validationParameters` define certain parameters of the validation, such as the expected audience.\n\n\u003c/details\u003e\n\n### Constructing the user ID\n\nIn this sample, we directly use the `sub` claim of the token as the user ID:\n\n\u003cdetails\u003e\u003csummary\u003eJavaScript\u003c/summary\u003e\n\n```js\n// server.js\n\nfunction getUserIdFromTokenClaims(tokenClaims) {\n    const sub = tokenClaims['sub'];\n\n    return (typeof sub === 'string' \u0026\u0026 sub.length \u003e 0) ? `dl_${sub}` : null;\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003eC#\u003c/summary\u003e\n\n```csharp\n// DirectLineTokenController.cs\n\nprivate static string GetUserIdFromTokenClaims(JwtSecurityToken token)\n{\n    ...\n    var subject = token.Subject;\n    return String.IsNullOrEmpty(subject) ? null : $\"dl_{subject}\";\n}\n```\n\n\u003c/details\u003e\n\nYou could customize this to use different claims depending on your needs. See [Architecture](#Architecture) for an explanation of why we chose the `sub` claim.\n\n### Retrieving a user-specific Direct Line token\n\nThe API calls the Direct Line API to retrieve a Direct Line token. Notice that we pass the user ID in the body of the request:\n\n\u003cdetails\u003e\u003csummary\u003eJavaScript\u003c/summary\u003e\n\n```js\n// fetchDirectLineToken.js\n\nconst response = await fetch('https://directline.botframework.com/v3/directline/tokens/generate', {\n    headers: {\n        'Content-Type': 'application/json',\n        Authorization: `Bearer ${secret}`,\n    },\n    method: 'post',\n    body: JSON.stringify({ user: { id: userId } })\n});\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003eC#\u003c/summary\u003e\n\n```csharp\n// DirectLineTokenService.cs\n\nhttpClient.BaseAddress = new Uri(\"https://directline.botframework.com/\");\n...\nvar fetchTokenRequestBody = new { user = new { id = userId } };\n\nvar fetchTokenRequest = new HttpRequestMessage(HttpMethod.Post, \"v3/directline/tokens/generate\")\n{\n    Headers =\n    {\n        { \"Authorization\", $\"Bearer {directLineSecret}\" },\n    },\n    Content = new StringContent(JsonSerializer.Serialize(fetchTokenRequestBody), Encoding.UTF8, MediaTypeNames.Application.Json),\n};\n\nvar fetchTokenResponse = await _httpClient.SendAsync(fetchTokenRequest, cancellationToken);\n```\n\n\u003c/details\u003e\n\nThe resulting Direct Line token will be bound to the passed user ID.\n\n### Calling the API and rendering Web Chat\n\nAfter the user signs in, the UI calls the API with the user's ID token and uses the resulting Direct Line token to render Web Chat:\n\n```js\n// index.html\n\nasync function getDirectLineToken(idToken) {\n    const res = await fetch('http://localhost:3000/api/direct-line-token', {\n        method: 'POST',\n        headers: {\n            'Content-Type': 'application/json',\n        },\n        body: JSON.stringify({ id_token: idToken }),\n    });\n    ...\n    return await res.json();\n}\n...\nconst directLineTokenResponse = await getDirectLineToken(idToken);\n...\nWebChat.renderWebChat(\n    {\n        directLine: WebChat.createDirectLine({ token: directLineTokenResponse.token }),\n    },\n    document.getElementById('webchat')\n);\n```\n\nNote that we do *not* specify a user ID when initiating Web Chat. Direct Line will handle sending the user ID to the bot based on the token.\n\n## Running the sample locally\n\n### Prerequisites\n- A registered Bot Framework bot (see [documentation on registering a bot with Azure Bot Service](https://docs.microsoft.com/en-us/azure/bot-service/bot-service-quickstart-registration?view=azure-bot-service-3.0))\n\n### Register an AAD application\nSince the user will be signing in to the web app using AAD, we must register an AAD application. See the [docs for registering an AAD application](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app) or follow these steps:\n1. Sign in to the [Azure portal](https://portal.azure.com/) and find the **Azure Active Directory** section.\n1. In **App registrations**, click **New registration** and fill in the following details:\n    - **Name**: A meaningful display name, like \"Direct Line Token Sample\"\n    - **Supported account types**: \tAccounts in any organizational directory (Any Azure AD directory - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)\n    - **Redirect URI**: Select **Web** and enter `http://localhost:5500`\n1. Click **Register**.\n1. In the **Overview** blade, copy the **Application (client) ID**. We will use this later.\n1. In the **Authentication** blade, under **Implicit grant**, check the box for **ID tokens** and click **Save**.\n\n\n### Run the bot\n1. Navigate to the `bot` directory.\n1. Fill in the environment variables in the `.env` file, according to the following table:\n    | Variable | Description | Example value |\n    | -------- | ----------- | ------------- |\n    | `PORT` | The port on which the bot server will run. | 3978 |\n    | `MICROSOFT_APP_ID` | The app ID of the registered Bot Framework bot. Can be found in the Azure Bot Channels Registration resource. | |\n    | `MICROSOFT_APP_SECRET` | The app secret of the registered Bot Framework Bot. Issued during registration. | |\n1. Run `npm install` to install the required dependencies.\n1. Run `npm start` to start the bot.\n1. Run `ngrok` to expose your bot to a public URL. For example:\n    ```bash\n    ngrok http -host-header=rewrite 3978\n    ```\n1. Update the messaging endpoint in your Bot Channels Registration to the ngrok URL. For example: `https://abcdef.ngrok.io/api/messages`\n\n### Run the API\n\nThe sample API is available in multiple languages. Choose one and expand the corresponding section for specific steps.\n\n\u003cdetails\u003e\u003csummary\u003eJavaScript API\u003c/summary\u003e\n\n1. Navigate to the `api/javascript` directory.\n1. Fill in the environment variables in the `.env` file. See the table below for descriptions.\n1. Run `npm install` to install the required dependencies.\n1. Run `npm start` to start the server.\n\n| Variable | Description | Example value |\n| -------- | ----------- | ------------- |\n| `PORT` | The port on which the API server will run. | 3000 |\n| `DIRECT_LINE_SECRET` | The Direct Line secret issued by Bot Framework. Can be found in the Azure Bot Channels Registration resource after enabling the Direct Line channel. |  |\n| `VALID_TOKEN_AUDIENCE` | The expected audience of the ID token. When using AAD, this should be the client ID of the app registration created above. | 34d690a0-a2fb-4163-9dde-404105d88c30 |\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003eC# API\u003c/summary\u003e\n\n1. Add the required secrets to the .NET Core secret manager. See the table below for descriptions.\n    ```bash\n    cd ./api/csharp\n    dotnet user-secrets set \"DirectLine:DirectLineSecret\" \"YOUR-DIRECT-LINE-SECRET-HERE\"\n    ```\n1. Fill in the environment variables in the `appsettings.json` file. See the table below for descriptions.\n1. (optional) Change the port specified in `./Properties/launchSettings.json`.\n1. Run `dotnet run` to start the server. (Alternatively, open and run the project in Visual Studio.)\n\n| Variable | Description | Example value |\n| -------- | ----------- | ------------- |\n| `DirectLine:DirectLineSecret` | The Direct Line secret issued by Bot Framework. Can be found in the Azure Bot Channels Registration resource after enabling the Direct Line channel. |  |\n| `TokenValidationSettings:ValidAudience` | The expected audience of the ID token. When using AAD, this should be the client ID of the AAD app created above. | 34d690a0-a2fb-4163-9dde-404105d88c30 |\n\n\u003c/details\u003e\n\n### Run the UI\n1. Navigate to the `ui` directory.\n1. Open `index.html` in an editor, find the empty variables at the top of the `script` tag, and fill in the values according to the following table:\n\n    | Variable | Description | Example value |\n    | -------- | ----------- | ------------- |\n    | `AAD_APP_ID` | The client ID of the AAD app created above. | 34d690a0-a2fb-4163-9dde-404105d88c30 |\n    | `AAD_REDIRECT_URI` | The redirect URI registered in the AAD app created above. | http://localhost:5500 |\n\n1. Serve `index.html` on `localhost:5500` using a web server.\n    - A quick way to get started is using the [http-server](https://www.npmjs.com/package/http-server) npm package. You can use `npx` to run it without installation:\n        ```bash\n        npx http-server ./ -p 5500\n        ```\n    - Another option is a local development server such as the [Live Server Visual Studio Code extension](https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer).\n1. Open `http://localhost:5500` in a browser and sign in.\n\n## Notes\n- Although this sample uses AAD, you can achieve the same result using a different identity provider.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnavzam%2Fuser-direct-line-token-sample","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnavzam%2Fuser-direct-line-token-sample","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnavzam%2Fuser-direct-line-token-sample/lists"}