diff --git a/contracts/control/authority/v1/openapi.yaml b/contracts/control/authority/v1/openapi.yaml index 7837ea763..d8e404b4e 100644 --- a/contracts/control/authority/v1/openapi.yaml +++ b/contracts/control/authority/v1/openapi.yaml @@ -2,7 +2,7 @@ openapi: 3.1.0 info: title: tnl authority API version: 1.0.0 - description: "Manage identities, teams, domains, company access, and workload sessions. API revisions and renewal sequences are decimal strings in JSON, including mutation preconditions. Values range from 0 through 9223372036854775807, subject to each operation's positive-value rules. Keep them as strings in JavaScript, or use BigInt for comparison and arithmetic. Signed identity-provider claims retain their documented claim encoding." + description: "Manage identities, teams, domains, company access, and workload sessions. API revisions and renewal sequences are decimal strings in JSON, including mutation preconditions and the connection revision in tnl_team_contexts. Values range from 0 through 9223372036854775807, subject to each operation's positive-value rules. Keep them as strings in JavaScript, or use BigInt for comparison and arithmetic. Standard identity-provider timestamps remain numeric Unix seconds." license: name: BSD-3-Clause identifier: BSD-3-Clause diff --git a/contracts/control/authority/v1/schemas/teams.yaml b/contracts/control/authority/v1/schemas/teams.yaml index a9b052397..5b003142d 100644 --- a/contracts/control/authority/v1/schemas/teams.yaml +++ b/contracts/control/authority/v1/schemas/teams.yaml @@ -8,7 +8,13 @@ schemas: properties: team_id: { type: string } identity_connection_id: { type: string } - connection_revision: { type: integer, format: int64, minimum: 1 } + connection_revision: + { + type: string, + pattern: "^[1-9][0-9]*$", + x-go-type: int64, + x-oapi-codegen-extra-tags: { json: "connection_revision,string" }, + } subject: { type: string } source_id: { type: string } authenticated_at: diff --git a/internal/control/oidc/company_context_test.go b/internal/control/oidc/company_context_test.go index bb7c94daf..c409103d5 100644 --- a/internal/control/oidc/company_context_test.go +++ b/internal/control/oidc/company_context_test.go @@ -13,7 +13,7 @@ func TestVerifierCompanyContextRequiresSignedTypedClaims(t *testing.T) { t.Fatal(err) } now := time.Now() - proof := TeamContext{TeamID: "team_company", IdentityConnectionID: "icn_company", ConnectionRevision: 3, SourceID: "source-company", Subject: "directory-subject", AuthenticatedAt: now.Unix()} + proof := TeamContext{TeamID: "team_company", IdentityConnectionID: "icn_company", ConnectionRevision: 9007199254740993, SourceID: "source-company", Subject: "directory-subject", AuthenticatedAt: now.Unix()} sign := func(value any) string { return p.signer.Token(t, "key-1", map[string]any{"iss": p.issuer, "sub": "identity-subject", "aud": "tnl-cli", "iat": now.Unix(), "exp": now.Add(time.Hour).Unix(), "tnl_team_contexts": value}) } @@ -21,7 +21,7 @@ func TestVerifierCompanyContextRequiresSignedTypedClaims(t *testing.T) { if err != nil || len(identity.TeamContexts) != 1 || identity.TeamContexts[0] != proof { t.Fatal("signed context lost", identity, err) } - for _, bad := range []any{true, map[string]any{"company_authenticated": true}, []TeamContext{proof, proof}, []map[string]any{{"team_id": proof.TeamID, "identity_connection_id": proof.IdentityConnectionID, "connection_revision": "3", "subject": proof.Subject, "authenticated_at": proof.AuthenticatedAt}}, []TeamContext{{TeamID: "team_company", IdentityConnectionID: "icn_company", ConnectionRevision: 3, Subject: "directory-subject"}}} { + for _, bad := range []any{true, map[string]any{"company_authenticated": true}, []TeamContext{proof, proof}, []map[string]any{{"team_id": proof.TeamID, "identity_connection_id": proof.IdentityConnectionID, "source_id": proof.SourceID, "connection_revision": 3, "subject": proof.Subject, "authenticated_at": proof.AuthenticatedAt}}, []TeamContext{{TeamID: "team_company", IdentityConnectionID: "icn_company", ConnectionRevision: 3, Subject: "directory-subject"}}} { if _, err = v.Verify(loginTestContext(t), sign(bad)); !errors.Is(err, ErrUnauthenticated) { t.Fatal("malformed signed context accepted", err) } diff --git a/internal/control/oidc/verifier.go b/internal/control/oidc/verifier.go index f1d5d47f8..0bf942a5b 100644 --- a/internal/control/oidc/verifier.go +++ b/internal/control/oidc/verifier.go @@ -49,7 +49,7 @@ type Identity struct { type TeamContext struct { TeamID string `json:"team_id"` IdentityConnectionID string `json:"identity_connection_id"` - ConnectionRevision int64 `json:"connection_revision"` + ConnectionRevision int64 `json:"connection_revision,string"` Subject string `json:"subject"` SourceID string `json:"source_id"` AuthenticatedAt int64 `json:"authenticated_at"` diff --git a/pkg/api/control/authority/v1/openapi.gen.go b/pkg/api/control/authority/v1/openapi.gen.go index ce18d9b0e..aef84b376 100644 --- a/pkg/api/control/authority/v1/openapi.gen.go +++ b/pkg/api/control/authority/v1/openapi.gen.go @@ -1002,7 +1002,7 @@ type TeamRole string type VerifiedOIDCTeamContext struct { // AuthenticatedAt Unix seconds of actual company authentication; not token refresh time AuthenticatedAt int64 `json:"authenticated_at"` - ConnectionRevision int64 `json:"connection_revision"` + ConnectionRevision int64 `json:"connection_revision,string"` IdentityConnectionId string `json:"identity_connection_id"` SourceId string `json:"source_id"` Subject string `json:"subject"`