@@ -71,23 +71,28 @@ func WithCurrentAPITokenAndOrgMiddleware(apiTokenUC *biz.APITokenUseCase, orgUC
7171
7272 // We've received an API-token
7373 if claimsHaveAudience (genericClaims , apitoken .Audience ) {
74- var err error
75- tokenID , ok := genericClaims ["jti" ].(string )
76- if ! ok || tokenID == "" {
74+ claims , err := apitoken .ClaimsFromMap (genericClaims )
75+ if err != nil {
76+ // This control plane never signs a claim of the wrong type. The log line never
77+ // includes the raw token or the claims map.
78+ id , _ := genericClaims ["jti" ].(string )
79+ logger .Errorw ("msg" , "[authN] API token claims do not decode" , "id" , id , "error" , err )
80+
7781 return nil , errors .New ("error mapping the API-token claims" )
7882 }
7983
80- // Project ID is optional
81- projectID , _ := genericClaims ["project_id" ].(string )
82-
83- workflowID , _ := genericClaims ["workflow_id" ].(string )
84+ if claims .ID == "" {
85+ return nil , errors .New ("error mapping the API-token claims" )
86+ }
8487
85- ctx , err = setCurrentOrgAndAPIToken (ctx , apiTokenUC , orgUC , tokenID , projectID , workflowID )
88+ ctx , _ , err = setCurrentOrgAndAPIToken (ctx , apiTokenUC , orgUC , claims , logger )
8689 if err != nil {
8790 return nil , fmt .Errorf ("error setting current org and user: %w" , err )
8891 }
8992
90- logger .Infow ("msg" , "[authN] processed credentials" , "id" , tokenID , "type" , "API-token" , "projectID" , projectID )
93+ // legacy_claims marks the tokens minted before the scope claims, to plan the end of
94+ // their support
95+ logger .Infow ("msg" , "[authN] processed credentials" , "id" , claims .ID , "type" , "API-token" , "projectID" , claims .ProjectID , "legacy_claims" , ! claims .HasScopeClaims ())
9196 }
9297
9398 return handler (ctx , req )
@@ -126,81 +131,54 @@ func WithAttestationContextFromAPIToken(apiTokenUC *biz.APITokenUseCase, orgUC *
126131 return nil , errors .New ("error mapping the API-token claims" )
127132 }
128133
129- ctx , err := setRobotAccountFromAPIToken (ctx , apiTokenUC , tokenID )
130- if err != nil {
131- return nil , fmt .Errorf ("error extracting organization from APIToken: %w" , err )
132- }
133-
134- ctx , err = setCurrentOrgAndAPIToken (ctx , apiTokenUC , orgUC , tokenID , claims .ProjectID , claims .WorkflowID )
134+ ctx , token , err := setCurrentOrgAndAPIToken (ctx , apiTokenUC , orgUC , claims , logger )
135135 if err != nil {
136136 return nil , fmt .Errorf ("error setting current org and user: %w" , err )
137137 }
138138
139- logger .Infow ("msg" , "[authN] processed credentials" , "id" , tokenID , "type" , "API-token" )
139+ // The robot account comes from the row that VerifyClaims checked.
140+ ctx = WithRobotAccount (ctx , & RobotAccount {OrgID : token .OrganizationID .String (), ProviderKey : attjwtmiddleware .APITokenProviderKey })
141+
142+ logger .Infow ("msg" , "[authN] processed credentials" , "id" , tokenID , "type" , "API-token" , "legacy_claims" , ! claims .HasScopeClaims ())
140143
141144 return handler (ctx , req )
142145 }
143146 }
144147}
145148
146- func setRobotAccountFromAPIToken (ctx context.Context , apiTokenUC * biz.APITokenUseCase , tokenID string ) (context.Context , error ) {
147- if tokenID == "" {
148- return nil , errors .New ("error retrieving the key ID from the API token" )
149- }
150-
151- // Check that the token exists and is not revoked
152- token , err := apiTokenUC .FindByID (ctx , tokenID )
153- if err != nil {
154- return nil , fmt .Errorf ("error retrieving the API token: %w" , err )
155- } else if token == nil {
156- return nil , errors .New ("API token not found" )
157- }
158-
159- // Note: Expiration time does not need to be checked because that's done at the JWT
160- // verification layer, which happens before this middleware is called
161- if token .RevokedAt != nil {
162- return nil , errors .New ("API token revoked" )
163- }
164-
165- ctx = WithRobotAccount (ctx , & RobotAccount {OrgID : token .OrganizationID .String (), ProviderKey : attjwtmiddleware .APITokenProviderKey })
166-
167- return ctx , nil
168- }
169-
170- // Set the current organization and API-Token in the context. The project and workflow claims are
171- // cross-checked against the token row, never an authorization input: the row decides what the
172- // token reaches.
173- func setCurrentOrgAndAPIToken (ctx context.Context , apiTokenUC * biz.APITokenUseCase , orgUC * biz.OrganizationUseCase , tokenID , projectIDInClaim , workflowIDInClaim string ) (context.Context , error ) {
174- if tokenID == "" {
175- return nil , errors .New ("error retrieving the key ID from the API token" )
149+ // setCurrentOrgAndAPIToken loads the token's row and checks it against the signed claims. Then it
150+ // puts the organization and the token in the context, and returns the row. The claims fix the
151+ // token's scope, organization, project and workflow. The row must match them. The policies, the
152+ // product project list and the revocation come only from the row.
153+ func setCurrentOrgAndAPIToken (ctx context.Context , apiTokenUC * biz.APITokenUseCase , orgUC * biz.OrganizationUseCase , claims * apitoken.CustomClaims , logger * log.Helper ) (context.Context , * biz.APIToken , error ) {
154+ if claims == nil || claims .ID == "" {
155+ return nil , nil , errors .New ("error retrieving the key ID from the API token" )
176156 }
177157
178158 // Check that the token exists and is not revoked
179- token , err := apiTokenUC .FindByID (ctx , tokenID )
159+ token , err := apiTokenUC .FindByID (ctx , claims . ID )
180160 if err != nil {
181- return nil , fmt .Errorf ("error retrieving the API token: %w" , err )
161+ return nil , nil , fmt .Errorf ("error retrieving the API token: %w" , err )
182162 } else if token == nil {
183- return nil , errors .New ("API token not found" )
163+ return nil , nil , errors .New ("API token not found" )
184164 }
185165
186- // Make sure that the projectID that comes in the token claim matches the one in the DB
187- if projectIDInClaim != "" {
188- if token .ProjectID == nil || token .ProjectID .String () != projectIDInClaim {
189- return nil , errors .New ("API token project mismatch" )
166+ if err := token .VerifyClaims (claims ); err != nil {
167+ // A row should never disagree with its signed claims. If it does, something wrote the row
168+ // incorrectly. The log line gives the reason and never includes the raw JWT. The caller
169+ // learns only that the token could not be verified.
170+ if errors .Is (err , biz .ErrAPITokenClaimsMismatch ) {
171+ logger .Errorw ("msg" , "[authN] API token row disagrees with its signed claims" , "id" , claims .ID , "error" , err )
172+ return nil , nil , biz .ErrAPITokenClaimsMismatch
190173 }
191- }
192174
193- // Same defense in depth for the workflow claim
194- if workflowIDInClaim != "" {
195- if token .WorkflowID == nil || token .WorkflowID .String () != workflowIDInClaim {
196- return nil , errors .New ("API token workflow mismatch" )
197- }
175+ return nil , nil , err
198176 }
199177
200178 // Note: Expiration time does not need to be checked because that's done at the JWT
201179 // verification layer, which happens before this middleware is called
202180 if token .RevokedAt != nil {
203- return nil , errors .New ("API token revoked" )
181+ return nil , nil , errors .New ("API token revoked" )
204182 }
205183
206184 // Handle instance admin tokens
@@ -212,9 +190,9 @@ func setCurrentOrgAndAPIToken(ctx context.Context, apiTokenUC *biz.APITokenUseCa
212190 // Load organization from header
213191 org , err := orgUC .FindByName (ctx , orgName )
214192 if err != nil {
215- return nil , fmt .Errorf ("error retrieving the organization: %w" , err )
193+ return nil , nil , fmt .Errorf ("error retrieving the organization: %w" , err )
216194 } else if org == nil {
217- return nil , errors .New ("organization not found" )
195+ return nil , nil , errors .New ("organization not found" )
218196 }
219197
220198 ctx = entities .WithCurrentOrg (ctx , & entities.Org {Name : org .Name , ID : org .ID , CreatedAt : org .CreatedAt , Suspended : org .Suspended })
@@ -225,15 +203,18 @@ func setCurrentOrgAndAPIToken(ctx context.Context, apiTokenUC *biz.APITokenUseCa
225203 } else {
226204 org , err := orgUC .FindByID (ctx , token .OrganizationID .String ())
227205 if err != nil {
228- return nil , fmt .Errorf ("error retrieving the organization: %w" , err )
206+ return nil , nil , fmt .Errorf ("error retrieving the organization: %w" , err )
229207 } else if org == nil {
230- return nil , errors .New ("organization not found" )
208+ return nil , nil , errors .New ("organization not found" )
231209 }
232210
233211 // Set the current organization in the context
234212 ctx = entities .WithCurrentOrg (ctx , & entities.Org {Name : org .Name , ID : org .ID , CreatedAt : org .CreatedAt , Suspended : org .Suspended })
235213 }
236214
215+ // Every value here comes from the row. VerifyClaims checked the scope, project and workflow
216+ // against the signed claims. The policies, the project list and the system flag come only from
217+ // the row.
237218 ctx = entities .WithCurrentAPIToken (ctx , & entities.APIToken {
238219 ID : token .ID .String (),
239220 Name : token .Name ,
@@ -243,19 +224,18 @@ func setCurrentOrgAndAPIToken(ctx context.Context, apiTokenUC *biz.APITokenUseCa
243224 ProjectName : token .ProjectName ,
244225 WorkflowID : token .WorkflowID ,
245226 WorkflowName : token .WorkflowName ,
246- // Every value here comes from token.*, i.e. the database row
247- Scope : token .Scope ,
248- ScopeID : token .ScopeID ,
249- ProjectIDs : token .ProjectIDs ,
250- Policies : token .Policies ,
251- IsSystem : token .IsSystem ,
227+ Scope : token .Scope ,
228+ ScopeID : token .ScopeID ,
229+ ProjectIDs : token .ProjectIDs ,
230+ Policies : token .Policies ,
231+ IsSystem : token .IsSystem ,
252232 })
253233
254234 // Set the authorization subject that will be used to check the policies
255235 subjectAPIToken := authz.SubjectAPIToken {ID : token .ID .String ()}
256236 ctx = WithAuthzSubject (ctx , subjectAPIToken .String ())
257237
258- return ctx , nil
238+ return ctx , token , nil
259239}
260240
261241func WithAPITokenUsageUpdater (apiTokenUC * biz.APITokenUseCase , logger * log.Helper ) middleware.Middleware {
0 commit comments