Security and Auth
Security policies control access to encrypted resources.
Ensuring the proper security of data is foundational to the Twisp infrastructure. Out of the box, data is securely encrypted both at rest and in transit and all access is authenticated via JWT tokens issued by the OpenID Connect 1.0 protocol.
Users can configure additional access settings by defining tenants and clients through GraphQL mutations to set precise permissions on specific operations.
The authentication & authorization flow
Twisp applies policies to an authenticated principal before allowing access to resources.
In a simplified form, the stages for the authentication process are:
- A principal issues an HTTPS request with their OIDC JWT and Twisp account IDs in the headers.
- The principal name is extracted from the JWT.
- Twisp finds the corresponding policies to apply using the principal name.
- The resulting policies are evaluated for authorization.
Making an authenticated request
All HTTPS requests to the GraphQL API must provide the following headers to allow for authorization:
Authorization: Bearer <JWT>
x-twisp-account-id: <Twisp account id>
The JWT can be issued either with OpenID Connect or AWS IAM.
Authorizing a principal
Within a client configuration, the security principal is the authenticated identity accessing the system. The client authorization system retrieves policies based on the principal name. For OIDC this is typically the issuer found in the iss claim. For AWS IAM it is the IAM role, user, or other AWS identity placed in the sub claim when Twisp vends a token.
Provisioning Tenants
Each cloud environment requires its own tenant. Use the admin GraphQL namespace to create tenants from an existing account:
mutation CreateTenants {
admin {
staging: createTenant(
input: {
id: "d9d8f1c0-0299-4d5b-b2b6-85beafdda28b"
accountId: "TwispStaging"
name: "staging"
description: "staging environment for Twisp"
}
) {
accountId
name
}
production: createTenant(
input: {
id: "848df974-4133-4ee4-ab45-86e5e29b6822"
accountId: "TwispProd"
name: "production"
description: "production environment for Twisp"
}
) {
accountId
name
}
}
}
Once a tenant exists, clients may be created within that tenant to govern access.
Creating Clients and Policies
Authentication to Twisp works with any OIDC-compliant token supplied through the Authorization: Bearer <token> header. A corresponding client must exist in Twisp for the principal identified in the token so Twisp knows which policies to apply.
Third-party OIDC client example
For tokens issued by an external identity provider, set the client principal to the OIDC issuer URL (iss claim). The following mutation creates per-user policies for tokens issued by Google:
mutation CreateGoogleCloudClient {
auth {
createClient(
input: {
name: "michael gcloud readonly"
principal: "https://accounts.google.com"
policies: [
{
actions: [SELECT]
resources: ["*"]
effect: ALLOW
assertions: { isMike: "context.auth.claims.email == 'michael@twisp.com'" }
}
{
actions: [SELECT, INSERT, UPDATE, DELETE]
resources: ["*"]
effect: ALLOW
assertions: { isJarred: "context.auth.claims.email == 'jarred@twisp.com'" }
}
]
}
) {
principal
}
}
}
AWS IAM principal example
Twisp can exchange a presigned AWS STS GetCallerIdentity request for an OIDC token. The resulting token uses the AWS identity as the principal. Create a client for that principal to grant access:
mutation CreateIAMAuthClient {
auth {
createClient(
input: {
principal: "arn:aws:iam::012345678901:role/example-role"
name: "example role policy"
policies: [{ effect: ALLOW, actions: [SELECT, INSERT, UPDATE, DELETE], resources: ["*"] }]
}
) {
principal
}
}
}
Understanding policies
Each policy defines an effect (ALLOW or DENY), the actions permitted or denied, the resources in scope, and optional CEL assertions that must evaluate to true.
Logical combination of policies
Policies are evaluated as a chain of logical AND statements scoped to the requested resource and action. A principal must have at least one ALLOW policy for a resource/action pair. A single DENY policy on that pair blocks the operation.
Values for resources and actions support * and ? wildcards. Actions include:
db:Select(SELECTin GraphQL) : read a documentdb:Insert(INSERT) : create a documentdb:Update(UPDATE) : change document fieldsdb:Delete(DELETE) : remove a document
Resource identifiers follow the format namespace.ledger.<document|indexes|joins|references>.propertyName. The current namespace is financial.
The ledger segment is a table name, and resources match it case-sensitively: financial.Account.* covers accounts, while financial.account.* covers nothing. Tables include the ledgers (Account, AccountSet, Transaction, Entry, Balance, TranCode, Journal, and others), KVValue, the action tables (Action, ActionExecution, Session, and others), and each of your views, under its view name. Writing a client's or an action's policies fails with a BAD_REQUEST when a resource names no table but differs from one only by letter case or, for names of four or more characters, by one character, and the error suggests the resource meant:
policies[0].resources[0]: financial.account matches no table; did you mean financial.Account.*?
A resource with a wildcard in its table segment, such as financial.Acc*, is not checked, and updating an action does not reject a resource its current version already has. Create a view before writing a policy for it when the view's name is that close to a table's, such as Accounts.
Tenant files (the files API) are not ledger documents, but the same policies govern them. Each file key is the resource financial.files.<key>, and the key keeps its / separators. Listing and downloading need SELECT on the key; a listing checks the requested keyPrefix and every key it returns. Uploading needs INSERT on the key. A resource of financial.* or * covers all files.
policies: [
{
effect: ALLOW
actions: [SELECT]
resources: ["financial.files.exports/*"]
}
{
effect: ALLOW
actions: [INSERT]
resources: ["financial.files.uploads/*"]
}
]
This client can list and download everything under exports/ and upload under uploads/, and nothing else in the file store.
Assertions use Common Expression Language (CEL) and can access context.auth.claims (token claims) and context.document (the document being acted on). If any assertion evaluates to false, the policy is skipped. An update is checked against the document as it was and as it becomes: db:Update must be allowed for both, so a principal cannot change a document its policies do not cover into one they do, or the reverse. A delete is checked against the document it removes.
Writing a version of a document needs db:Insert whether the write creates the document or changes it, so a principal that changes documents needs INSERT as well as UPDATE. A write that creates a document or replaces it, such as a KV put, needs UPDATE only when it replaces one.
Requests use a ledger's indexes as well as its documents, and an index has no document, so an assertion on context.document cannot be evaluated for an index resource. A policy that covers an index, directly or through a wildcard such as financial.KVValue.*, with such an assertion fails the request with assertion["<name>"] partially evaluated for <resource>. Grant the indexes without the assertion and put it on the document resource. This client can write KV records only in the support.disputes namespace:
policies: [
{ effect: ALLOW, actions: [SELECT], resources: ["financial.KVValue.*"] }
{ effect: ALLOW, actions: [INSERT, UPDATE], resources: ["financial.KVValue.indexes.*"] }
{
effect: ALLOW
actions: [INSERT, UPDATE]
resources: ["financial.KVValue.document"]
assertions: { disputesOnly: "context.document.namespace == 'support.disputes'" }
}
]
Using OpenID Connect tokens
Any JWT generated by an OpenID Connect 1.0 capable issuer is supported by Twisp for authentication. When an API endpoint receives the JWT, it validates the token signature against the issuer and—if valid—invokes the endpoint with a security principal set to the issuer iss claim. All claims are embedded in context.auth.claims for policy evaluation.
AWS IAM Outbound Identity Federation
AWS STS can issue a web identity token that Twisp accepts directly as an OIDC token. Configure the Twisp client principal with the AWS issuer URL from iss, and use policy assertions to check the intended audience and AWS identity. Follow Connecting with AWS Identity Federation for token retrieval, client setup, API requests, and refresh handling.
Issuing tokens with AWS Identity and Access Management (IAM)
Twisp can vend an OpenID Connect token in exchange for an authenticated AWS IAM role or user. This allows services running in AWS to retrieve a Twisp-issued token for access. The issuer of these tokens is https://auth.${AWS::Region}.prod.twisp.com/token/iam, and the resulting principal name equals the original IAM identity stored in the token sub field.
Twisp also provides a hosted service to exchange a presigned GetCallerIdentity request for a token, making it convenient to obtain OIDC tokens inside AWS environments.
Cloud endpoints
- AWS Twisp Token:
https://auth.us-east-1.cloud.twisp.com/token/iam - Financial GraphQL:
https://api.us-east-1.cloud.twisp.com/financial/v1/graphql - gRPC:
https://api.us-east-1.cloud.twisp.com:50051
Encryption
Encryption in transit
Data is encrypted in transit via HTTPS connections for all external and internal API operations. This protects against man-in-the-middle attacks and prevents eavesdropping on Twisp traffic.
Encryption at rest
All data stored in Twisp is encrypted at rest. Encryption keys are stored in AWS Key Management Service, so even if a malicious actor gains access to the storage layer they cannot read the data without the proper keys.