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 shows no consent screen: it yields sub, the person's share code, which is how allme identifies them outwardly anyway. 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.