Authentication and sessions
Searching mods, reading details and installing a public mod work for an anonymous player. A session is required for anything tied to a person: cross-device subscriptions, ratings, reporting, mod creation and uploads, premium ownership and purchases, and the profile itself.
cfcore-sdk-ue persists one auth token per platform user and offers two ways to generate it. The email one-time code flow works everywhere and is the only way a player can sign in to a CurseForge account they already own. The external provider flow exchanges a platform ticket for a session with no typing. Ship the provider flow for your store or console, and keep the email flow as the fallback.
For concepts, the provider matrix and portal setup, see Authentication overview, Email OTP authentication, Silent authentication and Sessions and account management. This page is the Unreal implementation: the real enum values, delegate shapes that compile, and the terms flow.
Authentication must be enabled for your game in the Authentication section of the Developer Portal at console.curseforge.com, and the SDK must be initialized. Every call here except Is Authenticated fails immediately with ECFCoreErrorCodes::FailedToInitialize and the description Not initialized, which means the SDK is not ready, not that sign-in failed.
Three interfaces, one session
Two of these look almost identical and only one creates a session.
| Interface | Reached via | What it does with the token |
|---|---|---|
ICFCoreAuthentication | CFCoreContext::GetInstance()->Authentication() | Obtains the token and persists it. Reports success or failure only. This is the one you want. |
ICFCoreApiAuthentication | CFCoreContext::GetInstance()->Api()->Authentication() | Thin REST wrapper. Hands you an FAuthToken and stores nothing. Not exposed to Blueprint at all. |
UCFCoreSubsystem | GEngine->GetEngineSubsystem<UCFCoreSubsystem>(), and the Blueprint palette | Wrapper over ICFCoreAuthentication, plus the initialization guard. |
The delegate signature tells you which one you are on. ICFCoreAuthentication uses one delegate carrying TOptional<FCFCoreError>. ICFCoreApiAuthentication uses one delegate carrying TOptional<FAuthToken> plus TOptional<FCFCoreApiResponseError>. UCFCoreSubsystem splits into a result pin and a separate on_error pin.
If your callback receives a token object, you are on the raw API, nothing was saved, and every authorized call will still fail.
The interfaces live in namespace cfcore while the model structs (FTerms, FMe, FAuthToken, FCFCoreError) are global UHT types, so add using namespace cfcore; to any file that calls these APIs. Callbacks arrive on the game thread. Delegates fire through ExecuteIfBound, so bind both pins: an unbound one is a silent no-op.
Method reference
Blueprint node (cfcore|Authentication) | UCFCoreSubsystem method | ICFCoreAuthentication method |
|---|---|---|
| Is Authenticated | IsAuthenticated | IsAuthenticated |
| Get Authentication Terms | GetAuthTerms | GetTerms |
| Send Security Code Email | SendSecurityCode | SendSecurityCode |
| Generate Auth Token from Email Code | GenerateAuthToken | GenerateAuthToken |
| Generate Auth Token for an external provider | GenerateAuthTokenByExternalProvider | GenerateAuthTokenByExternalProvider |
| Logout | Logout | Logout |
None of the six needs an existing session. Note that the terms method is named GetAuthTerms on the subsystem only; on both SDK interfaces it is GetTerms. Two calls on the authorized API do need a session, both under cfcore|Api Authorized.
| Blueprint node | UCFCoreSubsystem method | ICFCoreApiAuthorized method | Endpoint |
|---|---|---|---|
| Get Me | ApiGetMe | GetMe | GET /v1/users/me |
| Generate Temp User Token | ApiGenerateTempToken | GenerateTempToken | GET /v1/users/me/generate-temp-token |
GenerateTempToken gives you a short-lived string for handing the player to a CurseForge web surface, so the session token never goes in a URL. Its lifetime is undocumented, so request one per hand-off.
Terms and consent
Signing in creates or uses a CurseForge account, not just a profile in your title, so the player must see CurseForge's terms and get a real choice before the sign-in call runs. The consent requirement itself, including what console certification expects, is on the Xbox and PlayStation compliance page. Consent gates the call that mints the token: once GenerateAuthToken or GenerateAuthTokenByExternalProvider succeeds, it is too late to ask.
The SDK does not ship the terms and consent copy in the binary. It fetches the current text on every call, with no client-side cache, so new wording, new button labels and moved documents reach your shipped game without a patch. Render what comes back; do not hardcode a paraphrase, and if the fetch fails do not let the player sign in anyway.
There is no accept call. cfcore-sdk-ue has no AcceptTerms, no PostTerms and no consent endpoint. What exists is the one GET, one field on the provider sign-in path carrying the moment the player agreed (FExternalAuthAdditionalInfo.eulaAcceptTime), and one profile field reading the last agreement back (FMe.eulaLastAgreed). Do not build against an acceptance endpoint that does not exist.
How it works
- Initialize, then call Is Authenticated. A token already present means you can skip sign-in.
- If already signed in, call Get Me and compare
FTerms.updateDateagainstFMe.eulaLastAgreed. Skip the terms screen when the player already agreed to the current document. - Otherwise call
GetAuthTerms, which needs no session. The SDK issuesGET /v1/auth/terms, appending?lang=fromFCFCoreSettings.defaultLanguagewhen that value is non-empty. - Render the body, both button labels and the links in a blocking screen.
- On decline, stop. On agree, record
FDateTime::UtcNow()and start sign-in. - On the provider path, pass that time as
eulaAcceptTime, the only place in the SDK where an acceptance timestamp is transmitted. - After sign-in, if you skipped step 2 for a new player, call Get Me and re-present the screen when
updateDateis newer thaneulaLastAgreed.
#include <cfcore_context.h>
#include <authentication/cfcore_authentication.h>
using namespace cfcore;
void ATestGameMode::FetchTerms() {
CFCoreContext::GetInstance()->Authentication()->GetTerms(
ICFCoreAuthentication::FGetTermsDelegate::CreateLambda(
[this](const TOptional<FTerms>& opt_terms,
const TOptional<FCFCoreError>& opt_err) {
if (opt_err.IsSet()) {
// Retry. Never fall back to hardcoded consent copy.
ShowTermsRetry(opt_err.GetValue().description);
return;
}
const FTerms& terms = opt_terms.GetValue();
CachedTermsUpdateDate = terms.updateDate;
ShowTermsBody(terms.content.plainText);
SetAcceptLabel(terms.content.buttons.agree.text);
SetDeclineLabel(terms.content.buttons.disagree.text);
AddLink(terms.content.links.terms.text,
terms.content.links.terms.url);
})
);
}
Blueprint: Get Authentication Terms has pins on_success (carrying terms, an FTerms) and on_error (carrying error, an FCFCoreError). Right click terms, choose Split Struct Pin, then split content.
The terms model
Every struct in the family (FTerms, FTermsContent, FTermsButtons, FTermsButton, FTermsLinks, FTermsLink) is BlueprintType with BlueprintReadOnly fields, so the whole tree is readable by splitting pins. Paths are relative to the returned FTerms.
| Path | Type | Notes |
|---|---|---|
updateDate | FDateTime | When the document was last updated. Defaults to 0. Cache it to detect that the document changed. |
content | FTermsContent | The renderable payload. |
content.language | FString | Language code of the returned copy. Check it against what you requested before showing the screen. |
content.plainText | FString | Body as plain text. Use for a console-safe layout or a text-to-speech pass. |
content.html | FString | The same body as HTML. Use only if your widget can render it. These are two renderings of one document: render one, and if you cannot render HTML use plainText rather than stripping tags. |
content.buttons | FTermsButtons | Holds agree and disagree, each an FTermsButton whose only field is text. |
content.buttons.agree.text, content.buttons.disagree.text | FString | Labels for your accept and decline controls. Use them verbatim, do not localize them yourself, and show both: the struct always carries both. |
content.links | FTermsLinks | Holds website, terms and privacy, each an FTermsLink. |
FTermsLink fields:
| Field | Type | Notes |
|---|---|---|
required | bool | Defaults to false. Nothing in the SDK reads this field. |
text | FString | The link label to display. |
url | FString | The destination. Open it with your project's browser or overlay path; the SDK has no URL-opening helper. Never hardcode a CurseForge address. |
defaultLanguage is the only runtime-updatable setting in this version. To re-fetch in another language, fill an FCFCoreUpdatableSettings with updateDefaultLanguage true and the new value, call Update Settings, then Get Authentication Terms again. An empty value clears the query parameter. The defaults disagree: FCFCoreSettings.defaultLanguage is empty, Project Settings ships en, so set it explicitly.
Email one-time code sign-in
Two calls with a human step in the middle, so your UI holds state between them.
How it works
- Collect an email address and call
SendSecurityCode, which issuesPOST /v1/auth/passwordless/init. - CurseForge emails the player a code. Show a code entry field.
- Call
GenerateAuthTokenwith the same email plus the code, which issuesPOST /v1/auth/passwordless/token. The code is anint32and the wire field isotp, so parse the input to an integer (FCString::Atoiin C++, String to Int in Blueprint). - Before the request is sent, the SDK clears any token already on disk.
- On success the token is written to the user context file and the delegate fires with no error. On failure the player is left signed out.
Step 4 is a real trap. An already signed-in player who mistypes a code for a second account ends up signed out of both. Gate account switching behind an explicit sign-out.
// Step 1: ask the server to email a code.
void ATestGameMode::SendEmailCode(const FString& email) {
CFCoreContext::GetInstance()->Authentication()->SendSecurityCode(
email,
ICFCoreAuthentication::FSendSecurityCodeDelegate::CreateLambda(
[this, email](const TOptional<FCFCoreError>& opt_err) {
if (!opt_err.IsSet()) {
ShowCodeEntryUi(email); // step 2 must send the same address
}
})
);
}
// Step 2: exchange email plus code for a persisted session.
void ATestGameMode::SubmitEmailCode(const FString& email,
int32 security_code) {
CFCoreContext::GetInstance()->Authentication()->GenerateAuthToken(
email,
security_code,
ICFCoreAuthentication::FGenerateAuthTokenDelegate::CreateLambda(
[this](const TOptional<FCFCoreError>& opt_err) {
if (!opt_err.IsSet()) {
OnSignedIn(); // authenticated and persisted
return;
}
const FCFCoreError& err = opt_err.GetValue();
// FailedSettingAuthToken: the server took the code but the token
// could not be written. A storage problem, not a bad code.
if (err.code == ECFCoreErrorCodes::ApiError) {
if (err.apiError.badRequest) {
ShowInvalidCodeMessage();
} else if (err.apiError.serverUnreachable) {
OfferRetry();
}
}
})
);
}
Blueprint: Send Security Code Email takes email plus on_success and on_error. Generate Auth Token from Email Code takes email, security_code (an integer) and the same two pins. Its on_success carries no payload, so call Is Authenticated or Get Me afterwards to display the account.
External provider sign-in
Your code obtains a platform ticket through the platform's own SDK and hands it to CurseForge. ECFCoreExternalAuthProvider below is the complete set in the current release. The numeric values are the Blueprint dropdown ordering and the stored uint8 values, not the wire format.
| Value | Numeric | Provider | What to pass as external_token |
|---|---|---|---|
None | 0 | No provider | Not usable. Also the default of FCFCoreSettings.provider. |
Steam | 1 | Steam | Base64 representation of a session ticket from ISteamUser::GetAuthSessionTicket. |
PSN | 2 | PlayStation Network | An access token obtained using an authorization code. Optionally set environment to dev, qa or prod. |
XBL | 3 | Xbox Live | An access token obtained using an XSTS token. |
WB | 4 | Warner Brothers | |
Epic | 5 | Epic Games | The JWT from the EOS SDK call EOS_Auth_CopyIdToken. |
GOG | 6 | GOG Galaxy | Base64 representation of the raw encrypted app ticket bytes from galaxy::api::User()->RequestEncryptedAppTicket(). Requires the Encrypted App Ticket Key configured in your game's Authentication section in the Developer Portal. |
On the wire the SDK sends the enum's display name as a string in the body field provider, never the number, and the ticket goes in a field named token, not external_token.
FExternalAuthAdditionalInfo, the same struct for every provider, both fields BlueprintReadWrite:
| Field | Type | Default | Notes |
|---|---|---|---|
eulaAcceptTime | FDateTime | 0 | The moment the player pressed agree. Set it from FDateTime::UtcNow(). Serialized to ISO 8601, and always sent, so leaving the default sends 0001-01-01T00:00:00.000Z rather than omitting the field. |
environment | FString | empty | PSN only. Documented values are dev, qa, prod. Leave empty otherwise. |
How it works
- Present the terms and capture the acceptance time, unless the player already accepted the current terms in a past session (see the terms flow above for how to check).
- Obtain the platform ticket in exactly the form named above. The SDK does not inspect it, so a wrong encoding surfaces as a server rejection, not a local error.
- Fill an
FExternalAuthAdditionalInfo. - Call
GenerateAuthTokenByExternalProvider, which issuesPOST /v1/auth/external. Any stored token is cleared first, as in the email flow. - On success the token is persisted. If the platform identity is new to CurseForge, an anonymous account is created for it.
- On failure, fall back to the email flow rather than blocking the player out of every authenticated feature.
#include <api/models/enums/external_auth_provider.h>
#include <api/models/external_auth_additional_info.h>
void ATestGameMode::SignInSilently(const FString& platform_token,
const FDateTime& terms_accepted_at) {
FExternalAuthAdditionalInfo additional_info;
additional_info.eulaAcceptTime = terms_accepted_at;
// additional_info.environment = TEXT("prod"); // PSN only
CFCoreContext::GetInstance()->Authentication()
->GenerateAuthTokenByExternalProvider(
ECFCoreExternalAuthProvider::Steam, // set per build target
platform_token, // base64 session ticket for Steam
additional_info,
ICFCoreAuthentication::FGenerateAuthTokenDelegate::CreateLambda(
[this](const TOptional<FCFCoreError>& opt_err) {
if (opt_err.IsSet()) {
ShowEmailSignInUi(); // documented fallback
return;
}
OnSignedIn();
})
);
}
Blueprint: Generate Auth Token for an external provider has pins provider (an ECFCoreExternalAuthProvider dropdown), external_token, additional_info, on_success and on_error. Build the struct with the pure function MakeExternalAuthAdditionalInfo under cfcore|Auth: it is declared NativeMakeFunc, so Unreal offers it instead of the generic make node, and its only pin is eulaAcceptTime. Drive that from a UTC now node at click time. No Blueprint path sets environment.
The SDK does not validate the provider argument, and passing None sends the string "None" anyway. No helper picks a provider for the current platform, so branch on your build target.
This argument is not FCFCoreSettings.provider (also in Project Settings). The argument tells the server whose ticket to validate, for one call. The setting goes out as the x-provider header on every request, is omitted when None, and exists for premium mods across multiple providers, so set it if you sell premium mods on more than one storefront.
Checking, persisting and ending the session
IsAuthenticated has two shapes. On ICFCoreAuthentication it is synchronous and returns bool. On UCFCoreSubsystem and in Blueprint it takes an on_is_auth delegate carrying is_authenticated, and has no error pin at all.
// Synchronous, on the interface.
const bool signed_in =
CFCoreContext::GetInstance()->Authentication()->IsAuthenticated();
// Sign out. Clears the local token only.
CFCoreContext::GetInstance()->Authentication()->Logout(
ICFCoreAuthentication::FLogoutDelegate::CreateLambda(
[this](const TOptional<FCFCoreError>& opt_err) {
OnSignedOut(); // in the current implementation always unset
})
);
Blueprint: Is Authenticated has one pin, on_is_auth, carrying is_authenticated. Logout has on_success and on_error.
Is Authenticated returns false both for a signed-out player and for an uninitialized SDK, so gate on IsInitialized first. It only tests whether a non-empty token string exists locally and never contacts the server. FAuthToken has one field, token, with no expiry, no refresh token and no scope list, so you cannot compute whether a session is still valid. Design for "signed in until the server says otherwise" rather than a countdown.
Logout empties the token in memory, so IsAuthenticated returns false immediately, then starts an asynchronous write of the user context file. The delegate fires without waiting for it and never reports a real error. No request reaches CurseForge and the token is not revoked server-side, so sign-out is not a security boundary on a shared device. You do not need it before signing a different player in, because both token-generating calls reset the token themselves.
Where the token is persisted
| Element | Value |
|---|---|
| Root | FCFCoreSettings.userDataDirectory, which supports the escape tokens %USER_DIR%, %USER_SETTINGS_DIR%, %PROJECT_DIR%, %PROJECT_SAVED_DIR% |
| Path | <userDataDirectory>/<gameId>/<userContextId>/user_info.json |
| Format | JSON, unencrypted, not pretty-printed. Its authToken field holds the token, alongside installed-mod state. |
FCFCoreInitializationOptions.userContextId is a path component, which is what keeps two people on one machine or console out of each other's session. Pass the logged-in console or store username or identifier. With nothing supplied the SDK substitutes the literal local, so every profile on a shared console shares one session. Changing the value between runs orphans the previous session and presents the player as signed out.
The token is stored in clear text, so do not ship userDataDirectory inside a world-readable install directory, include it in crash-report bundles, or copy it between machines. If it is empty, Initialize fails with ECFCoreErrorCodes::MissingUserDataDirectory.
userContextId is read once at initialization and is not in FCFCoreUpdatableSettings, so switching platform user at runtime means re-initializing. Uninitialize does not clear the session: re-initializing with the same userContextId resumes it. Use Logout to end a session and a new userContextId to switch player.
Re-validation on Initialize
Initialize runs its sub-initializers in a fixed order: user context service, authentication, library, subscription. The token file loads before the authentication stage, and authentication completes before subscriptions, which is why subscription sync can rely on a settled session.
If a token was loaded, the authentication stage calls GetMe once as a liveness probe and waits for it. On tokenExpired the SDK logs a warning and clears the stored token, and initialization still completes successfully: an expired session is not an initialization failure. For the rest of the run, any response mapped to tokenExpired (HTTP 401) erases the local token on every request path, so you never clean up after a 401. Re-check Is Authenticated after initialization and after any call that returns tokenExpired. Initialization is the one moment it is authoritative; later it can return true for a token the server has already rejected.
Anonymous accounts and account association
When a player signs in through a provider and that platform identity is new to CurseForge, CurseForge creates an anonymous account for it. Everything works: they can subscribe, rate and own premium mods. But it is not the CurseForge account they may already use on the website or in another game, and their subscriptions will not appear there. Teams miss this, and it produces support tickets that look like data loss. Association is web-based and player-driven, and no SDK call performs it, so the SDK's job is to detect the state and route the player.
How it works
- Sign the player in through the provider, then call Get Me.
- Read
FMe.hasConnectedAccount. - If they need to link, surface https://www.curseforge.com/account/connected-accounts. Open the default browser on PC. On console, present a QR code or a short instruction, since consoles generally cannot hand off to a desktop browser.
- After the player says they finished, call Get Me again to refresh.
FMe fields, all BlueprintReadOnly:
| Field | Type | Notes |
|---|---|---|
id | int64 | CurseForge user id. Defaults to 0. |
displayName, username, email, avatarUrl | FString | Identity and display fields. Treat email as personal data. |
dateCreated | FDateTime | Account creation date. |
hasConnectedAccount | bool | Whether the account is linked to a real CurseForge account. Defaults to false. |
phasingData | FMePhasingData | Server-driven rollout percentages (each an int32 from 0 to 100, where 0 means disabled for all) that let CurseForge change SDK behaviour per player without a game patch. Read them, do not persist them, and do not branch game logic on them. |
eulaLastAgreed | FDateTime | When the player last agreed to the terms. Compare against FTerms.updateDate. |
diagnostics | bool | Defaults to false. Undocumented; confirm with cfforstudios@overwolf.com before building on it. |
Blueprint: call Get Me, wire on_results to a custom event taking an FMe named me, split the struct, and branch on hasConnectedAccount. There is no association node, because there is no SDK call. The node's on_error carries an FCFCoreError, so read error.apiError.
GetMe is the one authorized call with no local guard. Without a token it sends the request anyway and the backend answers 401, so it cannot be a pre-authentication check.
hasConnectedAccount is the only profile field about account connection and the SDK does not document how the server populates it. Confirm the semantics with cfforstudios@overwolf.com before gating a permanent UI decision on it, and keep the prompt dismissible meanwhile.
Error signals on this surface
FCFCoreError carries isError, a code of type ECFCoreErrorCodes, a nested apiError of type FCFCoreApiResponseError, and a description to log rather than show to players. For a remote failure the SDK always sets code to ApiError and copies the whole response error into apiError, so the reason is never in code: check code for local versus remote, then read the booleans on apiError.
| Signal | Where | Meaning and recovery |
|---|---|---|
FailedToInitialize | code | SDK not initialized. Initialize first. Not a sign-in error. |
AlreadyInitialized | code | Declared on the enum but never returned in the current release. A second Initialize call re-runs initialization instead of failing. See Error handling. |
MissingUserDataDirectory | code | userDataDirectory is empty. Returned from Initialize. |
FailedSettingAuthToken | code | Token obtained, could not be written to disk. Check userDataDirectory permissions. Retrying the code will not help. |
ApiError | code | The failure came from the server. Inspect apiError. |
UserNotAuthenticated | code | Not raised on this surface. It belongs to the creation and subscription calls, which guard locally. |
badRequest | apiError (400) | Malformed input, for example a rejected one-time code or a bad provider ticket. |
tokenExpired | apiError (401) | Token expired, missing or rejected. The SDK already cleared the local copy. Re-authenticate. |
missingPrivileges | apiError (403) | Authenticated but not allowed, or the token or API key is missing. Re-authenticating may not fix it. |
serverUnreachable | apiError (500, 502, 503, 504, or a transport failure with no HTTP response) | Transient. Retry with backoff. Do not sign the player out. |
entityNotFound, resourceExpired | apiError (404, 410) | Target does not exist, or the resource is gone. |
cancelled, failedToParseServerResponse | apiError | The request was cancelled, or the response body could not be deserialized. |
errorCode, description | apiError | Numeric backend code and message. errorCode is 0 when the backend supplied none. |
The platform CurseForge sees on an authentication request comes from the SDK's internal platform string, sent as the x-platform header. ECFCorePlatform is not a parameter of any authentication call. The Authorization: Bearer header is added only when a stored token is non-empty. Never build it yourself and never log the token.
Where to go when this is not enough
Provider enablement, the GOG Encrypted App Ticket Key and per-provider configuration live in the Authentication section of the Developer Portal at console.curseforge.com. For a provider that is not in the enum, the WB token format, the temporary token lifetime, the intended handling of FTermsLink.required, or the exact semantics of FMe.hasConnectedAccount and FMe.diagnostics, email cfforstudios@overwolf.com.