How to secure webhook endpoint
Securing the delivery of messages from end to end is crucial for ensuring the confidentiality, integrity, and trustworthiness of sensitive information transmitted between systems. Your ability and willingness to trust information received from a remote system relies on the sender providing their identity. Call Automation has two ways of communicating events that can be secured; the shared IncomingCall event sent by Azure Event Grid, and all other mid-call events sent by the Call Automation platform via webhook.
Incoming Call Event
Azure Communication Services relies on Azure Event Grid subscriptions to deliver the IncomingCall event. You can refer to the Azure Event Grid team for their documentation about how to secure a webhook subscription.
Call Automation webhook events
Call Automation events are sent to the webhook callback URI specified when you answer a call, or place a new outbound call. Your callback URI must be a public endpoint with a valid HTTPS certificate, DNS name, and IP address with the correct firewall ports open to enable Call Automation to reach it. This anonymous public webserver could create a security risk if you don't take the necessary steps to secure it from unauthorized access.
A common way you can improve this security is by implementing an API KEY mechanism. Your webserver can generate the key at runtime and provide it in the callback URI as a query parameter when you answer or create a call. Your webserver can verify the key in the webhook callback from Call Automation before allowing access. Some customers require more security measures. In these cases, a perimeter network device may verify the inbound webhook, separate from the webserver or application itself. The API key mechanism alone may not be sufficient.
Improving Call Automation webhook callback security
Each mid-call webhook callback sent by Call Automation uses a signed JSON Web Token (JWT) in the Authentication header of the inbound HTTPS request. You can use standard Open ID Connect (OIDC) JWT validation techniques to ensure the integrity of the token as follows. The lifetime of the JWT is five (5) minutes and a new token is created for every event sent to the callback URI.
- Obtain the Open ID configuration URL: https://acscallautomation.communication.azure.com/calling/.well-known/acsopenidconfiguration
- Install the Microsoft.AspNetCore.Authentication.JwtBearer NuGet package.
- Configure your application to validate the JWT using the NuGet package and the configuration of your Azure Communication Services resource. You need the
audience
values as it is present in the JWT payload. - Validate the issuer, audience and the JWT token.
- The audience is your Azure Communication Services resource ID you used to set up your Call Automation client. Refer here about how to get it.
- The JSON Web Key Set (JWKS) endpoint in the OpenId configuration contains the keys used to validate the JWT token. When the signature is valid and the token hasn't expired (within 5 minutes of generation), the client can use the token for authorization.
This sample code demonstrates how to use Microsoft.IdentityModel.Protocols.OpenIdConnect
to validate webhook payload
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Add Azure Communication Services CallAutomation OpenID configuration
var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
builder.Configuration["OpenIdConfigUrl"],
new OpenIdConnectConfigurationRetriever());
var configuration = configurationManager.GetConfigurationAsync().Result;
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Configuration = configuration;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = builder.Configuration["AllowedAudience"]
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.MapPost("/api/callback", (CloudEvent[] events) =>
{
// Your implemenation on the callback event
return Results.Ok();
})
.RequireAuthorization()
.WithOpenApi();
app.UseAuthentication();
app.UseAuthorization();
app.Run();
Improving Call Automation webhook callback security
Each mid-call webhook callback sent by Call Automation uses a signed JSON Web Token (JWT) in the Authentication header of the inbound HTTPS request. You can use standard Open ID Connect (OIDC) JWT validation techniques to ensure the integrity of the token as follows. The lifetime of the JWT is five (5) minutes and a new token is created for every event sent to the callback URI.
- Obtain the Open ID configuration URL: https://acscallautomation.communication.azure.com/calling/.well-known/acsopenidconfiguration
- The following sample uses Spring framework, created using spring initializr with Maven as project build tool.
- Add the following dependencies in your
pom.xml
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
- Configure your application to validate the JWT and the configuration of your Azure Communication Services resource. You need the
audience
values as it is present in the JWT payload. - Validate the issuer, audience and the JWT token.
- The audience is your Azure Communication Services resource ID you used to set up your Call Automation client. Refer here about how to get it.
- The JSON Web Key Set (JWKS) endpoint in the OpenId configuration contains the keys used to validate the JWT token. When the signature is valid and the token hasn't expired (within 5 minutes of generation), the client can use the token for authorization.
This sample code demonstrates how to configure OIDC client to validate webhook payload using JWT
package callautomation.example.security;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jwt.*;
@EnableWebSecurity
public class TokenValidationConfiguration {
@Value("ACS resource ID")
private String audience;
@Value("https://acscallautomation.communication.azure.com")
private String issuer;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
.mvcMatchers("/api/callbacks").permitAll()
.anyRequest()
.and()
.oauth2ResourceServer()
.jwt()
.decoder(jwtDecoder());
return http.build();
}
class AudienceValidator implements OAuth2TokenValidator<Jwt> {
private String audience;
OAuth2Error error = new OAuth2Error("invalid_token", "The required audience is missing", null);
public AudienceValidator(String audience) {
this.audience = audience;
}
@Override
public OAuth2TokenValidatorResult validate(Jwt token) {
if (token.getAudience().contains(audience)) {
return OAuth2TokenValidatorResult.success();
} else {
return OAuth2TokenValidatorResult.failure(error);
}
}
}
JwtDecoder jwtDecoder() {
OAuth2TokenValidator<Jwt> withAudience = new AudienceValidator(audience);
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuer);
OAuth2TokenValidator<Jwt> validator = new DelegatingOAuth2TokenValidator<>(withAudience, withIssuer);
NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder) JwtDecoders.fromOidcIssuerLocation(issuer);
jwtDecoder.setJwtValidator(validator);
return jwtDecoder;
}
}
Improving Call Automation webhook callback security
Each mid-call webhook callback sent by Call Automation uses a signed JSON Web Token (JWT) in the Authentication header of the inbound HTTPS request. You can use standard Open ID Connect (OIDC) JWT validation techniques to ensure the integrity of the token as follows. The lifetime of the JWT is five (5) minutes and a new token is created for every event sent to the callback URI.
- Obtain the Open ID configuration URL: https://acscallautomation.communication.azure.com/calling/.well-known/acsopenidconfiguration
- Install the following packages:
npm install express jwks-rsa jsonwebtoken
- Configure your application to validate the JWT and the configuration of your Azure Communication Services resource. You need the
audience
values as it is present in the JWT payload. - Validate the issuer, audience and the JWT token.
- The audience is your Azure Communication Services resource ID you used to set up your Call Automation client. Refer here about how to get it.
- The JSON Web Key Set (JWKS) endpoint in the OpenId configuration contains the keys used to validate the JWT token. When the signature is valid and the token hasn't expired (within 5 minutes of generation), the client can use the token for authorization.
This sample code demonstrates how to configure OIDC client to validate webhook payload using JWT
import express from "express";
import { JwksClient } from "jwks-rsa";
import { verify } from "jsonwebtoken";
const app = express();
const port = 3000;
const audience = "ACS resource ID";
const issuer = "https://acscallautomation.communication.azure.com";
app.use(express.json());
app.post("/api/callback", (req, res) => {
const token = req?.headers?.authorization?.split(" ")[1] || "";
if (!token) {
res.sendStatus(401);
return;
}
try {
verify(
token,
(header, callback) => {
const client = new JwksClient({
jwksUri: "https://acscallautomation.communication.azure.com/calling/keys",
});
client.getSigningKey(header.kid, (err, key) => {
const signingKey = key?.publicKey || key?.rsaPublicKey;
callback(err, signingKey);
});
},
{
audience,
issuer,
algorithms: ["RS256"],
});
// Your implementation on the callback event
res.sendStatus(200);
} catch (error) {
res.sendStatus(401);
}
});
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
Improving Call Automation webhook callback security
Each mid-call webhook callback sent by Call Automation uses a signed JSON Web Token (JWT) in the Authentication header of the inbound HTTPS request. You can use standard Open ID Connect (OIDC) JWT validation techniques to ensure the integrity of the token as follows. The lifetime of the JWT is five (5) minutes and a new token is created for every event sent to the callback URI.
- Obtain the Open ID configuration URL: https://acscallautomation.communication.azure.com/calling/.well-known/acsopenidconfiguration
- Install the following packages:
pip install flask pyjwt
- Configure your application to validate the JWT and the configuration of your Azure Communication Services resource. You need the
audience
values as it is present in the JWT payload. - Validate the issuer, audience and the JWT token.
- The audience is your Azure Communication Services resource ID you used to set up your Call Automation client. Refer here about how to get it.
- The JSON Web Key Set (JWKS) endpoint in the OpenId configuration contains the keys used to validate the JWT token. When the signature is valid and the token hasn't expired (within 5 minutes of generation), the client can use the token for authorization.
This sample code demonstrates how to configure OIDC client to validate webhook payload using JWT
from flask import Flask, jsonify, abort, request
import jwt
app = Flask(__name__)
@app.route("/api/callback", methods=["POST"])
def handle_callback_event():
token = request.headers.get("authorization").split()[1]
if not token:
abort(401)
try:
jwks_client = jwt.PyJWKClient(
"https://acscallautomation.communication.azure.com/calling/keys"
)
jwt.decode(
token,
jwks_client.get_signing_key_from_jwt(token).key,
algorithms=["RS256"],
issuer="https://acscallautomation.communication.azure.com",
audience="ACS resource ID",
)
# Your implementation on the callback event
return jsonify(success=True)
except jwt.InvalidTokenError:
print("Token is invalid")
abort(401)
except Exception as e:
print("uncaught exception" + e)
abort(500)
if __name__ == "__main__":
app.run()
Next steps
- Learn more about How to control and steer calls with Call Automation.