Skip to content

feat(oauth-server): Sign in with Fleetbase (OAuth 2.0 / OpenID Connect provider) - #324

Merged
roncodes merged 8 commits into
release/v1.6.74from
feature/oauth-server
Oct 25, 2026
Merged

roncodes merged 8 commits into
release/v1.6.74from
feature/oauth-server

Conversation

@roncodes

Copy link
Copy Markdown
Member

Makes Fleetbase an OAuth 2.0 / OpenID Connect provider, so registered apps can offer Continue with Fleetbase and, with the user's consent, act on one organization they administer. AppHelm is the first app to use it.

Includes #323 (App Review access). This branch is based on it, so merge #323 first. After that, this PR shows only its own two commits.

Library

league/oauth2-server 8.5, used directly, does the protocol and the crypto:

  • payload encryption for codes and refresh tokens;
  • PKCE verification;
  • JWT signing and validation.

The plan was Laravel Passport 12 with jeremy379/laravel-openid-connect. Neither works here:

  • Passport 12 can't be installed. It needs phpseclib ^2|^3, and Socialite 5.31 (used for the sign-in work) needs ^4. Passport 13 needs Laravel 11.
  • The OIDC package is unusable here. Composer auto-discovers its service provider, so it would also register Passport's grants (client credentials, personal access) and routes on every install.

Fleetbase's code is the glue around league: repositories, a 100-line ID token response on lcobucci/jwt (already a dependency), and controllers.

What's in it

  • Flow. Authorization code with PKCE S256 required for every client. Refresh tokens rotate on every use. Also: RFC 7009 revocation, OIDC ID tokens (nonce, auth_time, at_hash, kid), /oauth/userinfo, /oauth/jwks, /.well-known/openid-configuration, and iss on the authorization response (RFC 9207).
  • Redirect URIs and errors. Redirect URIs match exactly. Errors about the client itself never redirect, and are shown on the console page instead.
  • Consent in the console. GET /oauth/authorize checks the request and sends the user to the console's consent screen. int/v1/oauth-server/authorization (GET/POST/DELETE) runs on the console session, so sign-in and two-factor rules apply. Nothing creates an account.
  • Organization binding.
    • Every code and token belongs to a grant (oauth_grants: user, client, organization, scopes).
    • Only an organization's owner, its Administrator role, or a system admin can share it.
    • Every refresh and every API call re-checks that the user is active and still administers the organization; if not, the grant is revoked.
    • Endpoints act only on the token's organization, never on the console's current one.
  • Replay protection. A replayed code revokes its tokens; a replayed refresh token revokes the grant.
  • Scopes. openid, profile, email, organization:read (GET /oauth/v1/organization). Extensions register more with OAuthScopes::register().
  • App Review over OAuth. A scope can cover App Review types, served by GET|PUT|DELETE /oauth/v1/review-access/{type}/{id} on top of feat(app-review): App Review access for apps built on Fleetbase #323's AppReview.
  • Admin (int/v1/oauth-server/apps, system admins): register an app (secret hashed, shown once), edit, rotate the secret (the old one stops at once), turn off (revokes every grant), list grants and revoke one.
  • Users (int/v1/oauth-server/connected-apps): list and remove their apps. The account holder gets an email whenever they authorize a new app.
  • Artisan: oauth-server:keys (or --show for env), oauth-server:client create|rotate|list, oauth-server:prune (scheduled daily).
  • Sandbox mode. OAuth rows always use the core connection, even when the console is in sandbox mode.

Upgrade

  • Run php artisan migrate. It adds the oauth_clients, oauth_grants, oauth_auth_codes, oauth_access_tokens and oauth_refresh_tokens tables.
  • Give every API instance the same key pair: run php artisan oauth-server:keys, or set OAUTH_SERVER_PRIVATE_KEY / OAUTH_SERVER_PUBLIC_KEY.
  • Set OAUTH_SERVER_ISSUER if the public API URL isn't APP_URL. On Cloud it should be https://api.fleetbase.io.

Tests

tests/Unit/Auth/OAuthServer has 42 tests. They run league's server with the real repositories over sqlite and the real migration:

  • the full flow and ID token claims;
  • PKCE required, plain refused, wrong or missing verifier;
  • unregistered, extra-query, http and token-time redirect URIs;
  • expired and replayed codes, and codes from another client;
  • scopes not registered for the client, scope checks on endpoints, no widening on refresh, a scope removed from the client;
  • organization eligibility;
  • refresh rotation and replay;
  • leaving the organization or being deactivated, and forged tokens;
  • revoking a refresh token, an access token and a grant;
  • secret rotation and unsupported grants;
  • prompt and max_age;
  • discovery, JWKS, userinfo, organization, and review access, including another organization being refused.

The full core-api suite passes. CoreProviderContractsTest is updated for the new scheduled prune.

Not yet run end to end: the local Docker stack's MySQL keeps being killed for memory.

Related: console fleetbase/fleetbase (consent screen, OAuth Apps, Connected apps), fleetbase/storefront (storefront scopes), fleetbase/fleetbase.io (docs).

Apple and Google reviewers can't receive a texted sign-in code. Extensions register what their app signs reviewers in to (a Storefront store, an organization's Navigator app) with a review number from the 555-0100-0199 range reserved for fiction; the owner turns access on in the Console and gives the app store that number and a six-digit code, stored hashed and accepted only for that subject. New review_accesses table, Fleetbase\Support\AppReview, and GET|PUT int/v1/review-access/{type}/{id}.

Numbers in the fictional range are never texted: SmsService answers as sent without calling a provider, and SMS verification codes are saved without reserving or sending. Tests that sent to 555-01xx numbers use other test numbers.
…nect provider

Registered apps can offer "Continue with Fleetbase" and, with the user's
consent, act on one organization they administer. Built on
league/oauth2-server: authorization code with PKCE (S256, every client),
rotating refresh tokens, RFC 7009 revocation, ID tokens, userinfo, JWKS and
discovery. Codes and tokens are bound to a grant and its organization, which
every refresh and API call re-checks. Consent runs on the console session;
admins manage apps and users manage connected apps.
…sole sandbox mode

The consent screen runs on the console session, which switches models to the
sandbox connection when test mode is on. OAuth rows and the organization
checks now always use the core connection. Also raise the per-IP limit on the
token and resource endpoints, since an app's server makes every call from one
address.
…ints, commands and edge cases

core-api CI requires full coverage. Adds tests for the console endpoints, the
authorize redirect, the artisan commands, the authorized-app notice, real
spatie roles in the organization check, and the protocol edge cases. The
authorize endpoint builds its RedirectResponse directly.
…permission check

Unknown types, any type's number, a custom resolver, an unsaved or unregistered subject, the table before migration, an unreadable table, and the Console endpoint for nobody, a viewer, a permission or wildcard holder, an administrator and an unseeded permission. Brings both files to the 100% coverage gate.
@codecov

codecov Bot commented Oct 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (018c308) to head (f4e951d).

Additional details and impacted files
@@                  Coverage Diff                  @@
##             release/v1.6.74      #324     +/-   ##
=====================================================
  Coverage             100.00%   100.00%             
- Complexity              8999      9507    +508     
=====================================================
  Files                    476       514     +38     
  Lines                  28231     29566   +1335     
=====================================================
+ Hits                   28231     29566   +1335     
Flag Coverage Δ
backend 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@roncodes
roncodes merged commit fb6af2b into release/v1.6.74 Oct 25, 2026
5 checks passed
@roncodes
roncodes deleted the feature/oauth-server branch October 25, 2026 02:32
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