Skip to content

Commit 27f05d1

Browse files
authored
docs: Add Altcha captcha solution to manual (#992)
* docs: Add Altcha captcha solution to manual * added fixture to altcha
1 parent 35d3543 commit 27f05d1

1 file changed

Lines changed: 209 additions & 1 deletion

File tree

docs/chapter-16.rst

Lines changed: 209 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -816,7 +816,6 @@ You need to style the tags. For example:
816816
line-height: 1.2em;
817817
margin: 2px;
818818
cursor: pointer;
819-
opacity: 0.2;
820819
text-transform: capitalize;
821820
}
822821
ul.tags-list li[data-selected=true] {
@@ -873,3 +872,212 @@ but the entire page will be redirected.
873872
874873
The contents of the component html can contain `<script>...</script>` and they can modify global page variables
875874
as well as modify other components.
875+
876+
.. _altcha_captcha:
877+
878+
Adding a Captcha Solution with Altcha
879+
-------------------------------------
880+
881+
This section provides a simple captcha implementation for your py4web applications using the **Altcha** library. While not exhaustively tested, it serves as a practical example for integrating a robust, client-side captcha solution.
882+
More information in https://altcha.org
883+
884+
Prerequisites
885+
^^^^^^^^^^^^^
886+
887+
First, you need to install the Altcha library. You can do this using pip:
888+
889+
.. code-block:: bash
890+
891+
python3 -m pip install --upgrade altcha
892+
893+
You also need a secret key for HMAC verification. It's recommended to store this in your application's settings. For this example, we'll assume you have a file like ``.settings.py`` with the following variable:
894+
895+
.. code-block:: python
896+
897+
# .settings.py
898+
ALTCHA_HMAC_KEY = "your-very-secret-key-here"
899+
900+
Controller Logic
901+
^^^^^^^^^^^^^^^^
902+
903+
Next, you need to add the necessary actions to your controller file. The following code provides two actions: one to generate the captcha challenge (``altcha``) and another to handle a form with the captcha (``some_form``).
904+
905+
.. code-block:: python
906+
907+
# controllers/default.py
908+
from altcha import (
909+
create_challenge,
910+
verify_solution,
911+
ChallengeOptions,
912+
)
913+
from py4web import action, response, request, URL, Field, flash, Form
914+
from py4web.utils.form import XML, T
915+
from .settings import ALTCHA_HMAC_KEY
916+
917+
@action("altcha", method=["GET"])
918+
def get_altcha():
919+
"""Generates and returns an Altcha challenge."""
920+
try:
921+
challenge = create_challenge(
922+
ChallengeOptions(
923+
hmac_key=ALTCHA_HMAC_KEY,
924+
max_number=50000,
925+
)
926+
)
927+
response.headers["Content-Type"] = "application/json"
928+
return challenge.__dict__
929+
except Exception as e:
930+
response.status = 500
931+
return {"error": f"Failed to create challenge: {str(e)}"}
932+
933+
@action.uses("form_altcha.html", session, flash)
934+
def some_form():
935+
"""An example form that uses the Altcha captcha."""
936+
fields = [
937+
Field("name", requires=IS_NOT_EMPTY()),
938+
Field("color", type="string", requires=IS_IN_SET(["red", "blue", "green"])),
939+
]
940+
form = Form(fields,
941+
csrf_session=session,
942+
submit_button=T("Submit"))
943+
944+
# Insert the Altcha widget HTML before the submit button
945+
form.structure.insert(-1, XML('<altcha-widget></altcha-widget></br>'))
946+
947+
if form.accepted:
948+
altcha_payload = request.POST.get("altcha")
949+
if not altcha_payload:
950+
response.status = 400
951+
flash.set("NO ALTCHA payload")
952+
print("NO ALTCHA payload")
953+
else:
954+
ok, error = verify_solution(altcha_payload, ALTCHA_HMAC_KEY)
955+
if not ok:
956+
response.status = 400
957+
flash.set(f"ALTCHA verification fail: {error}")
958+
print("ALTCHA verification fail:", error)
959+
else:
960+
flash.set("ALTCHA verified.")
961+
962+
return dict(form=form)
963+
964+
View Templates
965+
^^^^^^^^^^^^^^
966+
967+
You need to include the Altcha JavaScript library and configure the widget in your HTML templates.
968+
969+
``form_altcha.html``
970+
""""""""""""""""""""
971+
972+
This template works with the ``some_form`` action. It loads the Altcha script and sets the ``challengeurl`` attribute to point to our ``altcha`` action.
973+
974+
.. code-block:: html
975+
976+
[[extend 'layout.html']]
977+
978+
<script async defer src="https://cdn.jsdelivr.net/gh/altcha-org/altcha/dist/altcha.min.js" type="module"></script>
979+
<script>
980+
document.addEventListener("DOMContentLoaded", function () {
981+
const altchaWidget = document.querySelector("altcha-widget");
982+
if (altchaWidget) {
983+
altchaWidget.setAttribute("challengeurl", "[[=URL('altcha')]]");
984+
}
985+
});
986+
</script>
987+
<div class="section">
988+
<div class="vars">[[=form]]</div>
989+
</div>
990+
991+
Custom Auth Form
992+
""""""""""""""""
993+
994+
For a custom authentication form, you can follow a similar approach. Make sure to insert the ``<altcha-widget>`` tag into the form's structure and include the necessary JavaScript.
995+
996+
.. code-block:: html
997+
998+
[[extend "layout.html"]]
999+
<script async defer src="https://cdn.jsdelivr.net/gh/altcha-org/altcha/dist/altcha.min.js" type="module"></script>
1000+
1001+
<script>
1002+
document.addEventListener("DOMContentLoaded", function () {
1003+
const altchaWidget = document.querySelector("altcha-widget");
1004+
if (altchaWidget) {
1005+
altchaWidget.setAttribute("challengeurl", "[[=URL('altcha')]]");
1006+
}
1007+
});
1008+
</script>
1009+
<style>
1010+
.auth-container {
1011+
max-width: 80%;
1012+
min-width: 400px;
1013+
margin-left: auto;
1014+
margin-right: auto;
1015+
border: 1px solid #e1e1e1;
1016+
border-radius: 10px;
1017+
padding: 20px;
1018+
}
1019+
</style>
1020+
1021+
[[form.structure.insert(-1,XML('<altcha-widget></altcha-widget></br>'))]]
1022+
1023+
<div class="auth-container">[[=form]]</div>
1024+
1025+
To enable Altcha in the auth form, you can use the following fixture:
1026+
1027+
.. code-block:: python
1028+
1029+
class AltchaServerFixture(Fixture):
1030+
def __init__(self, hmac_key=ALTCHA_HMAC_KEY):
1031+
super().__init__()
1032+
self._name = "altcha_server"
1033+
self.hmac_key = hmac_key
1034+
1035+
def on_success(self, context):
1036+
# Only verify Altcha for POST requests
1037+
if request.method != "POST":
1038+
return
1039+
payload = request.POST.get("altcha")
1040+
if not payload:
1041+
raise HTTP(400, "ALTCHA payload not received")
1042+
try:
1043+
verified, err = verify_solution(payload, self.hmac_key, True)
1044+
if not verified:
1045+
raise HTTP(400, "Invalid ALTCHA")
1046+
return {"success": True, "message": "Altcha verification passed"}
1047+
except Exception as e:
1048+
raise HTTP(500, "Exception in Altcha verification")
1049+
1050+
@property
1051+
def name(self):
1052+
return self._name
1053+
1054+
Make sure ``AltchaServerFixture`` is accessible in ``common.py`` where ``auth`` is instantiated:
1055+
1056+
.. code-block:: python
1057+
1058+
from .fixtures import AltchaServerFixture
1059+
auth.enable(uses=(session, T, db, AltchaServerFixture()), env=dict(T=T))
1060+
1061+
This will ensure Altcha verification is performed for POST requests in your authentication forms.
1062+
1063+
You can also use the ``AltchaServerFixture`` in a form:
1064+
1065+
.. code-block:: python
1066+
1067+
@action('other_form', method=['GET', 'POST'])
1068+
@action.uses('form_altcha.html', session, flash, AltchaServerFixture())
1069+
def other_form():
1070+
fields = [
1071+
Field("name", requires=IS_NOT_EMPTY()),
1072+
Field("color", type="string", requires=IS_IN_SET(["red","blue","green"])),
1073+
]
1074+
form = Form(fields,
1075+
csrf_session=session,
1076+
submit_button=T("Submit"))
1077+
antes_submit = len(form.structure) - 3
1078+
form.structure.insert(antes_submit, XML('<altcha-widget></altcha-widget></br>'))
1079+
if form.accepted:
1080+
# You can assume here that the Altcha payload was verified by the fixture
1081+
flash.set("Form and Altcha successfully verified.")
1082+
# Process the form data here
1083+
return dict(form=form)

0 commit comments

Comments
 (0)