Theme
Open the portal

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.

This is a compliance layer over the same surface described in Sign in with allme, not a separate product. The app you register in the portal is your OIDC client — same 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 fieldValue
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.

ClaimRequiresWhat 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
nonceyou sent oneEchoed back verbatim. Always send one and always check it.
emailscope=email, plaintext deliveryThe 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_verifiedscope=emailWhether 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.
namescope=profile, plaintext deliveryThe 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.
Scopes are honoured strictly. An app that asks only for 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

ModeWhat you getChoose 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.
plaintextThe 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

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:

  1. Fetch JWKS, select the key whose kid matches the token header, verify the RS256 signature.
  2. iss equals https://api.allme.fyi exactly.
  3. aud equals your client_id.
  4. exp is in the future.
  5. nonce equals the one you generated for this request.
Cache JWKS, but re-fetch on an unknown 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.

Hand the SDK the authorization code, not your library. An authorization code is single-use, so exactly one of the two can exchange it — and only the SDK path ends in plaintext. The redirect URI in the idw config must be the one your authorization request actually sent, because the exchange replays it. Whenever there are encrypted values to collect, 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"].verified
import { 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.verified
import (
    "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"].Verified
using 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"].Verified
import 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.