Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

stages/identification: add captcha to identification stage #11711

Merged
merged 15 commits into from
Oct 25, 2024

Conversation

gergosimonyi
Copy link
Collaborator

@gergosimonyi gergosimonyi commented Oct 17, 2024

Details

Adds an optional captcha to an Identification stage to run in the background while the user inputs their information.


Checklist

  • Local tests pass (ak test authentik/)
  • The code has been formatted (make lint-fix)

If an API change has been made

  • The API schema has been updated (make gen-build)

If changes to the frontend have been made

  • The code has been formatted (make web)

If applicable

  • The documentation has been updated
  • The documentation has been formatted (make website)

Copy link

netlify bot commented Oct 17, 2024

Deploy Preview for authentik-storybook ready!

Name Link
🔨 Latest commit bf2ae13
🔍 Latest deploy log https://app.netlify.com/sites/authentik-storybook/deploys/671a53b075ba21000813b932
😎 Deploy Preview https://deploy-preview-11711--authentik-storybook.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

Copy link

netlify bot commented Oct 17, 2024

Deploy Preview for authentik-docs canceled.

Name Link
🔨 Latest commit bf2ae13
🔍 Latest deploy log https://app.netlify.com/sites/authentik-docs/deploys/671a53b0b8bfc4000880c3ad

@gergosimonyi gergosimonyi force-pushed the stages/identification/add-captcha branch from 9593192 to 465010e Compare October 17, 2024 16:09
Copy link

codecov bot commented Oct 17, 2024

Codecov Report

Attention: Patch coverage is 97.26027% with 2 lines in your changes missing coverage. Please review.

Project coverage is 92.57%. Comparing base (2fa50de) to head (bf2ae13).
Report is 31 commits behind head on main.

✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
authentik/stages/captcha/stage.py 96.42% 1 Missing ⚠️
authentik/stages/identification/stage.py 88.88% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #11711      +/-   ##
==========================================
- Coverage   92.74%   92.57%   -0.17%     
==========================================
  Files         740      760      +20     
  Lines       36783    37788    +1005     
