Search by

creator-cloud / php-sdk

pdphilip

Sign people in with their Creator ID, and connect them to your Laravel app.

v0.2.0 2026-10-30 12:25 UTC

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:

PropertyScope
subopenidHow your app knows them. Store it. Don't parse it.
cloudIdprofileTheir Creator ID, CC-8K2M-4QXD. Safe to show.
givenName, familyName, name()profile
pictureprofileTheir one picture, the same everywhere they use their Creator ID. Null until they have one.
countryprofileNull until they've told Creator Cloud
email, emailVerifiedemail
amropenidHow they proved it: pwd, email, otp, google, facebook, plus mfa for two or more
authTimeopenidWhen they last proved it
claimsEverything, 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:

ExceptionWhen
SignInFailedThe callback couldn't sign anyone in. Read $error.
RequestFailedCreator Cloud answered with an error, or couldn't be reached (status 0, errorCode connection_failed).
InvalidCode, SignUpClosedAPI sign-up. Both are RequestFailed.
InvalidConfigurationA 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();
});
MethodGives 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

KeyEnvDefault
client_idCREATOR_CLOUD_CLIENT_ID
client_secretCREATOR_CLOUD_CLIENT_SECRET
redirect_uriCREATOR_CLOUD_REDIRECT_URI/auth/creator-cloud/callback
scopesCREATOR_CLOUD_SCOPESopenid profile email
backchannel_logout_pathCREATOR_CLOUD_BACKCHANNEL_LOGOUT_PATH/auth/creator-cloud/backchannel-logout
issuerCREATOR_CLOUD_ISSUERhttps://account.creatorcloud.io
api_urlCREATOR_CLOUD_API_URLhttps://api.creatorcloud.io
verify_sslCREATOR_CLOUD_VERIFY_SSLtrue
timeoutCREATOR_CLOUD_TIMEOUT10 seconds
cache_storeCREATOR_CLOUD_CACHE_STOREYour 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.