Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
082dcd3
feat(webhooks): API-key webhook endpoints and per-endpoint signing se…
roncodes Oct 22, 2026
f4deb0d
fix(files): derive the stored extension from the detected file type
shivanathd Oct 22, 2026
d63e5c7
fix(webhooks): keep the endpoint signing secret out of the activity log
roncodes Oct 23, 2026
3573a7b
chore(release): v1.6.74
roncodes Oct 23, 2026
91bb070
Merge pull request #319 from fleetbase/feat/api-webhook-endpoints-and…
roncodes Oct 23, 2026
018c308
Merge pull request #320 from shivanathd/mindcat/upload-extension-from…
roncodes Oct 23, 2026
84f6562
feat(app-review): App Review access for apps built on Fleetbase
roncodes Oct 24, 2026
5f41a21
docs(release): App Review access in the v1.6.74 notes
roncodes Oct 24, 2026
fe2f96b
feat(routing): Fleetbase Hosted Routing keys at install time
roncodes Oct 24, 2026
638febb
chore(routing): array types for phpstan in the hosted routing support…
roncodes Oct 24, 2026
0994c54
feat(oauth-server): Sign in with Fleetbase, an OAuth 2.0 / OpenID Con…
roncodes Oct 24, 2026
2a7f1e3
fix(oauth-server): keep grants and tokens on the core database in con…
roncodes Oct 24, 2026
8a5dfac
test(oauth-server): use past fixture dates so the date drift check pa…
roncodes Oct 24, 2026
55ab054
test(oauth-server): cover the consent, admin and connected-apps endpo…
roncodes Oct 24, 2026
b5fcd69
test(app-review): cover the registry's edge cases and the endpoint's …
roncodes Oct 24, 2026
f4e951d
Merge remote-tracking branch 'origin/feat/review-access' into feature…
roncodes Oct 24, 2026
6e31727
Merge pull request #323 from fleetbase/feat/review-access
roncodes Oct 25, 2026
fb6af2b
Merge pull request #324 from fleetbase/feature/oauth-server
roncodes Oct 25, 2026
053505b
docs(release): v1.6.74 notes for Sign in with Fleetbase and App Revie…
roncodes Oct 25, 2026
862a3cc
Merge pull request #325 from fleetbase/feature/hosted-routing-instanc…
roncodes Oct 25, 2026
2b964bd
fix(oauth-server): rename AuthorizationController::authorize to start
roncodes Oct 25, 2026
ae7740e
Merge pull request #326 from fleetbase/fix/oauth-authorize-route
roncodes Oct 25, 2026
e7dd97e
feat(oauth-server): dynamic client registration and the api-key:creat…
roncodes Oct 27, 2026
4200ce8
feat(oauth-server): per-installation API keys and DELETE /oauth/v1/ap…
roncodes Oct 27, 2026
64245ec
Merge pull request #327 from fleetbase/feat/oauth-dynamic-registration
roncodes Oct 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,29 @@
# v1.6.73 — `mysql:createdb` works on a fresh install again
# v1.6.74 — Webhook endpoints for API keys, signed per endpoint, safer upload names, App Review access, and Sign in with Fleetbase

## Fixed
## Added

