altcha-org / altcha

GDPR compliant, self-hosted CAPTCHA alternative with PoW mechanism and advanced anti-spam filter.
https://altcha.org
MIT License
386 stars 12 forks source link

Angular17 reactive forms integration #53

Closed Fipschen closed 1 month ago

Fipschen commented 1 month ago

Hey there,

thank you for this open source solution. I've a problem to use it in an angular 17 reactive forms. I'd seen the stackblitz-code, where the widget is included in the validation of the form. I can't see where the widget it bounded to the form to check the validation. I can't adopt it on myself. My form looks like this:

#form.component.html
<form [formGroup]="pwvForm" (ngSubmit)="doPwVerg()" class="text-center">
    <!-- email address -->
    <mat-form-field [style.width.px]="formFieldSize">
        <mat-label>email address</mat-label>
        <input type="text" formControlName="emailAdress" maxlength="75" placeholder="email@address.com" matInput />
    </mat-form-field>

    <altcha-widget
        [strings]=spamProtectionService.getAltchaConfig()
        auto="onload"
        [challengejson]=spamProtectionService.getAltchaChallenge()
        >
    </altcha-widget>

    <!-- Submit button -->
    <button type="submit" [disabled]="pwvForm.invalid" class="btn btn-primary btn-block mb-4">send</button>
</form>
in #form.component.ts
protected this.pwvForm = new FormGroup({
      emailAddress: new FormControl(null, [Validators.required, Validators.email]),
      altcha: new FormControl(null, Validators.required),
    });

The document.querySelector('#altcha').addEventListener havn't an effect. Could soneone help me?

Thank you very much

ovx commented 1 month ago

Hi, I don't know much about Angular's reactive forms, but in general when attaching event listener with addEventListener you have to do it after the altcha script loads, for example in the window.addEventListener('load', ...) handler. This has something to do with WebComponents where the events just don't attach before the component loads.

For validation - the widget creates a hidden input with name="altcha" (or other configured name) so you can use a query selector input[name="altcha"] to get the element and check if it's checked, probably better solution if you don't need events.

RickPoleshuck commented 1 month ago

Of course, full validation has to be done on the server, but it would be nice if the altcha-widget worked like other input widgets with angular forms and set and cleared the form.invalid field. I could just add [disabled]="form.invalid" to my submit button and not worry about eventlistening.

Fipschen commented 1 month ago

Hi, I've made it to get the value of name=altcha, but only with document.getElementsByName('altcha')[0]. With that I couldn't add an EventListener, with document.querySelect('#altcha') I didn't get an element, only null.

I'll going on to try to get an solution and will report here.

Thank you

Fipschen

Fipschen commented 1 month ago

Hi again,

now I'd found a workaround:

#form.component.html
<form [formGroup]="form" (ngSubmit)="sendForm()" class="text-center">
    <!-- email address -->
    <mat-form-field [style.width.px]="480">
        <mat-label>Email Address</mat-label>
        <input type="text" formControlName="emailAddress" maxlength="75" placeholder="email@address.com" matInput />
    </mat-form-field>

    <altcha-widget
        [strings]=spamProtectionService.getAltchaConfig()
        auto="onload"
        [challengejson]=spamProtectionService.getAltchaChallenge()
        (statechange)="altchaStateChange($event)">
    </altcha-widget>
    <br />

    <!-- Submit button -->
    <button type="submit" [disabled]="form.invalid || !formAltchaVerified" class="btn btn-primary btn-block mb-4">send</button>
</form>
form.component.ts
export class FormComponent
{
  protected form: FormGroup;
  protected formAltchaVerified: boolean = false;

  constructor(
    protected spamProtectionService: SpamProtectionService,)
  {
    this.form = new FormGroup({
      emailAddress: new FormControl(null, [Validators.required, Validators.email]),
    });
  }

