authentik.stages.captcha.tests

captcha tests

  1"""captcha tests"""
  2
  3from django.urls import reverse
  4from requests_mock import Mocker
  5
  6from authentik.core.tests.utils import create_test_admin_user, create_test_flow
  7from authentik.flows.markers import StageMarker
  8from authentik.flows.models import FlowDesignation, FlowStageBinding
  9from authentik.flows.planner import FlowPlan
 10from authentik.flows.tests import FlowTestCase
 11from authentik.flows.views.executor import SESSION_KEY_PLAN
 12from authentik.lib.generators import generate_id
 13from authentik.stages.captcha.models import CaptchaStage
 14from authentik.stages.captcha.stage import (
 15    PLAN_CONTEXT_CAPTCHA_PRIVATE_KEY,
 16    PLAN_CONTEXT_CAPTCHA_SITE_KEY,
 17)
 18
 19# https://developers.google.com/recaptcha/docs/faq#id-like-to-run-automated-tests-with-recaptcha.-what-should-i-do
 20RECAPTCHA_PUBLIC_KEY = "6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI"
 21RECAPTCHA_PRIVATE_KEY = "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe"
 22
 23
 24class TestCaptchaStage(FlowTestCase):
 25    """Captcha tests"""
 26
 27    def setUp(self):
 28        super().setUp()
 29        self.user = create_test_admin_user()
 30        self.flow = create_test_flow(FlowDesignation.AUTHENTICATION)
 31
 32        self.stage: CaptchaStage = CaptchaStage.objects.create(
 33            name="captcha",
 34            public_key=RECAPTCHA_PUBLIC_KEY,
 35            private_key=RECAPTCHA_PRIVATE_KEY,
 36        )
 37        self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
 38
 39    @Mocker()
 40    def test_valid(self, mock: Mocker):
 41        """Test valid captcha"""
 42        mock.post(
 43            "https://www.recaptcha.net/recaptcha/api/siteverify",
 44            json={
 45                "success": True,
 46                "score": 0.5,
 47            },
 48        )
 49        plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
 50        session = self.client.session
 51        session[SESSION_KEY_PLAN] = plan
 52        session.save()
 53        response = self.client.post(
 54            reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
 55            {"token": "PASSED"},
 56        )
 57        self.assertEqual(response.status_code, 200)
 58        self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
 59
 60    @Mocker()
 61    def test_valid_override(self, mock: Mocker):
 62        """Test valid captcha"""
 63        self.stage.private_key = generate_id()
 64        self.stage.public_key = generate_id()
 65        mock.post(
 66            "https://www.recaptcha.net/recaptcha/api/siteverify",
 67            json={
 68                "success": True,
 69                "score": 0.5,
 70            },
 71        )
 72        plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
 73        site_key = generate_id()
 74        private_key = generate_id()
 75        plan.context = {
 76            PLAN_CONTEXT_CAPTCHA_SITE_KEY: site_key,
 77            PLAN_CONTEXT_CAPTCHA_PRIVATE_KEY: private_key,
 78        }
 79        session = self.client.session
 80        session[SESSION_KEY_PLAN] = plan
 81        session.save()
 82        response = self.client.get(
 83            reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
 84        )
 85        self.assertStageResponse(response, component="ak-stage-captcha", site_key=site_key)
 86        response = self.client.post(
 87            reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
 88            {"token": "PASSED"},
 89        )
 90        self.assertEqual(response.status_code, 200)
 91        self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
 92        self.assertIn(private_key, mock.request_history[0].text)
 93
 94    @Mocker()
 95    def test_invalid_score_high(self, mock: Mocker):
 96        """Test invalid captcha (score too high)"""
 97        mock.post(
 98            "https://www.recaptcha.net/recaptcha/api/siteverify",
 99            json={
100                "success": True,
101                "score": 99,
102            },
103        )
104        plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
105        session = self.client.session
106        session[SESSION_KEY_PLAN] = plan
107        session.save()
108        response = self.client.post(
109            reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
110            {"token": "PASSED"},
111        )
112        self.assertEqual(response.status_code, 200)
113        self.assertStageResponse(
114            response,
115            component="ak-stage-captcha",
116            response_errors={"token": [{"string": "Invalid captcha response", "code": "invalid"}]},
117        )
118
119    @Mocker()
120    def test_invalid_score_low(self, mock: Mocker):
121        """Test invalid captcha (score too low)"""
122        mock.post(
123            "https://www.recaptcha.net/recaptcha/api/siteverify",
124            json={
125                "success": True,
126                "score": -3,
127            },
128        )
129        plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
130        session = self.client.session
131        session[SESSION_KEY_PLAN] = plan
132        session.save()
133        response = self.client.post(
134            reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
135            {"token": "PASSED"},
136        )
137        self.assertEqual(response.status_code, 200)
138        self.assertStageResponse(
139            response,
140            component="ak-stage-captcha",
141            response_errors={"token": [{"string": "Invalid captcha response", "code": "invalid"}]},
142        )
143
144    @Mocker()
145    def test_invalid_score_low_continue(self, mock: Mocker):
146        """Test invalid captcha (score too low, but continue)"""
147        self.stage.error_on_invalid_score = False
148        self.stage.save()
149        mock.post(
150            "https://www.recaptcha.net/recaptcha/api/siteverify",
151            json={
152                "success": True,
153                "score": -3,
154            },
155        )
156        plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
157        session = self.client.session
158        session[SESSION_KEY_PLAN] = plan
159        session.save()
160        response = self.client.post(
161            reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
162            {"token": "PASSED"},
163        )
164        self.assertEqual(response.status_code, 200)
165        self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
166
167    def test_urls(self):
168        """Test URLs captcha"""
169        self.stage.js_url = "https://test.goauthentik.io/test.js"
170        self.stage.api_url = "https://test.goauthentik.io/test"
171        self.stage.save()
172        plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
173        session = self.client.session
174        session[SESSION_KEY_PLAN] = plan
175        session.save()
176        response = self.client.get(
177            reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
178        )
179        self.assertEqual(response.status_code, 200)
180        self.assertStageResponse(
181            response,
182            self.flow,
183            js_url="https://test.goauthentik.io/test.js",
184        )
RECAPTCHA_PUBLIC_KEY = '6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI'
RECAPTCHA_PRIVATE_KEY = '6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe'
class TestCaptchaStage(authentik.flows.tests.FlowTestCase):
 25class TestCaptchaStage(FlowTestCase):
 26    """Captcha tests"""
 27
 28    def setUp(self):
 29        super().setUp()
 30        self.user = create_test_admin_user()
 31        self.flow = create_test_flow(FlowDesignation.AUTHENTICATION)
 32
 33        self.stage: CaptchaStage = CaptchaStage.objects.create(
 34            name="captcha",
 35            public_key=RECAPTCHA_PUBLIC_KEY,
 36            private_key=RECAPTCHA_PRIVATE_KEY,
 37        )
 38        self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
 39
 40    @Mocker()
 41    def test_valid(self, mock: Mocker):
 42        """Test valid captcha"""
 43        mock.post(
 44            "https://www.recaptcha.net/recaptcha/api/siteverify",
 45            json={
 46                "success": True,
 47                "score": 0.5,
 48            },
 49        )
 50        plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
 51        session = self.client.session
 52        session[SESSION_KEY_PLAN] = plan
 53        session.save()
 54        response = self.client.post(
 55            reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
 56            {"token": "PASSED"},
 57        )
 58        self.assertEqual(response.status_code, 200)
 59        self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
 60
 61    @Mocker()
 62    def test_valid_override(self, mock: Mocker):
 63        """Test valid captcha"""
 64        self.stage.private_key = generate_id()
 65        self.stage.public_key = generate_id()
 66        mock.post(
 67            "https://www.recaptcha.net/recaptcha/api/siteverify",
 68            json={
 69                "success": True,
 70                "score": 0.5,
 71            },
 72        )
 73        plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
 74        site_key = generate_id()
 75        private_key = generate_id()
 76        plan.context = {
 77            PLAN_CONTEXT_CAPTCHA_SITE_KEY: site_key,
 78            PLAN_CONTEXT_CAPTCHA_PRIVATE_KEY: private_key,
 79        }
 80        session = self.client.session
 81        session[SESSION_KEY_PLAN] = plan
 82        session.save()
 83        response = self.client.get(
 84            reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
 85        )
 86        self.assertStageResponse(response, component="ak-stage-captcha", site_key=site_key)
 87        response = self.client.post(
 88            reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
 89            {"token": "PASSED"},
 90        )
 91        self.assertEqual(response.status_code, 200)
 92        self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
 93        self.assertIn(private_key, mock.request_history[0].text)
 94
 95    @Mocker()
 96    def test_invalid_score_high(self, mock: Mocker):
 97        """Test invalid captcha (score too high)"""
 98        mock.post(
 99            "https://www.recaptcha.net/recaptcha/api/siteverify",
100            json={
101                "success": True,
102                "score": 99,
103            },
104        )
105        plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
106        session = self.client.session
107        session[SESSION_KEY_PLAN] = plan
108        session.save()
109        response = self.client.post(
110            reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
111            {"token": "PASSED"},
112        )
113        self.assertEqual(response.status_code, 200)
114        self.assertStageResponse(
115            response,
116            component="ak-stage-captcha",
117            response_errors={"token": [{"string": "Invalid captcha response", "code": "invalid"}]},
118        )
119
120    @Mocker()
121    def test_invalid_score_low(self, mock: Mocker):
122        """Test invalid captcha (score too low)"""
123        mock.post(
124            "https://www.recaptcha.net/recaptcha/api/siteverify",
125            json={
126                "success": True,
127                "score": -3,
128            },
129        )
130        plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
131        session = self.client.session
132        session[SESSION_KEY_PLAN] = plan
133        session.save()
134        response = self.client.post(
135            reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
136            {"token": "PASSED"},
137        )
138        self.assertEqual(response.status_code, 200)
139        self.assertStageResponse(
140            response,
141            component="ak-stage-captcha",
142            response_errors={"token": [{"string": "Invalid captcha response", "code": "invalid"}]},
143        )
144
145    @Mocker()
146    def test_invalid_score_low_continue(self, mock: Mocker):
147        """Test invalid captcha (score too low, but continue)"""
148        self.stage.error_on_invalid_score = False
149        self.stage.save()
150        mock.post(
151            "https://www.recaptcha.net/recaptcha/api/siteverify",
152            json={
153                "success": True,
154                "score": -3,
155            },
156        )
157        plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
158        session = self.client.session
159        session[SESSION_KEY_PLAN] = plan
160        session.save()
161        response = self.client.post(
162            reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
163            {"token": "PASSED"},
164        )
165        self.assertEqual(response.status_code, 200)
166        self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
167
168    def test_urls(self):
169        """Test URLs captcha"""
170        self.stage.js_url = "https://test.goauthentik.io/test.js"
171        self.stage.api_url = "https://test.goauthentik.io/test"
172        self.stage.save()
173        plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
174        session = self.client.session
175        session[SESSION_KEY_PLAN] = plan
176        session.save()
177        response = self.client.get(
178            reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
179        )
180        self.assertEqual(response.status_code, 200)
181        self.assertStageResponse(
182            response,
183            self.flow,
184            js_url="https://test.goauthentik.io/test.js",
185        )

