Skip to content

Commit 7a0e381

Browse files
Merge pull request #141 from limosa-io/allow-easier-routing
Enhance SCIM route configuration options and improve route registrati…
2 parents 0adc8f2 + fb4d05c commit 7a0e381

File tree

4 files changed

+208
-59
lines changed

4 files changed

+208
-59
lines changed

README.md

Lines changed: 91 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,54 @@ Optional "Me" routes can be enabled separately:
7777

7878
## Configuration
7979

80+
### Route Configuration
81+
82+
The SCIM server routes can be customized using the following configuration options in `config/scim.php`:
83+
84+
```php
85+
return [
86+
// Base path for all SCIM routes
87+
'path' => env('SCIM_BASE_PATH', '/scim'),
88+
89+
// Optional domain to restrict SCIM endpoints to
90+
'domain' => env('SCIM_DOMAIN', null),
91+
92+
// Middleware for protected routes (resource operations)
93+
'middleware' => env('SCIM_MIDDLEWARE', []),
94+
95+
// Middleware for public routes (ServiceProviderConfig, Schemas, ResourceTypes)
96+
'public_middleware' => env('SCIM_PUBLIC_MIDDLEWARE', []),
97+
];
98+
```
99+
100+
You can override these in your `.env` file:
101+
102+
```
103+
SCIM_BASE_PATH=/scim/api
104+
SCIM_MIDDLEWARE=api,auth:sanctum
105+
SCIM_PUBLIC_MIDDLEWARE=api
106+
```
107+
108+
If you need more control, you can disable route auto-publishing and register the routes manually:
109+
110+
```php
111+
// config/scim.php
112+
return [
113+
'publish_routes' => false,
114+
// other config...
115+
];
116+
117+
// In your RouteServiceProvider or a custom service provider:
118+
ArieTimmerman\Laravel\SCIMServer\RouteProvider::routes([
119+
'path' => '/custom-scim',
120+
'domain' => 'api.example.com',
121+
'middleware' => ['api', 'auth:api', 'scoped-tokens'],
122+
'public_middleware' => ['api', 'rate-limit'],
123+
]);
124+
```
125+
126+
### Resource Configuration
127+
80128
The package resolves configuration via `SCIMConfig::class`. Extend it to tweak resource definitions, attribute mappings, filters, or pagination defaults.
81129

