title | description | author | ms.service | ms.topic | ms.date | ms.author | ms.devlang | ms.custom |
---|---|---|---|---|---|---|---|---|
Tutorial: Authentication with Azure Functions - Azure SignalR Service | In this tutorial, you learn how to authenticate Azure SignalR Service clients for Azure Functions binding. | Y-Sindo | azure-signalr-service | tutorial | 03/18/2024 | zityang | javascript |
In this step-by-step tutorial, you build a chat room with authentication and private messaging by using these technologies:
- Azure Functions: Back-end API for authenticating users and sending chat messages.
- Azure SignalR Service: Service for broadcasting new messages to connected chat clients.
- Azure Storage: Storage service that Azure Functions requires.
- Azure App Service: Service that provides user authentication.
Note
You can get the code mentioned in this article from GitHub.
Note
You can get the code mentioned in this article from GitHub.
[!INCLUDE Connection string security]
- An Azure account with an active subscription. If you don't have one, you can create one for free.
- Node.js (version 20.x).
- Azure Functions Core Tools (version 4).
Your application will access an Azure SignalR Service instance. Use the following steps to create an Azure SignalR Service instance by using the Azure portal:
In the Azure portal, select the Create a resource (+) button.
Search for SignalR Service and select it.
Select Create.
Enter the following information.
Name Value Resource group Create a new resource group with a unique name. Resource name Enter a unique name for the Azure SignalR Service instance. Region Select a region close to you. Pricing Tier Select Free. Service mode Select Serverless. Select Review + Create.
Select Create.
From the home page in the Azure portal, select Create a resource (+).
Search for Function App and select it.
Select Create.
Enter the following information.
Name Value Resource group Use the same resource group with your Azure SignalR Service instance. Function App name Enter a unique name for the function app. Runtime stack Select Node.js. Region Select a region close to you. By default, a new Azure storage account is created in the same resource group together with your function app. If you want to use another storage account in the function app, switch to the Hosting tab to choose an account.
Select Review + Create, and then select Create.
From a command line, create a root folder for your project and change to the folder.
Run the following command in your terminal to create a new JavaScript Functions project:
func init --worker-runtime node --language javascript --name my-app --model V4
func init --worker-runtime node --language javascript --name my-app --model V3
By default, the generated project includes a host.json file that contains the extension bundles that include the SignalR extension. For more information about extension bundles, see Register Azure Functions binding extensions.
When you run and debug the Azure Functions runtime locally, the function app reads application settings from local.settings.json. Update this file with the connection strings of the Azure SignalR Service instance and the storage account that you created earlier.
[!INCLUDE Connection string security comment]
Replace the content of local.settings.json with the following code:
{ "IsEncrypted": false, "Values": { "FUNCTIONS_WORKER_RUNTIME": "node", "AzureWebJobsStorage": "<your-storage-account-connection-string>", "AzureSignalRConnectionString": "<your-Azure-SignalR-connection-string>" } }
In the preceding code:
Enter the Azure SignalR Service connection string into the
AzureSignalRConnectionString
setting.To get the string, go to your Azure SignalR Service instance in the Azure portal. In the Settings section, locate the Keys setting. Select the Copy button to the right of the connection string to copy it to your clipboard. You can use either the primary or secondary connection string.
Enter the storage account connection string into the
AzureWebJobsStorage
setting.To get the string, go to your storage account in the Azure portal. In the Security + networking section, locate the Access keys setting. Select the Copy button to the right of the connection string to copy it to your clipboard. You can use either the primary or secondary connection string.
When the chat app first opens in the browser, it requires valid connection credentials to connect to Azure SignalR Service. Create an HTTP trigger function named negotiate
in your function app to return this connection information.
Note
This function must be named negotiate
because the SignalR client requires an endpoint that ends in /negotiate
.
From the root project folder, create the
negotiate
function from a built-in template by using the following command:func new --template "HTTP trigger" --name negotiate
Open src/functions/negotiate.js, update the content as follows:
const{ app, input }=require('@azure/functions');constinputSignalR=input.generic({type: 'signalRConnectionInfo',name: 'connectionInfo',hubName: 'default',connectionStringSetting: 'AzureSignalRConnectionString',});app.post('negotiate',{authLevel: 'anonymous',handler: (request,context)=>{return{body: JSON.stringify(context.extraInputs.get(inputSignalR))}},route: 'negotiate',extraInputs: [inputSignalR],});
The function contains an HTTP trigger binding to receive requests from SignalR clients. The function also contains a SignalR input binding to generate valid credentials for a client to connect to an Azure SignalR Service hub named
default
.This function takes the SignalR connection information from the input binding and returns it to the client in the HTTP response body..
There's no
userId
property in thesignalRConnectionInfo
binding for local development. You'll add it later to set the username of a SignalR connection when you deploy the function app to Azure.
From the root project folder, create the
negotiate
function from a built-in template by using the following command:func new --template "SignalR negotiate HTTP trigger" --name negotiate
Open negotiate/function.json to view the function binding configuration.
The function contains an HTTP trigger binding to receive requests from SignalR clients. The function also contains a SignalR input binding to generate valid credentials for a client to connect to an Azure SignalR Service hub named
default
.{ "disabled": false, "bindings": [ { "authLevel": "anonymous", "type": "httpTrigger", "direction": "in", "methods": ["post"], "name": "req", "route": "negotiate" }, { "type": "http", "direction": "out", "name": "res" }, { "type": "signalRConnectionInfo", "name": "connectionInfo", "hubName": "default", "connectionStringSetting": "AzureSignalRConnectionString", "direction": "in" } ] }
There's no
userId
property in thesignalRConnectionInfo
binding for local development. You'll add it later to set the username of a SignalR connection when you deploy the function app to Azure.Close the negotiate/function.json file.
Open negotiate/index.js to view the body of the function:
module.exports=asyncfunction(context,req,connectionInfo){context.res.body=connectionInfo;};
This function takes the SignalR connection information from the input binding and returns it to the client in the HTTP response body. The SignalR client uses this information to connect to the Azure SignalR Service instance.
The web app also requires an HTTP API to send chat messages. Create an HTTP trigger function that sends messages to all connected clients that use Azure SignalR Service:
From the root project folder, create an HTTP trigger function named
sendMessage
from the template by using the following command:func new --name sendMessage --template "Http trigger"
Open the src/functions/sendMessage.js file, update the content as follows:
const{ app, output }=require('@azure/functions');constsignalR=output.generic({type: 'signalR',name: 'signalR',hubName: 'default',connectionStringSetting: 'AzureSignalRConnectionString',});app.http('messages',{methods: ['POST'],authLevel: 'anonymous',extraOutputs: [signalR],handler: async(request,context)=>{constmessage=awaitrequest.json();message.sender=request.headers&&request.headers.get('x-ms-client-principal-name')||'';letrecipientUserId='';if(message.recipient){recipientUserId=message.recipient;message.isPrivate=true;}context.extraOutputs.set(signalR,{'userId': recipientUserId,'target': 'newMessage','arguments': [message]});}});
The function contains an HTTP trigger and a SignalR output binding. It takes the body from the HTTP request and sends it to clients connected to Azure SignalR Service. It invokes a function named
newMessage
on each client.The function can read the sender's identity and can accept a
recipient
value in the message body to allow you to send a message privately to a single user. You'll use these functionalities later in the tutorial.Save the file.
From the root project folder, create an HTTP trigger function named
sendMessage
from the template by using the following command:func new --name sendMessage --template "Http trigger"
To configure bindings for the function, replace the content of sendMessage/function.json with the following code:
{ "disabled": false, "bindings": [ { "authLevel": "anonymous", "type": "httpTrigger", "direction": "in", "name": "req", "route": "messages", "methods": ["post"] }, { "type": "http", "direction": "out", "name": "res" }, { "type": "signalR", "name": "$return", "hubName": "default", "direction": "out" } ] }
The preceding code makes two changes to the original file:
- It changes the route to
messages
and restricts the HTTP trigger to thePOST
HTTP method. - It adds an Azure SignalR Service output binding that sends a message returned by the function to all clients connected to an Azure SignalR Service hub named
default
.
- It changes the route to
Replace the content of sendMessage/index.js with the following code:
module.exports=asyncfunction(context,req){constmessage=req.body;message.sender=(req.headers&&req.headers["x-ms-client-principal-name"])||"";letrecipientUserId="";if(message.recipient){recipientUserId=message.recipient;message.isPrivate=true;}return{userId: recipientUserId,target: "newMessage",arguments: [message],};};
This function takes the body from the HTTP request and sends it to clients connected to Azure SignalR Service. It invokes a function named
newMessage
on each client.The function can read the sender's identity and can accept a
recipient
value in the message body to allow you to send a message privately to a single user. You'll use these functionalities later in the tutorial.Save the file.
The chat application's UI is a simple single-page application (SPA) created with the Vue JavaScript framework by using the ASP.NET Core SignalR JavaScript client. For simplicity, the function app hosts the webpage. In a production environment, you can use Static Web Apps to host the webpage.
Create a file named index.html in the root directory of your function project.
Copy and paste the content of index.html to your file. Save the file.
From the root project folder, create an HTTP trigger function named
index
from the template by using this command:func new --name index --template "Http trigger"
Modify the content of src/functions/index.js to the following code:
const{ app }=require('@azure/functions');const{ readFile }=require('fs/promises');app.http('index',{methods: ['GET'],authLevel: 'anonymous',handler: async(context)=>{constcontent=awaitreadFile('index.html','utf8',(err,data)=>{if(err){context.err(err)return}});return{status: 200,headers: {'Content-Type': 'text/html'},body: content,};}});
The function reads the static webpage and returns it to the user.
Test your app locally. Start the function app by using this command:
func start
Open
http://localhost:7071/api/index
in your web browser. A chat webpage should appear.:::image type="content" source="./media/signalr-tutorial-authenticate-azure-functions/local-chat-client-ui.png" alt-text="Screenshot of a web user interface for a local chat client.":::
Enter a message in the chat box.
After you select the Enter key, the message appears on the webpage. Because the username of the SignalR client isn't set, you're sending all messages anonymously.
Create a folder named content in the root directory of your function project.
In the content folder, create a file named index.html.
Copy and paste the content of index.html to your file. Save the file.
From the root project folder, create an HTTP trigger function named
index
from the template by using this command:func new --name index --template "Http trigger"
Modify the content of
index/index.js
to the following code:constfs=require("fs");module.exports=asyncfunction(context,req){constfileContent=fs.readFileSync("content/index.html","utf8");context.res={// status: 200, /* Defaults to 200 */body: fileContent,headers: {"Content-Type": "text/html",},};};
The function reads the static webpage and returns it to the user.
Open index/function.json, and change the
authLevel
value of the bindings toanonymous
. Now the whole file looks like this example:{ "bindings": [ { "authLevel": "anonymous", "type": "httpTrigger", "direction": "in", "name": "req", "methods": ["get", "post"] }, { "type": "http", "direction": "out", "name": "res" } ] }
Test your app locally. Start the function app by using this command:
func start
Open
http://localhost:7071/api/index
in your web browser. A chat webpage should appear.:::image type="content" source="./media/signalr-tutorial-authenticate-azure-functions/local-chat-client-ui.png" alt-text="Screenshot of a web user interface for a local chat client.":::
Enter a message in the chat box.
After you select the Enter key, the message appears on the webpage. Because the username of the SignalR client isn't set, you're sending all messages anonymously.
You've been running the function app and chat app locally. Now, deploy them to Azure and enable authentication and private messaging.
So far, the chat app works anonymously. In Azure, you'll use App Service authentication to authenticate the user. The user ID or username of the authenticated user is passed to the SignalRConnectionInfo
binding to generate connection information authenticated as the user.
Open src/functions/negotiate.js file.
Insert a
userId
property into theinputSignalR
binding with the value{headers.x-ms-client-principal-name}
. This value is a binding expression that sets the username of the SignalR client to the name of the authenticated user. The binding should now look like this example:constinputSignalR=input.generic({type: 'signalRConnectionInfo',name: 'connectionInfo',hubName: 'default',connectionStringSetting: 'AzureSignalRConnectionString',userId: '{headers.x-ms-client-principal-name}'});
Save the file.
Open negotiate/function.json.
Insert a
userId
property into theSignalRConnectionInfo
binding with the value{headers.x-ms-client-principal-name}
. This value is a binding expression that sets the username of the SignalR client to the name of the authenticated user. The binding should now look like this example:{ "type": "signalRConnectionInfo", "name": "connectionInfo", "userId": "{headers.x-ms-client-principal-name}", "hubName": "default", "direction": "in" }
Save the file.
Deploy the function app to Azure by using the following command:
func azure functionapp publish <your-function-app-name> --publish-local-settings
The --publish-local-settings
option publishes your local settings from the local.settings.json file to Azure, so you don't need to configure them in Azure again.
Azure Functions supports authentication with Microsoft Entra ID, Facebook, X, Microsoft account, and Google. You'll use Microsoft as the identity provider for this tutorial.
In the Azure portal, go to the resource page of your function app.
Select Settings > Authentication.
Select Add identity provider.
:::image type="content" source="./media/signalr-tutorial-authenticate-azure-functions/function-app-authentication.png" alt-text="Screenshot of the function app Authentication page and the button for adding an identity provider.":::
In the Identity provider list, select Microsoft. Then select Add.
:::image type="content" source="media/signalr-tutorial-authenticate-azure-functions/function-app-select-identity-provider.png" alt-text="Screenshot of the page for adding an identity provider.":::
The completed settings create an app registration that associates your identity provider with your function app.
For more information about the supported identity providers, see the following articles:
- Open
https://<YOUR-FUNCTION-APP-NAME>.azurewebsites.net/api/index
. - Select Login to authenticate with your chosen authentication provider.
- Send public messages by entering them in the main chat box.
- Send private messages by selecting a username in the chat history. Only the selected recipient receives these messages.
:::image type="content" source="./media/signalr-tutorial-authenticate-azure-functions/online-chat-client-ui.png" alt-text="Screenshot of an authenticated online client chat app.":::
Congratulations! You deployed a real-time, serverless chat app.
To clean up the resources that you created in this tutorial, delete the resource group by using the Azure portal.
Caution
Deleting the resource group deletes all the resources that it contains. If the resource group contains resources outside the scope of this tutorial, they're also deleted.
In this tutorial, you learned how to use Azure Functions with Azure SignalR Service. Read more about building real-time serverless applications with Azure SignalR Service bindings for Azure Functions:
[!div class="nextstepaction"] Real-time apps with Azure SignalR Service and Azure Functions