Code samples

Sign-in with IRLProof.me in four common stacks. Each is the library's ordinary OpenID Connect support pointed at https://sandbox.irlproof.me; nothing here is specific to us beyond the address. Register the site in the developer console first, and see Sign-in for what comes back.

ASP.NET Core Next.js (Auth.js) Python (Flask and Authlib) PHP Webhooks

ASP.NET Core

dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect

Program.cs

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(o =>
    {
        o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        o.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie()
    .AddOpenIdConnect(o =>
    {
        o.Authority = "https://sandbox.irlproof.me";
        o.ClientId = builder.Configuration["IrlProof:ClientId"];
        o.ClientSecret = builder.Configuration["IrlProof:ClientSecret"];
        o.ResponseType = "code";
        o.UsePkce = true;
        o.Scope.Clear();
        o.Scope.Add("openid");
        o.SaveTokens = true;              // keeps the ID token for sign-out
        o.MapInboundClaims = false;       // keep "sub" as "sub"
        o.GetClaimsFromUserInfoEndpoint = true;
        o.SignedOutCallbackPath = "/signout-callback-oidc";
    });
builder.Services.AddAuthorization();

var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();

app.MapGet("/", (HttpContext http) => http.User.Identity?.IsAuthenticated == true
    ? Results.Text($"Signed in as {http.User.FindFirst("sub")?.Value}")
    : Results.Text("Not signed in. Go to /login"));

app.MapGet("/login", () => Results.Challenge(
    new AuthenticationProperties { RedirectUri = "/" },
    [OpenIdConnectDefaults.AuthenticationScheme]));

app.MapGet("/logout", () => Results.SignOut(
    new AuthenticationProperties { RedirectUri = "/" },
    [CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme]));

app.Run();

Register https://your-site/signin-oidc as the redirect address and https://your-site/signout-callback-oidc as the sign-out return address. Keep the secret in user secrets or your host's configuration, not in appsettings.json.

Next.js (Auth.js)

npm install next-auth@beta

auth.ts

import NextAuth from "next-auth";

export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [
    {
      id: "irlproof",
      name: "IRLProof.me",
      type: "oidc",
      issuer: "https://sandbox.irlproof.me/",
      clientId: process.env.AUTH_IRLPROOF_ID,
      clientSecret: process.env.AUTH_IRLPROOF_SECRET,
      checks: ["pkce", "state", "nonce"],
      authorization: { params: { scope: "openid" } },
    },
  ],
});

app/api/auth/[...nextauth]/route.ts

import { handlers } from "@/auth";
export const { GET, POST } = handlers;

app/page.tsx

import { auth, signIn, signOut } from "@/auth";

export default async function Home() {
  const session = await auth();
  if (!session) {
    return (
      <form action={async () => { "use server"; await signIn("irlproof"); }}>
        <button>Sign in with IRLProof.me</button>
      </form>
    );
  }
  return (
    <form action={async () => { "use server"; await signOut(); }}>
      <p>Signed in.</p>
      <button>Sign out</button>
    </form>
  );
}

Register https://your-site/api/auth/callback/irlproof as the redirect address. Set AUTH_SECRET as Auth.js requires. The person's identifier is the token's sub; read it in the jwt callback if your app stores users.

Python (Flask and Authlib)

pip install flask authlib requests

app.py

import os
from flask import Flask, redirect, session, url_for
from authlib.integrations.flask_client import OAuth

app = Flask(__name__)
app.secret_key = os.environ["FLASK_SECRET_KEY"]

oauth = OAuth(app)
oauth.register(
    name="irlproof",
    server_metadata_url="https://sandbox.irlproof.me/.well-known/openid-configuration",
    client_id=os.environ["IRLPROOF_CLIENT_ID"],
    client_secret=os.environ["IRLPROOF_CLIENT_SECRET"],
    client_kwargs={"scope": "openid", "code_challenge_method": "S256"},
)


@app.route("/")
def home():
    user = session.get("user")
    return f"Signed in as {user['sub']}" if user else '<a href="/login">Sign in with IRLProof.me</a>'


