Skip to content

Instantly share code, notes, and snippets.

@kgaughan
Last active March 24, 2023 13:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kgaughan/e15ea8373da8303d3eabd66abfad4a06 to your computer and use it in GitHub Desktop.
Save kgaughan/e15ea8373da8303d3eabd66abfad4a06 to your computer and use it in GitHub Desktop.
{
"detail": {
"attributes": {
"hideResponseInput": true,
"img": "static/img/FIDO-U2F-Security-Key-444x444.png",
"webAuthnSignRequest": {
"allowCredentials": [
{
"id": "nPr77L4_***",
"transports": [
"usb"
],
"type": "public-key"
}
],
"challenge": "ZvNo***",
"rpId": "local.gaughan.ie",
"timeout": 60000,
"userVerification": "preferred"
}
},
"client_mode": "webauthn",
"image": "static/img/FIDO-U2F-Security-Key-444x444.png",
"message": "Please confirm with your WebAuthn token (Yubico U2F EE Serial *********)",
"messages": [
"Please confirm with your WebAuthn token (Yubico U2F EE Serial *********)"
],
"multi_challenge": [...],
"serial": "WAN0000C6D7",
"threadid": 140139447605376,
"transaction_id": "06514302233648181969",
"transaction_ids": [
"06514302233648181969"
],
"type": "webauthn",
"preferred_client_mode": "webauthn"
},
"id": 2,
"jsonrpc": "2.0",
"result": {
"authentication": "CHALLENGE",
"status": true,
"value": false
},
"time": 1679603318.2812126,
"version": "privacyIDEA 3.8.1",
"versionnumber": "3.8.1",
"signature": "rsa_sha256_pss:***"
}
{
"detail": {
"message": "Response did not match the challenge.",
"serial": "WAN0000C6D7",
"threadid": 140139447605376,
"type": "webauthn"
},
"id": 2,
"jsonrpc": "2.0",
"result": {
"authentication": "REJECT",
"status": true,
"value": false
},
"time": 1679603320.8933861,
"version": "privacyIDEA 3.8.1",
"versionnumber": "3.8.1",
"signature": "rsa_sha256_pss:***"
}
{
"action": {
"webauthn_relying_party_id": "local.gaughan.ie",
"webauthn_relying_party_name": "Keith Gaughan",
},
"active": True,
"name": "enrollment",
"priority": 1,
"realm": ["users"],
"resolver": ["LDAP"],
"scope": "enrollment",
}
{
"action": {
"auditlog": True,
"delete": True,
"disable": True,
"enrollU2F": True,
"enrollWEBAUTHN": True,
"reset": True,
"revoke": True,
"setdescription": True,
},
"active": True,
"name": "selfservice",
"priority": 1,
"realm": ["users"],
"resolver": ["LDAP"],
"scope": "user",
}
{
"action": {
"u2f_facets": "pi.local.gaughan.ie auth.local.gaughan.ie",
"webauthn_allowed_transports": "usb",
},
"active": True,
"name": "sso",
"priority": 1,
"scope": "authentication",
}
POST /validate/check HTTP/1.1
Accept-Encoding: identity
Content-Type: application/x-www-form-urlencoded
Content-Length: 515
Host: pi.local.gaughan.ie
User-Agent: Python-urllib/3.11
Accept: application/json
Connection: close
user=keith.gaughan
pass=
transaction_id=06514302233648181969
credentialid=nPr77L4_***
clientdata=eyJ***
signaturedata=MEYC***
authenticatordata=biZW-***
import json
import logging
import ssl
from urllib.request import Request, build_opener, HTTPSHandler
from urllib.parse import urlencode
from bottle import Bottle, request, run, static_file, template
PI_HOST = "pi.local.gaughan.ie"
USERNAME_FORM = r"""<!DOCTYPE html>
<html>
<head>
<title>PrivacyIDEA minimal</title>
</head>
<body>
<h1>PrivacyIDEA minimal client</h1>
<form action="/" method="POST">
<label>Username:<br><input type="text" name="user"></label><br>
<input type="submit" value="Trigger check">
</form>
</body>
</html>
"""
SIGN_FORM = r"""<!DOCTYPE html>
<html>
<head>
<title>PrivacyIDEA minimal</title>
<script type="text/javascript" src="/pi-webauthn.js"></script>
<script type="text/javascript" defer>
window.addEventListener('DOMContentLoaded', () => {
'use strict';
pi_webauthn.sign({{! sign_req }}).then((response) => {
const frm = document.forms.validate;
for (const key in response) {
frm.elements[key].value = response[key];
}
frm.submit();
}).catch((error) => {
alert(error);
});
});
</script>
</head>
<body>
<form action="/validate" method="POST" name="validate">
<input type="hidden" name="user" value="{{ user }}">
<input type="hidden" name="transaction_id" value="{{ transaction_id }}">
<label>Credential ID:<br><input type="text" name="credentialid" value=""></label><br>
<label>Client data:<br><input type="text" name="clientdata" value=""></label><br>
<label>Authenticator data:<br><input type="text" name="authenticatordata" value=""></label><br>
<label>Signature data:<br><input type="text" name="signaturedata" value=""></label><br>
<label>User handle:<br><input type="text" name="userhandle" value=""></label><br>
<label>Assertion client extensions:<br><input type="text" name="assertionclientextensions" value=""></label><br>
<input type="submit" value="Trigger check">
</form>
</body>
</html>
"""
FINAL = r"""<!DOCTYPE html>
<html>
<head>
</head>
<body>
<pre>{{ result }}</pre>
</body>
</html>
"""
def send_check(args):
req = Request(
f"https://{PI_HOST}/validate/check",
data=urlencode(args).encode("ascii"),
headers={"Accept": "application/json"},
)
opener = build_opener(
HTTPSHandler(debuglevel=1, context=ssl.create_default_context()),
)
response = opener.open(req)
return json.load(response)
def get_challenge(user):
return send_check({"user": user, "pass": ""})
def validate_challenge(
user,
transaction_id,
credential_id,
client_data,
signature_data,
authenticator_data,
user_handle,
assertion_client_extensions,
):
req = {
"user": user,
"pass": "",
"transaction_id": transaction_id,
"credentialid": credential_id,
"clientdata": client_data,
"signaturedata": signature_data,
"authenticatordata": authenticator_data,
}
if user_handle != "":
req["userhandle"] = user_handle
if assertion_client_extensions != "":
req["assertionclientextensions"] = assertion_client_extensions
return send_check(req)
app = Bottle()
@app.get("/")
def index():
return USERNAME_FORM
@app.post("/")
def do_sign():
challenge_res = get_challenge(request.forms.get("user"))
detail = challenge_res["detail"]
print(challenge_res)
return template(
SIGN_FORM,
user=request.forms.get("user"),
transaction_id=detail["transaction_id"],
sign_req=json.dumps(detail["attributes"]["webAuthnSignRequest"]),
)
@app.post("/validate")
def do_validate():
validation_res = validate_challenge(
user=request.forms.get("user"),
transaction_id=request.forms.get("transaction_id"),
credential_id=request.forms.get("credentialid"),
client_data=request.forms.get("clientdata"),
signature_data=request.forms.get("signaturedata"),
authenticator_data=request.forms.get("authenticatordata"),
user_handle=request.forms.get("userhandle"),
assertion_client_extensions=request.forms.get("assertionclientextensions"),
)
return template(
FINAL,
result=json.dumps(validation_res, indent=2),
)
@app.get("/pi-webauthn.js")
def js():
return static_file("pi-webauthn.js", root="")
if __name__ == "__main__":
logging.basicConfig(format="%(asctime)s - %(message)s", level=logging.DEBUG)
run(
app=app,
server="cheroot",
host="127.0.0.1",
port=443,
certfile="cert.pem",
keyfile="key.pem",
)
@kgaughan
Copy link
Author

For anyone looking at this, it relates to this post on the PrivacyIDEA community forums. The root cause of the issues I was having is that PrivacyIDEA expects the Origin header to be sent in the API requests to /validate/check. Setting that to a domain under the relying party resolves the issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment