JWT
Every request to every service is authorised by a JWT. It is minted by the identity API and carries everything a downstream service needs to decide what the caller may do, so no service has to ask another who is calling.
Claims
{
"iss": "auth@tagshape.com",
"sub": "9c1e7a4d-3f52-4a18-9b6c-2d0e5f7a1b83",
"aud": "tagshape.com",
"nbf": 0,
"exp": 1786000000,
"iat": 1785996400,
"jti": "b2f1c8d0-…",
"name": "Neil Docherty",
"email": "neil@example.com",
"org": "05cc0c65-021c-4515-850f-366667e26656",
"permissions": [769, 1537, 1794]
}
| Claim | Meaning |
|---|---|
iss | Issuer. Configured; defaults to auth@tagshape.com. |
sub | The TagShape account ID, not the provider's identifier for the user. |
aud | Audience. Configured; defaults to tagshape.com. |
exp / iat / nbf / jti | Standard expiry, issued-at, not-before and token ID. |
name, email | Taken from the account where one exists, falling back to what the provider said. |
org | The organization the session belongs to. Scopes every query in every service. |
permissions | The numeric Permission values the caller holds, gathered from their groups. |
permissions is an array of the enum's integer values — see the
permissions catalogue for the mapping. It is a flat list of what the caller
was actually granted; dependency expansion happens at check time, not at mint time.
Edge cases in the claims
- An account with no permissions still gets a token, with an empty
permissionsarray. It is logged as a warning. Every permissioned endpoint will refuse it. - A login with no account (during provisioning) gets
subas the empty GUID rather than pointing at somebody else's account. - Name and email prefer the account over the provider. A rename reaches the token on the login that noticed it, rather than the one after.
Lifetime
| Setting | Default |
|---|---|
DurationInMinutes | 60 |
RefreshDurationInMinutes | 10080 (7 days) |
Configured under Jwt — see Configuration.
Signing
RS256, with an asymmetric key pair. The private key is configuration on the identity API
(Jwt:PrivateKey); the corresponding public key is what verifies tokens.
GET /generatejwk/{key} on the identity API generates a pair — public PEM, private PEM and JWK. It is an
operational tool run out of band, not part of the request path.
Minting
From a login
{
"userId": "…",
"provider": "Entra",
"providerUserId": "…",
"name": "Neil Docherty",
"email": "neil@example.com",
"orgId": "05cc0c65-021c-4515-850f-366667e26656",
"profileImage": null
}
Returns the token as plain text. Two failure modes are distinguished:
| Status | Meaning |
|---|---|
403 Forbidden | There is nobody to issue a token to and no pattern permits creating them. Not a fault in the request — the caller should end the session rather than report a problem with it. |
400 Bad Request | The request itself was wrong. |
From an API key
POST /apikeyjwt, with the key in x-tagshape-api. See API keys.
Renewal
{
"userId": "…",
"orgId": "…"
}
Renewal re-reads the account's permissions from GET /users/{id}/permissions before minting, so a permission
granted or withdrawn takes effect at the next refresh rather than at the next full login. It deliberately
creates nothing and changes nothing — provisioning only ever happens on a login.
That endpoint takes an ID rather than an email address because a renewal knows the account it is for, not the provider identity that started the session.
The management UI runs a background refresh against this, which is why the token also travels as a cookie.
Reading the token in a service
IJwtPayload, registered per request, reads the token from the header or the cookie and exposes UserId,
OrgId, Email, Name, Permissions and JwtFound. Anything that cannot be read leaves JwtFound false with
a FailureMessage, and every permission check fails closed.
public MyEndpointHandler(IJwtPayload jwtPayload)
{
var organizationId = jwtPayload.OrgId; // scope every query by this
var canSeeSecrets = jwtPayload.HasPermissions([Permission.VariableReadSecrets]);
}
Most requests — static files, health checks, anonymous endpoints — carry no permission requirement at all, and the middleware short-circuits before reading the token so those do not pay for it.