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

Configurable with environment variables #254

Open
Adambean opened this issue Oct 19, 2024 · 4 comments
Open

Configurable with environment variables #254

Adambean opened this issue Oct 19, 2024 · 4 comments

Comments

@Adambean
Copy link

Hello,

Would it be possible to support this component's configuration to be read from environment variables?

The purpose of this would be so that the configuration can be semi-exposed via service parameters to templating or API. This would allow a user account 2FA management interface to adapt according to the application's configuration without duplication of or detachment from the component's configuration. For example only showing a section for activating/managing Google Authenticator if it's been enabled in the component configuration.

This would also allow the configuration to be injected into translation messages, such as a span or tooltip alongside the "This is a trusted device" checkbox to inform the user how long their device will be remembered for.

I did attempt this though it encounters issues. (It may already work with me just getting it wrong instead.)

  • When trying to set "scheb_two_factor.email.enabled" with "%env(bool:2FA_EMAIL_ENABLED)%":

    Using a cast in "env(bool:2FA_EMAIL_ENABLED)" is incompatible with resolution at compile time in "Scheb\TwoFactorBundle\DependencyInjection\SchebTwoFactorExtension". The logic in the extension should be moved to a compiler pass, or an env parameter with no cast should be used instead.

  • When trying to set "scheb_two_factor.email.enabled" with "%env(2FA_EMAIL_ENABLED)%", same as above without a boolean processor:
    The value is always loosely juggled to false from an empty string.
  • When trying to set "scheb_two_factor.trusted_device.lifetime" "%env(int:2FA_TRUSTED_DEVICE_LIFETIME)%":
    The value is always loosely juggled to a 0 from an empty string.

    The value 0 is too small for path "scheb_two_factor.trusted_device.lifetime". Should be greater than or equal to 1

  • When trying to set "scheb_two_factor.trusted_device.lifetime" "%env(2FA_TRUSTED_DEVICE_LIFETIME)%", same as above without an int processor:
    The value is always seen as an empty string.

    Invalid type for path "scheb_two_factor.trusted_device.lifetime". Expected "int", but got "string".

Example environment variables:

2FA_EMAIL_ENABLED=true
2FA_EMAIL_DIGITS=6
2FA_GOOGLE_AUTHENTICATOR_ENABLED=true
2FA_GOOGLE_AUTHENTICATOR_LEEWAY=3
2FA_TRUSTED_DEVICE_ENABLED=true
2FA_TRUSTED_DEVICE_LIFETIME=604800
2FA_BACKUP_CODES_ENABLED=true

Example "scheb_2fa.yaml" configuration:

parameters:
    2fa_email_enabled: "%env(bool:2FA_EMAIL_ENABLED)%"
    2fa_email_digits: "%env(int:2FA_EMAIL_DIGITS)%"
    2fa_google_authenticator_enabled: "%env(bool:2FA_GOOGLE_AUTHENTICATOR_ENABLED)%"
    2fa_google_authenticator_leeway: "%env(int:2FA_GOOGLE_AUTHENTICATOR_LEEWAY)%"
    2fa_trusted_device_enabled: "%env(bool:2FA_TRUSTED_DEVICE_ENABLED)%"
    2fa_trusted_device_lifetime: "%env(int:2FA_TRUSTED_DEVICE_LIFETIME)%"
    2fa_backup_codes_enabled: "%env(bool:2FA_BACKUP_CODES_ENABLED)%"

scheb_two_factor:
    email:
        enabled: "%2fa_email_enabled%"
        digits: "%2fa_email_digits%"

    google:
        enabled: "%2fa_google_authenticator_enabled%"
        leeway: "%2fa_google_authenticator_leeway%"

    trusted_device:
        enabled: "%2fa_trusted_device_enabled%"
        lifetime: "%2fa_trusted_device_lifetime%"

    backup_codes:
        enabled: "%2fa_backup_codes_enabled%"

twig:
    globals:
        2fa_email_enabled: "%2fa_email_enabled%"
        2fa_email_digits: "%2fa_email_digits%"
        2fa_google_authenticator_enabled: "%2fa_google_authenticator_enabled%"
        2fa_google_authenticator_leeway: "%2fa_google_authenticator_leeway%"
        2fa_trusted_device_enabled: "%2fa_trusted_device_enabled%"
        2fa_trusted_device_lifetime: "%2fa_trusted_device_lifetime%"
        2fa_backup_codes_enabled: "%2fa_backup_codes_enabled%"