  altchaStateChange(event: any): void
  {
    if (event.detail.state == "verified")
    {
      this.formAltchaVerified = true;
    }
  }

So it's not really a part of the form, but it has an effect of the submit button.

Thank you very much

Fipschen

RickPoleshuck commented 1 month ago

I am using version: "altcha": "^0.6.5", and do not have 'statechange' event available. Change is available, but there is no details object in the $event. @Fipschen , this solution may actually be enough. I am validating on the server anyway and just the presence of the ALTCHA widget may be restrictive enough for my needs. Thanks

Fipschen commented 1 month ago

I'm using the same version of altcha, for me it work's, tested a lot of times.

That's only a solution for the frontend and I'll also change the challengejson to challengeurl when I get how to realise this on php symfony. The backend will get another spam protection or the M2M-Feature. At the moment for me it's important the get the side ready with basic security and doing the security better later.

ovx commented 1 month ago

I am using version: "altcha": "^0.6.5", and do not have 'statechange' event available. Change is available, but there is no details object in the $event. @Fipschen , this solution may actually be enough. I am validating on the server anyway and just the presence of the ALTCHA widget may be restrictive enough for my needs. Thanks

The statechange doesn't fire probably because the addEventListener must be called after the altcha script loads and the widget renders - it's some limitation with Web Components; I'll look into integrating with Angular and what could be improved here.

RickPoleshuck commented 1 month ago

I'm using the same version of altcha, for me it work's, tested a lot of times.

That's only a solution for the frontend and I'll also change the challengejson to challengeurl when I get how to realise this on php symfony. The backend will get another spam protection or the M2M-Feature. At the moment for me it's important the get the side ready with basic security and doing the security better later.

You were correct. The only problem was my IDE, IntelliJ, telling me that there was no statechange event.

Much thanks.

Fipschen commented 1 month ago

@RickPoleshuck VS Code also told me there is no statechange. I'd found it with console.log() in the Browser (just as an tip 😬).

RickPoleshuck commented 1 month ago

I am now having problems converting the pseudo-code for the challenge to Java. Are there any samples?

Also, the provided API challenge is returning a maxnumber in the JSON that is not documented. Is maxnumber the same as the complexity value, maximum secret number?

{ "algorithm": "SHA-256", "challenge": "69960f0e2d000eb8cf543cf4fa06881ddf79d4a22ed42b1863b9124236ab9536", "maxnumber": 20000, "salt": "129188ee83c58246f45fab15?expires=1722109713", "signature": "e323b1d95c91396d66a81a186b35e20010ea51ea8fee769c096eb24a21760c47" }

Fipschen commented 1 month ago

I'm don't know much about Java. In the Docs are this official integrations for other languages: https://altcha.org/de/docs/integrations/ I can show you also my code for the integration in PHP:

$chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
$chars_length = strlen($chars);

$random_string = "";
$length = 16;

for ($i = 0; $i < $length; $i++)
{
    $random_string .= $chars[random_int(0, $chars_length - 1)];
}

$salt = $random_string;
$algorithm = "sha256";
$hmac_key = "sampleKey";
$secret_number = random_int(10000, 150000);
$challende = hash($algorithm, $salt.$secret_number);
$signature = hash_hmac($algorithm, $challende, $hmac_key);

$return = json_encode([
    'salt'                  =>  $salt,
    'challenge'             =>  $challende,
    'algorithm'             =>  $algorithm,
    'signature'             =>  $signature, ]);

The content of $return I'd took in the challengejson.

My problem is to verify the challenge, but I didn't really tried this until now, I'll do this later. I hope my code could help you.

RickPoleshuck commented 1 month ago

Much thanks. Apparently, the maxnumber returned by the API challenge is not necessary.

I would very strongly recommend you make use of the expires feature for production.

Fipschen commented 1 month ago

Looks like that, yes. I'm glad, I could help you.

Yes, I'll do this, thank you. I'm sorry, my integration is as an class, for easier reading I wrote it as short version.

ovx commented 1 month ago

I am now having problems converting the pseudo-code for the challenge to Java. Are there any samples?

Also, the provided API challenge is returning a maxnumber in the JSON that is not documented. Is maxnumber the same as the complexity value, maximum secret number?

{ "algorithm": "SHA-256", "challenge": "69960f0e2d000eb8cf543cf4fa06881ddf79d4a22ed42b1863b9124236ab9536", "maxnumber": 20000, "salt": "129188ee83c58246f45fab15?expires=1722109713", "signature": "e323b1d95c91396d66a81a186b35e20010ea51ea8fee769c096eb24a21760c47" }

The maxnumber parameter is optional and it refers to the maximum number used to generate the random number. It helps the widget to better distribute work for multiple workers making it run faster in most cases, so it's recommended to return it. Its indeed missing in the docs, I'll add it.

The Java library will be available soon, until then you can try to generate code in Java with LLMs using for example the python library, that should be the most accurate.

ovx commented 1 month ago

I just updated the angular demo, it contains a new component altcha which can be used as a form control with validation. This should solve the problems with event listeners and it also provides the payload as the control's value.

Fipschen commented 1 month ago

Thank you for the altcha component. After debugging my own bug it work's for me 👍

RickPoleshuck commented 1 month ago

The Java library will be available soon, until then you can try to generate code in Java with LLMs using for example the python library, that should be the most accurate.

Maybe my failed attempt at writing a Java service to provide a challenge will be helpful. Maybe you can point out my error. :-)

