Developer documentation

Sign in with Corenexis

Standard OAuth 2.0 authorization code. The browser receives a short-lived code, your server exchanges it for a signed access token, a rotating refresh token and the user's verified profile. No SDK, no library — plain HTTP and JSON.

Introduction

Your users already have a Corenexis Account. This lets them use it to sign in to your app — you never handle their password, and you get a verified identity back.

1. RedirectSend the browser to Corenexis with your callback URL.
2. Get a codeWe send the user back with a 60-second, single-use code.
3. Exchange itYour server swaps the code for tokens and the profile.
What you need: a server that can make an HTTPS POST and keep a secret. That is the entire requirement. Everything on this page is plain HTTP — any language, any framework.

One integration covers every way in: password, magic link, Google, GitHub, Microsoft and Facebook, plus two-factor authentication, the consent screen and device management. You do not add providers one at a time, and you do not maintain them.

1. Create your app

Apps are created and managed on the identity platform itself — accounts.corenexis.com/developer. You need a Corenexis account with a verified email address and two-step verification turned on — an app owner can issue tokens for everyone who signs in to their app, so that account has to be the hardest one to steal. This site is documentation only; nothing is registered here.

FieldNotes
Display nameWhat users see on the sign-in and consent screens. Use the real name of your app — it is the main thing people judge before approving.
Redirect URLsWhere we return the user. Prefix matched — register the full path, one per line. Building a mobile or desktop app? Register your own scheme (com.yourapp.name://auth/callback) and read §11.
Website URLrequiredShown on the consent screen, used as the "back to your app" exit on our error pages, and where a cancelling user is sent if there is no usable callback. Point it at a real landing page, not a deep link.
App logorequiredUploaded, not linked. PNG, JPG or WebP, up to 1 MB — drag it onto the field or pick a file. We re-encode it to a 256×256 WebP and serve it from Corenexis, so the consent screen never fetches an image from your server: it stays fast, and your logo host never learns who signs in to you. A wide mark is padded onto the square, never cropped. SVG is not accepted.
Privacy policy URLrequiredShown to users on the consent screen when they approve your app. An app that asks strangers for their email and profile should be able to say what it does with them.
Terms of service URLoptionalShown next to your privacy policy on the consent screen, with the line “By continuing you agree to <your app>'s terms of service and privacy policy”. Required for verification (§13).
Token lifetimesFixed for self-service apps: access token 15 minutes, refresh token 7 days.
Data accessFixed starter set — see §12.
Every URL you enter is checked when you save. The domain has to resolve and something has to answer at that address — a 403 or a 404 is fine, we are only confirming a server is really there. A link to a domain that does not exist is worse than no link, and these end up on the screen where a user decides whether to trust you. http://localhost… is exempt while you develop.

You get back two values:

ValueWhat it is
client_id A generated, opaque identifier such as app_9f2c41ab7d05e8c3f1a6b204. It is public — it appears in redirect URLs and is safe to ship in config. You do not choose it, and users never see it; they see your display name.
client_secret 48 hex characters, shown exactly once. Only a bcrypt hash is stored, so it can never be recovered — if you lose it, rotate it from your app's page and redeploy. Treat it like a password: server-side only, never in a browser, mobile app or public repository.

Store them in your app's environment:

CRX_BASE_URL=https://accounts.corenexis.com
CRX_CLIENT_ID=app_9f2c41ab7d05e8c3f1a6b204
CRX_CLIENT_SECRET=<the secret shown once>
CRX_CALLBACK_URL=https://yourapp.com/auth/callback

You can hold up to five apps per account, and you can suspend or delete any of them yourself.

2. Manage your app

Everything below is self-service from your app's page under Developer → Your apps. No ticket, no waiting.

ActionWhat happensWhen to use it
EditName, logo, website and redirect URLs change immediately. Live sessions are unaffected.Rebrand, new callback path, moving domains.
Rotate secretA new secret is shown once; the old one stops working the moment you rotate.The secret leaked, a developer left, or it is simply old. Deploy the new value first-thing after rotating.
Sign all users outEvery refresh token for your app is revoked. Access tokens keep verifying until they expire (they are self-contained JWTs).Suspected compromise, or a breaking change to your session model.
SuspendSign-in redirects to your app are refused and every /api/token grant returns invalid_client. Fully reversible.Taking the app offline for a while.
DeleteThe app and all of its sessions are removed, and the client id can never be reused.Permanent shutdown. Prefer suspending.
Users can also cut you off from their side at any time, from their Connected apps page. That deletes both your sessions and their stored consent, so reconnecting shows the consent screen again. Handle a suddenly invalid refresh token by sending the user through sign-in — not by retrying.

3. The sign-in flow

Your app Corenexis Accounts Your server │ │ │ 1. └─ redirect user ────────────▶│ │ │ │ user signs in, approves │ 2. │◀──── redirect back with ?code=… ──────────────────────▶ │ │ │ │ 3. │ │◀── POST /api/token ────────┤ │ │ client_id + secret │ │ │──── access + refresh ─────▶│ │ │ token + user │ 4. └─ your own session cookie ◀───────────────────────────────┘

Steps 1 and 2 happen in the browser. Steps 3 and 4 are server-to-server — the code is useless to anyone who does not also hold your client secret.

4. Send the user to us

Redirect the browser to the Corenexis sign-in page with your callback URL, URL-encoded, in redirect:

https://accounts.corenexis.com/?redirect=https%3A%2F%2Fyourapp.com%2Fauth%2Fcallback

/login and /signup accept ?redirect= too if you would rather land users on one of those directly. From here we handle everything: password, magic link, Google, GitHub, Microsoft, two-factor — and, the first time, the consent screen showing your app's name, logo and exactly what will be shared.

The redirect value must start with one of your registered redirect URLs. If it does not match we stop and show the user a 400 error card naming the problem. We never sign someone in to an app we cannot identify, and we never quietly drop them on their dashboard. This mismatch is the single most common integration mistake.

Send a state — strongly recommended

Generate an unguessable value per sign-in, keep it in your own session, and pass it alongside redirect. We return it unchanged on the callback:

https://accounts.corenexis.com/?redirect=https%3A%2F%2Fyourapp.com%2Fauth%2Fcallback&state=8f2b…

Refuse any callback whose state does not match the one you stored — that is what stops someone feeding your app a callback it never asked for (RFC 6749 §10.12). It is opaque to us: never parsed, never interpreted, only handed back. Maximum 512 characters; anything longer is dropped rather than truncated. Omitting it changes nothing else about the flow.

Go straight to a provider — optional

If your own screen already has a Continue with Google button, add provider and we skip our sign-in page and open Google's account picker directly:

https://accounts.corenexis.com/?redirect=https%3A%2F%2Fyourapp.com%2Fauth%2Fcallback&state=8f2b…&provider=google

Accepted values: google, github, microsoft, facebook. It is honoured only together with a valid redirect and only while that provider is enabled; otherwise the normal sign-in page is shown. The picker appears even if the browser is already signed in to Corenexis, because the user asked to choose an account. Everything after that is unchanged: the account is a Corenexis account, and consent, the code and your callback work exactly as below.

5. Handle the callback

After a successful sign-in we send the browser back to your callback with a code:

https://yourapp.com/auth/callback?code=3f9c…64-hex-chars…7b&state=8f2b…

If the user pressed Cancel on the consent screen you get an error instead — handle both:

https://yourapp.com/auth/callback?error=access_denied&state=8f2b…

Cancel: what your app must do

When you receive error=access_denied, show your signed-out page — a landing page, or a sign-in screen with a button the user has to press.

Do not start sign-in again automatically. This is the one integration mistake that makes Cancel look broken, and it is easy to make in a single-page app: the callback runs, your router lands on a guarded route, an /api/me call returns 401, and the app redirects into SSO again. The user is then straight back on the consent screen they just dismissed, with no way out — and it reads as our bug, not yours. "The user declined" is a state your app should be able to sit in, exactly like a sign-out.

Your policy links on the consent screen

If your app has a privacy policy URL or a terms URL, the consent screen shows them under your app's name and closes with “By continuing you agree to <your app>'s terms of service and privacy policy” — the same treatment Google and Microsoft give a client's own policies. Set neither and nothing is rendered: users are never shown a link that goes nowhere. Both live on your app's page and take effect immediately.

As a safety net we refuse to render the consent screen twice in a row for an app the user just declined: the second request inside a short window gets a dead-end page with links back to your app and to their account. That stops the loop, but it is a guard rail, not a substitute for handling the error.

The code is single use and expires in 60 seconds. Exchange it immediately, server-side.

6. Exchange the code for tokens

Server-to-server, within 60 seconds of receiving the code. Pick your language — the choice sticks for every sample on this page.

curl -X POST https://accounts.corenexis.com/api/token \
  -d grant_type=authorization_code \
  -d code="$CODE" \
  -d client_id="$CRX_CLIENT_ID" \
  -d client_secret="$CRX_CLIENT_SECRET"

HTTP Basic works too — Authorization: Basic base64(client_id:client_secret) instead of the two body fields.

You may also send redirect_uri with the exchange (RFC 6749 §4.1.3). If you do, it must be exactly the redirect the code was issued for, or the exchange fails with invalid_grant. Leaving it out is fine.

What comes back

{
  "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6…",
  "token_type": "Bearer",
  "expires_in": 900,
  "refresh_token": "a1b2c3…",
  "refresh_expires_in": 604800,
  "user": {
    "id": "9c1f8e42-5b7a-4d3e-9a11-72c4e8f0b5d6",
    "email": "[email protected]",
    "email_verified": true,
    "name": "Someone",
    "avatar_url": "https://accounts.corenexis.com/uploads/avatars/abc123.webp",
    "country": "IN",
    "timezone": "Asia/Kolkata"
  }
}

7. What your app receives

An app only ever receives the fields it has been granted. Everything else is stripped from both /api/token and /api/userinfo — there is no way to ask for more at request time, and a user is never shown a permission your app cannot use.

FieldTypeNotes
iduuidThe stable identifier. Key everything on this.
emailstringCan change — never use it as a primary key.
email_verifiedboolCheck it before trusting the address.
namestring · nullableDisplay name.
avatar_urlstring · nullableAppend ?s=<width> for a resized copy. null if the user has set no picture.
country / timezonestring · nullableISO-3166 alpha-2 and an IANA zone.
phone_country_code / phone_numberstring · nullableOnly with the phone grant (§12).
totp_enabled / created_atbool · ISO 8601Only with the security grant.
A field you were not granted is simply absent from the JSON — not null. Write your code against the list on your app's page.
Account pinning — this one matters. Users can hold several Corenexis accounts in one browser and choose which to share. Whatever session your app already had, after a token exchange you must switch it to the returned user.id. Skip this and someone who switches accounts stays logged in as the previous one.

8. Verifying the access token

The access token is an RS256 JWT. You do not have to verify it for a plain login — it came straight from us over TLS. Verify when your API receives the token from somewhere else.

Public keys: https://accounts.corenexis.com/.well-known/jwks.json
Discovery: https://accounts.corenexis.com/.well-known/openid-configuration

import { createRemoteJWKSet, jwtVerify } from 'jose';

const JWKS = createRemoteJWKSet(
  new URL(process.env.CRX_BASE_URL + '/.well-known/jwks.json'));

const { payload } = await jwtVerify(accessToken, JWKS, {
  issuer:   process.env.CRX_BASE_URL,
  audience: process.env.CRX_CLIENT_ID,
});
// payload.sub === the user's id

Check iss, aud and expiry. Cache the JWKS — keys rotate rarely, and each key carries a kid.

9. Refreshing and signing out

Refresh. Refresh tokens rotate: every refresh returns a new one and retires the old. Always store the newest.

POST https://accounts.corenexis.com/api/token

grant_type=refresh_token
&refresh_token=<current refresh token>
&client_id=<your client_id>
&client_secret=<your client_secret>
Reuse detection is on. Presenting a refresh token that has already been rotated away revokes the whole session rather than issuing a new pair — at that point two parties hold the same token and neither can be told from the other (RFC 9700 §4.14.2). If you see this, fix the retry or storage bug that resent an old token; do not work around it by retrying.

Sign out. Revoke the refresh token when the user logs out of your app.

POST https://accounts.corenexis.com/api/token

grant_type=revoke
&refresh_token=<current refresh token>
&client_id=<your client_id>
&client_secret=<your client_secret>

10. Fetching the profile later

GET https://accounts.corenexis.com/api/userinfo
Authorization: Bearer <access_token>

Returns the same user object, fresh. Use it when you want current details rather than the copy from sign-in time.

11. Android, iOS and desktop apps

A native app has no https address to come back to, so register your own scheme as the redirect URL and the operating system hands the callback to your app.

com.yourapp.name://auth/callback

Open the sign-in URL in the system browser (or a Custom Tab / ASWebAuthenticationSession), with your scheme URL-encoded:

https://accounts.corenexis.com/?redirect=com.yourapp.name%3A%2F%2Fauth%2Fcallback
  &state=<random>&code_challenge=<challenge>&code_challenge_method=S256

The user signs in and the OS routes this straight back into your app:

com.yourapp.name://auth/callback?code=<authorization_code>&state=<random>

Android — the two pieces of wiring

Declare the scheme on the activity that should receive the callback:

<!-- AndroidManifest.xml -->
<activity android:name=".AuthCallbackActivity" android:exported="true">
  <intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="com.yourapp.name" android:host="auth" />
  </intent-filter>
</activity>

Open sign-in in a Custom Tab — never a WebView, which hides the address bar the user needs to trust the page:

// Kotlin
val verifier  = randomVerifier()                 // 43–128 chars, A-Z a-z 0-9 - . _ ~
val challenge = base64UrlNoPad(sha256(verifier)) // S256
store(verifier)                                  // survives the app being backgrounded

val url = Uri.parse("https://accounts.corenexis.com/")
    .buildUpon()
    .appendQueryParameter("redirect", "com.yourapp.name://auth/callback")
    .appendQueryParameter("state", state)
    .appendQueryParameter("code_challenge", challenge)
    .appendQueryParameter("code_challenge_method", "S256")
    .build()

CustomTabsIntent.Builder().build().launchUrl(context, url)

// AuthCallbackActivity.onCreate — the OS delivers the code here
val code = intent?.data?.getQueryParameter("code")
// POST { code, code_verifier } to YOUR backend, which does the real exchange.
Your app must not contain the client secret. Anything shipped to a device can be pulled back out of it, and on most platforms any app can claim your scheme and receive the code. That code is harmless on its own — the exchange in §6 still requires the secret — so send it to your own server and exchange it there. Put the secret in the app and an attacker who catches a code has both halves.
your app ──code + verifier──▶ your backend ──code + verifier + client_secret──▶ /api/token │ └──▶ returns your own session token to the app

A desktop app can instead open a short-lived local listener and register http://127.0.0.1:<port>/callback — that is accepted too. Either way a bare scheme (com.yourapp.name://) is rejected: redirect URLs are prefix-matched, so it would hand the entire scheme to one app.

Register every URL your sign-in ends on. The consent screen is a form, and browsers apply its security policy to every hop that follows it — so the last hop must be one of your registered redirect URLs. This matters when your https callback hands the result on to your app (https://yourapp.com/auth/callback → 302 → com.yourapp.name://auth?…): register com.yourapp.name://auth as a redirect URL too. Otherwise a new user's first sign-in stops on the consent screen, while returning users (who skip consent) work fine — a confusing bug to chase.

Corenexis's own apps can additionally sign users in with the phone's Google account sheet, without a browser. That grant is limited to first-party apps and is not available to self-service apps; the flow on this page is the one to build.

PKCE — supported, and recommended here

PKCE (RFC 7636) ties the code to a secret your app generates fresh for each sign-in, so a code caught by another app on the device is useless on its own. It is optional: send a challenge and we require the verifier; send nothing and the flow is unchanged.

code_challenge        = BASE64URL( SHA256( code_verifier ) )
code_challenge_method = S256

Then send the verifier with the exchange in §6:

grant_type=authorization_code&code=…&client_id=…&client_secret=…&code_verifier=<verifier>

Only S256 is accepted — plain defeats the point. PKCE does not replace the client secret here: the exchange still needs it, so keep doing it from your backend. PKCE is the layer on top that makes a custom-scheme callback safe.

12. Scopes and asking for more

Every self-service app starts with the same set, and it is enforced on the server after the request is parsed — a hand-crafted request cannot widen it.

GrantFieldsNew apps
Account IDidAlways — not a scope, it is the subject identifier
Profilename, avatar_urlIncluded
Emailemail, email_verifiedIncluded
Localecountry, timezoneIncluded
Phonephone_country_code, phone_numberOn request
Securitytotp_enabled, created_atOn request

Requesting an increase

Phone and security are not self-service — an app has to justify them. Write to us from the email address that owns the app, through the Corenexis helpdesk, with:

  • your client_id and the app's display name;
  • which extra grant you need, and the feature that needs it — "for completeness" is not a reason;
  • where the data will be stored, and for how long;
  • a link to your privacy policy.

If it is approved, the change takes effect on the account without a redeploy. Users who already approved your app are asked again for the new permission the next time they sign in — a scope you were granted later is never handed over silently.

13. Getting your app verified

By default a self-service app is labelled Third-party app — not built or operated by Corenexis on the consent screen, and shows as Third-party on the user's Connected apps page. That label is honest, and for most apps it is fine.

A verified app drops the third-party warning and carries a Verified badge instead. Verification is presentation only — it does not change your scopes, tokens or sessions.

Both policy URLs must be filled in on your app before you apply. A privacy policy is required to register at all; terms of service is the one you may still be missing. A verified badge tells users Corenexis looked at this app, and we do not say that about an app whose terms nobody can read. Set it under Developer → your app; it appears on the consent screen immediately, no redeploy.

To apply, contact us through the helpdesk with:

  • your client_id and display name;
  • the live URL where the app runs, reachable without signing in;
  • a privacy policy URL and a terms of service URL, both set on the app and both reachable on that domain;
  • proof you control the domain (a DNS TXT record or a file we name);
  • a working sign-in we can complete end to end.
Verification is reviewed by a person and can be withdrawn if the app changes materially — a verified badge is a statement to users that we looked at it, so keep the details current.

14. Errors

Before the user reaches your callback

Some requests are stopped at Corenexis. The user sees an error page on accounts.corenexis.com, the flow ends there, and nothing is sent to your callback — no code, no error parameter. That is deliberate: an auth error must never be forwarded to a URL we could not verify.

What the user seesWhyFix
"We don't recognise that app"Your redirect does not prefix-match any URL registered for your app, or the app is suspendedCompare the exact URL you send with what is on your app's page. By far the most common mistake.
"This sign-in request has expired"They landed on the consent screen with no pending request — usually a bookmarked or stale URLAlways start the flow from your app; never link users straight to /consent
"This sign-in request has already been used"The consent form was submitted twice — back button, double click, or a retry after your callback failed to loadTheir approval is already saved. Send them through sign-in again; they will not be asked twice.
Prefix matching is literal. https://app.example.com/auth/callback does not match a registration of https://app.example.com/callback, and a trailing slash counts. If sign-in dies at the very first hop, check this before anything else.

At the token endpoint

Token errors are standard OAuth 2.0 JSON with the matching HTTP status:

{ "error": "invalid_grant", "error_description": "Code is invalid, expired or already used." }
ErrorMeaningWhat to do
invalid_clientUnknown client_id, wrong secret, or the app is suspendedCheck both values, and that the app is active on its page
invalid_grantCode expired (60s), already used, issued to another client, or the refresh token was revokedRestart the sign-in flow
invalid_grant (retired token)A refresh token that had already been rotated away was presented — treated as replay, so the session was revokedSign the user in again, then fix the retry/storage bug that resent it
invalid_grant (code_verifier)PKCE was used at sign-in but the verifier was missing or did not matchSend the same code_verifier you derived the challenge from
invalid_grant (redirect_uri)You sent a redirect_uri that differs from the one the code was issued forSend the exact same value, or leave the field out
unsupported_grant_typeNot one of the three grantsUse authorization_code, refresh_token or revoke
slow_downRate limitedBack off and retry
access_deniedThe user pressed Cancel (arrives on your callback, not here)Show a friendly "sign-in cancelled" — and do not re-enter SSO

15. Security rules

  • Register full callback paths. Redirect URLs are prefix-matched, so registering https://yourapp.com makes every page on your domain a valid callback — any one of them could capture auth codes.
  • Never ship the client secret to a browser or mobile app. There is no public-client flow here; if you cannot keep a secret, you need a small backend.
  • Exchange the code immediately, server-side. 60 seconds, one use.
  • Send a state and verify it — reject any callback whose state does not match the one you stored.
  • Handle error=access_denied without restarting sign-in. See §5.
  • Pin the session to user.id after every exchange.
  • Never key records on the email address. It can change; id cannot.
  • Store refresh tokens encrypted at rest, and always replace them after a refresh.
  • Rotate the secret immediately if it ever appears in a log, a repository or a screenshot.
  • We never send you the user's password, 2FA codes or backup codes. Nothing you receive can be used to sign in as them anywhere else.

Before you go live

  • Callback registered as a full path, and the redirect you send starts with it exactly.
  • Website URL, app logo and privacy policy set — all three appear on the consent screen, and users decide whether to trust you from them.
  • You send state and reject a mismatch.
  • Your callback handles three outcomes: code, error=access_denied, and neither (someone opened the URL directly).
  • Cancel leaves the user on your signed-out page — no automatic re-entry into SSO.
  • The code is exchanged server-side within 60 seconds, and the secret is not in your client bundle.
  • Your records are keyed on user.id, never the email address.
  • Native or desktop app? You are using PKCE and exchanging through your own backend — see §11.
  • Web callback that hands off to your app? The app URL is registered too, and you have tested a brand-new account through the consent screen — see §11.
  • You have tested a second sign-in: consent must not appear again, and a refresh must return a new refresh token that you store in place of the old one.

16. Endpoint reference

EndpointMethodAuthPurpose
/?redirect=…GET—Start sign-in (browser)
/api/tokenPOSTclient_id + secretCode exchange, refresh, revoke
/api/userinfoGETBearer access tokenCurrent profile
/.well-known/jwks.jsonGET—Public keys for JWT verification
/.well-known/openid-configurationGET—Discovery document

All of them live under https://accounts.corenexis.com.

17. FAQ

Can I choose my own client ID?
No. Self-service apps get a generated opaque ID so no app can impersonate a first-party Corenexis product. Users identify your app by its display name and logo, not its ID.
Will my app show up on other people's Corenexis dashboards?
No. Apps you create are private — reachable only through the sign-in link you send users, and clearly marked Third-party on the consent screen until they are verified.
I lost the client secret.
It cannot be recovered — only a bcrypt hash is stored. Rotate it from your app's page and redeploy with the new value. The old secret stops working the moment you rotate.
How do I test without a second account?
Corenexis supports several accounts in one browser: add another from the account menu and switch between them. That is also the fastest way to check your account pinning is right.
Do you support OpenID Connect discovery?
Yes — /.well-known/openid-configuration and a JWKS endpoint. Access tokens are RS256 JWTs, so a standard OAuth 2.0 client library is all you need.
What happens when I suspend my app?
Sign-in redirects to it are refused and every /api/token grant returns invalid_client. Existing access tokens still verify until they expire, but nothing new can be issued. Fully reversible.
Ready to build?

Create an app, copy the two values into your environment, and you can have sign-in working today.

Create an app

Stay in the Loop

Get the latest updates delivered straight to your inbox