- **`mysql:createdb` failed on a fresh install with "Unknown database".** At boot, `Setting::doesntHaveConnection()` opens the `mysql` connection with the configured schema selected and Laravel caches it. Clearing the schema name in config afterwards had no effect on that cached connection, so `CREATE DATABASE` ran on a connection pointing at the database it was meant to create. The command now purges the `mysql` connection before each `CREATE DATABASE`, and again after restoring the schema name. `Setting::hasConnection()` also purges the connection when its check fails, so a broken connection isn't left cached for other code.
- **Webhook endpoints can be managed with an API key** (#319). `/v1/webhook-endpoints` supports create, list (optionally filtered by `url`), show, update, `POST {id}/rotate-secret` and delete. Endpoints are scoped to the key's company and environment (`live`/`test`). The URL must pass `PublicWebhookUrl`, and events are checked against `config('api.events')` when it is set. Integrations such as WooCommerce and Shopify connectors can now register their own endpoints instead of merchants copying URLs into the console.
- **Per-endpoint signing secrets** (#319). Each endpoint has its own secret (`whsec_…`), stored encrypted, hidden from serialisation and kept out of the activity log. Endpoints that already exist get one the first time it is needed. Deliveries now also carry `X-Fleetbase-Signature: t=<unix>,v1=<hmac_sha256("<t>.<body>", secret)>`; `EndpointSignature::verify()` documents the receiver side. The legacy `Signature` header is unchanged. The console can reveal (`GET int/v1/webhook-endpoints/{id}/secret`, view permission) and rotate (`POST int/v1/webhook-endpoints/{id}/rotate-secret`, create permission) the secret.
- **App Review access** (#323). Apple and Google reviewers can't receive a texted sign-in code. Extensions register what their app signs reviewers in to (`Fleetbase\Support\AppReview::register()`, e.g. a Storefront store or an organization's Navigator app) with a review number from the 555-0100–0199 range reserved for fiction. Owners turn access on in the Console (`GET|PUT int/v1/review-access/{type}/{id}`) and give the app store that number and a six-digit code, stored hashed, shown once, and accepted only for that subject. Works on Fleetbase Cloud and self-hosted. `AppReview::configuredAccountMatches()` is the shared env fallback for self-hosted review accounts.
- **Sign in with Fleetbase**, an OAuth 2.0 / OpenID Connect provider (#324). 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 flow with PKCE (`S256`) required for every client, rotating refresh tokens, RFC 7009 revocation, ID tokens, `/oauth/userinfo`, `/oauth/jwks` and `/.well-known/openid-configuration`.
- The consent screen runs in the console, so the console's sign-in and two-factor rules apply. Endpoints: `int/v1/oauth-server/authorization` (GET, POST, DELETE).
- Each consent is an `oauth_grants` row bound to the organization the user picked. Every refresh and every API call checks that the user is still active and still administers that organization.
- Scopes: `openid`, `profile`, `email` and `organization:read` (`GET /oauth/v1/organization`). Extensions register their own scopes with `Fleetbase\Auth\OAuthServer\OAuthScopes::register()`, optionally covering App Review types served by `/oauth/v1/review-access/{type}/{id}`.
- Replayed codes revoke their tokens; a replayed refresh token revokes the whole grant.
- System admins register apps, rotate secrets, turn apps off and revoke grants (`int/v1/oauth-server/apps`). Users list and remove their connected apps (`int/v1/oauth-server/connected-apps`) and get an email when they authorize a new one.
- Artisan: `oauth-server:keys`, `oauth-server:client create|rotate|list`, `oauth-server:prune` (scheduled daily).
- **Dynamic client registration for software installed on many sites** (RFC 7591). A plugin on a merchant's own domain cannot share one registered redirect URI, so each install registers its own public PKCE client at `POST /oauth/register` (throttled, advertised as `registration_endpoint` in discovery). Only software listed under `oauth-server.registration.software` can register, with the scopes listed there; today that is `fleetbase-woocommerce` (`organization:read`, `api-key:create`). Each client has exactly one redirect URI (https, or http on loopback as a development client), is never official, and is named after its host, e.g. "Fleetbase for WooCommerce (shop.example.com)", which is what the consent screen and Connected apps show. Registering again returns the same client; clients that never get a grant are pruned after a day. `OAUTH_SERVER_REGISTRATION_ENABLED=false` turns it off.
- **`api-key:create` scope** and `POST /oauth/v1/api-key`. Creates a live API key named after the app in the token's organization, or returns the one that app created before (tracked by the new `api_credentials.oauth_client_id`, never by name). The key is listed under Developers → API Keys. When the app's last grant in that organization is revoked (Connected apps, an admin revoking it, or the app being turned off), the key expires. This lets an integration such as Fleetbase for WooCommerce connect without anyone pasting a key. An app that serves many installations from one client (the Shopify app, one per shop) passes `installation` and gets a key per installation, named "{app} ({installation})" and tracked by `api_credentials.oauth_installation`. `DELETE /oauth/v1/api-key` (same scope, same optional `installation`) expires one installation's key without touching the grant or other installations.

## Security

- **Numbers reserved for fiction are never texted** (#323). `SmsService::send()` answers as sent without calling a provider, and SMS verification codes for those numbers are saved but not sent.
- **Uploaded files are stored under an extension that matches their contents** (#320). The stored name used to keep the client's extension, so a file that passed content-type validation could be stored as `.php`. The client's extension is now kept only when it matches the detected type (or is a loosely detected data format such as `csv`, `json` or `xlsx`); otherwise the extension comes from the detected type. Script extensions (`php`, `phtml`, `phar`, `shtml`, `htaccess`, `cgi`, …) are never used and become `bin`. `original_filename` is unchanged.
- **OAuth provider hardening** (#324). OAuth client secrets are stored only as bcrypt hashes and shown once. Redirect URIs match exactly, and errors about an unknown client or unregistered redirect URI are never redirected. OAuth rows always use the core database connection, even when the console is in sandbox mode.

## Upgrade Steps

- None. Deployments that worked around this with an override of `CreateDatabase` in `api/app/Console/Commands` can delete the override and its binding in `AppServiceProvider`.
- Run migrations: `php artisan migrate` adds the nullable `webhook_endpoints.secret` column, the `review_accesses` table, the `oauth_*` provider tables (`oauth_clients`, `oauth_grants`, `oauth_auth_codes`, `oauth_access_tokens`, `oauth_refresh_tokens`), `oauth_clients.software_id` and `registration_key`, and `api_credentials.oauth_client_id` and `oauth_installation`.
- To use Sign in with Fleetbase, give every API instance the same signing key pair: `php artisan oauth-server:keys` (key files in `storage/`), or `php artisan oauth-server:keys --show` and set `OAUTH_SERVER_PRIVATE_KEY` / `OAUTH_SERVER_PUBLIC_KEY`. Set `OAUTH_SERVER_ISSUER` if the public API URL differs from `APP_URL`. New dependencies: `league/oauth2-server`, `nyholm/psr7`, `symfony/psr-http-message-bridge`.
- The consent screen, Admin → Auth Config → OAuth Apps and Account → Auth → Connected apps ship in the console (fleetbase/fleetbase#734). Storefront's `storefront:*` scopes ship in fleetbase/storefront#163.
5 changes: 4 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fleetbase/core-api",
"version": "1.6.73",
"version": "1.6.74",
"description": "Core Framework and Resources for Fleetbase API",
"keywords": [
"fleetbase",
Expand Down Expand Up @@ -45,9 +45,11 @@
"laravel/socialite": "^5.31",
"lcobucci/clock": "3.3.1",
"lcobucci/jwt": "^5.4",
"league/oauth2-server": "^8.5.5",
"maatwebsite/excel": "^3.1",
"milon/barcode": "^10.0",
"mossadal/math-parser": "^1.3",
"nyholm/psr7": "^1.5",
"phpoffice/phpspreadsheet": "^1.28",
"phrity/websocket": "^1.7",
"pragmarx/google2fa": "^8.0",
Expand All @@ -61,6 +63,7 @@
"spatie/laravel-schedule-monitor": "^3.7",
"spatie/laravel-sluggable": "^3.5",
"sqids/sqids": "^0.4.1",
"symfony/psr-http-message-bridge": "^2.1|^6.0|^7.0",
"xantios/mimey": "^2.2.0"
},
"require-dev": {
Expand Down
46 changes: 32 additions & 14 deletions config/fleetbase.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,55 @@
/**
* -------------------------------------------
* Fleetbase Core API Configuration
* -------------------------------------------
* -------------------------------------------.
*/

return [
'api' => [
'version' => 'v1',
'routing' => [
'prefix' => env('API_PREFIX'),
'internal_prefix' => env('INTERNAL_API_PREFIX', 'int')
]
'prefix' => env('API_PREFIX'),
'internal_prefix' => env('INTERNAL_API_PREFIX', 'int'),
],
],
'console' => [
'path' => env('CONSOLE_PATH', '/fleetbase/console'),
'host' => env('CONSOLE_HOST', 'fleetbase.io'),
'path' => env('CONSOLE_PATH', '/fleetbase/console'),
'host' => env('CONSOLE_HOST', 'fleetbase.io'),
'subdomain' => env('CONSOLE_SUBDOMAIN'),
'secure' => env('CONSOLE_SECURE', !app()->environment(['development', 'local']))
'secure' => env('CONSOLE_SECURE', !app()->environment(['development', 'local'])),
],
'services' => [
'ipinfo' => [
'api_key' => env('IPINFO_API_KEY')
]
'api_key' => env('IPINFO_API_KEY'),
],
],
'connection' => [
'db' => env('DB_CONNECTION', 'mysql'),
'sandbox' => env('SANDBOX_DB_CONNECTION', 'sandbox')
'db' => env('DB_CONNECTION', 'mysql'),
'sandbox' => env('SANDBOX_DB_CONNECTION', 'sandbox'),
],
'branding' => [
'logo_url' => 'https://flb-assets.s3.ap-southeast-1.amazonaws.com/static/fleetbase-logo.png',
'icon_url' => 'https://flb-assets.s3.ap-southeast-1.amazonaws.com/static/fleetbase-icon.png'
'icon_url' => 'https://flb-assets.s3.ap-southeast-1.amazonaws.com/static/fleetbase-icon.png',
],
'version' => env('FLEETBASE_VERSION', '0.7.1'),

/*
|--------------------------------------------------------------------------
| Fleetbase Hosted Routing
|--------------------------------------------------------------------------
|
| A free routing key at install time (Support\HostedRouting), with the
| first admin's consent at onboarding or `php artisan routing:register`.
| ROUTING_AUTO_REGISTER=false hides the onboarding choice and makes the
| command refuse unless --force.
*/
'hosted_routing' => [
'auto_register' => env('ROUTING_AUTO_REGISTER', true),
'registration_url' => env('ROUTING_REGISTRATION_URL', 'https://api.fleetbase.io/__/licenses/register-instance'),
'api_host' => env('ROUTING_API_HOST', 'https://valhalla.routing.fleetbase.io'),
'domain' => env('ROUTING_HOSTED_DOMAIN', 'routing.fleetbase.io'),
'timeout' => env('ROUTING_REGISTRATION_TIMEOUT', 10),
],
'instance_id' => env('FLEETBASE_INSTANCE_ID') ?? (file_exists(base_path('.fleetbase-id')) ? trim(file_get_contents(base_path('.fleetbase-id'))) : null),

/*
Expand All @@ -52,8 +70,8 @@
'sms_auth_bypass_code' => env('SMS_AUTH_BYPASS_CODE'),

'user_cache' => [
'enabled' => env('USER_CACHE_ENABLED', true),
'server_ttl' => (int) env('USER_CACHE_SERVER_TTL', 900), // 15 minutes
'enabled' => env('USER_CACHE_ENABLED', true),
'server_ttl' => (int) env('USER_CACHE_SERVER_TTL', 900), // 15 minutes
'browser_ttl' => (int) env('USER_CACHE_BROWSER_TTL', 300), // 5 minutes
],

Expand Down
77 changes: 77 additions & 0 deletions config/oauth-server.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

/*
|--------------------------------------------------------------------------
| Fleetbase as an OAuth 2.0 / OpenID Connect provider
|--------------------------------------------------------------------------
|
| "Continue with Fleetbase": registered apps sign people in with their
| Fleetbase account and, with consent, act on one of their organizations.
| Not to be confused with config/oauth.php, which is Fleetbase signing
| people in WITH Google, Microsoft, GitHub and Apple.
|
*/

return [
'enabled' => env('OAUTH_SERVER_ENABLED', true),

/*
* The issuer identifier (`iss`), and the base of every endpoint in the discovery document.
* Defaults to APP_URL, e.g. https://api.fleetbase.io.
*/
'issuer' => env('OAUTH_SERVER_ISSUER'),

/*
* PEM key pair for signing access tokens and ID tokens. When unset, the keys are read from
* storage/oauth-private.key and storage/oauth-public.key (`php artisan oauth-server:keys`).
*/
'private_key' => env('OAUTH_SERVER_PRIVATE_KEY'),
'public_key' => env('OAUTH_SERVER_PUBLIC_KEY'),

/*
* Lifetimes, as ISO 8601 durations.
*/
'ttl' => [
'auth_code' => env('OAUTH_SERVER_AUTH_CODE_TTL', 'PT10M'),
'access_token' => env('OAUTH_SERVER_ACCESS_TOKEN_TTL', 'PT1H'),
'refresh_token' => env('OAUTH_SERVER_REFRESH_TOKEN_TTL', 'P30D'),
'id_token' => env('OAUTH_SERVER_ID_TOKEN_TTL', 'PT1H'),
],

/*
* `prompt=login` re-authenticates a console session older than this many seconds.
*/
'reauthenticate_after' => (int) env('OAUTH_SERVER_REAUTHENTICATE_AFTER', 300),

/*
* The console page that shows the consent screen.
*/
'console_authorize_path' => '/oauth/authorize',

/*
* Dynamic client registration (RFC 7591), for software installed on many sites.
*
* A plugin such as Fleetbase for WooCommerce runs on every merchant's own domain, so it cannot
* share one registered redirect URI. Each install registers its own public PKCE client at
* POST /oauth/register instead, naming the software it is and its one redirect URI.
*
* Only the software listed here can register, and only with the scopes listed for it. The
* client is named "{name} ({redirect host})" and is never marked official, so the consent
* screen always shows which site is asking. Registering again with the same software and
* redirect URI returns the same client. Clients that never get a grant are pruned after
* `unused_client_hours`.
*/
'registration' => [
'enabled' => env('OAUTH_SERVER_REGISTRATION_ENABLED', true),
'unused_client_hours' => (int) env('OAUTH_SERVER_REGISTRATION_UNUSED_HOURS', 24),
'software' => [
'fleetbase-woocommerce' => [
'name' => 'Fleetbase for WooCommerce',
'description' => 'Live delivery quotes, order sync and tracking for a WooCommerce store.',
'website_url' => 'https://github.com/fleetbase/woocommerce',
'logo_url' => env('OAUTH_SERVER_WOOCOMMERCE_LOGO_URL'),
'scopes' => ['organization:read', 'api-key:create'],
],
],
],
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
/**
* Run the migrations.
*
* Each webhook endpoint gets its own signing secret (stored encrypted), so receivers can
* verify deliveries with a value they were given instead of the API credential's secret.
* Existing endpoints get a secret the first time one is needed.
*/
public function up(): void
{
Schema::table('webhook_endpoints', function (Blueprint $table) {
$table->text('secret')->nullable()->after('version');
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('webhook_endpoints', function (Blueprint $table) {
$table->dropColumn('secret');
});
}
};
48 changes: 48 additions & 0 deletions migrations/2026_10_24_000000_create_review_accesses_table.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
/**
* Run the migrations.
*
* App Review access: one row per thing an app signs reviewers in to (a Storefront store
* or network, an organization's Navigator app, ...). Apple and Google reviewers can't
* receive a texted code, so the owner turns access on and gives the app store a fixed
* fictional number and a code. `subject_type` is the type an extension registered with
* Fleetbase\Support\AppReview. The code is only ever stored hashed.
*/
public function up(): void
{
if (Schema::hasTable('review_accesses')) {
return;
}

Schema::create('review_accesses', function (Blueprint $table) {
$table->bigIncrements('id');
$table->uuid('uuid')->unique();
$table->uuid('company_uuid')->nullable()->index();
$table->string('subject_type', 64);
$table->uuid('subject_uuid');
$table->boolean('enabled')->default(false);
$table->string('code')->nullable();
$table->timestamp('code_generated_at')->nullable();
$table->timestamp('enabled_at')->nullable();
$table->uuid('updated_by_uuid')->nullable();
$table->timestamps();
$table->softDeletes();

$table->unique(['subject_type', 'subject_uuid']);
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('review_accesses');
}
};
Loading
Loading