@scheb
Copy link
Owner

scheb commented Oct 20, 2024

The *_enabled options

These can be passed as an environment variable, but only without the :bool casting. The bundle will evaluate string values true / on to a boolean true value. (I checked with the test application, it does work).

The reason why you're having problems with the casting might be related to your environment variable names starting with a number. Symfony doesn't like that, rightfully so, as the official definition of environment variable names seems to be:

The name can contain upper- and lowercase letters, numbers, and underscores (_), but it must start with a letter or underscore.

This is likely the reason why your environment variable values are not recognized and evaluate to a default value of false.

Other options

I had to do some research to figure out why you're having problems passing the integer configuration values. I've seen these problems as well during my tests. Here's what I learned:

Symfony is only reading environment variable values when the application is started. At this point, the container has been compiled and finalized. To build the container, Symfony requires the configuration options to have some value for the sake of validating the configuration. Symfony uses some "dummy values" (hardcoded here: https://github.com/symfony/symfony/blob/7.2/src/Symfony/Component/DependencyInjection/Compiler/ValidateEnvPlaceholdersPass.php#L29) as stand-ins for environment variables, because it doesn't know the environment variable's actual value at this point. Though these hardcoded dummy values might conflict with the constraints of configuration options, e.g. the lifetime requires a positive non-zero number, and the dummy value of 0 doesn't fulfill this requirement. This is why you see a The value 0 is too small for path "scheb_two_factor.trusted_device.lifetime". error, even when the environment variable value is totally fine (your environment variable value hasn't been evaluated at this point).

There is obviously no way around this, it's how environment variables in Symfony config work. The only thing I could do about it, is relaxing the constraints on configuration options to allow the dummy values to pass. Though I don't know how I feel about that. There's a reason these constraints exist.

@Adambean
Copy link
Author

Adambean commented Oct 22, 2024

Thank you very much for your detailed response. :)

These can be passed as an environment variable, but only without the :bool casting. The bundle will evaluate string values true / on to a boolean true value. (I checked with the test application, it does work).

That makes sense. I'll change these to strings and use "on" or "off" accordingly.

The reason why you're having problems with the casting might be related to your environment variable names starting with a number. Symfony doesn't like that, rightfully so, as the official definition of environment variable names seems to be:

To be honest, my variables are all prefixed as "MFA_" already. I had only renamed them to "2FA_" for here to match more closely with the bundle's name for example purposes. (Personally I prefer "MFA" to "2FA" linguistically for some silly meaningless reason.)

Symfony is only reading environment variable values when the application is started. At this point, the container has been compiled and finalized. To build the container, Symfony requires the configuration options to have some value for the sake of validating the configuration. Symfony uses some "dummy values" as stand-ins for environment variables, because it doesn't know the environment variable's actual value at this point.

This looks like the more likely cause for the issue I've had. Thanks for pointing to this primary source.

There is obviously no way around this, it's how environment variables in Symfony config work. The only thing I could do about it, is relaxing the constraints on configuration options to allow the dummy values to pass. Though I don't know how I feel about that. There's a reason these constraints exist.

I don't think you should relax the constraints to fit dummy values. I'd be more interested in this issue going up to Symfony to find out more about why the container's values get locked in such a way, and whether anything can be done there to benefit everyone's components. It appears other people have already had this problem:

symfony/symfony#57210
symfony/symfony#40794

In the short term I'll amend my configuration to be more static with some sharp commentary about duplications needing to be updated if amended.

Thanks again for your response and excellent bundle.

@scheb
Copy link
Owner

scheb commented Oct 23, 2024

That makes sense. I'll change these to strings and use "on" or "off" accordingly.

What did you use? Wouldn't be much of a hassle to add more strings, that would be evaluated as booleans. Maybe prevents other people from running into the same issue.

@Adambean
Copy link
Author

Adambean commented Oct 23, 2024

I used "on"/"off" to be in line with Symfony's bool environment processor. It would be pretty fair for your component to only accept those listed here in my opinion. ("true", "on", "yes", or a non-zero number.)

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

No branches or pull requests

2 participants