@app.route("/login")
def login():
    return oauth.irlproof.authorize_redirect(url_for("callback", _external=True))


@app.route("/callback")
def callback():
    token = oauth.irlproof.authorize_access_token()
    session["user"] = token["userinfo"]      # verified ID token claims
    session["id_token"] = token["id_token"]
    return redirect("/")


@app.route("/logout")
def logout():
    id_token = session.pop("id_token", None)
    session.clear()
    metadata = oauth.irlproof.load_server_metadata()
    return redirect(metadata["end_session_endpoint"]
                    + "?id_token_hint=" + (id_token or "")
                    + "&post_logout_redirect_uri=" + url_for("home", _external=True))

Register https://your-site/callback as the redirect address and https://your-site/ as the sign-out return address.

PHP

composer require jumbojett/openid-connect-php

login.php

<?php
require __DIR__ . '/vendor/autoload.php';

use Jumbojett\OpenIDConnectClient;

session_start();

$oidc = new OpenIDConnectClient(
    'https://sandbox.irlproof.me',
    getenv('IRLPROOF_CLIENT_ID'),
    getenv('IRLPROOF_CLIENT_SECRET')
);
$oidc->setRedirectURL('https://your-site/login.php');
$oidc->setCodeChallengeMethod('S256');
$oidc->addScope(['openid']);

// Sends the browser to IRLProof.me, and on the way back redeems the code.
$oidc->authenticate();

$_SESSION['sub']      = $oidc->requestUserInfo('sub');
$_SESSION['id_token'] = $oidc->getIdToken();
header('Location: /');

logout.php

<?php
require __DIR__ . '/vendor/autoload.php';

use Jumbojett\OpenIDConnectClient;

session_start();
$idToken = $_SESSION['id_token'] ?? null;
session_destroy();

$oidc = new OpenIDConnectClient('https://sandbox.irlproof.me', getenv('IRLPROOF_CLIENT_ID'), getenv('IRLPROOF_CLIENT_SECRET'));
$oidc->signOut($idToken, 'https://your-site/');

Register https://your-site/login.php as the redirect address and https://your-site/ as the sign-out return address. For WordPress, use the plugin instead.

Checking a webhook

Compute the signature over the raw body, before any JSON parsing changes it.

C#

// header: IRLProof-Signature: t=1789000000,v1=5f2a...
static bool IsFromIrlProof(string header, string body, string secret)
{
    var parts = header.Split(',').Select(p => p.Split('=', 2)).Where(p => p.Length == 2)
        .ToDictionary(p => p[0], p => p[1]);
    if (!parts.TryGetValue("t", out var t) || !parts.TryGetValue("v1", out var v1)) return false;
    if (Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - long.Parse(t)) > 300) return false;
    var mac = HMACSHA256.HashData(Encoding.UTF8.GetBytes(secret), Encoding.UTF8.GetBytes(t + "." + body));
    return CryptographicOperations.FixedTimeEquals(
        Encoding.ASCII.GetBytes(Convert.ToHexString(mac).ToLowerInvariant()), Encoding.ASCII.GetBytes(v1));
}

JavaScript

import { createHmac, timingSafeEqual } from "node:crypto";

export function isFromIrlProof(header, rawBody, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)));
  if (!parts.t || !parts.v1) return false;
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  return expected.length === parts.v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

Python

import hashlib, hmac, time

def is_from_irlproof(header: str, raw_body: bytes, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
    if "t" not in parts or "v1" not in parts:
        return False
    if abs(time.time() - int(parts["t"])) > 300:
        return False
    expected = hmac.new(secret.encode(), parts["t"].encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

PHP

function is_from_irlproof(string $header, string $body, string $secret): bool {
    $parts = [];
    foreach (explode(',', $header) as $piece) {
        [$k, $v] = array_pad(explode('=', trim($piece), 2), 2, null);
        $parts[$k] = $v;
    }
    if (empty($parts['t']) || empty($parts['v1'])) return false;
    if (abs(time() - (int) $parts['t']) > 300) return false;
    $expected = hash_hmac('sha256', $parts['t'] . '.' . $body, $secret);
    return hash_equals($expected, (string) $parts['v1']);
}