Skip to content

Support authorization check in Consumer authenticateJwtLocal#155

Merged
jlai-stytch merged 4 commits into
mainfrom
jlai/support_authz_check_local
Jul 20, 2026
Merged

Support authorization check in Consumer authenticateJwtLocal#155
jlai-stytch merged 4 commits into
mainfrom
jlai/support_authz_check_local

Conversation

@jlai-stytch

@jlai-stytch jlai-stytch commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Description

Adds support for the authorizationCheck param for the Consumer (B2C) authenticateJwtLocal() method. Currently only the B2B method supports it:

authorizationCheck: AuthorizationCheck? = null,

but not the B2C one:

Since the RBAC helper in this repo only exists for B2B today, this PR also generalizes the existing PolicyCache. It extracts a consumer-flavored authorization check that skips the tenancy check and org-policy merge, operating only on the global policy's roles.

I tried to keep the implementation in line with Node SDK's shared-base + thin-wrapper structure, since that SDK supports the authZ check for both B2C and B2B.

Since this touches some codegen'd files and not just the manual/hand-written methods, this also requires a small PR there: https://github.com/stytchauth/sdk-codegen/pull/388

None of the B2B-specific files were modified.

New files

  • common/BasePolicyCache.kt — shared base holding the cache/refresh machinery (getPolicy, lazy + background refresh, cancelBackgroundRefresh), a normalized RoleView/PermissionView, the shared hasRolePermission matcher, and PermissionException (moved here, retyped to Any so it works for both verticals' AuthorizationCheck)
  • common/ConsumerPolicyCache.kt — thin B2C wrapper over the consumer RBAC client. No org policy, no tenancy check — evaluates purely against the project policy's roles
  • common/ConsumerPolicyCacheTest.kt — consumer analogue of the B2B tests (matching resource/action, wildcard, permission denied, fetch-once/caching, cancel)

Modified files

  • common/PolicyCache.kt — B2B cache keeps its name (so zero B2B call-site churn) but now extends BasePolicyCache<Policy>; retains org-policy cache + TenancyException, and maps its roles into the shared matcher
  • consumer/api/sessions/Sessions.kt — added authorizationCheck: AuthorizationCheck? = null (before leeway) to all three authenticateJwtLocal overloads; the suspend impl throws MissingRolesClaim if roles are absent, else calls policyCache.performAuthorizationCheck; SessionsImpl gained a policyCache constructor param; imports + ADDIMPORT hints added
  • consumer/StytchClient.kt — constructs ConsumerPolicyCache, injects it into SessionsImpl, cancels it in close(). Pass policyCache to IDPImpl.
  • consumer/api/sessions/SessionsTest.kt — passes a relaxed policyCache mock to the updated constructor
  • consumer/api/idp/IDP.kt — added policyCache: ConsumerPolicyCache param + import

Motivation

Contracted customer feature request, see https://twilio-engineering.atlassian.net/browse/STDS-347

Testing

  • Added unit tests
  • Manually tested passing in authZ check in local JWT validation

Manual validation process

I first created a custom role called foo_reader with * permissions on a custom resource called foo, and assigned my User this role.

Then, I used the SDK's workbench to test by passing in a valid session_jwt for my User and an authorizationCheck:

suspend fun main() {
    StytchClient(
        projectId = "project-test-...",
        secret = "secret-test-...",
    ).use { stytchClient ->
        val sessionJwt = "eyJ..."
        val result =
            stytchClient.sessions.authenticateJwtLocal(
                jwt = sessionJwt,
                maxTokenAgeSeconds = null,
                authorizationCheck =
                    AuthorizationCheck(
                        resourceId = "foo",
                        action = "read",
                    ),
            )
        println(result)
    }
}

The build succeeds: https://gradle.com/s/t5k6xrjvs7cla

and I correctly get a Success:

Success(value=Session(sessionId=session-test-900a6880-45fb-4336-9ba9-c62cd2d79b4b, userId=user-test-96b6c8b3-44f3-45dc-a325-f70b851021ae, authenticationFactors=[AuthenticationFactor(type=MAGIC_LINK, deliveryMethod=EMAIL, lastAuthenticatedAt=2026-07-20T20:50:25Z, createdAt=null, updatedAt=null, emailFactor=EmailFactor(emailId=email-test-5bd5dda6-903c-447f-a2cd-29d39bc9a1c9, emailAddress=jlai@stytch.com), phoneNumberFactor=null, googleOAuthFactor=null, microsoftOAuthFactor=null, appleOAuthFactor=null, webauthnFactor=null, authenticatorAppFactor=null, githubOAuthFactor=null, recoveryCodeFactor=null, facebookOAuthFactor=null, cryptoWalletFactor=null, amazonOAuthFactor=null, bitbucketOAuthFactor=null, coinbaseOAuthFactor=null, discordOAuthFactor=null, figmaOAuthFactor=null, gitLabOAuthFactor=null, instagramOAuthFactor=null, linkedInOAuthFactor=null, shopifyOAuthFactor=null, slackOAuthFactor=null, snapchatOAuthFactor=null, spotifyOAuthFactor=null, steamOAuthFactor=null, tikTokOAuthFactor=null, twitchOAuthFactor=null, twitterOAuthFactor=null, embeddableMagicLinkFactor=null, biometricFactor=null, samlSSOFactor=null, oidcSSOFactor=null, salesforceOAuthFactor=null, yahooOAuthFactor=null, hubspotOAuthFactor=null, slackOAuthExchangeFactor=null, hubspotOAuthExchangeFactor=null, githubOAuthExchangeFactor=null, googleOAuthExchangeFactor=null, impersonatedFactor=null, oauthAccessTokenExchangeFactor=null, trustedAuthTokenFactor=null)], roles=[stytch_user, foo_reader], startedAt=2026-07-20T20:50:25Z, lastAccessedAt=2026-07-20T20:56:51Z, expiresAt=2026-07-21T20:50:25Z, attributes=Attributes(ipAddress=170.85.154.181, userAgent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36), customClaims={https://stytch.com/session={id=session-test-900a6880-45fb-4336-9ba9-c62cd2d79b4b, started_at=2026-07-20T20:50:25Z, last_accessed_at=2026-07-20T20:56:51Z, expires_at=2026-07-21T20:50:25Z, attributes={user_agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36, ip_address=170.85.154.181}, authentication_factors=[{type=magic_link, delivery_method=email, last_authenticated_at=2026-07-20T20:50:25Z, email_factor={email_id=email-test-5bd5dda6-903c-447f-a2cd-29d39bc9a1c9, email_address=jlai@stytch.com}}], roles=[stytch_user, foo_reader]}}))

@jlai-stytch
jlai-stytch requested a review from a team as a code owner July 17, 2026 00:57
@jlai-stytch
jlai-stytch requested review from logan-twilio and removed request for a team July 17, 2026 00:57
@jlai-stytch
jlai-stytch marked this pull request as draft July 17, 2026 00:57
@jlai-stytch
jlai-stytch marked this pull request as ready for review July 20, 2026 17:06
@jlai-stytch
jlai-stytch merged commit 4b062c2 into main Jul 20, 2026
12 checks passed
@jlai-stytch
jlai-stytch deleted the jlai/support_authz_check_local branch July 20, 2026 20:40
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.

2 participants