I use the attached classes like this:

@GetMapping("/challenge") public ResponseEntity getChallenge() throws Exception { AltchaChallenge altchaChallenge = new AltchaService().complexity(20000).expiresSeconds(60 * 60).saltLength(24).generateChallenge(); return ResponseEntity.ok(altchaChallenge); }

AltchaService.txt AltchaChallenge.txt

RickPoleshuck commented 1 month ago

Thank you for the altcha component. After debugging my own bug it work's for me 👍

Did you do something other than add 'required' like this:

Thanks.

RickPoleshuck commented 1 month ago

I discovered what I believe my real problem is. The new widget does not send a message to the form to recheck the validation. If I click on the ALTCHA first the form works correctly, but if I click on the ALTCHA last then the form doesn't get notified to recheck the validation. I know this is an Angular problem and not a ALTCHA problem, but...

Much thanks again.

ovx commented 1 month ago

@RickPoleshuck The Java library is out: https://github.com/altcha-org/altcha-lib-java, it's not in Maven Central yet, but it's installable with JitPack. Please report any issues.

RickPoleshuck commented 1 month ago

@RickPoleshuck The Java library is out: https://github.com/altcha-org/altcha-lib-java, it's not in Maven Central yet, but it's installable with JitPack. Please report any issues.

Thanks. I have made a pull request for the Readme.

Fipschen commented 1 month ago

Hi there,

I'll try to build the php backend for challenge generation, but I'll get an CORS-Error:

Cross-source (cross-origin) request blocked: The same-source rule prohibits reading the external resource at https://localhost:8000/funktion/altcha/open-challenge. (Reason: Header ‘x-altcha-spam-filter’ is not permitted due to the header ‘Access-Control-Allow-Headers’ from the CORS preflight response).

Header send:

OPTIONS /funktion/altcha/open-challenge HTTP/2 Host: localhost:8000 User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:130.0) Gecko/20100101 Firefox/130.0 Accept: / Accept-Language: de,en-US;q=0.7,en;q=0.3 Accept-Encoding: gzip, deflate, br, zstd Access-Control-Request-Method: GET Access-Control-Request-Headers: x-altcha-spam-filter Referer: http://localhost:4200/ Origin: http://localhost:4200 Connection: keep-alive Sec-Fetch-Dest: empty Sec-Fetch-Mode: cors Sec-Fetch-Site: same-site Priority: u=4 TE: trailers

Header anwser:

HTTP/2 200 access-control-allow-headers: X-API-KEY, Origin, X-Requested-With, Content-Type, Accept, Access-Control-Request-Method, Authorization access-control-allow-methods: GET, POST, OPTIONS, PATCH, DELETE access-control-allow-origin: http://localhost:4200 allow: GET, POST, OPTIONS, PATCH, DELETE content-type: text/html; charset=UTF-8 date: Tue, 30 Jul 2024 04:15:59 GMT x-powered-by: PHP/8.3.9 X-Firefox-Spdy: h2

I can't find something in the docs about the x-altcha-spam-filter header. What do I need to do? I'll also send me integration for php symfony7 and plain php here.

I discovered what I believe my real problem is. The new widget does not send a message to the form to recheck the validation. If I click on the ALTCHA first the form works correctly, but if I click on the ALTCHA last then the form doesn't get notified to recheck the validation. I know this is an Angular problem and not a ALTCHA problem, but...