Captcha tests

def setUp(self):
28    def setUp(self):
29        super().setUp()
30        self.user = create_test_admin_user()
31        self.flow = create_test_flow(FlowDesignation.AUTHENTICATION)
32
33        self.stage: CaptchaStage = CaptchaStage.objects.create(
34            name="captcha",
35            public_key=RECAPTCHA_PUBLIC_KEY,
36            private_key=RECAPTCHA_PRIVATE_KEY,
37        )
38        self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)

Hook method for setting up the test fixture before exercising it.

@Mocker()
def test_valid(self, mock: requests_mock.mocker.Mocker):
40    @Mocker()
41    def test_valid(self, mock: Mocker):
42        """Test valid captcha"""
43        mock.post(
44            "https://www.recaptcha.net/recaptcha/api/siteverify",
45            json={
46                "success": True,
47                "score": 0.5,
48            },
49        )
50        plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
51        session = self.client.session
52        session[SESSION_KEY_PLAN] = plan
53        session.save()
54        response = self.client.post(
55            reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
56            {"token": "PASSED"},
57        )
58        self.assertEqual(response.status_code, 200)
59        self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))

Test valid captcha

@Mocker()
def test_valid_override(self, mock: requests_mock.mocker.Mocker):
61    @Mocker()
62    def test_valid_override(self, mock: Mocker):
63        """Test valid captcha"""
64        self.stage.private_key = generate_id()
65        self.stage.public_key = generate_id()
66        mock.post(
67            "https://www.recaptcha.net/recaptcha/api/siteverify",
68            json={
69                "success": True,
70                "score": 0.5,
71            },
72        )
73        plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
74        site_key = generate_id()
75        private_key = generate_id()
76        plan.context = {
77            PLAN_CONTEXT_CAPTCHA_SITE_KEY: site_key,
78            PLAN_CONTEXT_CAPTCHA_PRIVATE_KEY: private_key,
79        }
80        session = self.client.session
81        session[SESSION_KEY_PLAN] = plan
82        session.save()
83        response = self.client.get(
84            reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
85        )
86        self.assertStageResponse(response, component="ak-stage-captcha", site_key=site_key)
87        response = self.client.post(
88            reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
89            {"token": "PASSED"},
90        )
91        self.assertEqual(response.status_code, 200)
92        self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
93        self.assertIn(private_key, mock.request_history[0].text)