==========================================
+ Hits        34113    34981     +868     
- Misses       2670     2807     +137     
Flag Coverage Δ
e2e 49.13% <15.06%> (-0.12%) ⬇️
integration 24.93% <12.32%> (-0.01%) ⬇️
unit 90.15% <97.26%> (-0.12%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Contributor

github-actions bot commented Oct 17, 2024

authentik PR Installation instructions

Instructions for docker-compose

Add the following block to your .env file:

AUTHENTIK_IMAGE=ghcr.io/goauthentik/dev-server
AUTHENTIK_TAG=gh-bf2ae13c52de89306b7babc609b99aa2068ddea2
AUTHENTIK_OUTPOSTS__CONTAINER_IMAGE_BASE=ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s

For arm64, use these values:

AUTHENTIK_IMAGE=ghcr.io/goauthentik/dev-server
AUTHENTIK_TAG=gh-bf2ae13c52de89306b7babc609b99aa2068ddea2-arm64
AUTHENTIK_OUTPOSTS__CONTAINER_IMAGE_BASE=ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s

Afterwards, run the upgrade commands from the latest release notes.

Instructions for Kubernetes

Add the following block to your values.yml file:

authentik:
    outposts:
        container_image_base: ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s
global:
    image:
        repository: ghcr.io/goauthentik/dev-server
        tag: gh-bf2ae13c52de89306b7babc609b99aa2068ddea2

For arm64, use these values:

authentik:
    outposts:
        container_image_base: ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s
global:
    image:
        repository: ghcr.io/goauthentik/dev-server
        tag: gh-bf2ae13c52de89306b7babc609b99aa2068ddea2-arm64

Afterwards, run the upgrade commands from the latest release notes.

@gergosimonyi gergosimonyi marked this pull request as ready for review October 17, 2024 16:49
@gergosimonyi gergosimonyi requested review from a team as code owners October 17, 2024 16:49
@BeryJu BeryJu changed the title add captcha to identification stage stages/identification: add captcha to identification stage Oct 17, 2024
Copy link
Contributor

@kensternberg-authentik kensternberg-authentik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made some comments and requested two changes. I've marked comment where code change suggestions are just that. I hope that's helpful. Two of the suggestions are about bad habits, and one is just a pointer to a different way to use SearchSelect.

.selected=${(stage: Stage): boolean => {
return stage.pk === this.instance?.captchaStage;
}}
?blankable=${true}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a change request. It is only a suggestion.

Not a bad habit, but there is a wrapper for SearchSelect called SearchSelectEz with a slightly more compact API. You don't have to use it, but I like to; it includes the externally-supplied machinery as a single syntactical and semantic unit, and I think it's a bit more readable. This example also shows the arrow functions as function expressions, and includes blankable in its pure-attribute form.

<ak-search-select-ez .config=${{
    fetchObjects: async (query?: string): Promise<Stage[]> => {
        const args: StagesCaptchaListRequest = {
            ordering: "name",
        };
        if (query !== undefined) {
            args.search = query;
        }
        const stages = await new StagesApi(DEFAULT_CONFIG).stagesCaptchaList(args);
        return stages.results;
    },
    groupBy: (items: Stage[]) => groupBy(items, (stage) => stage.verboseNamePlural),
    renderElement: (stage: Stage): string => stage.name,
    value: (stage: Stage | undefined): string | undefined => stage?.pk,
    selected: (stage: Stage): boolean => stage.pk === this.instance?.captchaStage,
}} blankable></ak-search-select-ez>

I know the ${{ }} looks a bit React-y, but it is, because it's doing something that's much more common in React than in Lit: passing a whole object to a property. The outer ${ } creates a template scope, and the inner { } creates a new object to be passed via the template scope.

}}
.selected=${(stage: Stage): boolean => {
return stage.pk === this.instance?.captchaStage;
}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a change request.

The second bad habit: ?blankable=${true} is better written as just blankable; the Lit cruft here adds nothing. Lit does the right thing with boolean attributes. Only use the Lit form for a boolean attribute if blankable could change dynamically.

embedded = false;

@property()
onTokenChange: (token: string) => void = (token) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a change request.

I don't think the default value you set here does what you think it should do. onTokenChange is a way for the client code to pass a mutator, a function that reaches up into the client and sets a value there, as you're doing in the IdentificationStage.ts changes below.

You have two choices. The first is

onTokenChange: (token: string) => void = (_token: string) => undefined;

... which will swallow any errors until you realize that you forgot to supply onTokenChange in your client code.

The better choice is:

onTokenChange: (token: string) => void = (_token: string) => { 
     throw new Error("Client failed to supply a handling function to CaptchaStage"); 
};

... which will crash early and crash informatively if you do forget. (Regarding my comment about arrow functions, a throw is always a statement, so it has to be wrapped in scope delimiters.)

In either case, the function declaration's argument signature must begin with an underscore (the _token), because it is a declaration and eslint will complain that all declared variables are not being used, but it is a convention that eslint should not make that complaint about variable names that begin with an underscore. (The names in the definition's signature are pure documentation, except when they're keys for a defined record object being passed in. TypeScript does have its little annoyances.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a change request. It is just a suggestion.

Also? That type-and-default pairing makes my head hurt. Maybe you could, somewhere above the class declaration, put:

type TokenHandler = (token: string) => void;

... and then:

onTokenChange: TokenHandler = (_token: string) => { 
     throw new Error("Client failed to supply a handling function to CaptchaStage");
};

@@ -157,10 +155,16 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
if (this.captchaInteractive) {
return html`${this.captchaContainer}`;
}
if (this.embedded) {
return html``;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a change request.

Please return nothing instead of an empty html() call. Lit will create empty DOM-state tracking tags for an empty html(), which in the small is irrelevant, but it's still messy.

You can add nothing in the Lit base import:

import { CSSResult, PropertyValues, TemplateResult, html, nothing } from "lit";

web/src/flow/stages/captcha/CaptchaStage.ts Show resolved Hide resolved

## Captcha stage

To run a captcha in the background while the user is inputting their identification, a Captcha stage can be selected here. If a Captcha stage is selected in the Identification stage, the Captcha stage should not be bound to the flow.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion (and maybe @tanberry can chime in): "entering" instead of "inputting". "Inputting" is such an ugly phrase. Either that or "... while the user inputs their identification, ..." but that's still mechanizing the human, and I like my humans to stay human.

@gergosimonyi
Copy link
Collaborator Author

@kensternberg-authentik

Thank you for the extensive review! I've implemented everything, except

  • ak-search-select-ez: I'll skip this for now as it doesn't seem to be used anywhere, I'd rather not have its maiden flight be so close to a release

  • nothing instead of html``: because nothing doesn't fit the function's return type, TemplateResult, and this is prettier (in my opinion) than changing 2 function signatures like

@@ -147,7 +147,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
         return true;
     }
 
-    renderBody(): TemplateResult {
+    renderBody(): TemplateResult | typeof nothing {
         if (this.error) {
             return html`<ak-empty-state icon="fa-times" header=${this.error}> </ak-empty-state>`;
         }
@@ -160,7 +160,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
         return html`<ak-empty-state loading header=${msg("Verifying...")}></ak-empty-state>`;
     }
 
-    render(): TemplateResult {
+    render(): TemplateResult | typeof nothing {
         if (this.embedded) {
             return this.renderBody();
         }

Let me know if that's acceptable or if you have any other insights.

authentik/stages/identification/stage.py Outdated Show resolved Hide resolved
Copy link
Contributor

@tanberry tanberry left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple of edits... and questions... but I'm approving now so that I am not the bottleneck. ;)

To prompt users for their password on the same step as identifying themselves, a password stage can be selected here. If a password stage is selected in the Identification stage, the password stage should not be bound to the flow.
To prompt users for their password on the same step as identifying themselves, a Password stage can be selected here. If a Password stage is selected in the Identification stage, the Password stage should not be bound to the flow.

## Captcha stage
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
## Captcha stage
## CAPTCHA stage


## Captcha stage

To run a captcha in the background while the user is entering their identification, a Captcha stage can be selected here. If a Captcha stage is selected in the Identification stage, the Captcha stage should not be bound to the flow.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
To run a captcha in the background while the user is entering their identification, a Captcha stage can be selected here. If a Captcha stage is selected in the Identification stage, the Captcha stage should not be bound to the flow.
To run a CAPTCHA process in the background while the user is entering their identification, a CAPTCHA stage can be selected here. If a CAPTCHA stage is selected in the Identification stage, the CAPTCHA stage should not be bound to the flow.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do feel like we could explain more about when the CAPTCHA prompt is shown to the user... what do we mean by running CAPTCHA in the background? And when is that process brought to the foreground, for user to see?

And I want to look to see if we have docs on "binding a stage binding to a stage". Your docs cover enough to get it doen, but would be great if we could link to more info. I think you gave me a lot more details when we first spoke about this... and I cannot find my notes, whah.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @gergosimonyi for explaining about he background.. that CATCHA is being run in the background if CATCHA has been configured. Makes sesne, haha. For the second paragraph, though, I do still want to look at how we can better explain this... that can happen later though.

Copy link
Contributor

@kensternberg-authentik kensternberg-authentik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I approve of the changes you made at my request, but please review @tanberry's suggestion and requests before committing. At this point, the code should work as expected and is not unmaintainable.

Copy link
Member

@BeryJu BeryJu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just 2 small things this time around

@@ -27,6 +27,60 @@ class CaptchaChallenge(WithUserInfoChallenge):
component = CharField(default="ak-stage-captcha")


def get_friendly_captcha_error(error: str) -> str:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I'm not sure if these error codes are the same for all the Captcha implementations we support. Also things like invalid/missing secret are probably not something we should disclose to end-users
I think we can probably fall back to Invalid captcha response as a generic error message

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, fair enough. I was caught up on how to help admins, but this is not the way, I'll add some logging for that.

I think it's reasonable to keep the ones where we advise end users to try again, and I'll make them even more friendly. I'll include all "recoverable-by-retry" error codes from reCAPTCHA, hCaptcha and Turnstile.

@@ -45,6 +46,11 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
@state()
scriptElement?: HTMLScriptElement;

@property()
onTokenChange: TokenHandler = (_token: string) => {
throw new Error("Client failed to supply a handling function to CaptchaStage");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the default for this be

this.host?.submit({
    token: token,
});

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ken and I don't think so, see some discussion above.

(By the rules of refactoring, you're right. I'm altering component contract here and I changed every callsite to match.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm mainly asking because when the stage is rendered as default, there's no default handler there, which doesn't seem correct? Or did I miss it

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the point I realize I'm probably missing something. What does "rendered as default" mean here?

My thinking is: this component will always need to be passed challenge and host. Whoever's supplying that (e.g. the FlowExecutor) should also supply onTokenChange.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah I must've overlooked https://github.com/goauthentik/authentik/pull/11711/files#diff-4ed6c41b430eb15195ace2424e99a073db6c9e588f4627cf0f899ef9fad0f084R311-R312, I'd prefer that from the aspect of the flow executor all stages have the same signature so imo it should be the default callback handler on the stage and then it can be overridden when rendered embedded

@gergosimonyi gergosimonyi requested a review from BeryJu October 23, 2024 08:34
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
@gergosimonyi gergosimonyi merged commit 9ee0ba1 into main Oct 25, 2024
66 checks passed
@gergosimonyi gergosimonyi deleted the stages/identification/add-captcha branch October 25, 2024 06:13
kensternberg-authentik added a commit that referenced this pull request Oct 29, 2024
* main: (22 commits)
  lifecycle: fix missing krb5 deps for full testing in image (#11815)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#11810)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh-Hans (#11809)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_CN (#11808)
  web: bump API Client version (#11807)
  core: bump goauthentik.io/api/v3 from 3.2024083.12 to 3.2024083.13 (#11806)
  core: bump ruff from 0.7.0 to 0.7.1 (#11805)
  core: bump twilio from 9.3.4 to 9.3.5 (#11804)
  core, web: update translations (#11803)
  providers/scim: handle no members in group in consistency check (#11801)
  stages/identification: add captcha to identification stage (#11711)
  website/docs: improve root page and redirect (#11798)
  providers/scim: clamp batch size for patch requests (#11797)
  web/admin: fix missing div in wizard forms (#11794)
  providers/proxy: fix handling of AUTHENTIK_HOST_BROWSER (#11722)
  core, web: update translations (#11789)
  core: bump goauthentik.io/api/v3 from 3.2024083.11 to 3.2024083.12 (#11790)
  core: bump gssapi from 1.8.3 to 1.9.0 (#11791)
  web: bump API Client version (#11792)
  stages/authenticator_validate: autoselect last used 2fa device (#11087)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants