creator-cloud / php-sdk
Sign people in with their Creator ID, and connect them to your Laravel app.
Requires
- php: ^8.2
- ext-json: *
- ext-openssl: *
- firebase/php-jwt: ^6.10||^7.0
- laravel/framework: ^12.0||^13.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.17
- orchestra/testbench: ^10.0||^11.0
- pestphp/pest: ^3.0||^4.0||^5.0
- pestphp/pest-plugin-laravel: ^3.0||^4.0||^5.0
- phpstan/extension-installer: ^1.3
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-10-30 17:29:24 UTC
README
Sign people in with their Creator ID, sign them up from your own form, and sign them out when Creator Cloud says so. For Laravel 12 and 13.
Route::get('/auth/creator-cloud', fn () => CreatorCloud::signIn()->redirect());
Route::get('/auth/creator-cloud/callback', function () {
$signedIn = CreatorCloud::signIn()->callback();
$user = User::firstOrCreate(
['creator_cloud_sub' => $signedIn->person->sub],
['name' => $signedIn->person->name(), 'email' => $signedIn->person->email],
);
Auth::login($user);
return redirect('/dashboard');
});
That's sign-in. State, nonce, code exchange, signature, issuer, audience, expiry: checked. You get a person you can trust, or an exception.
Creator Cloud signs people in. Your session stays yours. The developer docs explain the model. This package is the Laravel end of it.
Requirements
- PHP 8.2+
- Laravel 12 or 13
- An app registered on Creator Cloud, with its client id, secret and redirect URI
Install
composer require creator-cloud/php-sdk
Then .env:
CREATOR_CLOUD_CLIENT_ID=your-client-id
CREATOR_CLOUD_CLIENT_SECRET=your-client-secret
CREATOR_CLOUD_REDIRECT_URI=/auth/creator-cloud/callback
A path is made absolute with url(). Whatever it becomes has to match a redirect URI registered on your app, character for character.
Pointing at a local Creator Cloud:
CREATOR_CLOUD_ISSUER=https://account.creatorcloud.loc
CREATOR_CLOUD_API_URL=https://api.creatorcloud.loc
CREATOR_CLOUD_VERIFY_SSL=false
Want the config file? php artisan vendor:publish --tag=creator-cloud-config. You probably don't.
Sign in
Send them
return CreatorCloud::signIn()->redirect();
A fresh state and nonce go into the session, and the person goes to account.creatorcloud.io. Both routes need the web middleware, because the session is where the callback finds them again.
Inertia app? A redirect to another host can't be followed by XHR, so hand Inertia the address instead:
return Inertia::location(CreatorCloud::signIn()->url());
Options chain, and each signIn() starts clean:
CreatorCloud::signIn()->prompt('create')->redirect(); // "Create account": they make a Creator ID and come straight back
CreatorCloud::signIn()->prompt('login')->redirect(); // prove it again, even with a live session
CreatorCloud::signIn()->scopes('openid email')->redirect(); // ask for less than the config says
openid is always asked for. Leave it out and there's no ID token, and sign-in can't finish. So it can't be left out.
Bring them back
use CreatorCloud\Exceptions\SignInFailed;
try {
$signedIn = CreatorCloud::signIn()->callback();
} catch (SignInFailed $e) {
if ($e->wasCancelled()) {
return redirect('/')->with('info', 'No problem. Sign in whenever you like.');
}
report($e);
return redirect('/')->with('error', 'Sign-in failed. Try again.');
}
callback() checks the state, swaps the code for tokens, and verifies the ID token. State and nonce are single use, so a replayed callback fails.
$signedIn->person:
| Property | Scope | |
|---|---|---|
sub | openid | How your app knows them. Store it. Don't parse it. |
cloudId | profile | Their Creator ID, CC-8K2M-4QXD. Safe to show. |
givenName, familyName, name() | profile | |
picture | profile | Their one picture, the same everywhere they use their Creator ID. Null until they have one. |
country | profile | Null until they've told Creator Cloud |
email, emailVerified | email | |
amr | openid | How they proved it: pwd, email, otp, google, facebook, plus mfa for two or more |
authTime | openid | When they last proved it |
claims | Everything, including claims newer than this package |
$signedIn->tokens has accessToken, refreshToken, idToken and expiresAt.
SignInFailed::$error is the OAuth error Creator Cloud sent (access_denied, login_required, ...) or one of ours: invalid_state, missing_code, invalid_id_token. After a prompt=none check, needsInteraction() tells you to send them through the normal flow.
Their tokens, later
$tokens = CreatorCloud::signIn()->refresh($refreshToken); // null: they took their access back
$person = CreatorCloud::signIn()->userinfo($accessToken); // null: expired, or they removed your app
Both answer null for the expected "they're gone", and throw for anything that's your problem (a wrong secret, say).
Signing out
Signing someone out of your app ends your session. That's it. They stay signed in to Creator Cloud, the way leaving Spotify doesn't sign you out of Google.
Kiosk or shared tablet? End theirs too:
return redirect(CreatorCloud::signIn()->logoutUrl($idToken, '/signed-out'));
Back-channel logout
Sometimes Creator Cloud needs someone signed out of your app now. They hit "sign out everywhere", changed their password, got frozen, or removed your app. Creator Cloud posts a signed logout token to your server.
This package registers the route for you at POST /auth/creator-cloud/backchannel-logout, outside the web middleware so CSRF never eats it. Register https://your-app.example/auth/creator-cloud/backchannel-logout as your app's back-channel logout address. Then listen:
use CreatorCloud\Events\BackchannelLogoutReceived;
Event::listen(function (BackchannelLogoutReceived $event) {
$user = User::firstWhere('creator_cloud_sub', $event->sub);
if (! $user) {
return;
}
DB::table('sessions')->where('user_id', $user->id)->delete();
});
End every session they have. There's no session id in the token, on purpose.
That DB::table('sessions') line only works on the database session driver. On Redis or cookies, Laravel can't find a user's sessions. Stamp the user instead (sessions_valid_after), and have a middleware sign out any session that started before the stamp.
A listener that throws makes the route answer 500, and Creator Cloud tries again later. Anything that isn't a valid logout token for your app gets a 400 and no event.
Want the route somewhere else? Set CREATOR_CLOUD_BACKCHANNEL_LOGOUT_PATH. Want to write your own? Set it to an empty value and call CreatorCloud::signIn()->verifyLogoutToken($token), which hands back the sub or null.
API sign-up
For our own services, with the identities.create scope. Your form collects their details, Creator Cloud emails them a code, they type the code into your form.
$signUp = CreatorCloud::identities()->create([
'email' => 'thandi@example.com',
'first_name' => 'Thandi',
'last_name' => 'Mokoena',
'country' => 'ZA',
'external_id' => 'salt-user-48213',
]);
session(['signup_id' => $signUp->id]);
Every email gets the same answer and the same email. Nothing here tells you whether Thandi already had a Creator ID. That's the point.
use CreatorCloud\Exceptions\InvalidCode;
use CreatorCloud\Exceptions\SignUpClosed;
try {
$connection = CreatorCloud::identities()->verify(session('signup_id'), $request->input('code'));
} catch (InvalidCode $e) {
return back()->withErrors(['code' => "That's not it. {$e->attemptsLeft()} tries left."]);
} catch (SignUpClosed) {
return redirect('/join')->with('error', 'That code expired. Let\'s send a new one.');
}
// $connection->sub is yours now
Spaces in the code are stripped, because people type 418 207.
Connections
$connection = CreatorCloud::connections()->find($sub); // null if there's no such connection
$connection = CreatorCloud::connections()->disconnect($sub); // ends it from your side
$connection->isActive();
$connection->cloudId; // null unless they shared their profile
$connection->currencyCode; // null until they've said where they live
Their picture
A person has one picture, the way a Google account does. You get it as $person->picture at every sign-in. When they change it with you, send it back and it's their picture everywhere:
$connection = CreatorCloud::connections()->updatePicture($sub, $request->file('avatar'));
$connection->picture; // the address everyone gets from now on
An upload, a file or a path. JPEG, PNG or WebP, up to 5 MB, at least 96 pixels a side. Needs pictures.write, which we grant our own services. Creator Cloud keeps its own copy, so deleting yours later breaks nothing.
Server calls use a client-credentials token, minted once per scope and cached until just before it expires. A 401 (a regenerated secret revokes every token) mints a new one and tries once more.
Errors
Everything thrown is a CreatorCloud\Exceptions\CreatorCloudException. The ones you'll meet:
| Exception | When |
|---|---|
SignInFailed | The callback couldn't sign anyone in. Read $error. |
RequestFailed | Creator Cloud answered with an error, or couldn't be reached (status 0, errorCode connection_failed). |
InvalidCode, SignUpClosed | API sign-up. Both are RequestFailed. |
InvalidConfiguration | A missing setting, or a callback route without a session. Names the fix. |
Branch on RequestFailed::$errorCode, never on the message. Messages get reworded. $errors maps fields to what's wrong with them on a 422.
Testing
CreatorCloud::fake() stands in for Creator Cloud's sign-in. The tokens are real, signed with a throwaway key and checked by the same code that runs in production. No mocks of this package's internals, and nothing to keep in sync.
use CreatorCloud\Facades\CreatorCloud;
it('signs Max in', function () {
$fake = CreatorCloud::fake();
$redirect = $this->get('/auth/creator-cloud');
$callback = $fake->approve($redirect, ['sub' => '64f1c0e2a7b3c91d2e4f5a6b', 'given_name' => 'Max']);
$this->get($callback)->assertRedirect('/dashboard');
$this->assertAuthenticated();
});
| Method | Gives you |
|---|---|
approve($redirect, $claims) | The callback URL after they approved. Your claims replace the defaults (Max Fourie, max@example.com, CC-8K2M-4QXD). |
deny($redirect) | The callback URL after they said no |
logoutToken($sub, $claims) | A logout token to post to /auth/creator-cloud/backchannel-logout |
idToken($claims) | A signed ID token, for when you need to build a bad one |
Claims you pass win, so breaking things on purpose is one line:
$fake->approve($redirect, ['exp' => time() - 3600]); // expired
$fake->approve($redirect, ['aud' => 'someone-else']); // meant for another app
$fake->approve($redirect, ['nonce' => 'nope']); // replayed from another browser
The fake answers the token, userinfo and signing-key endpoints. /v1 calls stay yours to fake with Http::fake(), like any other API.
Config
| Key | Env | Default |
|---|---|---|
client_id | CREATOR_CLOUD_CLIENT_ID | |
client_secret | CREATOR_CLOUD_CLIENT_SECRET | |
redirect_uri | CREATOR_CLOUD_REDIRECT_URI | /auth/creator-cloud/callback |
scopes | CREATOR_CLOUD_SCOPES | openid profile email |
backchannel_logout_path | CREATOR_CLOUD_BACKCHANNEL_LOGOUT_PATH | /auth/creator-cloud/backchannel-logout |
issuer | CREATOR_CLOUD_ISSUER | https://account.creatorcloud.io |
api_url | CREATOR_CLOUD_API_URL | https://api.creatorcloud.io |
verify_ssl | CREATOR_CLOUD_VERIFY_SSL | true |
timeout | CREATOR_CLOUD_TIMEOUT | 10 seconds |
cache_store | CREATOR_CLOUD_CACHE_STORE | Your default store |
Settings are checked when they're first needed, not at boot. An app that hasn't set its credentials yet still runs everything else.
Signing keys are cached for an hour. A token signed with a key nobody's seen means Creator Cloud rotated its keys, so they're fetched again, at most once a minute. A stream of junk tokens can't turn your app into a JWKS hammer.
Not yet
- Paying people. Funding, credits and batches arrive with the API that serves them.
- Public clients. Mobile and single-page apps can't keep a secret. PKCE comes when Creator Cloud decides who may sign in without one.
License
MIT. See LICENSE.md.