allme as an OIDC provider
allme is a standards-compliant OpenID Connect identity provider. If your platform already speaks OIDC — WordPress, Auth0, Keycloak, Okta, a storefront's customer accounts, or anything else that takes an issuer URL and client credentials — you can offer “Sign in with allme” with no allme-specific code at all.
client_id, same client_secret, same redirect URI. Use this page when your platform wants an OIDC provider; use the other page when you want allme's richer modes (encrypted one-time claims, or a lasting connection that keeps your records up to date).Endpoints
Point your platform at the issuer and it will discover the rest:
Issuer https://api.allme.fyi
Discovery https://api.allme.fyi/.well-known/openid-configuration
JWKS https://api.allme.fyi/oauth2/jwks
Authorization https://api.allme.fyi/oauth2/authorize
Token https://api.allme.fyi/oauth2/token
The discovery document and JWKS are public — your platform reads them before it holds any credential. Fetch the discovery document to confirm what is supported rather than trusting this page to stay current:
curl -s https://api.allme.fyi/.well-known/openid-configuration | jq
| Discovery field | Value |
|---|---|
response_types_supported | ["code"] — authorization code flow only |
grant_types_supported | ["authorization_code", "refresh_token"] |
scopes_supported | ["openid", "email", "profile"] |
id_token_signing_alg_values_supported | ["RS256"] |
subject_types_supported | ["public"] |
token_endpoint_auth_methods_supported | ["client_secret_post"] — send credentials in the POST body, not the Authorization header |
code_challenge_methods_supported | ["S256", "plain"] — PKCE supported; use S256 |
The id_token
Ask for scope=openid and the token response carries an id_token alongside the access token: a JWT signed RS256, valid for one hour, verifiable against the JWKS by its kid.
| Claim | Requires | What it is |
|---|---|---|
iss | — | Always https://api.allme.fyi, on every region |
sub | — | The person's share code — allme's permanent outward-facing identifier for them. Key your account records on this. |
aud | — | Your client_id |
iat / exp | — | Issued-at and expiry (1 hour) |
auth_time | — | When the person actually authenticated — compare against max_age if you send one |
nonce | you sent one | Echoed back verbatim. Always send one and always check it. |
email | scope=email, plaintext delivery | The email address the person chose to share with you — one of their own email fields, or one they typed at the consent screen. Never their account email: that is a credential, not shared data. |
email_verified | scope=email | Whether the address they chose has been through allme's field-verification round-trip. A typed value is honestly false. Present under both delivery modes — it is a boolean, not a value. |
name | scope=profile, plaintext delivery | The name the person chose. It is an ordinary text claim: the consent screen suggests their lastname field and they may pick any of their text fields (their profile display name among them) or type a value. There is no built-in profile source, so if they decline it, it is simply absent. |
openid receives identity and nothing else — no email, no name. Ask for what you need: scope=openid email profile.Consent, and what you actually receive
scope=openid on its own is the one exchange that asks no claim consent: it yields sub, the person's share code, which is how allme identifies them outwardly anyway. The person still approves your app once, on their first sign-in to it. Anything beyond that is consented, every claim individually. The person picks which of their own fields answers each claim — or types a value.
They can decline an OPTIONAL claim, in which case it is simply absent from what you receive —
handle that gracefully; it is a normal outcome, not an error. A claim you mark required
offers no decline at all: the person answers it or the authorization does not complete, and cancelling returns the
standard error=access_denied.
A second sign-in by the same person is one tap: the choices are remembered per app and per claim, and each login
resolves the mapped field's current value. The stored choice is re-checked against your claim as it
stands now — if you have since made a claim required, or marked it verified-only and their
field has lost its verification (which happens whenever they re-save it), that box is shown again instead of
one-tapping, and under prompt=none you get error=login_required rather than a stale value.
If you later widen your scopes, they are asked again — but only about the additions.
Asking for required or verified-only
The OIDC protocol gives you no way to say "this claim is required" or "only a verified address will do" in the request. Set them per claim on your app in the allus portal, beside the redirect URIs: the scope then resolves to a claim carrying those flags on every login. Verified-only is available for the field types allme can verify — email, phone, address and the person attributes an identity check establishes (a name among them); the portal refuses it on any other type, because such an app would fail every login.
Under encrypted delivery, a verified claim's proof travels beside the value in
userinfo as values_attestation — see
Sign in with allme. Note that email_verified in the
id_token is always false for an encrypted-delivery app: allme holds only
ciphertext and will not assert a correspondence it cannot check. values_attestation is your
verification channel there; email_verified is the plaintext-delivery one.
Choose your claims delivery at registration
| Mode | What you get | Choose it when |
|---|---|---|
encrypted (default) | The id_token carries identity and email_verified. The values themselves are encrypted to your app key on the person's device and read from GET /api/oauth/userinfo; decrypt them with the allme SDK. allme's servers never see them in the clear. | You hold an app keypair and can use an SDK. This is the default because it fails closed. |
plaintext | The values ride the id_token as ordinary OIDC claims, readable by any standards library. | Your platform only speaks stock OIDC and cannot run allme code — a storefront's customer-account login, for example (see Webshop plugins). The consent screen tells the person outright that you will receive the values readable, and they consent to exactly that. |
The mode is a property of your registered app, not a request parameter — you cannot switch per request, and a plaintext app is never offered the "stay connected" live-update link (that pipeline is encryption-only).
That live-update link is offered per claim, and only for a claim you have bound to one of your service's request fields in the portal — one claim, one field. It is deliberately not something your authorization request can ask for or point at: a claim in the URL describes what you need, and only the person decides which of their own fields answers it. A claim you have not bound simply shows the person no "stay connected" option.
Plugin claims
An encrypted-delivery app may declare a plugin claim in the portal: a claim the person answers through a plugin your company added, picking from the options it serves. A plaintext-delivery app cannot declare one — its value never rides the id_token. A plugin claim is always one-time: it is never remembered, so a login of an app that declares one always shows the consent screen. Its value reaches you through userinfo like any encrypted claim, as the plugin answer's JSON string; each SDK's parsePluginValue reads it. Details: Sign in with allme → Plugin claims.
What the person sees
Your authorization request is never rendered by the API. The person is sent to allme's own sign-in screen, in their own language, on the device they are already signed in on — and if they are at a desktop, they can finish the approval on their phone instead (allme pushes a notification to it; your redirect happens in the original browser regardless). None of this changes anything for you: you still only ever observe authorize → your redirect_uri.
An account under parental supervision can complete a bare openid sign-in but cannot share data claims; such a request comes back as error=access_denied.
Standard parameters
nonce— stored with the authorization code and echoed into theid_token. Fully supported; send one on every request.max_age— honoured;auth_timetells you when the person authenticated.prompt=none— supported, and genuinely silent: nothing is rendered in either direction. You get the code back when the person has a live session AND has already consented to everything you are asking for; otherwise you are redirected back witherror=login_required, per spec. An older session that needs the person to re-confirm their identity also answerslogin_requiredrather than prompting.prompt=login— forces a fresh authentication even if a session exists.state, PKCE (code_challenge/code_challenge_method=S256) — standard, and recommended.
An authorization code is single-use: replaying one is refused.
Verifying the token
Use your platform's own OIDC library — that is the point of this layer. If you are validating by hand, the checks are the standard set:
- Fetch JWKS, select the key whose
kidmatches the token header, verify the RS256 signature. issequalshttps://api.allme.fyiexactly.audequals yourclient_id.expis in the future.nonceequals the one you generated for this request.
kid. Signing keys rotate: a new key starts signing while the previous one stays published until every token it signed has expired. A library that caches JWKS forever will fail the first time a key rotates — most handle this for you by re-fetching when they meet a kid they do not know.The SDK quickstart
Under plaintext delivery there is nothing to add. The values ride the
id_token as ordinary claims and your own OIDC library reads them — that is the whole point of this
layer, and an allme SDK would buy you nothing.
Under encrypted delivery — the default — the claim values are not in the
id_token at all. They are ciphertext under your app's key, collected from
GET /api/oauth/userinfo, so a stock OIDC client cannot read them: it gets the identity and stops
there. That is what each SDK's OAuthClient is for. Point it at an
idw-role config (client id, redirect URI, the app private key + passphrase) and it exchanges the
code, calls userinfo, and decrypts the values with the key from config — you never touch a key in code.
mode comes back as oidc on this
leg, and values is keyed by the claim name — for the standard scopes that is
email and name.from allus_company_data import OAuthClient
oauth = OAuthClient.from_config("idw.json") # client_id, redirect_uri, app private key + passphrase
# on your callback, with the code your OIDC redirect delivered:
result = oauth.complete_sign_in(code, code_verifier=verifier)
print(result["mode"]) # "oidc"
print(result["user"]["sub"]) # the person's share code — key your accounts on this
print(result["values"]["email"]) # decrypted
# the ciphertext it came from is result["values_cipher"]["email"]
# a verified claim also lands in result["attestations"]["email"].verifiedimport { OAuthClient } from "@allus-fyi/company-data";
const oauth = OAuthClient.fromConfig("idw.json");
// on your callback:
const result = await oauth.completeSignIn(code, codeVerifier);
console.log(result.mode); // "oidc"
console.log(result.user.sub); // the person's share code
console.log(result.values.email); // decrypted
// the ciphertext it came from is result.values_cipher.email
// a verified claim also lands in result.attestations.email.verifiedimport (
"fmt"
"log"
"github.com/allus-fyi/company-data-go/companydata"
)
oauth, err := companydata.OAuthClientFromConfig("idw.json")
if err != nil {
log.Fatal(err)
}
// on your callback:
result, err := oauth.CompleteSignIn(code, verifier)
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Mode) // "oidc"
fmt.Println(result.User["sub"]) // the person's share code
fmt.Println(result.Values["email"]) // decrypted
// the ciphertext it came from is result.ValuesCipher["email"]
// a verified claim also lands in result.Attestations["email"].Verifiedusing Allus.CompanyData;
var oauth = OAuthClient.FromConfig("idw.json");
// on your callback:
var result = await oauth.CompleteSignInAsync(code, codeVerifier);
Console.WriteLine(result.Mode); // "oidc"
Console.WriteLine(result.Sub); // the person's share code
Console.WriteLine(result.Values["email"]); // decrypted
// the ciphertext it came from is result.ValuesCipher["email"]
// a verified claim also lands in result.Attestations["email"].Verifiedimport fyi.allme.allus.companydata.OAuthClient;
OAuthClient oauth = OAuthClient.fromConfig("idw.json");
// on your callback:
OAuthClient.SignInResult result = oauth.completeSignIn(code, verifier);
System.out.println(result.mode()); // "oidc"
System.out.println(result.user().get("sub")); // the person's share code
System.out.println(result.values().get("email")); // decrypted
// the ciphertext it came from is result.valuesCipher().get("email")
// a verified claim also lands in result.attestations().get("email").verified()use Allus\CompanyData\OAuthClient;
$oauth = OAuthClient::fromConfig('idw.json');
// on your callback:
$result = $oauth->completeSignIn($code, codeVerifier: $verifier);
echo $result['mode']; // "oidc"
echo $result['user']['sub']; // the person's share code
echo $result['values']['email']; // decrypted
// the ciphertext it came from is $result['values_cipher']['email']
// a verified claim also lands in $result['attestations']['email']['verified']The same client also carries exchangeCode and userinfo as separate steps if you want
the raw token response — the id_token is in it, so you can still verify it with your own JWT library
— but userinfo hands back the values as ciphertext, and completeSignIn is the call that
decrypts. Naming follows each language's idiom; every result carries the same conclusions: the person’s
sub/share_code, the mode, the 2FA flag, decrypted values, the raw ciphertext each one
was decrypted from, and attestations. C# exposes
Sub/ShareCode directly on the result; the other five group them under user.
Each entry also carries the proof trio verifiedMethod / verifiedProvider /
verificationId read from the opened seal — HOW the value was proved
(email_code | sms_code | sumsub_id | sumsub_address), by whom
(allme | sumsub), and the proof id to quote back to allme in a dispute. All three arrive
together or not at all, and a value verified before that record existed carries none of them.
A claim absent from attestations is "not attested"; an entry present with
verified false is a mismatch you must reject.
The full RP walkthrough — building the button URL, the detached "approve on your phone" poll, and
the richer one_time and connect modes — is on
Sign in with allme.
Other platforms
Anything that accepts an issuer URL works the same way: WordPress OIDC plugins, Auth0 or Okta as an enterprise connection, Keycloak as an identity broker, and application frameworks' generic OIDC clients. Supply the issuer, client_id, client_secret and openid email profile; the platform discovers everything else. If a platform asks for endpoints individually rather than an issuer, take them from the discovery document above.