Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
401 changes: 401 additions & 0 deletions auth/open-api/authorize.yaml

Large diffs are not rendered by default.

196 changes: 196 additions & 0 deletions auth/open-api/introspect.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
openapi: 3.1.0
info:
title: Versola Auth — Token Introspection Endpoint
version: 1.0.0
description: |
OAuth 2.0 Token Introspection (RFC 7662). Lets an authenticated client or resource server
ask whether a token is currently active and retrieve its metadata.

Both access tokens (JWTs) and refresh tokens (opaque) are accepted; the token format is
detected automatically, so `token_type_hint` is unnecessary.

A token that cannot be verified, has expired, was revoked, or does not belong to the
calling client is reported as `{"active": false}` rather than as an error — the caller is
never told why a token is not active.
license:
name: Business Source License 1.1
url: https://github.com/versolauth/versola/blob/main/LICENCE.md

servers:
- url: https://auth.example.com
description: Auth service issuer origin

paths:
/introspect:
post:
operationId: introspect
summary: Introspect a token
tags: [Token]
security:
- clientSecretBasic: []
- clientSecretPost: []
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/IntrospectionRequest'
example:
token: eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCJ9...
responses:
'200':
$ref: '#/components/responses/Introspection'
'400':
$ref: '#/components/responses/InvalidRequest'
'401':
$ref: '#/components/responses/InvalidClient'

components:
securitySchemes:
clientSecretBasic:
type: http
scheme: basic
description: '`client_secret_basic` — `client_id` as the username, `client_secret` as the password.'
clientSecretPost:
type: apiKey
in: cookie

@augmentcode augmentcode Bot Aug 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extractCredentials reads client_secret_post exclusively from the parsed form; it never reads a client_secret cookie. Declaring this as a cookie apiKey causes OpenAPI tooling to send the secret in a location the endpoint ignores, despite the body schema description. Other locations where this applies: auth/open-api/par.yaml:72, auth/open-api/revoke.yaml:55, auth/open-api/token.yaml:85.

Severity: medium

Other Locations
  • auth/open-api/par.yaml:72
  • auth/open-api/revoke.yaml:55
  • auth/open-api/token.yaml:85

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

name: client_secret
description: |
`client_secret_post` — `client_id` and `client_secret` sent as form fields. Modelled
here as a scheme only because OpenAPI has no dedicated form-credential type.

schemas:
IntrospectionRequest:
type: object
required: [token]
properties:
token:
type: string
description: |
The token to introspect. A value in JWT form is treated as an access token,
anything else as an opaque refresh token.
client_id:
type: string
description: Required for `client_secret_post`.
client_secret:
type: string
description: Confidential clients using `client_secret_post`.

IntrospectionResponse:
type: object
required: [active]
description: |
Only `active` is guaranteed. When `active` is `false` every other member is absent.
properties:
active:
type: boolean
client_id:
type: string
description: Client the token was issued to.
scope:
type: string
description: Space-delimited granted scopes.
username:
type: string
token_type:
type: string
description: '`Bearer` for access tokens.'
exp:
type: integer
format: int64
description: Expiry, seconds since the epoch.
iat:
type: integer
format: int64
description: Issued at, seconds since the epoch.
nbf:
type: integer
format: int64
description: Not valid before, seconds since the epoch.
sub:
type: string
description: User id, or the `client_id` for a `client_credentials` token.
aud:
type: array
items:
type: string
iss:
type: string
jti:
type: string

ErrorResponse:
type: object
required: [error]
properties:
error:
type: string
enum: [invalid_request, invalid_client]
error_description:
type: string

responses:
Introspection:
description: |
Introspection result. Returned with `200` whether or not the token is active.
headers:
Cache-Control:
schema:
type: string
const: no-store

@augmentcode augmentcode Bot Aug 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When JWT deserialization fails, IntrospectionController returns the inactive 200 response directly without either cache header. Therefore this response contract promises Cache-Control: no-store and Pragma: no-cache for an ordinary documented outcome that does not actually include them.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Pragma:
schema:
type: string
const: no-cache
content:
application/json:
schema:
$ref: '#/components/schemas/IntrospectionResponse'
examples:
active:
summary: Active access token
value:
active: true
client_id: web-app
scope: openid profile email
token_type: Bearer
exp: 1735689600
iat: 1735686000
sub: 018f3c6e-5a2b-7c11-9d0e-4f8a2b6c1d33
aud: [https://api.example.com]
iss: https://auth.example.com
jti: 9x8VJ0mQ0iKQxRZ8m6bTsw
inactive:
summary: Unknown, expired, revoked, or another client's token
value:
active: false

InvalidRequest:
description: The body is not a readable form, or the `token` field is missing.
headers:
Cache-Control:
schema:
type: string
const: no-store
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
error: invalid_request
error_description: Request is missing a required parameter or is otherwise malformed

InvalidClient:
description: Client authentication failed.
headers:
Cache-Control:
schema:
type: string
const: no-store
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
error: invalid_client
error_description: Client authentication failed
95 changes: 95 additions & 0 deletions auth/open-api/jwks.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
openapi: 3.1.0
info:
title: Versola Auth — JWKS Endpoint
version: 1.0.0
description: |
JSON Web Key Set endpoint (RFC 7517, OpenID Connect Discovery §3). Publishes the public
keys that verify the signatures on tokens issued by this server.

Resource servers fetch this document to validate access tokens and ID tokens offline. The
`kid` in a token's JOSE header selects the key to verify it with.

The set contains the currently active signing key plus any keys still needed to verify
tokens issued before the last rotation, so a resource server that caches the document
keeps working across a rotation. Only public key material is exposed.

The response is cacheable for 24 hours. On encountering an unknown `kid`, refetch rather
than waiting for the cache to expire.
license:
name: Business Source License 1.1
url: https://github.com/versolauth/versola/blob/main/LICENCE.md

servers:
- url: https://auth.example.com
description: Auth service issuer origin

paths:
/.well-known/jwks.json:
get:
operationId: jwks
summary: Retrieve the JSON Web Key Set
description: No authentication required.
tags: [Discovery]
security: []
responses:
'200':
$ref: '#/components/responses/Jwks'

components:
schemas:
JsonWebKeySet:
type: object
required: [keys]
properties:
keys:
type: array
items:
$ref: '#/components/schemas/JsonWebKey'

JsonWebKey:
type: object
required: [kty, kid]
additionalProperties: true
properties:
kty:
type: string
description: Key type.
example: RSA
use:
type: string
description: Intended use; signature verification.
const: sig
kid:
type: string
description: Key identifier, matched against the `kid` in a token's JOSE header.
alg:
type: string
description: Signing algorithm the key is used with.
example: RS256
n:
type: string
description: RSA modulus, base64url-encoded.
e:
type: string
description: RSA public exponent, base64url-encoded.

responses:
Jwks:
description: The current key set.
headers:
Cache-Control:
schema:
type: string
const: public, max-age=86400
content:
application/json:
schema:
$ref: '#/components/schemas/JsonWebKeySet'
example:
keys:
- kty: RSA
use: sig
kid: 2026-08-01
alg: RS256
n: 0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4...
e: AQAB
Loading
Loading