Skip to content

BFF: baseUrl is the backend, and permissions come from the session - #49

Merged
brionmario merged 1 commit into
thunder-id:mainfrom
brionmario:granted-scopes-backend-session
Oct 30, 2026
Merged

brionmario merged 1 commit into
thunder-id:mainfrom
brionmario:granted-scopes-backend-session

Conversation

@brionmario

@brionmario brionmario commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Purpose

Two changes to backend mode (endpoints.backend), where a backend the app owns runs sign-in and keeps the tokens (the BFF pattern of RFC 10017).

baseUrl is the backend. The backend's own address used to be endpoints.backend.baseUrl, while the top-level baseUrl was the API behind the proxy. Now the top-level baseUrl is the backend, and backend endpoints resolve against it, like the server's address in redirect mode. API requests go to the backend's proxy, which maps them to the API from its own configuration, so the app never needs the API's address: the SDK's own calls go to {baseUrl}{proxy}/users/me, {baseUrl}{proxy}/groups and so on.

Permissions come from the session. ThunderIDClient.getGrantedPermissions() read the permissions by decoding the access token. In backend mode getAccessToken() asks the backend's token endpoint for one, which a BFF doesn't offer, since keeping tokens away from the app is the point of the pattern (RFC 10017 §6.1). The app then saw no permissions, and Compose's rememberGrantedPermissions() hid everything. An OAuth client learns what it was granted from the token response's scope (RFC 6749 §5.1), so the backend reports it with the session, and the SDK reads it from there.

Approach

  • Backend base: in backend mode the config's baseUrl is the backend and must use HTTPS. endpoints.backend.baseUrl is removed.
  • Endpoints: a relative endpoint (starting with /) resolves against baseUrl, and an absolute HTTPS URL is used as is. Anything else, including an http URL, is refused at initialize(). Unset endpoints still default to paths under /api/auth.
  • API calls go to the proxy: in backend mode, the /users/me profile calls and the management APIs (collections and administration flows) are built on the resolved proxy, {baseUrl}{proxy}/…. Collection overrides in endpoints still win.
  • httpRequest: a relative path is sent under the proxy, and a URL already under the proxy is sent as is, never prefixed twice. A bypass URL goes straight there, as before. Any other absolute URL is refused without being sent.
  • Session contract: BackendSession gains an optional scope, the granted scopes as space-separated text, parsed from the backend's session response.
  • Permissions: in backend mode getGrantedPermissions() reads only the session's scope. It never asks for the access token, and a session without scope grants nothing. Redirect mode is unchanged throughout.
  • Docs: the README gains a "Backend mode" section with a configuration example, and the playground uses THUNDERID_BACKEND_URL as baseUrl and calls /users/me relative to the proxy.

This is the Android port of thunder-id/javascript-sdks#148 (permissions from the session) and #149 (baseUrl is the backend). In #149 JavaScript also added an endpoints.backend.apiBaseUrl; this port doesn't, and thunder-id/javascript-sdks#150 removes it from JavaScript.

Related Issues

  • N/A

Related PRs

Checklist

  • Followed the contribution guidelines.
  • Manual test round performed and verified.
  • Documentation provided. (Add links if there are any)
    • README "Backend mode", playground README.
  • Tests provided. (Add links if there are any)
    • Unit Tests (BackendEndpointsTest: endpoints resolve against baseUrl, absolute HTTPS endpoints used as is, non-HTTPS baseUrl and endpoints refused. BackendModeTest: the SDK's own calls are built on baseUrl and the configured proxy, a relative path goes under the proxy, a URL already under it isn't prefixed twice, a foreign absolute URL is refused, permissions from the session with no token request, no scope grants nothing.)
    • Integration Tests
  • Breaking changes. (Fill if applicable)
    • Breaking changes section filled.
    • breaking change label added.
  • Cross-SDK parity. Exactly one of parity/prs-raised or parity/prs-not-needed added.
    • If parity/prs-raised, the port links are posted as a reply on the parity check's comment.

Verified locally with JDK 17: ./gradlew ktlintCheck testDebugUnitTest assembleDebug passes (336 unit tests, 0 failures), and the playground's compileDebugKotlin passes. Android lint (./gradlew lint) was not run.

Security checks

  • Followed secure coding standards.
  • Confirmed that this PR doesn't commit any keys, passwords, tokens, usernames, or other secrets.

Every backend URL must use HTTPS, and the app never holds the API's address. In backend mode the app never asks for an access token to read permissions; the permissions decide only what the app shows, and the server still checks every request.

@brionmario brionmario added the parity/prs-raised Ports exist in the other SDKs, or are recorded as not applying there label Oct 30, 2026
@coderabbitai

coderabbitai Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

🔀 Cross-SDK feature parity, ports raised

This pull request is labelled parity/prs-raised, so the capability it introduces is expected in the other ThunderID SDKs as well.

Ports (DO NOT EDIT)

  • thunder-id/javascript-sdks, JavaScript (React, Vue, Nuxt, Next.js, Express, React Router, TanStack Router, Browser, Node) => #148, #149, #150
  • thunder-id/ios-sdks, iOS => #41
  • thunder-id/flutter-sdks, Flutter => #62

3 of 3 sibling SDKs accounted for.

Every sibling SDK is accounted for.

The contract is in the SDK development specification.

@brionmario

Copy link
Copy Markdown
Member Author

…ession

In backend mode the config's baseUrl is now the backend (the BFF). A
relative backend endpoint resolves against it, and an absolute HTTPS URL
is used as is. The API whose requests go through the backend's proxy is
endpoints.backend.apiBaseUrl, which defaults to baseUrl.
endpoints.backend.baseUrl is gone. baseUrl, the API base, absolute
endpoints and bypass URLs must use HTTPS. The SDK's own API calls in
backend mode (the /users/me profile and the management APIs) go to the
API base, through the proxy. Redirect mode is unchanged.

getGrantedPermissions() used to decode the access token, which in
backend mode meant asking the backend's token endpoint for one; a BFF
doesn't offer that, so the app saw no permissions. BackendSession now
carries the granted scopes (scope, the token response's scope), and in
backend mode getGrantedPermissions() reads them from the session. A
session without scope grants nothing, and the SDK never asks for a
token. In redirect mode it still reads the access token.

The README documents backend mode, and the playground uses the backend
URL as baseUrl with the ThunderID server as the API base.

Signed-off-by: Brion <info@brionmario.com>
@brionmario
brionmario force-pushed the granted-scopes-backend-session branch from 554a5a4 to 67e5828 Compare October 30, 2026 08:47
@brionmario brionmario changed the title Read granted permissions from the backend session in backend mode BFF: baseUrl is the backend, and permissions come from the session Oct 30, 2026
@brionmario
brionmario merged commit e53d83b into thunder-id:main Oct 30, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

parity/prs-raised Ports exist in the other SDKs, or are recorded as not applying there

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants