Authorizing a WebRTC Stream#

To secure the initial handshake of a WebRTC stream, you can validate a user is authorized to access a specific stream. This is achieved by passing a header with the signaling traffic of the WebRTC connection and verifying it on the receiving end.

The standard Sec-WebSocket-Protocol header, which already includes the stream’s session-id, can be extended to carry an authorization token, such as a JWT.

On the receiving end, a proxy, like Envoy Proxy, can be used to front the Kit streaming application and handle token validation.

This guide demonstrates how to pass a token in the header and validate it on the server side. The generation and validation of the token are customizable, allowing developers full control over the process.

Prerequisites#

  • All API and WebRTC traffic must be TLS encrypted

  • An IDP, or similar solution, that is able to generate and validate a token

  • The web client has a way of requesting a token from the above solution in order to pass it through when the connection is established.

  • A proxy, such as Envoy Proxy or HAProxy, is available at a pod level or within the cluster that can access and act on TCP traffic.

Required changes#

Web client#

  1. To enable the passing of a token, the client side code will need to be updated.

    The AppStreamer.setup function will need the authenticate: true entry:

    AppStreamer.setup({
       streamConfig: streamConfig,
       authenticate: true,
       onUpdate: (message: any) => this._onUpdate(message),
       onStart: (message: any) => this._onStart(message),
       onCustomEvent: (message: any) => this._onCustomEvent(message),
       onStop: (message: any) => this._onStop(message),
       onTerminate: (message: any) => this._onTerminate(message),
       onISSOUpdate: (message: any) => this._onISSOUpdate(message)
    }
    
  2. An accessToken value is needed to add to the URI passed to the streaming library.

    const uri = `signalingserver=${serverIP}&
       signalingport=${signalingData.source_port}&
       mediaserver=${serverIP}&
       mediaport=${mediaData.source_port}&
       sessionid=${sessionId}&
       accessToken=${token}`;
    

    Note: In the provided client sample the relevant variable is called url.

Streaming pod#

By default, the Kit session helm chart shown below is deployed when a stream is generated. This deployment already contains an Envoy Proxy set up through which the TCP traffic of the WebRTC connection is routed.

../_images/streaming_pod_arch.png

Envoy Proxy includes several filters that enable the extraction and processing of headers in various ways.

The example shown below, which is also included in the Samples and Resources, extracts the headers from the Sec-Websocket-Protocol header and adds them as more standard HTTP headers before passing it on to an envoy filter that uses JWKS to validate a JWT token. Other filters exist as well, for example the ext_authz which would allow reaching out to an API to process the token.

To simplify managing the Envoy Proxy configuration, it has been exposed as a variable in the Helm chart and can be configured using an application profile.

It is also possible to adapt and modify the helm chart of the Kit streaming app as well to make more fundamental changes.

Example configuration (also available within the profile yaml resources ending with *-auth.yaml) and traffic flow description:

Incoming Traffic to WebRTC Signaling Listener (port 49200):#

  • The traffic arrives at webrtc_signaling_listener.

  • The envoy.filters.network.http_connection_manager handles HTTP connections.

  • The connection manager handles WebSocket upgrades.

  • The request is routed to local_route based on the route configuration.

Lua Filter Execution:#

  • The Lua filter checks the Sec-WebSocket-Protocol header.

  • If the header is missing, it sets a metadata flag and responds with a 403 Forbidden status.

  • If the header is present, it extracts x-nv-sessionid and Authorization values and adds them as headers.

  • If the extraction fails, it responds with a 403 Forbidden status.

JWT Authentication:#

  • The JWT authentication filter verifies the token using Keycloak public keys.

  • If authentication fails, the request is denied.

  • If authentication succeeds, the request proceeds.

Routing:#

  • The request is routed to service_cluster, which forwards it to the local service on port 49100 where the actual stream is running.

Logging:#

  • Access logs are generated and sent to stdout with detailed request and response information.

Example application profile with TLS encryption and authentication for WebRTC signaling.#
apiVersion: omniverse.nvidia.com/v1
kind: ApplicationProfile
spec:
  chartValues:
    streamingKit:
      envoy:
        config:
          static_resources:
            listeners:
              filter_chains:
              - filters:
                # Parent HTTP filter to allow websocket, token extraction, JWT and routing of the signaling channel
                - name: envoy.filters.network.http_connection_manager
                  typed_config:
                    # This Lua filter processes the Authorization header, extracting and validating the JWT token.
                    http_filters:
                    - name: envoy.filters.http.lua
                      typed_config:
                        "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
                        inline_code: |
                          function envoy_on_request(request_handle)
                            local headers = request_handle:headers()
                            local sec_websocket_protocol = headers:get("Sec-WebSocket-Protocol")
    
                            request_handle:logInfo("Lua filter: Checking Sec-WebSocket-Protocol header")
                            if sec_websocket_protocol == nil then
                              local checked = request_handle:streamInfo():dynamicMetadata():get("lua_checked")
                              if checked == nil then
                                request_handle:streamInfo():dynamicMetadata():set("lua_checked", "true")
                                request_handle:respond({[":status"] = "403"}, "Forbidden")
                                return
                              end
                            else
                              -- Correctly match and extract x-nv-sessionid and Authorization values
                              local sessionid, authorization = sec_websocket_protocol:match("x%-nv%-sessionid%.([%w%-]+)%-Authorization%.Bearer%-(.+)")
                              if sessionid and authorization then
                                headers:add("x-nv-sessionid", sessionid)
                                headers:add("Authorization", "Bearer " .. authorization)
                                request_handle:logInfo("Lua filter: Extracted x-nv-sessionid and Authorization headers")
                              else
                                request_handle:logInfo("Lua filter: Failed to extract x-nv-sessionid and Authorization headers")
                                request_handle:respond({[":status"] = "403"}, "Forbidden")
                                return
                              end
                            end
                          end
                    # JWT Authentication Filter - Validates the extracted JWT token.
                    - name: envoy.filters.http.jwt_authn
                      typed_config:
                        "@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication
                        providers:
                          keycloak:
                            issuer: "<replace with issuer>"    # Replace with your JWT issuer (e.g., Keycloak URL).
                            remote_jwks:
                              http_uri:
                                uri: "<replace with jwks URL>"    # Replace with JWKS URL to fetch public keys.
                                cluster: keycloak_cluster    # Use the Keycloak cluster for fetching keys.
                                timeout: 60s
                              cache_duration:
                                seconds: 1    # Caches the JWKS for 1 second.
                        rules:
                        - match:
                            prefix: "/"
                          requires:
                            provider_name: "keycloak"    # Use the Keycloak JWT provider for authentication.
            clusters:
            # Keycloak cluster - Used to securely validate JWT tokens.
            - name: keycloak_cluster
              connect_timeout: 0.25s
              type: STRICT_DNS
              lb_policy: ROUND_ROBIN
              load_assignment:
                cluster_name: keycloak_cluster
                endpoints:
                - lb_endpoints:
                  - endpoint:
                      address:
                        socket_address:
                          address: <replace with FQDN of keycloak server>    # Replace with the Keycloak server address
                          port_value: 443    # Port for HTTPS.
              transport_socket:
                name: envoy.transport_sockets.tls    # Use TLS for secure communication.
                typed_config:
                  "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
                  # TLS context ensures encrypted communication with the Keycloak server.
Enables listening of the WebRTC signaling channel#
listeners:
- name: webrtc_signaling_listener
  address:
    socket_address:
      address: 0.0.0.0
      port_value: 49200    # Listener for WebRTC signaling on port 49200.
Lua filter to extract the JWT token from the header#
# This Lua filter processes the Authorization header, extracting and validating the JWT token.
http_filters:
- name: envoy.filters.http.lua
  typed_config:
    "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
    inline_code: |
      function envoy_on_request(request_handle)
        local headers = request_handle:headers()
        local sec_websocket_protocol = headers:get("Sec-WebSocket-Protocol")

        request_handle:logInfo("Lua filter: Checking Sec-WebSocket-Protocol header")
        if sec_websocket_protocol == nil then
          local checked = request_handle:streamInfo():dynamicMetadata():get("lua_checked")
          if checked == nil then
            request_handle:streamInfo():dynamicMetadata():set("lua_checked", "true")
            request_handle:respond({[":status"] = "403"}, "Forbidden")
            return
          end
        else
          -- Correctly match and extract x-nv-sessionid and Authorization values
          local sessionid, authorization = sec_websocket_protocol:match("x%-nv%-sessionid%.([%w%-]+)%-Authorization%.Bearer%-(.+)")
          if sessionid and authorization then
            headers:add("x-nv-sessionid", sessionid)
            headers:add("Authorization", "Bearer " .. authorization)
            request_handle:logInfo("Lua filter: Extracted x-nv-sessionid and Authorization headers")
          else
            request_handle:logInfo("Lua filter: Failed to extract x-nv-sessionid and Authorization headers")
            request_handle:respond({[":status"] = "403"}, "Forbidden")
            return
          end
        end
      end
Validate the JWT token using Keycloak (in this example)#
# JWT Authentication Filter - Validates the extracted JWT token.
- name: envoy.filters.http.jwt_authn
  typed_config:
    "@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication
    providers:
      keycloak:
        issuer: "<replace with issuer>"    # Replace with your JWT issuer (e.g., Keycloak URL).
        remote_jwks:
          http_uri:
            uri: "<replace with jwks URL>"    # Replace with JWKS URL to fetch public keys.
            cluster: keycloak_cluster    # Use the Keycloak cluster for fetching keys.
            timeout: 60s
          cache_duration:
            seconds: 1    # Caches the JWKS for 1 second.
    rules:
    - match:
        prefix: "/"
      requires:
        provider_name: "keycloak"    # Use the Keycloak JWT provider for authentication.
Example application profile with sample authentication#
  1apiVersion: omniverse.nvidia.com/v1
  2kind: ApplicationProfile
  3metadata:
  4  name: default-auth
  5spec:
  6  name: Default profile with authorisation check
  7  description: Updated memory and CPU settings.
  8  supportedApplications:
  9    - name: "usd-viewer"
 10      versions: 
 11        - "*"
 12  chartMappings:
 13    container: streamingKit.image.repository
 14    container_version: streamingKit.image.tag
 15    name: streamingKit.name
 16  chartValues:
 17    global:
 18      imagePullSecrets:
 19        - name: regcred    # Refers to image pull secret for accessing private container registry.
 20    streamingKit:
 21      envoy:
 22        config:
 23          node:
 24            id: node0
 25            cluster: envoy-cluster    # Defines the cluster that Envoy is part of.
 26          static_resources:
 27            listeners:
 28            - name: webrtc_signaling_listener
 29              address:
 30                socket_address:
 31                  address: 0.0.0.0
 32                  port_value: 49200    # Listener for WebRTC signaling on port 49200.
 33              filter_chains:
 34              - filters:
 35                # Parent HTTP filter to allow websocket, token extraction, JWT and routing of the signaling channel
 36                - name: envoy.filters.network.http_connection_manager
 37                  typed_config:
 38                    "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
 39                    stat_prefix: signaling_http
 40                    codec_type: AUTO
 41                    upgrade_configs:
 42                    - upgrade_type: websocket    # Enables WebSocket upgrade for WebRTC signaling.
 43                    route_config:
 44                      name: local_route
 45                      virtual_hosts:
 46                      - name: local_service
 47                        domains: ["*"]
 48                        routes:
 49                        - match:
 50                            prefix: "/"
 51                          route:
 52                            cluster: service_cluster
 53                    # This Lua filter processes the Authorization header, extracting and validating the JWT token.
 54                    http_filters:
 55                    - name: envoy.filters.http.lua
 56                      typed_config:
 57                        "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
 58                        inline_code: |
 59                          function envoy_on_request(request_handle)
 60                            local headers = request_handle:headers()
 61                            local sec_websocket_protocol = headers:get("Sec-WebSocket-Protocol")
 62    
 63                            request_handle:logInfo("Lua filter: Checking Sec-WebSocket-Protocol header")
 64                            if sec_websocket_protocol == nil then
 65                              local checked = request_handle:streamInfo():dynamicMetadata():get("lua_checked")
 66                              if checked == nil then
 67                                request_handle:streamInfo():dynamicMetadata():set("lua_checked", "true")
 68                                request_handle:respond({[":status"] = "403"}, "Forbidden")
 69                                return
 70                              end
 71                            else
 72                              -- Correctly match and extract x-nv-sessionid and Authorization values
 73                              local sessionid, authorization = sec_websocket_protocol:match("x%-nv%-sessionid%.([%w%-]+)%-Authorization%.Bearer%-(.+)")
 74                              if sessionid and authorization then
 75                                headers:add("x-nv-sessionid", sessionid)
 76                                headers:add("Authorization", "Bearer " .. authorization)
 77                                request_handle:logInfo("Lua filter: Extracted x-nv-sessionid and Authorization headers")
 78                              else
 79                                request_handle:logInfo("Lua filter: Failed to extract x-nv-sessionid and Authorization headers")
 80                                request_handle:respond({[":status"] = "403"}, "Forbidden")
 81                                return
 82                              end
 83                            end
 84                          end
 85                    # JWT Authentication Filter - Validates the extracted JWT token.
 86                    - name: envoy.filters.http.jwt_authn
 87                      typed_config:
 88                        "@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication
 89                        providers:
 90                          keycloak:
 91                            issuer: "<replace with issuer>"    # Replace with your JWT issuer (e.g., Keycloak URL).
 92                            remote_jwks:
 93                              http_uri:
 94                                uri: "<replace with jwks URL>"    # Replace with JWKS URL to fetch public keys.
 95                                cluster: keycloak_cluster    # Use the Keycloak cluster for fetching keys.
 96                                timeout: 60s
 97                              cache_duration:
 98                                seconds: 1    # Caches the JWKS for 1 second.
 99                        rules:
100                        - match:
101                            prefix: "/"
102                          requires:
103                            provider_name: "keycloak"    # Use the Keycloak JWT provider for authentication.
104                    - name: envoy.filters.http.router
105                    access_log:
106                      - name: envoy.access_loggers.stream
107                        typed_config:
108                          "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
109                          log_format:
110                            text_format: |
111                              [START_TIME: %START_TIME%]
112                              REQUEST_METHOD: %REQ(:METHOD)%
113                              PATH: %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%
114                              PROTOCOL: %PROTOCOL%
115                              RESPONSE_CODE: %RESPONSE_CODE%
116                              RESPONSE_FLAGS: %RESPONSE_FLAGS%
117                              BYTES_RECEIVED: %BYTES_RECEIVED%
118                              BYTES_SENT: %BYTES_SENT%
119                              DURATION: %DURATION%
120                              UPSTREAM_HOST: %UPSTREAM_HOST%
121                              DOWNSTREAM_REMOTE_ADDRESS: %DOWNSTREAM_REMOTE_ADDRESS%
122            - name: health_listener
123              address:
124                socket_address:
125                  address: 0.0.0.0
126                  port_value: 8080
127              filter_chains:
128              - filters:
129                - name: envoy.filters.network.http_connection_manager
130                  typed_config:
131                    "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
132                    stat_prefix: health_check
133                    codec_type: AUTO
134                    route_config:
135                      name: local_route
136                      virtual_hosts:
137                      - name: local_service
138                        domains: ["*"]
139                        routes:
140                        - match:
141                            prefix: "/health"
142                          direct_response:
143                            status: 200
144                            body:
145                              inline_string: "OK"
146                    http_filters:
147                    - name: envoy.filters.http.router
148            clusters:
149            - name: service_cluster
150              connect_timeout: 0.25s
151              type: STATIC
152              lb_policy: ROUND_ROBIN
153              load_assignment:
154                cluster_name: service_cluster
155                endpoints:
156                - lb_endpoints:
157                  - endpoint:
158                      address:
159                        socket_address:
160                          address: 127.0.0.1
161                          port_value: 49100  # Forwarding to the stream
162            # Keycloak cluster - Used to securely validate JWT tokens.
163            - name: keycloak_cluster
164              connect_timeout: 0.25s
165              type: STRICT_DNS
166              lb_policy: ROUND_ROBIN
167              load_assignment:
168                cluster_name: keycloak_cluster
169                endpoints:
170                - lb_endpoints:
171                  - endpoint:
172                      address:
173                        socket_address:
174                          address: <replace with FQDN of keycloak server>    # Replace with the Keycloak server address
175                          port_value: 443    # Port for HTTPS.
176              transport_socket:
177                name: envoy.transport_sockets.tls    # Use TLS for secure communication.
178                typed_config:
179                  "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
180                  # TLS context ensures encrypted communication with the Keycloak server.
181      image:
182        repository: nvcr.io/nvidia/omniverse/usd-viewer
183        pullPolicy: Always
184        tag: '0.2.0'
185      sessionId: session_id
186      name: kit-app
187      resources:
188        requests:
189          nvidia.com/gpu: "1"
190        limits:
191          cpu: "3"
192          memory: "20Gi"
193          nvidia.com/gpu: "1"
194      env:
195        - name: USD_PATH
196          value: "/app/data/Forklift_A/Forklift_A01_PR_V_NVD_01.usd"