Test valid captcha

@Mocker()
def test_invalid_score_high(self, mock: requests_mock.mocker.Mocker):
 95    @Mocker()
 96    def test_invalid_score_high(self, mock: Mocker):
 97        """Test invalid captcha (score too high)"""
 98        mock.post(
 99            "https://www.recaptcha.net/recaptcha/api/siteverify",
100            json={
101                "success": True,
102                "score": 99,
103            },
104        )
105        plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
106        session = self.client.session
107        session[SESSION_KEY_PLAN] = plan
108        session.save()
109        response = self.client.post(
110            reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
111            {"token": "PASSED"},
112        )
113        self.assertEqual(response.status_code, 200)
114        self.assertStageResponse(
115            response,
116            component="ak-stage-captcha",
117            response_errors={"token": [{"string": "Invalid captcha response", "code": "invalid"}]},
118        )

Test invalid captcha (score too high)

@Mocker()
def test_invalid_score_low(self, mock: requests_mock.mocker.Mocker):
120    @Mocker()
121    def test_invalid_score_low(self, mock: Mocker):
122        """Test invalid captcha (score too low)"""
123        mock.post(
124            "https://www.recaptcha.net/recaptcha/api/siteverify",
125            json={
126                "success": True,
127                "score": -3,
128            },
129        )
130        plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
131        session = self.client.session
132        session[SESSION_KEY_PLAN] = plan
133        session.save()
134        response = self.client.post(
135            reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
136            {"token": "PASSED"},
137        )
138        self.assertEqual(response.status_code, 200)
139        self.assertStageResponse(
140            response,
141            component="ak-stage-captcha",
142            response_errors={"token": [{"string": "Invalid captcha response", "code": "invalid"}]},
143        )

