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#
To enable the passing of a token, the client side code will need to be updated.
The
AppStreamer.setup
function will need theauthenticate: 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) }
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.
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
andAuthorization
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 port49100
where the actual stream is running.
Logging:#
Access logs are generated and sent to stdout with detailed request and response information.
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.
listeners:
- name: webrtc_signaling_listener
address:
socket_address:
address: 0.0.0.0
port_value: 49200 # Listener for WebRTC signaling on port 49200.
# 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.
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"