如何保護 Webhook 端點

保護端對端傳遞訊息,對於確保系統之間所傳輸敏感性資訊的機密性、完整性和可信度至關重要。 您信任從遠端系統所接收資訊的能力和意願,依賴提供其身分識別的傳送者。 通話自動化有兩種方式可以傳達可保護的事件;由 Azure 事件方格傳送的共用 IncomingCall 事件,以及由通話自動化平台透過 Webhook 傳送的所有其他通話中事件。

傳入通話事件

Azure 通訊服務依賴 Azure 事件方格訂用帳戶來傳遞 IncomingCall 事件。 您可以參閱 Azure 事件方格小組,以了解其如何保護 Webhook 訂用帳戶的文件

通話自動化 Webhook 事件

通話自動化事件會在您接聽或撥打新的通話時,傳送至指定的 Webhook 回撥 URI。 您的回撥 URI 必須是具有有效 HTTPS 憑證、DNS 名稱和 IP 位址的公用端點,且已開啟正確的防火牆連接埠,才能讓通話自動化觸達該端點。 如果您未採取必要步驟來保護其免於未經授權的存取,此匿名公用 Web 伺服器可能會造成安全性風險。

改善此安全性的常見方式是實作 API 金鑰機制。 Web 伺服器可以在執行階段產生金鑰,並在您接聽或建立通話時,以查詢參數的形式在回撥 URI 中提供金鑰。 您的 Web 伺服器可以在允許存取之前,先從通話自動化驗證 Webhook 回撥中的金鑰。 有些客戶需要更多安全性措施。 在這些情況下,周邊網路裝置可能會驗證輸入 Webhook,與 Web 伺服器或應用程式本身分開。 僅僅 API 金鑰機制可能有所不足。

改善通話自動化 Webhook 回撥安全性

通話自動化所傳送的每個通話中 Webhook 回撥都會在輸入 HTTPS 要求的驗證標頭中使用已簽署的 JSON Web 權杖 (JWT)。 您可以使用標準 Open ID Connect (OIDC) JWT 驗證技術來確保權杖的完整性,如下所示。 JWT 的存留期為五 (5) 分鐘,而且會針對傳送至回撥 URI 的每個事件建立新的權杖。

  1. 取得 Open ID 組態 URL:https://acscallautomation.communication.azure.com/calling/.well-known/acsopenidconfiguration
  2. 安裝 Microsoft.AspNetCore.Authentication.JwtBearer NuGet 套件。
  3. 設定您的應用程式,以使用 NuGet 套件和 Azure 通訊服務資源的組態來驗證 JWT。 您需要 audience 值,因為其存在於 JWT 承載中。
  4. 驗證簽發者、對象和 JWT 權杖。
    • 對象是您用來設定通話自動化用戶端的 Azure 通訊服務資源識別碼。 請參閱此處的如何取得。
    • OpenId 組態中的 JSON Web 金鑰集 (JWKS) 端點包含用來驗證 JWT 權杖的金鑰。 當簽章有效且權杖未過期 (產生的 5 分鐘內) 時,用戶端可以使用權杖進行授權。

此範例程式碼示範如何使用 Microsoft.IdentityModel.Protocols.OpenIdConnect 來驗證 Webhook 承載

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();

改善通話自動化 Webhook 回撥安全性

通話自動化所傳送的每個通話中 Webhook 回撥都會在輸入 HTTPS 要求的驗證標頭中使用已簽署的 JSON Web 權杖 (JWT)。 您可以使用標準 Open ID Connect (OIDC) JWT 驗證技術來確保權杖的完整性,如下所示。 JWT 的存留期為五 (5) 分鐘,而且會針對傳送至回撥 URI 的每個事件建立新的權杖。

  1. 取得 Open ID 組態 URL:https://acscallautomation.communication.azure.com/calling/.well-known/acsopenidconfiguration
  2. 下列範例使用 Spring 架構,使用 spring initializr 搭配 Maven 作為專案建置工具所建立。
  3. 在您的 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>
  1. 設定您的應用程式,以驗證 JWT 和 Azure 通訊服務資源的組態。 您需要 audience 值,因為其存在於 JWT 承載中。
  2. 驗證簽發者、對象和 JWT 權杖。
    • 對象是您用來設定通話自動化用戶端的 Azure 通訊服務資源識別碼。 請參閱此處的如何取得。
    • OpenId 組態中的 JSON Web 金鑰集 (JWKS) 端點包含用來驗證 JWT 權杖的金鑰。 當簽章有效且權杖未過期 (產生的 5 分鐘內) 時,用戶端可以使用權杖進行授權。

此範例程式碼示範如何設定 OIDC 用戶端以使用 JWT 來驗證 Webhook 承載

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;
    }
}

改善通話自動化 Webhook 回撥安全性

通話自動化所傳送的每個通話中 Webhook 回撥都會在輸入 HTTPS 要求的驗證標頭中使用已簽署的 JSON Web 權杖 (JWT)。 您可以使用標準 Open ID Connect (OIDC) JWT 驗證技術來確保權杖的完整性,如下所示。 JWT 的存留期為五 (5) 分鐘,而且會針對傳送至回撥 URI 的每個事件建立新的權杖。

  1. 取得 Open ID 組態 URL:https://acscallautomation.communication.azure.com/calling/.well-known/acsopenidconfiguration
  2. 安裝下列套件:
npm install express jwks-rsa jsonwebtoken
  1. 設定您的應用程式,以驗證 JWT 和 Azure 通訊服務資源的組態。 您需要 audience 值,因為其存在於 JWT 承載中。
  2. 驗證簽發者、對象和 JWT 權杖。
    • 對象是您用來設定通話自動化用戶端的 Azure 通訊服務資源識別碼。 請參閱此處的如何取得。
    • OpenId 組態中的 JSON Web 金鑰集 (JWKS) 端點包含用來驗證 JWT 權杖的金鑰。 當簽章有效且權杖未過期 (產生的 5 分鐘內) 時,用戶端可以使用權杖進行授權。

此範例程式碼示範如何設定 OIDC 用戶端以使用 JWT 來驗證 Webhook 承載

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}`);
});

改善通話自動化 Webhook 回撥安全性

通話自動化所傳送的每個通話中 Webhook 回撥都會在輸入 HTTPS 要求的驗證標頭中使用已簽署的 JSON Web 權杖 (JWT)。 您可以使用標準 Open ID Connect (OIDC) JWT 驗證技術來確保權杖的完整性,如下所示。 JWT 的存留期為五 (5) 分鐘,而且會針對傳送至回撥 URI 的每個事件建立新的權杖。

  1. 取得 Open ID 組態 URL:https://acscallautomation.communication.azure.com/calling/.well-known/acsopenidconfiguration
  2. 安裝下列套件:
pip install flask pyjwt
  1. 設定您的應用程式,以驗證 JWT 和 Azure 通訊服務資源的組態。 您需要 audience 值,因為其存在於 JWT 承載中。
  2. 驗證簽發者、對象和 JWT 權杖。
    • 對象是您用來設定通話自動化用戶端的 Azure 通訊服務資源識別碼。 請參閱此處的如何取得。
    • OpenId 組態中的 JSON Web 金鑰集 (JWKS) 端點包含用來驗證 JWT 權杖的金鑰。 當簽章有效且權杖未過期 (產生的 5 分鐘內) 時,用戶端可以使用權杖進行授權。

此範例程式碼示範如何設定 OIDC 用戶端以使用 JWT 來驗證 Webhook 承載

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()

下一步