-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
core: support hashed password in users API #18686
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
Draft
dominic-r
wants to merge
5
commits into
main
Choose a base branch
from
sdko/hash-password-bootstrap
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from 2 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
2c275a8
bootstrap: ability to use hashed password
dominic-r 93d50dd
Potential fix for code scanning alert no. 272: Information exposure t…
dominic-r 112b04d
Apply suggestion from @dominic-r
dominic-r 8cfdc98
Apply suggestion from @dominic-r
dominic-r 42c4a21
Merge branch 'main' into sdko/hash-password-bootstrap
dominic-r File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| """Hash password using Django's password hashers""" | ||
|
|
||
| from django.contrib.auth.hashers import make_password | ||
| from django.core.management.base import BaseCommand, CommandError | ||
|
|
||
|
|
||
| class Command(BaseCommand): | ||
| """Hash a password using Django's password hashers""" | ||
|
|
||
| help = "Hash a password for use with AUTHENTIK_BOOTSTRAP_PASSWORD_HASH" | ||
|
|
||
| def add_arguments(self, parser): | ||
| parser.add_argument( | ||
| "password", | ||
| type=str, | ||
| help="Password to hash", | ||
| ) | ||
|
|
||
| def handle(self, *args, **options): | ||
| password = options["password"] | ||
|
|
||
| if not password: | ||
| raise CommandError("Password cannot be empty") | ||
|
|
||
| if len(password) < 1: | ||
| raise CommandError("Password must be at least 1 character long") | ||
|
|
||
| try: | ||
| hashed = make_password(password) | ||
| if not hashed: | ||
| raise CommandError("Failed to hash password") | ||
| self.stdout.write(hashed) | ||
| except Exception as exc: | ||
| raise CommandError(f"Error hashing password: {exc}") from exc | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| """Tests for hash_password management command""" | ||
|
|
||
| from io import StringIO | ||
|
|
||
| from django.contrib.auth.hashers import check_password | ||
| from django.core.management import call_command | ||
| from django.core.management.base import CommandError | ||
| from django.test import TestCase | ||
|
|
||
|
|
||
| class TestHashPasswordCommand(TestCase): | ||
| """Test hash_password management command""" | ||
|
|
||
| def test_hash_password_basic(self): | ||
| """Test basic password hashing""" | ||
| out = StringIO() | ||
| call_command("hash_password", "test123", stdout=out) | ||
| hashed = out.getvalue().strip() | ||
|
|
||
| # Verify it's a valid hash | ||
| self.assertTrue(hashed.startswith("pbkdf2_sha256$")) | ||
| # Verify the hash can be validated | ||
| self.assertTrue(check_password("test123", hashed)) | ||
|
|
||
| def test_hash_password_special_chars(self): | ||
| """Test hashing password with special characters""" | ||
| out = StringIO() | ||
| password = "P@ssw0rd!#$%^&*(){}[]" # nosec | ||
| call_command("hash_password", password, stdout=out) | ||
| hashed = out.getvalue().strip() | ||
|
|
||
| self.assertTrue(hashed.startswith("pbkdf2_sha256$")) | ||
| self.assertTrue(check_password(password, hashed)) | ||
|
|
||
| def test_hash_password_unicode(self): | ||
| """Test hashing password with unicode characters""" | ||
| out = StringIO() | ||
| password = "пароль123" # nosec | ||
| call_command("hash_password", password, stdout=out) | ||
| hashed = out.getvalue().strip() | ||
|
|
||
| self.assertTrue(hashed.startswith("pbkdf2_sha256$")) | ||
| self.assertTrue(check_password(password, hashed)) | ||
|
|
||
| def test_hash_password_long(self): | ||
| """Test hashing a very long password""" | ||
| out = StringIO() | ||
| password = "a" * 1000 | ||
| call_command("hash_password", password, stdout=out) | ||
| hashed = out.getvalue().strip() | ||
|
|
||
| self.assertTrue(hashed.startswith("pbkdf2_sha256$")) | ||
| self.assertTrue(check_password(password, hashed)) | ||
|
|
||
| def test_hash_password_spaces(self): | ||
| """Test hashing password with spaces""" | ||
| out = StringIO() | ||
| password = "my super secret password" # nosec | ||
| call_command("hash_password", password, stdout=out) | ||
| hashed = out.getvalue().strip() | ||
|
|
||
| self.assertTrue(hashed.startswith("pbkdf2_sha256$")) | ||
| self.assertTrue(check_password(password, hashed)) | ||
|
|
||
| def test_hash_password_empty_fails(self): | ||
| """Test that empty password raises error""" | ||
| with self.assertRaises(CommandError) as ctx: | ||
| call_command("hash_password", "") | ||
| self.assertIn("Password cannot be empty", str(ctx.exception)) | ||
|
|
||
| def test_hash_different_passwords_different_hashes(self): | ||
| """Test that different passwords produce different hashes""" | ||
| out1 = StringIO() | ||
| out2 = StringIO() | ||
|
|
||
| call_command("hash_password", "password1", stdout=out1) | ||
| call_command("hash_password", "password2", stdout=out2) | ||
|
|
||
| hash1 = out1.getvalue().strip() | ||
| hash2 = out2.getvalue().strip() | ||
|
|
||
| self.assertNotEqual(hash1, hash2) | ||
| self.assertTrue(check_password("password1", hash1)) | ||
| self.assertTrue(check_password("password2", hash2)) | ||
| self.assertFalse(check_password("password1", hash2)) | ||
| self.assertFalse(check_password("password2", hash1)) | ||
|
|
||
| def test_hash_same_password_different_hashes(self): | ||
| """Test that same password produces different hashes (due to salt)""" | ||
| out1 = StringIO() | ||
| out2 = StringIO() | ||
|
|
||
| call_command("hash_password", "samepassword", stdout=out1) | ||
| call_command("hash_password", "samepassword", stdout=out2) | ||
|
|
||
| hash1 = out1.getvalue().strip() | ||
| hash2 = out2.getvalue().strip() | ||
|
|
||
| # Hashes should be different due to random salt | ||
| self.assertNotEqual(hash1, hash2) | ||
| # But both should validate the same password | ||
| self.assertTrue(check_password("samepassword", hash1)) | ||
| self.assertTrue(check_password("samepassword", hash2)) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,7 @@ context: | |
| group_name: authentik Admins | ||
| email: !Env [AUTHENTIK_BOOTSTRAP_EMAIL, "[email protected]"] | ||
| password: !Env [AUTHENTIK_BOOTSTRAP_PASSWORD, null] | ||
| password_hash: !Env [AUTHENTIK_BOOTSTRAP_PASSWORD_HASH, null] | ||
| token: !Env [AUTHENTIK_BOOTSTRAP_TOKEN, null] | ||
| entries: | ||
| - model: authentik_core.group | ||
|
|
@@ -31,6 +32,7 @@ entries: | |
| groups: | ||
| - !KeyOf admin-group | ||
| password: !Context password | ||
| password_hash: !Context password_hash | ||
| - model: authentik_core.token | ||
| state: created | ||
| conditions: | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,8 +6,44 @@ To install authentik automatically (skipping the Out-of-box experience), you can | |
|
|
||
| ### `AUTHENTIK_BOOTSTRAP_PASSWORD` | ||
|
|
||
| :::note | ||
| For improved security, consider using a [hashed password](#authentik_bootstrap_password_hash) to avoid storing plaintext passwords in environment variables. | ||
| ::: | ||
|
|
||
| Configure the default password for the `akadmin` user. Only read on the first startup. Can be used for any flow executor. | ||
|
|
||
| ### `AUTHENTIK_BOOTSTRAP_PASSWORD_HASH` | ||
|
|
||
| Configure the default password for the `akadmin` user using a pre-hashed password. Only read on the first startup. | ||
|
|
||
| This is useful when you want to avoid storing plaintext passwords in environment variables or configuration files. | ||
|
|
||
| To generate a hash, run this command before your initial deployment: | ||
|
|
||
| ```bash | ||
| # In local development environment | ||
| uv run ak hash_password your-password | ||
|
|
||
| # Or using a temporary Docker container | ||
| docker run --rm -it ghcr.io/goauthentik/server:<The latest release at this time> ak hash_password your-password | ||
|
|
||
| # Or on an existing authentik installation | ||
| docker exec -it authentik-worker ak hash_password your-password | ||
| ``` | ||
|
|
||
| If both `AUTHENTIK_BOOTSTRAP_PASSWORD` and `AUTHENTIK_BOOTSTRAP_PASSWORD_HASH` are set, the hash takes precedence. | ||
|
|
||
| :::warning Docker Compose | ||
| When using Docker Compose with a `.env` file, escape the `$` characters in the password hash by doubling them (`$$`): | ||
|
|
||
| ```bash | ||
| # .env file | ||
| AUTHENTIK_BOOTSTRAP_PASSWORD_HASH="pbkdf2_sha256$$870000$$salt$$hash..." | ||
| ``` | ||
|
|
||
| This prevents Docker Compose from interpreting `$` as variable references. | ||
| ::: | ||
|
|
||
| ### `AUTHENTIK_BOOTSTRAP_TOKEN` | ||
|
|
||
| Create a token for the default `akadmin` user. Only read on the first startup. The string you specify for this variable is the token key you can use to authenticate yourself to the API. | ||
|
|
@@ -36,3 +72,15 @@ global: | |
| ``` | ||
|
|
||
| where _some-secret_ contains the environment variables as in the documentation above. | ||
|
|
||
| ### Using password hash with Helm values | ||
|
|
||
| For improved security, you can use a pre-hashed password directly in your Helm values: | ||
|
|
||
| ```yaml | ||
| authentik: | ||
| bootstrap_password_hash: "pbkdf2_sha256$870000$abc123$xyz..." | ||
| bootstrap_email: "[email protected]" | ||
| ``` | ||
|
|
||
| Or store the password hash in a secret and reference it via `envFrom` (same method as shown above for storing password and token in a secret). | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.