Privileged Access for Web Apps Overview
Add per-request user identity to internal web apps behind a Twingate Gateway.
Beta
Privileged Access for Web Apps is currently in beta. To learn more or request access, please contact us.
Twingate Privileged Access for Web Apps
Twingate Privileged Access for Web Apps adds per-request user identity to internal web applications. When you put a web app behind a Twingate Gateway as a Web App Resource, the Gateway injects a signed JWT into every HTTP request. Your app verifies the JWT and gets the authenticated user’s identity without any OIDC integration, client secrets, or redirect flows.
Key benefits
- No OIDC integration required. The Gateway handles authentication using the user’s existing Twingate session. Your app only needs to verify a JWT.
- Per-request identity. Every request carries a cryptographically signed token with the user’s identity, device info, and Resource metadata. No session state to manage on your backend.
- Group-based authorization. The JWT includes the Twingate Group names that authorized the user’s access. Use these for fine-grained access control in your app.
- Convenience headers for logging. The Gateway also injects
X-Twingate-*headers with individual identity fields, useful for access logs and UI personalization without parsing the JWT.
How It Works
The Twingate Gateway acts as a Layer 7 reverse proxy between the Twingate Client and your web app. Before the Client can reach the resource, the Twingate Controller authorizes the user and issues a Gateway Access Token (GAT) scoped to that user, device, and resource. The Client presents the token to the Gateway when it opens the connection, and the Gateway injects it into every HTTP request it forwards upstream.
Browser → Twingate Client → Gateway → Your App ↓ [Injects headers]Here is what happens on each request:
- A user navigates to an internal web app (e.g., http://app.internal) in their browser.
- The Twingate Client intercepts the request and routes it through the Twingate network to the Gateway assigned to that Resource.
- If the Client does not already hold a valid Gateway Access Token for the Resource, it requests one from the Controller. The Controller authorizes the user against their existing Twingate session and the Resource’s Security Policy, then returns an ES256-signed token carrying the user’s identity, device info, and Resource metadata. No separate login prompt or redirect is needed.
- The Client connects to the Gateway and presents the token. The Gateway verifies the token and confirms it was issued to that Client.
- On each HTTP request over that connection, the Gateway injects the headers you configured, such as
Authorization: Bearer <token>, and forwards the request to the upstream app. - Your app verifies the JWT against Twingate’s JWKS endpoint and extracts the user’s identity.
JWKS Endpoint
Twingate exposes its public signing keys at a standard JWKS endpoint:
https://<your-tenant>.twingate.com/api/v1/jwk/ecThis endpoint is unauthenticated, following the JWKS convention. Most JWT libraries fetch and cache these keys automatically.
JWT Header
The Controller issues an ES256-signed JWT on every forwarded request. The token is a standard JWT in compact form (header.payload.signature), and its header looks like this:
{ "alg": "ES256", "typ": "GAT", "kid": "a1b2c3d4"}| Field | Description |
|---|---|
alg | Used to tell the consumer what signing algorithm is used for verification purposes. |
typ | Always GAT, for Gateway Access Token. This is not the conventional JWT value. |
kid | Key ID identifying which JWKS key signed this token. Your JWT library uses it to pick the right key. |
The typ field is GAT, not JWT
Some JWT libraries validate that typ equals JWT and reject anything else. If verification fails with a type or header error even though the signature is good, check whether your library enforces typ and configure it to accept GAT.
JWT Payload
Here is the full payload structure:
{ "rnw": 1720003200, "jti": "a7f2e891-4c3d-48b6-9e1a-5d2f7b8c0e3a", "iss": "twingate", "aud": "tenant-slug", "exp": 1720040400, "iat": 1720000000, "ver": "1", "zver": "0.1.2405001", "cpk": "LS0tLS1CRUdJTi...LS0tLS0K", "resource": { "id": "UmVzb3VyY2U6MTIzNDU2Nzg5", "type": "WEB_APP", "address": "internal-dashboard.tenant.svc.cluster.local", "aliases": [ "dashboard.internal" ], "gateway_metadata": { "downstream": { "port": 80 }, "upstream": { "port": 80 }, "request_header_rewrites": {} } }, "user": { "id": "VXNlcjo1NTU1NQ==", "username": "jane.doe@company.com", "email": "jane.doe@company.com", "first_name": "Jane", "last_name": "Doe", "groups": [ "Everyone", "twingate:authenticated", "engineering" ] }, "device": { "id": "RGV2aWNlOjg4ODg4", "location": { "lat": 37.77, "lon": -122.42, "country": "US", "region": "California", "city": "San Francisco" } }}Common Token Claims
| Claim | Type | Description |
|---|---|---|
rnw | Integer | Unix timestamp for the renewal window. |
jti | String | Unique JWT identifier (UUID). |
aud | String | Audience — your Twingate network name. |
exp | Integer | Unix timestamp after which the token is invalid. Your middleware should reject expired tokens. |
iat | Integer | Unix timestamp when the token was created. |
ver | String | Token format version. |
User Claims
| Claim | Type | Description |
|---|---|---|
user.id | String | Twingate user ID. Stable across sessions for the same user. |
user.username | String | The user’s email address or IdP username. |
user.email | String | The user’s email address. |
user.first_name | String | The user’s first name. |
user.last_name | String | The user’s last name. |
user.groups | Array | Twingate Group names that authorized access to this Resource. Always includes twingate:authenticated. Use these for authorization decisions in your app. |
Device Claims
| Claim | Type | Description |
|---|---|---|
device.id | String | Twingate device ID. Identifies the specific device the request came from. |
device.location.lat | Number | Latitude of the device’s location. |
device.location.lon | Number | Longitude of the device’s location. |
device.location.country | String | ISO 3166-1 alpha-2 country code of the device’s location. |
device.location.region | String | Region or state of the device’s location. |
device.location.city | String | City name of the device’s location. |
Resource Claims
| Claim | Type | Description |
|---|---|---|
resource.id | String | Twingate Resource ID. |
resource.type | String | Always WEB_APP for Web App Resources. |
resource.address | String | The upstream address configured for this Resource. |
resource.aliases | Array | DNS aliases configured for this Resource. |
resource.gateway_metadata.downstream.port | Integer | The port on which the Twingate Client intercepts the user’s request. |
resource.gateway_metadata.upstream.port | Integer | The port of the web application that the Gateway connects to. |
resource.gateway_metadata.request_header_rewrites | Object | Header rewrite rules applied to requests. |
Request Headers
The Gateway can inject custom HTTP headers into every forwarded request. Until you configure at least one header, your app receives an ordinary request with no identity or extra information injected.
Where Headers Are Defined
Headers come from two places, and both are opt-in:
- Gateway-wide headers apply to every web app a Gateway proxies. Use these for a header every app should receive, such as
Authorization. You configure them on the Gateway itself, which means in your Helm values if you deploy with the Twingate Operator. - Per-Resource header rewrites apply to a single Resource, letting one app receive a header in the exact form it expects. You can manage these in the Admin Console, through the Twingate API, or in your Kubernetes manifests.
The Gateway applies per-Resource rewrites after Gateway-wide headers, so a rewrite overrides a Gateway-wide header of the same name.
Configuring Headers
Each header is defined with a key and a value template. The key can be any HTTP header name. The value supports template variables that the Gateway replaces with request-specific data.
Available Template Variables
| Variable | Description | Example Value |
|---|---|---|
{{jwt}} | The signed ES256 JWT containing the full user identity. | eyJhbG... |
{{username}} | The user’s email address or username. | user@company.com |
{{groups}} | Comma-separated list of Twingate Group names. Always includes the built-in twingate:authenticated group (and Everyone) in addition to the Groups you assign. | twingate:authenticated,Everyone,engineering |
{{clientGeoLatLong}} | Client’s latitude and longitude. | 37.7749,-122.4194 |
{{clientGeoCity}} | Client’s city. | San Francisco |
{{clientGeoRegion}} | Client’s region or state. | California |
{{clientGeoCountry}} | Client’s country code. | US |
Example Configuration
The recommended configuration is to pass the JWT as a Bearer token for applications that can validate JWTs signed by Twingate. As a fallback, you can use plaintext headers, but make sure they come only from the Gateway:
| Header Key | Value Template |
|---|---|
Authorization | Bearer {{jwt}} |
| Header Key | Value Template |
| X-Twingate-User | {{username}} |
| X-Twingate-Groups | {{groups}} |
Configuring Headers with the Twingate Operator
If you deploy the Gateway with the Twingate Kubernetes Operator, you can define headers in your manifests rather than the Admin Console. Each surface expresses the same idea in a different shape, so check which one you are editing.
Gateway-wide headers are a map in your Helm values:
gateway: webApp: enabled: true requestHeaders: Authorization: "Bearer {{jwt}}"Per-Resource rewrites are a list of name and value pairs on the TwingateResource:
apiVersion: twingate.com/v1betakind: TwingateResourcemetadata: name: internal-dashboardspec: type: WebApp name: "Internal Dashboard" address: internal-dashboard.default.svc.cluster.local alias: dashboard.internal gatewayRef: name: twingate-gateway downstream: port: 80 upstream: port: 3000 requestHeaderRewrites: - name: X-Twingate-User value: "{{username}}"If you let the operator create the Resource from a Service annotation instead, the same rewrites are a JSON object inside a string:
metadata: annotations: resource.twingate.com: "true" resource.twingate.com/type: "WebApp" resource.twingate.com/gatewayName: "example-gateway" resource.twingate.com/requestHeaderRewrites: '{"Authorization": "Bearer {{jwt}}"}'Getting Started
To add Privileged Access to your web app, you need:
- A Twingate account with administrator privileges
- A deployed Twingate Gateway with a Web App Resource configured
See the developer guides for drop-in middleware you can add to your app in minutes. Guides are available for Express.js, Django, Next.js, and Next.js + Auth.js.
For off-the-shelf apps you run but don’t build, see the application integration guides. Apps like Grafana and Jenkins already support trusted-header authentication, so the Gateway can sign users in without any code changes.
For general information about deploying Twingate Identity Firewall, see the Identity Firewall Overview.
Join us in the community subreddit to share your setup experience or ask questions.
Last updated 1 hour ago