I can't fully confirm this. If I do the altcha at last in the form I need to click somewhere outside the altcha-widget to change the validation state. But that's the same like on every other form input.

Thank you

ovx commented 1 month ago

Hi,

CORS: it's mention in the error message, that the header x-altcha-spam-filter isn't listed in Access-Control-Allow-Headers (which is the server response to CORS), so just add it there. I'll also add documentation about this.

I can't fully confirm this. If I do the altcha at last in the form I need to click somewhere outside the altcha-widget to change the validation state. But that's the same like on every other form input.

I think validation works fine in the angular demo, or is it reproducible also there?

RickPoleshuck commented 1 month ago

@Fipschen , If you were using nginx in front of your webserver, you would add something like this to your config:

if ($request_method = 'OPTIONS') { add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Credentials true; add_header Access-Control-Allow-Methods GET,HEAD,OPTIONS,POST,PUT,DELETE; add_header Access-Control-Allow-Headers Origin,X-Requested-With,‘X-Altcha-Spam-Filter,Content-Type,Accept,Authorization; return 204; } CORS is an annoyance, it is designed to poorly protect the user, not the web site. I know how to fix it with nginx. You need to find the documentation for CORS for your particular web server framework.IMO, ALTCHA cannot possibly document all the possible solutions. Sorry, I can't be more helpful.

RickPoleshuck commented 1 month ago

I think validation works fine in the angular demo, or is it reproducible also there? Yes, your demo works correctly. I might have to switch from TemplateForm to ReactiveForms.

Fipschen commented 1 month ago

I know what CORS is, it has already cost me a lot of nerves, is just didn't get to add this in control 🤦‍♂️ I'm sorry, now it work's fine.

The Angular Demo works fine for me. I'll just used the json-challende til now, but the next days I would code the backend part, report again and show you my implementation. I'm not fit in GitHub, so I can't do a pull request.. to learn is on my to-do-list.. sorry

RickPoleshuck commented 1 month ago

The App-Altcha widget works fine in my first app, which is not yet ready for production. But I am having trouble with my second Angular app. The widget is there but zero by zero size and not visible. The first app is based on the jHipster framework and the non working app is just raw Angular. I would be happy to share my code with @ovx , but not with the world. Daniel, I am hoping this is an opportunity to look at addressing future issues for other users. But let me know if I am asking too much. https://www.linkedin.com/in/rickpoleshuck/

What is interesting is that our production app has not received one bogus message in a year, but my 'toy' app which is not advertised at all has had about 20 bogus messages. Altcha has addressed that problem on my 'toy' app already. Thanks again.

Here is my working app: https://fterm.tech/#/contact. If you try it out, please leave a comment that says why.

Fipschen commented 1 month ago

My backend work's now. Now I have one question left: I'd read, it's better for security to check the result of the widget. I thought til now the widget would send an second request to validate the solution, but my widget doesn't it. Did this tip mean to send the solution with the form data and check it there? I'm sorry for that stupid questions, my english isn't the best.. 😅

Thank you

ovx commented 1 month ago

My backend work's now. Now I have one question left: I'd read, it's better for security to check the result of the widget. I thought til now the widget would send an second request to validate the solution, but my widget doesn't it. Did this tip mean to send the solution with the form data and check it there? I'm sorry for that stupid questions, my english isn't the best.. 😅

Thank you

The widget creates a payload (a base64 encoded string) and send's it together with the form data as altcha field (if you're using custom ajax handler, add the altcha payload to the submitted data). You have to validate it on the server using the secret HMAC key - otherwise anyone could just bypass it. So there's no second call from the widget, once validated, the payload is all you need to verify it on the server in the submission handler.

Fipschen commented 1 month ago

The widget creates a payload (a base64 encoded string) and send's it together with the form data as altcha field (if you're using custom ajax handler, add the altcha payload to the submitted data). You have to validate it on the server using the secret HMAC key - otherwise anyone could just bypass it. So there's no second call from the widget, once validated, the payload is all you need to verify it on the server in the submission handler.

Okay, thank you very much. I'll finish the backend next days and send it than. Thank you