82130
Register your custom config in `app/Providers/AppServiceProvider.php`:
@@ -120,30 +168,55 @@ Cursor-based pagination is enabled by default via the [SCIM cursor pagination dr
120168
## Security & app integration
121169
SCIM grants the ability to view, add, update, and delete users or groups. Make sure you secure the routes before shipping to production.
122170

123-
1. Disable automatic route publishing if you plan to wrap routes in your own middleware:
171+
You have two approaches to securing your SCIM endpoints:
124172

125-
```php
126-
// config/scim.php
127-
return [
128-
'publish_routes' => false,
129-
];
130-
```
173+
### Option 1: Configure middleware via config
174+
The simplest approach is to set middleware in your config:
131175

132-
2. Re-register the routes with your preferred middleware stack:
176+
```php
177+
// config/scim.php
178+
return [
179+
'middleware' => ['api', 'auth:sanctum'], // For protected resource routes
180+
'public_middleware' => ['api'], // For schema/discovery endpoints
181+
];
182+
```
133183

134-
```php
135-
use ArieTimmerman\Laravel\SCIMServer\RouteProvider as SCIMServerRouteProvider;
184+
Or via environment variables:
185+
```
186+
SCIM_MIDDLEWARE=api,auth:sanctum
187+
SCIM_PUBLIC_MIDDLEWARE=api
188+
```
189+
190+
### Option 2: Manual route registration
191+
For more control, disable automatic route publishing:
136192

137-
SCIMServerRouteProvider::publicRoutes();
193+
```php
194+
// config/scim.php
195+
return [
196+
'publish_routes' => false,
197+
];
198+
```
138199

139-
Route::middleware('auth:api')->group(function () {
140-
SCIMServerRouteProvider::routes([
141-
'public_routes' => false,
142-
]);
200+
Then re-register the routes with your preferred middleware and configuration:
143201

144-
SCIMServerRouteProvider::meRoutes();
145-
});
146-
```
202+
```php
203+
use ArieTimmerman\Laravel\SCIMServer\RouteProvider as SCIMServerRouteProvider;
204+
205+
SCIMServerRouteProvider::publicRoutes([
206+
'path' => '/scim',
207+
'middleware' => ['api'],
208+
]);
209+
210+
Route::middleware('auth:api')->group(function () {
211+
SCIMServerRouteProvider::routes([
212+
'path' => '/scim',
213+
'middleware' => ['custom-scim-check'],
214+
'public_routes' => false,
215+
]);
216+
217+
SCIMServerRouteProvider::meRoutes();
218+
});
219+
```
147220

148221
## Test server
149222
Bring up the full demo stack with Docker Compose:

config/scim.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
'omit_main_schema_in_return' => false,
66
'omit_null_values' => true,
77

8+
'path' => env('SCIM_BASE_PATH', '/scim'),
9+
'domain' => env('SCIM_DOMAIN', null),
10+
'middleware' => env('SCIM_MIDDLEWARE', []),
11+
'public_middleware' => env('SCIM_PUBLIC_MIDDLEWARE', []),
12+
813
'pagination' => [
914
'defaultPageSize' => 10,
1015
'maxPageSize' => 100,

src/RouteProvider.php

Lines changed: 104 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,69 +3,84 @@
33
namespace ArieTimmerman\Laravel\SCIMServer;
44

55
use ArieTimmerman\Laravel\SCIMServer\Middleware\SCIMHeaders;
6-
use Illuminate\Support\Facades\Route;
76
use Illuminate\Routing\Middleware\SubstituteBindings;
7+
use Illuminate\Support\Facades\Route;
88

99
/**
10-
* Helper class for the URL shortener
10+
* Registers SCIM routes with configurable prefix, domain and middleware.
1111
*/
1212
class RouteProvider
1313
{
14-
protected static $prefix = 'scim';
1514

1615
public static function routes(array $options = [])
1716
{
18-
17+
$config = static::getRoutingConfig($options);
18+
1919
if (!isset($options['public_routes']) || $options['public_routes'] === true) {
20-
static::publicRoutes($options);
20+
static::publicRoutes([
21+
'prefix' => $config['prefix'],
22+
'middleware' => $config['public_middleware'],
23+
'domain' => $config['domain'],
24+
]);
2125
}
2226

23-
Route::prefix(static::$prefix)->group(
24-
function () use ($options) {
25-
Route::prefix('v2')->middleware([
26-
SubstituteBindings::class,
27-
SCIMHeaders::class,
28-
])->group(
29-
function () use ($options) {
30-
static::allRoutes($options);
31-
}
32-
);
33-
34-
Route::get('v1', [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\ResourceController::class, 'wrongVersion']);
35-
Route::prefix('v1')->group(
36-
function () {
37-
Route::fallback([\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\ResourceController::class, 'wrongVersion']);
38-
}
39-
);
40-
}
41-
);
27+
$group = static::buildRouteGroup($config, 'middleware');
28+
29+
Route::group($group, function () use ($options) {
30+
Route::prefix('v2')->middleware([
31+
SubstituteBindings::class,
32+
SCIMHeaders::class,
33+
])->group(function () use ($options) {
34+
static::allRoutes($options);
35+
});
36+
37+
Route::get('v1', [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\ResourceController::class, 'wrongVersion']);
38+
Route::prefix('v1')->group(function () {
39+
Route::fallback([\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\ResourceController::class, 'wrongVersion']);
40+
});
41+
});
4242
}
4343

4444
public static function meRoutes(array $options = [])
4545
{
46-
Route::prefix(static::$prefix)->group(function () {
47-
Route::get("/v2/Me", [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\MeController::class, 'getMe'])->name('scim.me.get');
46+
$config = static::getRoutingConfig($options);
47+
$group = static::buildRouteGroup($config, 'middleware');
48+
49+
Route::group($group, function () {
50+
Route::get('/v2/Me', [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\MeController::class, 'getMe'])->name('scim.me.get');
4851
Route::put('/v2/Me', [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\MeController::class, 'replaceMe'])->name('scim.me.put');
4952
});
5053
}
5154

5255
public static function meRoutePost(array $options = [])
5356
{
54-
Route::prefix(static::$prefix)->group(function () {
57+
$config = static::getRoutingConfig($options);
58+
$group = static::buildRouteGroup($config, 'middleware');
59+
60+
Route::group($group, function () {
5561
Route::post('/v2/Me', [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\MeController::class, 'createMe'])->name('scim.me.post');
5662
});
5763
}
5864

5965
public static function publicRoutes(array $options = [])
6066
{
61-
Route::prefix(static::$prefix)->group(function () {
62-
Route::get("/v2/ServiceProviderConfig", [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\ServiceProviderController::class, 'index'])->name('scim.serviceproviderconfig');
67+
if (isset($options['prefix']) && isset($options['middleware'])) {
68+
$config = $options;
69+
} else {
70+
$config = static::getRoutingConfig($options);
71+
$config['middleware'] = $config['public_middleware'];
72+
}
73+
74+
$group = static::buildRouteGroup($config, 'middleware');
6375

64-
Route::get("/v2/Schemas", [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\SchemaController::class, 'index']);
65-
Route::get("/v2/Schemas/{id}", [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\SchemaController::class, 'show'])->name('scim.schemas');
76+
Route::group($group, function () {
77+
Route::get('/v2/ServiceProviderConfig', [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\ServiceProviderController::class, 'index'])->name('scim.serviceproviderconfig');
6678

67-
Route::get("/v2/ResourceTypes", [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\ResourceTypesController::class, 'index']);
68-
Route::get("/v2/ResourceTypes/{id}", [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\ResourceTypesController::class, 'show'])->name('scim.resourcetype');
79+
Route::get('/v2/Schemas', [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\SchemaController::class, 'index']);
80+
Route::get('/v2/Schemas/{id}', [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\SchemaController::class, 'show'])->name('scim.schemas');
81+
82+
Route::get('/v2/ResourceTypes', [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\ResourceTypesController::class, 'index']);
83+
Route::get('/v2/ResourceTypes/{id}', [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\ResourceTypesController::class, 'show'])->name('scim.resourcetype');
6984
});
7085
}
7186

@@ -75,18 +90,67 @@ private static function allRoutes(array $options = [])
7590
Route::get('', [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\ResourceController::class, 'crossResourceIndex']);
7691
Route::post('.search', [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\ResourceController::class, 'crossResourceSearch']);
7792

78-
Route::post("/Bulk", [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\BulkController::class, 'processBulkRequest']);
93+
Route::post('/Bulk', [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\BulkController::class, 'processBulkRequest']);
7994

8095
// TODO: Use the attributes parameters ?attributes=userName, excludedAttributes=asdg,asdg (respect "returned" settings "always")
8196
Route::get('/{resourceType}/{resourceObject}', [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\ResourceController::class, 'show'])->name('scim.resource');
82-
Route::get("/{resourceType}", [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\ResourceController::class, 'index'])->name('scim.resources');
83-
Route::post("/{resourceType}/.search", [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\ResourceController::class, 'search']);
84-
Route::post("/{resourceType}", [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\ResourceController::class, 'create']);
97+
Route::get('/{resourceType}', [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\ResourceController::class, 'index'])->name('scim.resources');
98+
Route::post('/{resourceType}/.search', [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\ResourceController::class, 'search']);
99+
Route::post('/{resourceType}', [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\ResourceController::class, 'create']);
85100

86-
Route::put("/{resourceType}/{resourceObject}", [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\ResourceController::class, 'replace']);
87-
Route::patch("/{resourceType}/{resourceObject}", [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\ResourceController::class, 'update']);
88-
Route::delete("/{resourceType}/{resourceObject}", [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\ResourceController::class, 'delete']);
101+
Route::put('/{resourceType}/{resourceObject}', [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\ResourceController::class, 'replace']);
102+
Route::patch('/{resourceType}/{resourceObject}', [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\ResourceController::class, 'update']);
103+
Route::delete('/{resourceType}/{resourceObject}', [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\ResourceController::class, 'delete']);
89104

90105
Route::fallback([\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\ResourceController::class, 'notImplemented']);
91106
}
107+
108+
private static function getRoutingConfig(array $options = []): array
109+
{
110+
$path = $options['path'] ?? config('scim.path', '/scim');
111+
$domain = $options['domain'] ?? config('scim.domain');
112+
$protectedMiddleware = static::normalizeMiddleware($options['middleware'] ?? config('scim.middleware', []));
113+
$publicMiddleware = static::normalizeMiddleware($options['public_middleware'] ?? config('scim.public_middleware', []));
114+
115+
$prefix = trim($path, '/');
116+
if (empty($prefix)) {
117+
$prefix = 'scim';
118+
}
119+
120+
return [
121+
'prefix' => $prefix,
122+
'domain' => $domain,
123+
'middleware' => $protectedMiddleware,
124+
'public_middleware' => $publicMiddleware,
125+
];
126+
}
127+
128+
private static function buildRouteGroup(array $config, string $middlewareKey = 'middleware'): array
129+
{
130+
$group = ['prefix' => $config['prefix']];
131+
132+
$middleware = $config[$middlewareKey] ?? [];
133+
if (!empty($middleware)) {
134+
$group['middleware'] = $middleware;
135+
}
136+
137+
if (!empty($config['domain'])) {
138+
$group['domain'] = $config['domain'];
139+
}
140+
141+
return $group;
142+
}
143+
144+
private static function normalizeMiddleware($middleware): array
145+
{
146+
if (empty($middleware)) {
147+
return [];
148+
}
149+
150+
if (is_string($middleware)) {
151+
$middleware = array_filter(array_map('trim', explode(',', $middleware)));
152+
}
153+
154+
return is_array($middleware) ? $middleware : [$middleware];
155+
}
92156
}

src/ServiceProvider.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,14 @@ function ($id, $route) {
6868
$router->middleware('SCIMHeaders', 'ArieTimmerman\Laravel\SCIMServer\Middleware\SCIMHeaders');
6969

7070
if (config('scim.publish_routes')) {
71-
\ArieTimmerman\Laravel\SCIMServer\RouteProvider::routes();
71+
$routeOptions = [
72+
'path' => config('scim.path', '/scim'),
73+
'domain' => config('scim.domain'),
74+
'middleware' => config('scim.middleware', []),
75+
'public_middleware' => config('scim.public_middleware', []),
76+
];
77+
78+
\ArieTimmerman\Laravel\SCIMServer\RouteProvider::routes($routeOptions);
7279
}
7380
}
7481

0 commit comments

Comments
 (0)