Test invalid captcha (score too low)

@Mocker()
def test_invalid_score_low_continue(self, mock: requests_mock.mocker.Mocker):
145    @Mocker()
146    def test_invalid_score_low_continue(self, mock: Mocker):
147        """Test invalid captcha (score too low, but continue)"""
148        self.stage.error_on_invalid_score = False
149        self.stage.save()
150        mock.post(
151            "https://www.recaptcha.net/recaptcha/api/siteverify",
152            json={
153                "success": True,
154                "score": -3,
155            },
156        )
157        plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
158        session = self.client.session
159        session[SESSION_KEY_PLAN] = plan
160        session.save()
161        response = self.client.post(
162            reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
163            {"token": "PASSED"},
164        )
165        self.assertEqual(response.status_code, 200)
166        self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))

Test invalid captcha (score too low, but continue)

def test_urls(self):
168    def test_urls(self):
169        """Test URLs captcha"""
170        self.stage.js_url = "https://test.goauthentik.io/test.js"
171        self.stage.api_url = "https://test.goauthentik.io/test"
172        self.stage.save()
173        plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
174        session = self.client.session
175        session[SESSION_KEY_PLAN] = plan
176        session.save()
177        response = self.client.get(
178            reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
179        )
180        self.assertEqual(response.status_code, 200)
181        self.assertStageResponse(
182            response,
183            self.flow,
184            js_url="https://test.goauthentik.io/test.js",
185        )

Test URLs captcha