RickPoleshuck commented 1 month ago

I am curious what html error you plan on throwing if the validation of the altchaChallenge fails. Right now I am throwing a BadRequest, but maybe that is giving hackers too much information. One thought is to retain the connection for maybe 60 seconds and then throw a timeout exception.

Fipschen commented 1 month ago

I'll handle it like wrong validation error, like an bad login request or post request with invalid data. I think hackers can't do much with that.

I've a problem again with the widget: When using the expire-param the widget can't solve the challenge.

#Console
ALTCHA mounted 0.6.5
ALTCHA workers 12
ALTCHA auto onload
ALTCHA fetching challenge from https://localhost:8000/funktion/altcha/open-challenge
ALTCHA expire 599050
ALTCHA challenge 
Object { id: null, _create_datetime: "2024-08-01T18:32:04+02:00", salt: "54Di61GGB9BpN2Iy?expires=1722530524", challenge: "f535c272e5fe15df15b3cec484275cbf7c1fcf0c071ee5211748bf9447d02ce2", algorithm: "SHA-256", signature: "f72e653cb790360f4bf2dafe6e7a068c39c2b94204299fd7996ceeb515279431", secret_number: 147224, maxnumber: 150000 }
ALTCHA solution null
ALTCHA Unable to find a solution. Ensure that the 'maxnumber' attribute is greater than the randomly generated number. 
ALTCHA Error: Unexpected result returned.
#Challenge form Server
{
{"id": null,
"_create_datetime":"2024-08-01T18:32:04+02:00",
"salt":"54Di61GGB9BpN2Iy?expires=1722530524",
"challenge":"f535c272e5fe15df15b3cec484275cbf7c1fcf0c071ee5211748bf9447d02ce2",
"algorithm":"SHA-256",
"signature":"f72e653cb790360f4bf2dafe6e7a068c39c2b94204299fd7996ceeb515279431",
"secret_number":147224,
"maxnumber":150000}
}

(I've added the secret number for debugging only). When opening the widget it says immediately Verification expired. Try again.. After a little time it goes to Verification failed. Try again later. and returns the last 3 lines in the console. Do I set the param wrong? I'd adopted it from the docs https://altcha.org/docs/server-integration/#salt-parameters 🤔

RickPoleshuck commented 1 month ago

Are you getting the time in seconds? A very common mistake is to think a unix timestamp is milliseconds.

Fipschen commented 1 month ago

I'm using the unix timestamp in seconds, php has milliseconds only with * 1000. The console print this: ALTCHA expire 599029 (10 minutes) But why fails the validation by the parameter?

RickPoleshuck commented 1 month ago

The current unix timestamp is 1722535587 + (10 * 60) = 1722536187 This is the number of seconds since January 1, 1970 + the increment of 10 seconds. Way more than 599029 seconds. I am betting you are getting the number of seconds your server has been running, about 7 days, rather than the unix timestamp.

Fipschen commented 1 month ago

In the challenge in the salt is the timestamp: "salt":"54Di61GGB9BpN2Iy?expires=1722530524" 599029 is the expire-time in milliseconds, the widget returns that in console.

RickPoleshuck commented 1 month ago

In the challenge in the salt is the timestamp: "salt":"54Di61GGB9BpN2Iy?expires=1722530524" 599029 is the expire-time in milliseconds, the widget returns that in console.

That timestamp is almost two hours ago. About 45 minutes before your first post on the topic. The expires option works fine for me. But I did lose my bet. :-)

Fipschen commented 1 month ago

The 2 hours should be the timezone. The widget calculate the expire output in console right, that couldn't be the mistake. Adding 2 h (7 200 seconds) the output is + 7 200 000 and the same error. Could you please show me a salt with expire param from you?

RickPoleshuck commented 1 month ago

A Unix timestamp, also known as Unix time or POSIX time, is a numerical value that represents the number of seconds that have passed since midnight UTC on January 1, 1970, also known as the Unix epoch.

No timezone should be included.

Here is one that works for me:

cd115b6c9117da1d255653bb99cf9b7bcc0a94dedd1482ad?expires=1722542885

This is a useful website for working with unix timestamps: https://www.unixtimestamp.com/

