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']);
}