Skip to main content

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]
}
ClaimMeaning
issIssuer. Configured; defaults to auth@tagshape.com.
subThe TagShape account ID, not the provider's identifier for the user.
audAudience. Configured; defaults to tagshape.com.
exp / iat / nbf / jtiStandard expiry, issued-at, not-before and token ID.
name, emailTaken from the account where one exists, falling back to what the provider said.
orgThe organization the session belongs to. Scopes every query in every service.
permissionsThe 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 permissions array. It is logged as a warning. Every permissioned endpoint will refuse it.
  • A login with no account (during provisioning) gets sub as 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

SettingDefault
DurationInMinutes60
RefreshDurationInMinutes10080 (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

POST /generatejwt
{
"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:

StatusMeaning
403 ForbiddenThere 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 RequestThe request itself was wrong.

From an API key

POST /apikeyjwt, with the key in x-tagshape-api. See API keys.

Renewal

POST /refreshjwt
{
"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.