ovx commented 1 month ago

I'll handle it like wrong validation error, like an bad login request or post request with invalid data. I think hackers can't do much with that.

I've a problem again with the widget: When using the expire-param the widget can't solve the challenge.

#Console
ALTCHA mounted 0.6.5
ALTCHA workers 12
ALTCHA auto onload
ALTCHA fetching challenge from https://localhost:8000/funktion/altcha/open-challenge
ALTCHA expire 599050
ALTCHA challenge 
Object { id: null, _create_datetime: "2024-08-01T18:32:04+02:00", salt: "54Di61GGB9BpN2Iy?expires=1722530524", challenge: "f535c272e5fe15df15b3cec484275cbf7c1fcf0c071ee5211748bf9447d02ce2", algorithm: "SHA-256", signature: "f72e653cb790360f4bf2dafe6e7a068c39c2b94204299fd7996ceeb515279431", secret_number: 147224, maxnumber: 150000 }
ALTCHA solution null
ALTCHA Unable to find a solution. Ensure that the 'maxnumber' attribute is greater than the randomly generated number. 
ALTCHA Error: Unexpected result returned.
#Challenge form Server
{
{"id": null,
"_create_datetime":"2024-08-01T18:32:04+02:00",
"salt":"54Di61GGB9BpN2Iy?expires=1722530524",
"challenge":"f535c272e5fe15df15b3cec484275cbf7c1fcf0c071ee5211748bf9447d02ce2",
"algorithm":"SHA-256",
"signature":"f72e653cb790360f4bf2dafe6e7a068c39c2b94204299fd7996ceeb515279431",
"secret_number":147224,
"maxnumber":150000}
}

(I've added the secret number for debugging only). When opening the widget it says immediately Verification expired. Try again.. After a little time it goes to Verification failed. Try again later. and returns the last 3 lines in the console. Do I set the param wrong? I'd adopted it from the docs https://altcha.org/docs/server-integration/#salt-parameters 🤔

Hi,

  1. make sure that you're hashing the whole salt as it is returned to the client (including the added expire parameter)
  2. as Rick mentioned, unit timestamps don't have timezones, but if you're running your server in a virtual machine, the time might be set wrong, so make sure to check that the returned expires timestamp is correct
Fipschen commented 1 month ago

I know what an unix timestamp is and it's calculated right in my integration (checked with online-tools).

When I'm add ?expires=$timestamp to the salt (after hashing) the widget print an output in the console ALTCHA expire 559754, I think it's the left time before is expired. If I add it to the salt before hashing, the widget don't print ALTCHA expire .... So I think the parameter is added right? My syntax of the salt looks like Rick ones.

ovx commented 1 month ago

I know what an unix timestamp is and it's calculated right in my integration (checked with online-tools).

When I'm add ?expires=$timestamp to the salt (after hashing) the widget print an output in the console ALTCHA expire 559754, I think it's the left time before is expired. If I add it to the salt before hashing, the widget don't print ALTCHA expire .... So I think the parameter is added right? My syntax of the salt looks like Rick ones.

The parameter is right; I checked your challenge you posted and the hash doesn't match, so there's something wrong on the server when generating the challenge: the expected hash for salt + number (54Di61GGB9BpN2Iy?expires=1722530524147224) is 94c03c51c1d701a535aa62dbbac9a25d5549b48eaf22a378e32cef6b5b049f7c.

Fipschen commented 1 month ago

Oh hell... now I'd found the error: I always add the expires-param to the salt OR the challenge, not to both.. I thought only the salt need it for export to the widget.. 🤦‍♂️🤦‍♂️ I'm sorry and thank you very much for your help

RickPoleshuck commented 1 month ago

Oh hell... now I'd found the error: I always add the expires-param to the salt OR the challenge, not to both.. I thought only the salt need it for export to the widget.. 🤦‍♂️🤦‍♂️ I'm sorry and thank you very much for your help Too be a little clearer for other readers: Any options added to the salt, including the expires option, must be done before generating the challenge hash value.

Fipschen commented 1 month ago

Hi there, now I've the integration of server side in plain php finish, added as attachments. For Symfony I have to look for how to create libs first.

altcha-plain-php.txt

Have a nice weekend