Issue every token to exactly one audience #7
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "review/single-audience"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
Every token Consent mints is signed with the issuer domain I (
authorityDomain). I also did a second job: it was the audience of Consent's own API, soFinalizeAuthorizationminted every app token withaud = [I, Ax]. Nothing keeps I apart from Consent's public host P or from app audiences Ax, which allowed two collisions:The model
issand nowhere else. Every validator checksiss == Iand the signature, and nothing compares I with P or with any app audience, so any value of I is safe.aud = Ax. Consent's web UI tokens haveaud = P, because the web UI is the internalconsentintegration.tokens.InitClient) and the web UI accept a token only whenaudequals their audience exactly.consentneeds no group check and gets no capabilities. Userinfo with thegroupsscope is denied. In practiceconsenttokens carry no scopes, so they fail the identity scope check.Service.issuerDomainnow only feeds the capability budget, which counts the issuer once.Commits
fixed: issued every token to its integration's audience onlycovers the behaviour change, the regression tests, and the budget. It still uses[]stringaudiences.changed: made a token's audience a single stringis the pkg/tokens type change. It comes after the behaviour commit, not before it. With astringaudience,FinalizeAuthorizationcan't mint[I, Ax], and userinfo's resource validator required I inaud, so a type-only first commit couldn't pass the tests. Each commit compiles and passesmake test.added: documented the issuer and single-audience token modelcovers README, pkg/client docs, and the consent-integrate skills.Public API changes in pkg/tokens and pkg/testing
RefreshTokenOptions.AudienceandAccessTokenOptions.Audienceare nowstring.AccessToken.Audience()andRefreshToken.Audience()now returnstring.Validatoris nowValidateIssuer(string) bool,ValidateAudience(string) bool, andVerifySignature(...).ValidateIssuerreplacesValidateDomain.ValidateAudiencereplacesValidateAudiencesandShouldValidateAudience.Clientaccepts only an exact, non-blank match.Serveraccepts any audience.audis missing, blank, contains whitespace (a space-separated list), or isn't a JSON string. This applies to both validators.CapabilityBudgetcounts the issuer once inissand the audience once inaud.TestEnv.IssueAccessTokenWithAudienceandIssueRefreshTokenWithAudiencenow take astringaudience.New tests
internal/server/server_test.gouses thewebClientOptionsthatbuildProdAuthConfigbuilds, extracted for the test. App access and refresh tokens are rejected and the web UI's own tokens decode.internal/integration/audience_test.goruns end to end with I set to the Consent host. App cookies on/adminredirect to login without a refresh call, and Consent cookies on the app are refused.internal/service/audience_test.go): the app's validator rejects other apps' and Consent's tokens. Refresh checks group access, and losing access denies refresh and userinfo and revokes the token.consentweb token getsErrInsufficientScope. Aconsenttoken gets no capabilities or groups, and one with the groups scope is denied.FinalizeAuthorizationandLoginmint exactly one audience. Refresh of an unknown audience revokes the token.pkg/tokens/audience_internal_test.go). Existing issuance tests cover rejection of blank audiences.TestCapabilityBudget_CountsIssuerAndAudienceOnce.Existing tests edited
(a) Type-driven:
[]stringtostring, and the Validator method renamesStoreTestRefreshTokenandIssueTestRefreshToken.testAudience1/2and call sites.IssueRefreshTokenoptions inTestApplyIntegrationManifest_LongIssuerDomainStillIssuesLargestCatalog.TestAccessToken_Issue_EmptyAudienceandTestRefreshToken_Issue_EmptyAudience:nilbecame"".TestServer_IssueAccessToken_InvalidAudienceandTestServer_IssueRefreshToken_InvalidAudience: theemptyandwhitespacecases now use string values.TestClient_ValidateDomainbecameTestClient_ValidateIssuer, andTestServer_ValidateDomainbecameTestServer_ValidateIssuer.TestClient_ValidateAudiences_SinglebecameTestClient_ValidateAudience_Single.TestClient_ShouldValidateAudiencebecameTestClient_ValidateAudience_Blank, which asserts that a blank audience is rejected.TestServer_ShouldValidateAudiencebecameTestServer_ValidateAudience, which asserts that any audience is accepted.issueExpiredAccessTokenparameter type. The harness now takes its issuer domain fromnewE2EHarnessWithIssuer.newE2EHarnesskeeps the old fixed issuer, and no assertions changed.(b) Tests that encoded the old model: I in
aud, multi-audience tokens, or the I-skip ruleconsentAudience) fromaudinTestAPIUserInfo_IdentityOnly,TestAPIUserInfo_ProfileScope,TestAPIUserInfo_CapabilitiesAndGroupsAreLiveAndNotCached,TestAPIUserInfo_RequiresIdentityScope, andTestAPIUserInfo_BearerSchemeIsCaseInsensitive.TestAPIUserInfo_RequiresConsentAudiencebecameTestAPIUserInfo_RejectsUnknownAudience, and its expected status changed from 400 to 403. The token now decodes, and its unregistered audience is access denied.audinTestRefreshAccessToken_AccessDeniedWhenGroupAccessLost,TestUserInfoGroupsRequireScopeAndListOnlyGrantingGroups, and thesetupCapabilityAuthhelper. The helper is used byTestCapabilitiesAreIssuedWithoutScopeAndReadLiveandTestCapabilitiesFollowRemainingGrantingGroups.TestApplyIntegrationManifest_LargestAcceptedCatalogStillIssuesTokensno longer puts I inaud.TestApplyIntegrationManifest_LongIssuerDomainStillIssuesLargestCatalogno longer puts I inaud, and its comment no longer says the issuer is carried twice.TestCapabilityBudget_EncodedSizeMatchesIssuedTokenandTestCapabilityBudget_CheckAgreesWithIssuernow issue the comparison token with one audience.TestClient_ValidateAudiences_Multiple, nowTestClient_ValidateAudience_Multiple, asserts that a list containing the audience is rejected.*_Issue_BlankAudienceEntrynow uses the list"aud other"instead of["aud", ""], and expects rejection.*_Fieldsnow issues one audience instead of two.blankcase (["aud", ""]) in the InvalidAudience tables became alistcase.TestServer_MultipleAudiencesbecameTestServer_AcceptsAnyAudience.No other existing test failed.
Notes
service.BuildIntegrationCapabilityBudgetstays where it is here. It will also conflict with PR #6 ininternal/service/auth_test.go. New service tests are inaudience_test.goto keep that conflict small. Resolve both when merging.pkg/testing.TestVerifiervalidates with a server validator, so it still accepts any audience from its own key. This was already true, and dev mode issuesaud = Ponly, so nothing there assumed multiple audiences. cmd/dev-client already usedInitClientwith its single audience.🤖 Generated with Claude Code
91caae5474to50f1fba727c19e7a5481toe1e6a4e70be76f0ea7f4tod7f5c4780atwo small comment changes,
@ -85,2 +86,2 @@Subject stringAudience []stringSubject string// Audience names the one integration the token is issued to.comment is unnecessary
@ -94,2 +96,2 @@Subject stringAudience []stringSubject string// Audience names the one integration the token is issued to.comment is unnecessary
794c517bf2to627ad17811