Your first ceremony
ReadyDrive the whole ceremony from your own backend in about thirty lines: create an opaque user, issue a challenge over an exact action, await the signed result on a stream, and verify it. The same four moves you ran from the CLI in the Quickstart — now in your code.
Who this is for: a backend engineer wiring Ydentifi into a server. You will finish with the full create→challenge→await→verify path in your language of choice. Every call is server-to-server over mutual TLS; the client certificate identifies your relying party, so you never send who-you-are in a body.
- A running deployment — the local one from the Quickstart is perfect.
- An SDK for your language (or call the gRPC API directly), and your relying-party mTLS client certificate.
- At least one enrolled device for the user —
ydfi seed demoprints QR codes you can scan.
The four moves, in code#
Every call maps onto one move of the ceremony. Read these four, then copy the complete program below.
CreateUserMint an opaque identity for the user. The request is empty; your mTLS certificate says which RP you are.
IssueAuthChallengeAsk for proof over the exact action, at the tier you require. You get back a challenge_id and a QR-encodable URL.
AwaitChallengeBlock until the challenge reaches a terminal state — the SDK holds the stream open for you and returns the deployment-verified verdict. No polling, no webhooks.
On APPROVED, verify the device’s signature over the canonical payload, then let the action proceed. The decision stays yours.
Create one opaque user per real individual and keep that mapping for their lifetime. Minting a second identity for the same person fragments their devices and history across unrelated tokens and silently weakens every guarantee — de-duplicate on your side before calling CreateUser. See CreateUser in the API reference.
The whole ceremony#
End to end, in your language — each snippet’s first line shows how to install that SDK, once the packages are published at launch. Pick a tab:
// install: go get github.com/ydentifi/go // 0. Connect over mutual TLS — your client cert identifies your RP. client, err := ydentifi.New( ydentifi.WithServerAddr("ydentifi.acme.internal:443"), ydentifi.WithCertFiles("rp.crt", "rp.key"), // your RP client cert ydentifi.WithCAFile("ca.crt"), // your deployment root CA ) if err != nil { /* handle */ } defer client.Close() // 1. Create an opaque user. Do this ONCE per real person and keep the // opaque_id -> person mapping on your side (see the gotcha below). opaqueID, err := client.CreateUser(ctx) if err != nil { /* handle */ } // 2. Issue a challenge over the EXACT action, at the tier you require. ch, err := client.IssueAuthChallenge(ctx, ydentifi.IssueAuthChallengeRequest{ OpaqueID: opaqueID, TTLSeconds: 120, Context: []byte("Approve €4,200 transfer to ···· 8842"), MinStrength: ydentifi.AuthStrengthBiometric, // required — no implicit default }) if err != nil { /* handle */ } // Render ch.QREncoded as a QR, or push it to the enrolled device. // 3. Block until the challenge is terminal — the SDK holds the stream open // and returns the deployment-verified verdict. No polling, no webhook. result, err := client.AwaitChallenge(ctx, ch.ChallengeID) if err != nil { /* handle */ } switch result.Status { case ydentifi.ChallengeStatusApproved: proceed() // 4. the deployment already verified the signature + strength case ydentifi.ChallengeStatusDenied: reject(result.DenialReason) case ydentifi.ChallengeStatusExpired: // issue a fresh challenge }
// no SDK — generate Go stubs from the public proto: buf generate // Raw gRPC — no SDK. Dial with your mTLS material, then call. creds := credentials.NewTLS(&tls.Config{ Certificates: []tls.Certificate{rpCert}, // your client cert RootCAs: trustPool, // your deployment CA }) conn, err := grpc.Dial("ydentifi.acme.internal:443", grpc.WithTransportCredentials(creds)) if err != nil { /* handle */ } defer conn.Close() client := identity.NewIdentityServiceClient(conn) u, err := client.CreateUser(ctx, &identity.CreateUserRequest{}) if err != nil { /* handle */ } iss, err := client.IssueAuthChallenge(ctx, &identity.IssueAuthChallengeRequest{ OpaqueId: u.OpaqueId, TtlSeconds: 120, Context: []byte("Approve €4,200 transfer to ···· 8842"), MinStrength: identity.AuthStrength_BIOMETRIC, // required — no implicit default }) if err != nil { /* handle */ } stream, err := client.AwaitChallenge(ctx, &identity.AwaitChallengeRequest{ChallengeId: iss.ChallengeId}) if err != nil { /* handle */ } for { frame, err := stream.Recv() if err == io.EOF { break } if err != nil { /* handle */ } if frame.Status == identity.AuthChallengeStatus_AUTH_CHALLENGE_STATUS_APPROVED { proceed() // deployment-verified; frame.Signature is yours to log } }
# install: pip install ydentifi # 0. Connect over mutual TLS. client = ydentifi.Client( server_addr="ydentifi.acme.internal:443", cert_path="/etc/ydentifi/rp.crt", # your RP client cert key_path="/etc/ydentifi/rp.key", ca_path="/etc/ydentifi/ca.crt", # your deployment root CA ) # 1. Create an opaque user (once per real person; keep the mapping). opaque_id = client.create_user() # 2. Issue a challenge over the exact action, at the tier you require. challenge = client.issue_auth_challenge(ydentifi.IssueAuthChallengeRequest( opaque_id=opaque_id, ttl_seconds=120, context="Approve €4,200 transfer to ···· 8842".encode(), min_strength=ydentifi.AuthStrength.BIOMETRIC, # required )) # render challenge.qr_encoded as a QR # 3. Block until terminal — the deployment verifies and returns the verdict. result = client.await_challenge(challenge.challenge_id) if result.status == ydentifi.ChallengeStatus.APPROVED: proceed() # 4. proceed elif result.status == ydentifi.ChallengeStatus.DENIED: reject(result.denial_reason.name) elif result.status == ydentifi.ChallengeStatus.EXPIRED: pass # issue a fresh challenge
// install: npm install @ydentifi/client // 0. Connect over mutual TLS. const client = new Client({ serverAddr: "ydentifi.acme.internal:443", certPath: "/etc/ydentifi/rp.crt", // your RP client cert keyPath: "/etc/ydentifi/rp.key", caPath: "/etc/ydentifi/ca.crt", // your deployment root CA }); // 1. Create an opaque user (once per real person; keep the mapping). const opaqueId = await client.createUser(); // 2. Issue a challenge over the exact action, at the tier you require. const ch = await client.issueAuthChallenge({ opaqueId, ttlSeconds: 120, context: Buffer.from("Approve €4,200 transfer to ···· 8842"), minStrength: AuthStrength.Biometric, // required }); // render ch.qrEncoded as a QR // 3. Block until terminal — the deployment verifies and returns the verdict. const result = await client.awaitChallenge(ch.challengeId, AbortSignal.timeout(120_000)); if (result.status === ChallengeStatus.Approved) { proceed(); // 4. proceed } else if (result.status === ChallengeStatus.Denied) { reject(result.denialReason); } else if (result.status === ChallengeStatus.Expired) { // issue a fresh challenge }
Each snippet uses the published SDK API — every backend SDK ships the same calls as runnable programs in its examples/ directory. See the API reference for each call’s full request, response, and typed errors.
Where to go next#
- Integrate: step-up — drop this in front of one high-stakes action, leaving the rest of your login untouched.
- Integrate: full — make Ydentifi your second factor across the product.
- Factors & levels — choose the right level per action.
- API reference — every RPC, request and response, with runnable examples.