Skip to content

feat(internal): jwtsign + public-request auth exemptions - #1375

Merged
NitinKumar004 merged 3 commits into
developmentfrom
feat/internal-jwtsign-public-auth
Sep 27, 2026
Merged

NitinKumar004 merged 3 commits into
developmentfrom
feat/internal-jwtsign-public-auth

Conversation

@NitinKumar004

@NitinKumar004 NitinKumar004 commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Work item F1 from build-out plan A (Cognito, API Gateway, AppSync). It lays two foundations the Cognito and API Gateway PRs build on, and it tightens how --enforce-auth treats STS session credentials.

internal/jwtsign

  • RS256 Sign and Verify. Verify checks alg (only RS256, so none and HS256 are rejected), kid, signature, exp (required), nbf, and iat with a 60 second skew allowance, against a config.Clock.
  • Typed errors. ErrExpired is kept apart from ErrInvalidToken so Cognito can return its separate "token has expired" message.
  • NewRSAKey makes 2048-bit keys. The kid is the RFC 7638 JWK thumbprint, so it survives persistence and two keys never share one.
  • JWKS(keys...) publishes the public halves in Cognito's jwks.json field set. Rotation means adding a key and keeping the old one in the verify set until its tokens expire.
  • MarshalPKCS8 and ParsePKCS8 cover snapshot persistence.

Nothing calls the package yet. Cognito token issuance (C2) is the first consumer.

Public (noAuth) requests under --enforce-auth

The exemption is decided by the handler that dispatch will pick, for that exact request:

  • server.PublicRequester is a new optional handler interface: PublicRequest(r) bool. A handler implements it only for the public routes it serves itself.
  • The gate runs the dispatcher's own Match on a probe copy of the request. The probe has fresh form state and the same body bytes dispatch will read. The gate then asks the matched handler. When no handler matches, the request is not exempt.
  • A form body or query string that does not parse fails closed.

Implementations:

  • Cognito (server/aws/cognito/public.go). Only the noAuth cognito-idp operations, and only as POST / with the operation's X-Amz-Target. The list comes from the botocore model ("auth": ["smithy.api#noAuth"]). TestPublicOpsMatchSDKModel re-derives it from the aws-sdk-go-v2 auth resolver and fails on drift.
  • API Gateway. Only data-plane invokes (execute-api host, or the _user_request_ path). ServeHTTP routes on the same two predicates.

A request that borrows a public marker is still gated: a hosted-UI Host, a /.well-known path, an execute-api Host with a DynamoDB target, or an AppSync host on /v1/apis. So is anything that is not one of these two surfaces.

STS session credentials are authorized

Before this change, temporary (ASIA) credentials were authenticated but never authorized. Each STS session now records its owner, and the gate authorizes requests as that owner:

  • A role session (AssumeRole, AssumeRoleWithWebIdentity, AssumeRoleWithSAML) is checked strictly against the role's own policies. A role with no allowing policy is denied, and so is a role that does not exist. The root and no-policy bootstrap shortcuts do not apply.
  • A GetSessionToken or GetFederationToken session is checked as the calling user, with the same rules as that user's long-term key. A session minted with session credentials inherits its owner, so it cannot widen permissions.

Authorization still covers JSON-RPC only. Query and REST requests stay authenticate-only, for long-term keys and sessions alike. That limit already existed and is documented in authzgate.go.

Deferred (each has a PR slot)

  • STS AssumeRoleWithWebIdentity / AssumeRoleWithSAML stay gated. The handler validates neither the token nor the assertion against a registered OIDC/SAML provider, and it does not check the role's trust policy. Opening these unsigned would let anyone mint credentials. They open once that validation lands.
  • cognito-identity GetId / GetCredentialsForIdentity / GetOpenIdToken / UnlinkIdentity. No handler serves identity pools yet (C10).
  • Cognito JWKS, /.well-known/openid-configuration and the hosted UI. These become public when the Cognito handler serves them as target-less GETs (C-series).
  • AppSync GraphQL data plane (S-series).
  • Per-method AWS_IAM authorization on execute-api invokes (A5b/A8).

Tests

  • internal/jwtsign: round trip, tamper, wrong key under the same kid, alg none/HS256, malformed tokens, the expiry boundary, nbf, iat skew, missing exp, unknown kid, rotation, JWKS contents, and the PKCS#8 round trip.
  • server/aws/authbypass_test.go: a table of 30 bypass attempts that must all get the gate's 403 MissingAuthenticationToken. It covers every reviewed repro plus these variants: duplicate and case-changed Actions, a bad query string, case-changed Cognito targets, and execute-api markers on DynamoDB, Lambda and IAM requests. A separate test sends the real STS SDK's anonymous AssumeRoleWithWebIdentity and expects rejection.
  • server/aws/publicauth_test.go: unsigned public ops are let through, authz skips public ops, and STS sessions are authorized as their owner (missing role, role with no policies, role outside its policy, a policy-limited user's session, an unrestricted user's session).
  • server/aws/cognito/public_test.go: the model cross-check and the route shape.
  • contrib/server: an --enforce-auth case. InitiateAuth gets past the gate and CreateUserPool is rejected.

The bypass and session tests fail on the previous commit of this branch.

E2E (cloudemu serve --enforce-auth, AWS on 57966)

All the curl repros are rejected with 403 MissingAuthenticationToken:

  • unsigned AssumeRoleWithWebIdentity for a nonexistent role
  • unsigned AssumeRoleWithSAML
  • a bad body with a public Action in the query string (both the GetSessionToken and AssumeRole forms)
  • a duplicate or lower-case Action
  • a hosted-UI Host or a /_cognito path with CreateUserPool
  • the well-known path with a ListUserPools target
  • an AppSync host with CreateGraphqlApi
  • an execute-api Host with a DynamoDB target or a Lambda path
  • a lower-case Cognito op
  • DescribeInstances and iam CreateUser

These reach their handler:

  • an unsigned InitiateAuth
  • execute-api invokes by host and by path

With the aws CLI, --no-sign-request cognito-idp initiate-auth gets past the gate. --no-sign-request sts assume-role-with-web-identity is rejected.

@NitinKumar004
NitinKumar004 marked this pull request as ready for review September 27, 2026 16:15
@NitinKumar004
NitinKumar004 merged commit 2893c7e into development Sep 27, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant