Compare commits
	
		
			10 Commits
		
	
	
		
			policies-e
			...
			version/20
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 318e0cf9f8 | |||
| bd0815d894 | |||
| af35ecfe66 | |||
| 0c05cd64bb | |||
| cb80b76490 | |||
| 061d4bc758 | |||
| 8ff27f69e1 | |||
| 045cd98276 | |||
| b520843984 | |||
| 92216e4ea8 | 
| @ -1,16 +1,16 @@ | |||||||
| [bumpversion] | [bumpversion] | ||||||
| current_version = 2023.10.7 | current_version = 2024.2.0-rc2 | ||||||
| tag = True | tag = True | ||||||
| commit = True | commit = True | ||||||
| parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))? | parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))? | ||||||
| serialize = | serialize =  | ||||||
| 	{major}.{minor}.{patch}-{rc_t}{rc_n} | 	{major}.{minor}.{patch}-{rc_t}{rc_n} | ||||||
| 	{major}.{minor}.{patch} | 	{major}.{minor}.{patch} | ||||||
| message = release: {new_version} | message = release: {new_version} | ||||||
| tag_name = version/{new_version} | tag_name = version/{new_version} | ||||||
|  |  | ||||||
| [bumpversion:part:rc_t] | [bumpversion:part:rc_t] | ||||||
| values = | values =  | ||||||
| 	rc | 	rc | ||||||
| 	final | 	final | ||||||
| optional_value = final | optional_value = final | ||||||
|  | |||||||
							
								
								
									
										11
									
								
								.github/actions/docker-push-variables/action.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								.github/actions/docker-push-variables/action.yml
									
									
									
									
										vendored
									
									
								
							| @ -68,18 +68,21 @@ runs: | |||||||
|             for name in image_names: |             for name in image_names: | ||||||
|                 image_tags += [ |                 image_tags += [ | ||||||
|                     f"{name}:{version}", |                     f"{name}:{version}", | ||||||
|                     f"{name}:{version_family}", |  | ||||||
|                 ] |                 ] | ||||||
|             if not prerelease: |             if not prerelease: | ||||||
|                 image_tags += [f"{name}:latest"] |                 image_tags += [ | ||||||
|  |                     f"{name}:latest", | ||||||
|  |                     f"{name}:{version_family}", | ||||||
|  |                 ] | ||||||
|         else: |         else: | ||||||
|             suffix = "" |             suffix = "" | ||||||
|             if image_arch and image_arch != "amd64": |             if image_arch and image_arch != "amd64": | ||||||
|                 suffix = f"-{image_arch}" |                 suffix = f"-{image_arch}" | ||||||
|             for name in image_names: |             for name in image_names: | ||||||
|                 image_tags += [ |                 image_tags += [ | ||||||
|                     f"{name}:gh-{sha}{suffix}", |                     f"{name}:gh-{sha}{suffix}",  # Used for ArgoCD and PR comments | ||||||
|                     f"{name}:gh-{safe_branch_name}{suffix}", |                     f"{name}:gh-{safe_branch_name}{suffix}",  # For convenience | ||||||
|  |                     f"{name}:gh-{safe_branch_name}-{int(time())}-{sha[:7]}{suffix}",  # Use by FluxCD | ||||||
|                 ] |                 ] | ||||||
|  |  | ||||||
|         image_main_tag = image_tags[0] |         image_main_tag = image_tags[0] | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							| @ -70,7 +70,7 @@ jobs: | |||||||
|           cp authentik/lib/default.yml local.env.yml |           cp authentik/lib/default.yml local.env.yml | ||||||
|           cp -R .github .. |           cp -R .github .. | ||||||
|           cp -R scripts .. |           cp -R scripts .. | ||||||
|           git checkout version/$(python -c "from authentik import __version__; print(__version__)") |           git checkout $(git tag --sort=version:refname | grep '^version/' | grep -vE -- '-rc[0-9]+$' | tail -n1) | ||||||
|           rm -rf .github/ scripts/ |           rm -rf .github/ scripts/ | ||||||
|           mv ../.github ../scripts . |           mv ../.github ../scripts . | ||||||
|       - name: Setup authentik env (stable) |       - name: Setup authentik env (stable) | ||||||
|  | |||||||
							
								
								
									
										4
									
								
								.github/workflows/release-publish.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/release-publish.yml
									
									
									
									
										vendored
									
									
								
							| @ -172,8 +172,8 @@ jobs: | |||||||
|           image-name: ghcr.io/goauthentik/server |           image-name: ghcr.io/goauthentik/server | ||||||
|       - name: Get static files from docker image |       - name: Get static files from docker image | ||||||
|         run: | |         run: | | ||||||
|           docker pull ghcr.io/goauthentik/server:${{ steps.ev.outputs.imageMainTag }} |           docker pull ${{ steps.ev.outputs.imageMainTag }} | ||||||
|           container=$(docker container create ghcr.io/goauthentik/server:${{ steps.ev.outputs.imageMainTag }}) |           container=$(docker container create ${{ steps.ev.outputs.imageMainTag }}) | ||||||
|           docker cp ${container}:web/ . |           docker cp ${container}:web/ . | ||||||
|       - name: Create a Sentry.io release |       - name: Create a Sentry.io release | ||||||
|         uses: getsentry/action-release@v1 |         uses: getsentry/action-release@v1 | ||||||
|  | |||||||
| @ -3,7 +3,7 @@ | |||||||
| from os import environ | from os import environ | ||||||
| from typing import Optional | from typing import Optional | ||||||
|  |  | ||||||
| __version__ = "2023.10.7" | __version__ = "2024.2.0" | ||||||
| ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -481,13 +481,6 @@ def _update_settings(app_path: str): | |||||||
|         pass |         pass | ||||||
|  |  | ||||||
|  |  | ||||||
| # Load subapps's settings |  | ||||||
| for _app in set(SHARED_APPS + TENANT_APPS): |  | ||||||
|     if not _app.startswith("authentik"): |  | ||||||
|         continue |  | ||||||
|     _update_settings(f"{_app}.settings") |  | ||||||
| _update_settings("data.user_settings") |  | ||||||
|  |  | ||||||
| if DEBUG: | if DEBUG: | ||||||
|     CELERY["task_always_eager"] = True |     CELERY["task_always_eager"] = True | ||||||
|     os.environ[ENV_GIT_HASH_KEY] = "dev" |     os.environ[ENV_GIT_HASH_KEY] = "dev" | ||||||
| @ -512,5 +505,13 @@ except ImportError: | |||||||
| # being imported for @prefill_task | # being imported for @prefill_task | ||||||
| TENANT_APPS.append("authentik.events") | TENANT_APPS.append("authentik.events") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # Load subapps's settings | ||||||
|  | for _app in set(SHARED_APPS + TENANT_APPS): | ||||||
|  |     if not _app.startswith("authentik"): | ||||||
|  |         continue | ||||||
|  |     _update_settings(f"{_app}.settings") | ||||||
|  | _update_settings("data.user_settings") | ||||||
|  |  | ||||||
| SHARED_APPS = list(OrderedDict.fromkeys(SHARED_APPS + TENANT_APPS)) | SHARED_APPS = list(OrderedDict.fromkeys(SHARED_APPS + TENANT_APPS)) | ||||||
| INSTALLED_APPS = list(OrderedDict.fromkeys(SHARED_APPS + TENANT_APPS)) | INSTALLED_APPS = list(OrderedDict.fromkeys(SHARED_APPS + TENANT_APPS)) | ||||||
|  | |||||||
| @ -32,7 +32,7 @@ services: | |||||||
|     volumes: |     volumes: | ||||||
|       - redis:/data |       - redis:/data | ||||||
|   server: |   server: | ||||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.10.7} |     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.2.0} | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|     command: server |     command: server | ||||||
|     environment: |     environment: | ||||||
| @ -53,7 +53,7 @@ services: | |||||||
|       - postgresql |       - postgresql | ||||||
|       - redis |       - redis | ||||||
|   worker: |   worker: | ||||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.10.7} |     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.2.0} | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|     command: worker |     command: worker | ||||||
|     environment: |     environment: | ||||||
|  | |||||||
| @ -29,4 +29,4 @@ func UserAgent() string { | |||||||
| 	return fmt.Sprintf("authentik@%s", FullVersion()) | 	return fmt.Sprintf("authentik@%s", FullVersion()) | ||||||
| } | } | ||||||
|  |  | ||||||
| const VERSION = "2023.10.7" | const VERSION = "2024.2.0" | ||||||
|  | |||||||
| @ -86,6 +86,7 @@ elif [[ "$1" == "bash" ]]; then | |||||||
|     /bin/bash |     /bin/bash | ||||||
| elif [[ "$1" == "test-all" ]]; then | elif [[ "$1" == "test-all" ]]; then | ||||||
|     prepare_debug |     prepare_debug | ||||||
|  |     chmod 777 /root | ||||||
|     check_if_root "python -m manage test authentik" |     check_if_root "python -m manage test authentik" | ||||||
| elif [[ "$1" == "healthcheck" ]]; then | elif [[ "$1" == "healthcheck" ]]; then | ||||||
|     run_authentik healthcheck $(cat $MODE_FILE) |     run_authentik healthcheck $(cat $MODE_FILE) | ||||||
|  | |||||||
| @ -113,7 +113,7 @@ filterwarnings = [ | |||||||
|  |  | ||||||
| [tool.poetry] | [tool.poetry] | ||||||
| name = "authentik" | name = "authentik" | ||||||
| version = "2023.10.7" | version = "2024.2.0" | ||||||
| description = "" | description = "" | ||||||
| authors = ["authentik Team <hello@goauthentik.io>"] | authors = ["authentik Team <hello@goauthentik.io>"] | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| openapi: 3.0.3 | openapi: 3.0.3 | ||||||
| info: | info: | ||||||
|   title: authentik |   title: authentik | ||||||
|   version: 2023.10.7 |   version: 2024.2.0 | ||||||
|   description: Making authentication simple. |   description: Making authentication simple. | ||||||
|   contact: |   contact: | ||||||
|     email: hello@goauthentik.io |     email: hello@goauthentik.io | ||||||
|  | |||||||
| @ -6,3 +6,4 @@ dist | |||||||
| coverage | coverage | ||||||
| src/locale-codes.ts | src/locale-codes.ts | ||||||
| storybook-static/ | storybook-static/ | ||||||
|  | src/locales/** | ||||||
|  | |||||||
| @ -22,25 +22,36 @@ import { AdminApi, Settings, SettingsRequest } from "@goauthentik/api"; | |||||||
|  |  | ||||||
| @customElement("ak-admin-settings-form") | @customElement("ak-admin-settings-form") | ||||||
| export class AdminSettingsForm extends Form<SettingsRequest> { | export class AdminSettingsForm extends Form<SettingsRequest> { | ||||||
|     @property({ attribute: false }) |     // | ||||||
|     set settings(value: Settings) { |     // Custom property accessors in Lit 2 require a manual call to requestUpdate(). See: | ||||||
|  |     // https://lit.dev/docs/v2/components/properties/#accessors-custom | ||||||
|  |     // | ||||||
|  |     set settings(value: Settings | undefined) { | ||||||
|         this._settings = value; |         this._settings = value; | ||||||
|  |         this.requestUpdate(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @property({ type: Object }) | ||||||
|  |     get settings() { | ||||||
|  |         return this._settings; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private _settings?: Settings; |     private _settings?: Settings; | ||||||
|  |  | ||||||
|  |     static get styles(): CSSResult[] { | ||||||
|  |         return super.styles.concat(PFList); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     getSuccessMessage(): string { |     getSuccessMessage(): string { | ||||||
|         return msg("Successfully updated settings."); |         return msg("Successfully updated settings."); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async send(data: SettingsRequest): Promise<Settings> { |     async send(data: SettingsRequest): Promise<Settings> { | ||||||
|         return new AdminApi(DEFAULT_CONFIG).adminSettingsUpdate({ |         const result = await new AdminApi(DEFAULT_CONFIG).adminSettingsUpdate({ | ||||||
|             settingsRequest: data, |             settingsRequest: data, | ||||||
|         }); |         }); | ||||||
|     } |         this.dispatchEvent(new CustomEvent("ak-admin-setting-changed")); | ||||||
|  |         return result; | ||||||
|     static get styles(): CSSResult[] { |  | ||||||
|         return super.styles.concat(PFList); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     renderForm(): TemplateResult { |     renderForm(): TemplateResult { | ||||||
|  | |||||||
| @ -14,8 +14,8 @@ import "@goauthentik/elements/buttons/SpinnerButton"; | |||||||
| import "@goauthentik/elements/forms/ModalForm"; | import "@goauthentik/elements/forms/ModalForm"; | ||||||
|  |  | ||||||
| import { msg } from "@lit/localize"; | import { msg } from "@lit/localize"; | ||||||
| import { CSSResult, TemplateResult, html } from "lit"; | import { html, nothing } from "lit"; | ||||||
| import { customElement, property } from "lit/decorators.js"; | import { customElement, query, state } from "lit/decorators.js"; | ||||||
|  |  | ||||||
| import PFBanner from "@patternfly/patternfly/components/Banner/banner.css"; | import PFBanner from "@patternfly/patternfly/components/Banner/banner.css"; | ||||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||||
| @ -32,7 +32,7 @@ import { AdminApi, Settings } from "@goauthentik/api"; | |||||||
|  |  | ||||||
| @customElement("ak-admin-settings") | @customElement("ak-admin-settings") | ||||||
| export class AdminSettingsPage extends AKElement { | export class AdminSettingsPage extends AKElement { | ||||||
|     static get styles(): CSSResult[] { |     static get styles() { | ||||||
|         return [ |         return [ | ||||||
|             PFBase, |             PFBase, | ||||||
|             PFButton, |             PFButton, | ||||||
| @ -46,41 +46,46 @@ export class AdminSettingsPage extends AKElement { | |||||||
|             PFBanner, |             PFBanner, | ||||||
|         ]; |         ]; | ||||||
|     } |     } | ||||||
|     @property({ attribute: false }) |  | ||||||
|  |     @query("ak-admin-settings-form#form") | ||||||
|  |     form?: AdminSettingsForm; | ||||||
|  |  | ||||||
|  |     @state() | ||||||
|     settings?: Settings; |     settings?: Settings; | ||||||
|  |  | ||||||
|     loadSettings(): void { |     constructor() { | ||||||
|         new AdminApi(DEFAULT_CONFIG).adminSettingsRetrieve().then((settings) => { |         super(); | ||||||
|  |         AdminSettingsPage.fetchSettings().then((settings) => { | ||||||
|             this.settings = settings; |             this.settings = settings; | ||||||
|         }); |         }); | ||||||
|  |         this.save = this.save.bind(this); | ||||||
|  |         this.reset = this.reset.bind(this); | ||||||
|  |         this.addEventListener("ak-admin-setting-changed", this.handleUpdate.bind(this)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     firstUpdated(): void { |     static async fetchSettings() { | ||||||
|         this.loadSettings(); |         return await new AdminApi(DEFAULT_CONFIG).adminSettingsRetrieve(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async save(): Promise<void> { |     async handleUpdate() { | ||||||
|         const form = this.shadowRoot?.querySelector<AdminSettingsForm>("ak-admin-settings-form"); |         this.settings = await AdminSettingsPage.fetchSettings(); | ||||||
|         if (!form) { |     } | ||||||
|  |  | ||||||
|  |     async save() { | ||||||
|  |         if (!this.form) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|         await form.submit(new Event("submit")); |         await this.form.submit(new Event("submit")); | ||||||
|         this.resetForm(); |         this.settings = await AdminSettingsPage.fetchSettings(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     resetForm(): void { |     async reset() { | ||||||
|         const form = this.shadowRoot?.querySelector<AdminSettingsForm>("ak-admin-settings-form"); |         this.form?.resetForm(); | ||||||
|         if (!form) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         this.loadSettings(); |  | ||||||
|         form.settings = this.settings!; |  | ||||||
|         form.resetForm(); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     render(): TemplateResult { |     render() { | ||||||
|         if (!this.settings) { |         if (!this.settings) { | ||||||
|             return html``; |             return nothing; | ||||||
|         } |         } | ||||||
|         return html` |         return html` | ||||||
|             <ak-page-header icon="fa fa-cog" header="" description=""> |             <ak-page-header icon="fa fa-cog" header="" description=""> | ||||||
| @ -93,18 +98,10 @@ export class AdminSettingsPage extends AKElement { | |||||||
|                         </ak-admin-settings-form> |                         </ak-admin-settings-form> | ||||||
|                     </div> |                     </div> | ||||||
|                     <div class="pf-c-card__footer"> |                     <div class="pf-c-card__footer"> | ||||||
|                         <ak-spinner-button |                         <ak-spinner-button .callAction=${this.save} class="pf-m-primary" | ||||||
|                             .callAction=${async () => { |  | ||||||
|                                 await this.save(); |  | ||||||
|                             }} |  | ||||||
|                             class="pf-m-primary" |  | ||||||
|                             >${msg("Save")}</ak-spinner-button |                             >${msg("Save")}</ak-spinner-button | ||||||
|                         > |                         > | ||||||
|                         <ak-spinner-button |                         <ak-spinner-button .callAction=${this.reset} class="pf-m-secondary" | ||||||
|                             .callAction=${() => { |  | ||||||
|                                 this.resetForm(); |  | ||||||
|                             }} |  | ||||||
|                             class="pf-m-secondary" |  | ||||||
|                             >${msg("Cancel")}</ak-spinner-button |                             >${msg("Cancel")}</ak-spinner-button | ||||||
|                         > |                         > | ||||||
|                     </div> |                     </div> | ||||||
|  | |||||||
| @ -125,6 +125,7 @@ export class RelatedGroupList extends Table<Group> { | |||||||
|             actionSubtext=${msg( |             actionSubtext=${msg( | ||||||
|                 str`Are you sure you want to remove user ${this.targetUser?.username} from the following groups?`, |                 str`Are you sure you want to remove user ${this.targetUser?.username} from the following groups?`, | ||||||
|             )} |             )} | ||||||
|  |             buttonLabel=${msg("Remove")} | ||||||
|             .objects=${this.selectedElements} |             .objects=${this.selectedElements} | ||||||
|             .delete=${(item: Group) => { |             .delete=${(item: Group) => { | ||||||
|                 if (!this.targetUser) return; |                 if (!this.targetUser) return; | ||||||
|  | |||||||
| @ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success"; | |||||||
| export const ERROR_CLASS = "pf-m-danger"; | export const ERROR_CLASS = "pf-m-danger"; | ||||||
| export const PROGRESS_CLASS = "pf-m-in-progress"; | export const PROGRESS_CLASS = "pf-m-in-progress"; | ||||||
| export const CURRENT_CLASS = "pf-m-current"; | export const CURRENT_CLASS = "pf-m-current"; | ||||||
| export const VERSION = "2023.10.7"; | export const VERSION = "2024.2.0"; | ||||||
| export const TITLE_DEFAULT = "authentik"; | export const TITLE_DEFAULT = "authentik"; | ||||||
| export const ROUTE_SEPARATOR = ";"; | export const ROUTE_SEPARATOR = ";"; | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| import { AKElement } from "@goauthentik/elements/Base"; | import { AKElement } from "@goauthentik/elements/Base"; | ||||||
| import { PFSize } from "@goauthentik/elements/Spinner"; | import { PFSize } from "@goauthentik/elements/Spinner"; | ||||||
|  |  | ||||||
| import { CSSResult, TemplateResult, html } from "lit"; | import { CSSResult, TemplateResult, css, html } from "lit"; | ||||||
| import { customElement, property } from "lit/decorators.js"; | import { customElement, property } from "lit/decorators.js"; | ||||||
|  |  | ||||||
| import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css"; | import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css"; | ||||||
| @ -23,7 +23,17 @@ export class EmptyState extends AKElement { | |||||||
|     header = ""; |     header = ""; | ||||||
|  |  | ||||||
|     static get styles(): CSSResult[] { |     static get styles(): CSSResult[] { | ||||||
|         return [PFBase, PFEmptyState, PFTitle]; |         return [ | ||||||
|  |             PFBase, | ||||||
|  |             PFEmptyState, | ||||||
|  |             PFTitle, | ||||||
|  |             css` | ||||||
|  |                 i.pf-c-empty-state__icon { | ||||||
|  |                     height: var(--pf-global--icon--FontSize--2xl); | ||||||
|  |                     line-height: var(--pf-global--icon--FontSize--2xl); | ||||||
|  |                 } | ||||||
|  |             `, | ||||||
|  |         ]; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     render(): TemplateResult { |     render(): TemplateResult { | ||||||
|  | |||||||
| @ -131,6 +131,9 @@ export class DeleteBulkForm<T> extends ModalButton { | |||||||
|     @property() |     @property() | ||||||
|     actionSubtext?: string; |     actionSubtext?: string; | ||||||
|  |  | ||||||
|  |     @property() | ||||||
|  |     buttonLabel = msg("Delete"); | ||||||
|  |  | ||||||
|     @property({ attribute: false }) |     @property({ attribute: false }) | ||||||
|     metadata: (item: T) => BulkDeleteMetadata = (item: T) => { |     metadata: (item: T) => BulkDeleteMetadata = (item: T) => { | ||||||
|         const rec = item as Record<string, unknown>; |         const rec = item as Record<string, unknown>; | ||||||
| @ -222,7 +225,7 @@ export class DeleteBulkForm<T> extends ModalButton { | |||||||
|                     }} |                     }} | ||||||
|                     class="pf-m-danger" |                     class="pf-m-danger" | ||||||
|                 > |                 > | ||||||
|                     ${msg("Delete")} </ak-spinner-button |                     ${this.buttonLabel} </ak-spinner-button | ||||||
|                 >  |                 >  | ||||||
|                 <ak-spinner-button |                 <ak-spinner-button | ||||||
|                     .callAction=${async () => { |                     .callAction=${async () => { | ||||||
|  | |||||||
| @ -15,7 +15,7 @@ import "@goauthentik/flow/sources/apple/AppleLoginInit"; | |||||||
| import "@goauthentik/flow/sources/plex/PlexLoginInit"; | import "@goauthentik/flow/sources/plex/PlexLoginInit"; | ||||||
| import "@goauthentik/flow/stages/FlowErrorStage"; | import "@goauthentik/flow/stages/FlowErrorStage"; | ||||||
| import "@goauthentik/flow/stages/RedirectStage"; | import "@goauthentik/flow/stages/RedirectStage"; | ||||||
| import { StageHost } from "@goauthentik/flow/stages/base"; | import { StageHost, SubmitOptions } from "@goauthentik/flow/stages/base"; | ||||||
|  |  | ||||||
| import { msg } from "@lit/localize"; | import { msg } from "@lit/localize"; | ||||||
| import { CSSResult, TemplateResult, css, html, nothing } from "lit"; | import { CSSResult, TemplateResult, css, html, nothing } from "lit"; | ||||||
| @ -189,12 +189,17 @@ export class FlowExecutor extends Interface implements StageHost { | |||||||
|         return globalAK()?.brand.uiTheme || UiThemeEnum.Automatic; |         return globalAK()?.brand.uiTheme || UiThemeEnum.Automatic; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async submit(payload?: FlowChallengeResponseRequest): Promise<boolean> { |     async submit( | ||||||
|  |         payload?: FlowChallengeResponseRequest, | ||||||
|  |         options?: SubmitOptions, | ||||||
|  |     ): Promise<boolean> { | ||||||
|         if (!payload) return Promise.reject(); |         if (!payload) return Promise.reject(); | ||||||
|         if (!this.challenge) return Promise.reject(); |         if (!this.challenge) return Promise.reject(); | ||||||
|         // @ts-ignore |         // @ts-expect-error | ||||||
|         payload.component = this.challenge.component; |         payload.component = this.challenge.component; | ||||||
|         this.loading = true; |         if (!options?.invisible) { | ||||||
|  |             this.loading = true; | ||||||
|  |         } | ||||||
|         try { |         try { | ||||||
|             const challenge = await new FlowsApi(DEFAULT_CONFIG).flowsExecutorSolve({ |             const challenge = await new FlowsApi(DEFAULT_CONFIG).flowsExecutorSolve({ | ||||||
|                 flowSlug: this.flowSlug, |                 flowSlug: this.flowSlug, | ||||||
|  | |||||||
| @ -40,6 +40,7 @@ export class AuthenticatorStaticStage extends BaseStage< | |||||||
|                     columns: 2; |                     columns: 2; | ||||||
|                     -webkit-columns: 2; |                     -webkit-columns: 2; | ||||||
|                     -moz-columns: 2; |                     -moz-columns: 2; | ||||||
|  |                     column-width: 1em; | ||||||
|                     margin-left: var(--pf-global--spacer--xs); |                     margin-left: var(--pf-global--spacer--xs); | ||||||
|                 } |                 } | ||||||
|                 ul li { |                 ul li { | ||||||
|  | |||||||
| @ -2,13 +2,12 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | |||||||
| import "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStageCode"; | import "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStageCode"; | ||||||
| import "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStageDuo"; | import "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStageDuo"; | ||||||
| import "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStageWebAuthn"; | import "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStageWebAuthn"; | ||||||
| import { BaseStage, StageHost } from "@goauthentik/flow/stages/base"; | import { BaseStage, StageHost, SubmitOptions } from "@goauthentik/flow/stages/base"; | ||||||
| import { PasswordManagerPrefill } from "@goauthentik/flow/stages/identification/IdentificationStage"; | import { PasswordManagerPrefill } from "@goauthentik/flow/stages/identification/IdentificationStage"; | ||||||
|  |  | ||||||
| import { msg } from "@lit/localize"; | import { msg } from "@lit/localize"; | ||||||
| import { CSSResult, TemplateResult, css, html } from "lit"; | import { CSSResult, TemplateResult, css, html } from "lit"; | ||||||
| import { customElement, state } from "lit/decorators.js"; | import { customElement, state } from "lit/decorators.js"; | ||||||
| import { ifDefined } from "lit/directives/if-defined.js"; |  | ||||||
|  |  | ||||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||||
| import PFForm from "@patternfly/patternfly/components/Form/form.css"; | import PFForm from "@patternfly/patternfly/components/Form/form.css"; | ||||||
| @ -59,7 +58,7 @@ export class AuthenticatorValidateStage | |||||||
|         // We don't use this.submit here, as we don't want to advance the flow. |         // We don't use this.submit here, as we don't want to advance the flow. | ||||||
|         // We just want to notify the backend which challenge has been selected. |         // We just want to notify the backend which challenge has been selected. | ||||||
|         new FlowsApi(DEFAULT_CONFIG).flowsExecutorSolve({ |         new FlowsApi(DEFAULT_CONFIG).flowsExecutorSolve({ | ||||||
|             flowSlug: this.host.flowSlug || "", |             flowSlug: this.host?.flowSlug || "", | ||||||
|             query: window.location.search.substring(1), |             query: window.location.search.substring(1), | ||||||
|             flowChallengeResponseRequest: { |             flowChallengeResponseRequest: { | ||||||
|                 // @ts-ignore |                 // @ts-ignore | ||||||
| @ -73,8 +72,11 @@ export class AuthenticatorValidateStage | |||||||
|         return this._selectedDeviceChallenge; |         return this._selectedDeviceChallenge; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     submit(payload: AuthenticatorValidationChallengeResponseRequest): Promise<boolean> { |     submit( | ||||||
|         return this.host?.submit(payload) || Promise.resolve(); |         payload: AuthenticatorValidationChallengeResponseRequest, | ||||||
|  |         options?: SubmitOptions, | ||||||
|  |     ): Promise<boolean> { | ||||||
|  |         return this.host?.submit(payload, options) || Promise.resolve(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     static get styles(): CSSResult[] { |     static get styles(): CSSResult[] { | ||||||
| @ -253,23 +255,7 @@ export class AuthenticatorValidateStage | |||||||
|                 ? this.renderDeviceChallenge() |                 ? this.renderDeviceChallenge() | ||||||
|                 : html`<div class="pf-c-login__main-body"> |                 : html`<div class="pf-c-login__main-body"> | ||||||
|                           <form class="pf-c-form"> |                           <form class="pf-c-form"> | ||||||
|                               <ak-form-static |                               ${this.renderUserInfo()} | ||||||
|                                   class="pf-c-form__group" |  | ||||||
|                                   userAvatar="${this.challenge.pendingUserAvatar}" |  | ||||||
|                                   user=${this.challenge.pendingUser} |  | ||||||
|                               > |  | ||||||
|                                   <div slot="link"> |  | ||||||
|                                       <a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}" |  | ||||||
|                                           >${msg("Not you?")}</a |  | ||||||
|                                       > |  | ||||||
|                                   </div> |  | ||||||
|                               </ak-form-static> |  | ||||||
|                               <input |  | ||||||
|                                   name="username" |  | ||||||
|                                   autocomplete="username" |  | ||||||
|                                   type="hidden" |  | ||||||
|                                   value="${this.challenge.pendingUser}" |  | ||||||
|                               /> |  | ||||||
|                               ${this.selectedDeviceChallenge |                               ${this.selectedDeviceChallenge | ||||||
|                                   ? "" |                                   ? "" | ||||||
|                                   : html`<p>${msg("Select an authentication method.")}</p>`} |                                   : html`<p>${msg("Select an authentication method.")}</p>`} | ||||||
|  | |||||||
| @ -1,59 +1,34 @@ | |||||||
|  | import { BaseDeviceStage } from "@goauthentik/app/flow/stages/authenticator_validate/base"; | ||||||
| import "@goauthentik/elements/EmptyState"; | import "@goauthentik/elements/EmptyState"; | ||||||
| import "@goauthentik/elements/forms/FormElement"; | import "@goauthentik/elements/forms/FormElement"; | ||||||
| import "@goauthentik/flow/FormStatic"; |  | ||||||
| import { AuthenticatorValidateStage } from "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStage"; |  | ||||||
| import { BaseStage } from "@goauthentik/flow/stages/base"; |  | ||||||
| import { PasswordManagerPrefill } from "@goauthentik/flow/stages/identification/IdentificationStage"; | import { PasswordManagerPrefill } from "@goauthentik/flow/stages/identification/IdentificationStage"; | ||||||
|  |  | ||||||
| import { msg } from "@lit/localize"; | import { msg } from "@lit/localize"; | ||||||
| import { CSSResult, TemplateResult, css, html } from "lit"; | import { CSSResult, TemplateResult, css, html } from "lit"; | ||||||
| import { customElement, property } from "lit/decorators.js"; | import { customElement } from "lit/decorators.js"; | ||||||
| import { ifDefined } from "lit/directives/if-defined.js"; |  | ||||||
|  |  | ||||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; |  | ||||||
| import PFForm from "@patternfly/patternfly/components/Form/form.css"; |  | ||||||
| import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css"; |  | ||||||
| import PFLogin from "@patternfly/patternfly/components/Login/login.css"; |  | ||||||
| import PFTitle from "@patternfly/patternfly/components/Title/title.css"; |  | ||||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; |  | ||||||
|  |  | ||||||
| import { | import { | ||||||
|     AuthenticatorValidationChallenge, |     AuthenticatorValidationChallenge, | ||||||
|     AuthenticatorValidationChallengeResponseRequest, |     AuthenticatorValidationChallengeResponseRequest, | ||||||
|     DeviceChallenge, |  | ||||||
|     DeviceClassesEnum, |     DeviceClassesEnum, | ||||||
| } from "@goauthentik/api"; | } from "@goauthentik/api"; | ||||||
|  |  | ||||||
| @customElement("ak-stage-authenticator-validate-code") | @customElement("ak-stage-authenticator-validate-code") | ||||||
| export class AuthenticatorValidateStageWebCode extends BaseStage< | export class AuthenticatorValidateStageWebCode extends BaseDeviceStage< | ||||||
|     AuthenticatorValidationChallenge, |     AuthenticatorValidationChallenge, | ||||||
|     AuthenticatorValidationChallengeResponseRequest |     AuthenticatorValidationChallengeResponseRequest | ||||||
| > { | > { | ||||||
|     @property({ attribute: false }) |  | ||||||
|     deviceChallenge?: DeviceChallenge; |  | ||||||
|  |  | ||||||
|     @property({ type: Boolean }) |  | ||||||
|     showBackButton = false; |  | ||||||
|  |  | ||||||
|     static get styles(): CSSResult[] { |     static get styles(): CSSResult[] { | ||||||
|         return [ |         return super.styles.concat(css` | ||||||
|             PFBase, |             .icon-description { | ||||||
|             PFLogin, |                 display: flex; | ||||||
|             PFForm, |             } | ||||||
|             PFFormControl, |             .icon-description i { | ||||||
|             PFTitle, |                 font-size: 2em; | ||||||
|             PFButton, |                 padding: 0.25em; | ||||||
|             css` |                 padding-right: 0.5em; | ||||||
|                 .icon-description { |             } | ||||||
|                     display: flex; |         `); | ||||||
|                 } |  | ||||||
|                 .icon-description i { |  | ||||||
|                     font-size: 2em; |  | ||||||
|                     padding: 0.25em; |  | ||||||
|                     padding-right: 0.5em; |  | ||||||
|                 } |  | ||||||
|             `, |  | ||||||
|         ]; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     render(): TemplateResult { |     render(): TemplateResult { | ||||||
| @ -62,92 +37,62 @@ export class AuthenticatorValidateStageWebCode extends BaseStage< | |||||||
|             </ak-empty-state>`; |             </ak-empty-state>`; | ||||||
|         } |         } | ||||||
|         return html`<div class="pf-c-login__main-body"> |         return html`<div class="pf-c-login__main-body"> | ||||||
|                 <form |             <form | ||||||
|                     class="pf-c-form" |                 class="pf-c-form" | ||||||
|                     @submit=${(e: Event) => { |                 @submit=${(e: Event) => { | ||||||
|                         this.submitForm(e); |                     this.submitForm(e); | ||||||
|                     }} |                 }} | ||||||
|  |             > | ||||||
|  |                 ${this.renderUserInfo()} | ||||||
|  |                 <div class="icon-description"> | ||||||
|  |                     <i | ||||||
|  |                         class="fa ${this.deviceChallenge?.deviceClass == DeviceClassesEnum.Sms | ||||||
|  |                             ? "fa-key" | ||||||
|  |                             : "fa-mobile-alt"}" | ||||||
|  |                         aria-hidden="true" | ||||||
|  |                     ></i> | ||||||
|  |                     ${this.deviceChallenge?.deviceClass == DeviceClassesEnum.Sms | ||||||
|  |                         ? html`<p>${msg("A code has been sent to you via SMS.")}</p>` | ||||||
|  |                         : html`<p> | ||||||
|  |                               ${msg( | ||||||
|  |                                   "Open your two-factor authenticator app to view your authentication code.", | ||||||
|  |                               )} | ||||||
|  |                           </p>`} | ||||||
|  |                 </div> | ||||||
|  |                 <ak-form-element | ||||||
|  |                     label="${this.deviceChallenge?.deviceClass === DeviceClassesEnum.Static | ||||||
|  |                         ? msg("Static token") | ||||||
|  |                         : msg("Authentication code")}" | ||||||
|  |                     ?required="${true}" | ||||||
|  |                     class="pf-c-form__group" | ||||||
|  |                     .errors=${(this.challenge?.responseErrors || {})["code"]} | ||||||
|                 > |                 > | ||||||
|                     <ak-form-static |                     <!-- @ts-ignore --> | ||||||
|                         class="pf-c-form__group" |                     <input | ||||||
|                         userAvatar="${this.challenge.pendingUserAvatar}" |                         type="text" | ||||||
|                         user=${this.challenge.pendingUser} |                         name="code" | ||||||
|                     > |                         inputmode="${this.deviceChallenge?.deviceClass === DeviceClassesEnum.Static | ||||||
|                         <div slot="link"> |                             ? "text" | ||||||
|                             <a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}" |                             : "numeric"}" | ||||||
|                                 >${msg("Not you?")}</a |                         pattern="${this.deviceChallenge?.deviceClass === DeviceClassesEnum.Static | ||||||
|                             > |                             ? "[0-9a-zA-Z]*" | ||||||
|                         </div> |                             : "[0-9]*"}" | ||||||
|                     </ak-form-static> |                         placeholder="${msg("Please enter your code")}" | ||||||
|                     <div class="icon-description"> |                         autofocus="" | ||||||
|                         <i |                         autocomplete="one-time-code" | ||||||
|                             class="fa ${this.deviceChallenge?.deviceClass == DeviceClassesEnum.Sms |                         class="pf-c-form-control" | ||||||
|                                 ? "fa-key" |                         value="${PasswordManagerPrefill.totp || ""}" | ||||||
|                                 : "fa-mobile-alt"}" |                         required | ||||||
|                             aria-hidden="true" |                     /> | ||||||
|                         ></i> |                 </ak-form-element> | ||||||
|                         ${this.deviceChallenge?.deviceClass == DeviceClassesEnum.Sms |  | ||||||
|                             ? html`<p>${msg("A code has been sent to you via SMS.")}</p>` |  | ||||||
|                             : html`<p> |  | ||||||
|                                   ${msg( |  | ||||||
|                                       "Open your two-factor authenticator app to view your authentication code.", |  | ||||||
|                                   )} |  | ||||||
|                               </p>`} |  | ||||||
|                     </div> |  | ||||||
|                     <ak-form-element |  | ||||||
|                         label="${this.deviceChallenge?.deviceClass === DeviceClassesEnum.Static |  | ||||||
|                             ? msg("Static token") |  | ||||||
|                             : msg("Authentication code")}" |  | ||||||
|                         ?required="${true}" |  | ||||||
|                         class="pf-c-form__group" |  | ||||||
|                         .errors=${(this.challenge?.responseErrors || {})["code"]} |  | ||||||
|                     > |  | ||||||
|                         <!-- @ts-ignore --> |  | ||||||
|                         <input |  | ||||||
|                             type="text" |  | ||||||
|                             name="code" |  | ||||||
|                             inputmode="${this.deviceChallenge?.deviceClass === |  | ||||||
|                             DeviceClassesEnum.Static |  | ||||||
|                                 ? "text" |  | ||||||
|                                 : "numeric"}" |  | ||||||
|                             pattern="${this.deviceChallenge?.deviceClass === |  | ||||||
|                             DeviceClassesEnum.Static |  | ||||||
|                                 ? "[0-9a-zA-Z]*" |  | ||||||
|                                 : "[0-9]*"}" |  | ||||||
|                             placeholder="${msg("Please enter your code")}" |  | ||||||
|                             autofocus="" |  | ||||||
|                             autocomplete="one-time-code" |  | ||||||
|                             class="pf-c-form-control" |  | ||||||
|                             value="${PasswordManagerPrefill.totp || ""}" |  | ||||||
|                             required |  | ||||||
|                         /> |  | ||||||
|                     </ak-form-element> |  | ||||||
|  |  | ||||||
|                     <div class="pf-c-form__group pf-m-action"> |                 <div class="pf-c-form__group pf-m-action"> | ||||||
|                         <button type="submit" class="pf-c-button pf-m-primary pf-m-block"> |                     <button type="submit" class="pf-c-button pf-m-primary pf-m-block"> | ||||||
|                             ${msg("Continue")} |                         ${msg("Continue")} | ||||||
|                         </button> |                     </button> | ||||||
|                     </div> |                     ${this.renderReturnToDevicePicker()} | ||||||
|                 </form> |                 </div> | ||||||
|             </div> |             </form> | ||||||
|             <footer class="pf-c-login__main-footer"> |         </div>`; | ||||||
|                 <ul class="pf-c-login__main-footer-links"> |  | ||||||
|                     ${this.showBackButton |  | ||||||
|                         ? html`<li class="pf-c-login__main-footer-links-item"> |  | ||||||
|                               <button |  | ||||||
|                                   class="pf-c-button pf-m-secondary pf-m-block" |  | ||||||
|                                   @click=${() => { |  | ||||||
|                                       if (!this.host) return; |  | ||||||
|                                       ( |  | ||||||
|                                           this.host as AuthenticatorValidateStage |  | ||||||
|                                       ).selectedDeviceChallenge = undefined; |  | ||||||
|                                   }} |  | ||||||
|                               > |  | ||||||
|                                   ${msg("Return to device picker")} |  | ||||||
|                               </button> |  | ||||||
|                           </li>` |  | ||||||
|                         : html``} |  | ||||||
|                 </ul> |  | ||||||
|             </footer>`; |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,20 +1,10 @@ | |||||||
|  | import { BaseDeviceStage } from "@goauthentik/app/flow/stages/authenticator_validate/base"; | ||||||
| import "@goauthentik/elements/EmptyState"; | import "@goauthentik/elements/EmptyState"; | ||||||
| import "@goauthentik/elements/forms/FormElement"; | import "@goauthentik/elements/forms/FormElement"; | ||||||
| import "@goauthentik/flow/FormStatic"; |  | ||||||
| import { AuthenticatorValidateStage } from "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStage"; |  | ||||||
| import { BaseStage } from "@goauthentik/flow/stages/base"; |  | ||||||
|  |  | ||||||
| import { msg } from "@lit/localize"; | import { msg } from "@lit/localize"; | ||||||
| import { CSSResult, TemplateResult, html } from "lit"; | import { TemplateResult, html } from "lit"; | ||||||
| import { customElement, property } from "lit/decorators.js"; | import { customElement, property, state } from "lit/decorators.js"; | ||||||
| import { ifDefined } from "lit/directives/if-defined.js"; |  | ||||||
|  |  | ||||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; |  | ||||||
| import PFForm from "@patternfly/patternfly/components/Form/form.css"; |  | ||||||
| import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css"; |  | ||||||
| import PFLogin from "@patternfly/patternfly/components/Login/login.css"; |  | ||||||
| import PFTitle from "@patternfly/patternfly/components/Title/title.css"; |  | ||||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; |  | ||||||
|  |  | ||||||
| import { | import { | ||||||
|     AuthenticatorValidationChallenge, |     AuthenticatorValidationChallenge, | ||||||
| @ -23,7 +13,7 @@ import { | |||||||
| } from "@goauthentik/api"; | } from "@goauthentik/api"; | ||||||
|  |  | ||||||
| @customElement("ak-stage-authenticator-validate-duo") | @customElement("ak-stage-authenticator-validate-duo") | ||||||
| export class AuthenticatorValidateStageWebDuo extends BaseStage< | export class AuthenticatorValidateStageWebDuo extends BaseDeviceStage< | ||||||
|     AuthenticatorValidationChallenge, |     AuthenticatorValidationChallenge, | ||||||
|     AuthenticatorValidationChallengeResponseRequest |     AuthenticatorValidationChallengeResponseRequest | ||||||
| > { | > { | ||||||
| @ -33,14 +23,24 @@ export class AuthenticatorValidateStageWebDuo extends BaseStage< | |||||||
|     @property({ type: Boolean }) |     @property({ type: Boolean }) | ||||||
|     showBackButton = false; |     showBackButton = false; | ||||||
|  |  | ||||||
|     static get styles(): CSSResult[] { |     @state() | ||||||
|         return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton]; |     authenticating = false; | ||||||
|     } |  | ||||||
|  |  | ||||||
|     firstUpdated(): void { |     firstUpdated(): void { | ||||||
|         this.host?.submit({ |         this.authenticating = true; | ||||||
|             duo: this.deviceChallenge?.deviceUid, |         this.host | ||||||
|         }); |             ?.submit( | ||||||
|  |                 { | ||||||
|  |                     duo: this.deviceChallenge?.deviceUid, | ||||||
|  |                 }, | ||||||
|  |                 { invisible: true }, | ||||||
|  |             ) | ||||||
|  |             .then(() => { | ||||||
|  |                 this.authenticating = false; | ||||||
|  |             }) | ||||||
|  |             .catch(() => { | ||||||
|  |                 this.authenticating = false; | ||||||
|  |             }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     render(): TemplateResult { |     render(): TemplateResult { | ||||||
| @ -49,56 +49,25 @@ export class AuthenticatorValidateStageWebDuo extends BaseStage< | |||||||
|             </ak-empty-state>`; |             </ak-empty-state>`; | ||||||
|         } |         } | ||||||
|         const errors = this.challenge.responseErrors?.duo || []; |         const errors = this.challenge.responseErrors?.duo || []; | ||||||
|  |         const errorMessage = errors.map((err) => err.string); | ||||||
|         return html`<div class="pf-c-login__main-body"> |         return html`<div class="pf-c-login__main-body"> | ||||||
|                 <form |             <form | ||||||
|                     class="pf-c-form" |                 class="pf-c-form" | ||||||
|                     @submit=${(e: Event) => { |                 @submit=${(e: Event) => { | ||||||
|                         this.submitForm(e); |                     this.submitForm(e); | ||||||
|                     }} |                 }} | ||||||
|  |             > | ||||||
|  |                 ${this.renderUserInfo()} | ||||||
|  |                 <ak-empty-state | ||||||
|  |                     ?loading="${this.authenticating}" | ||||||
|  |                     header=${this.authenticating | ||||||
|  |                         ? msg("Sending Duo push notification...") | ||||||
|  |                         : errorMessage.join(", ") || msg("Failed to authenticate")} | ||||||
|  |                     icon="fas fa-times" | ||||||
|                 > |                 > | ||||||
|                     <ak-form-static |                 </ak-empty-state> | ||||||
|                         class="pf-c-form__group" |                 <div class="pf-c-form__group pf-m-action">${this.renderReturnToDevicePicker()}</div> | ||||||
|                         userAvatar="${this.challenge.pendingUserAvatar}" |             </form> | ||||||
|                         user=${this.challenge.pendingUser} |         </div>`; | ||||||
|                     > |  | ||||||
|                         <div slot="link"> |  | ||||||
|                             <a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}" |  | ||||||
|                                 >${msg("Not you?")}</a |  | ||||||
|                             > |  | ||||||
|                         </div> |  | ||||||
|                     </ak-form-static> |  | ||||||
|  |  | ||||||
|                     ${errors.length > 0 |  | ||||||
|                         ? errors.map((err) => { |  | ||||||
|                               if (err.code === "denied") { |  | ||||||
|                                   return html` <ak-stage-access-denied-icon |  | ||||||
|                                       errorMessage=${err.string} |  | ||||||
|                                   > |  | ||||||
|                                   </ak-stage-access-denied-icon>`; |  | ||||||
|                               } |  | ||||||
|                               return html`<p>${err.string}</p>`; |  | ||||||
|                           }) |  | ||||||
|                         : html`${msg("Sending Duo push notification")}`} |  | ||||||
|                 </form> |  | ||||||
|             </div> |  | ||||||
|             <footer class="pf-c-login__main-footer"> |  | ||||||
|                 <ul class="pf-c-login__main-footer-links"> |  | ||||||
|                     ${this.showBackButton |  | ||||||
|                         ? html`<li class="pf-c-login__main-footer-links-item"> |  | ||||||
|                               <button |  | ||||||
|                                   class="pf-c-button pf-m-secondary pf-m-block" |  | ||||||
|                                   @click=${() => { |  | ||||||
|                                       if (!this.host) return; |  | ||||||
|                                       ( |  | ||||||
|                                           this.host as AuthenticatorValidateStage |  | ||||||
|                                       ).selectedDeviceChallenge = undefined; |  | ||||||
|                                   }} |  | ||||||
|                               > |  | ||||||
|                                   ${msg("Return to device picker")} |  | ||||||
|                               </button> |  | ||||||
|                           </li>` |  | ||||||
|                         : html``} |  | ||||||
|                 </ul> |  | ||||||
|             </footer>`; |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,23 +1,14 @@ | |||||||
|  | import { BaseDeviceStage } from "@goauthentik/app/flow/stages/authenticator_validate/base"; | ||||||
| import { | import { | ||||||
|     checkWebAuthnSupport, |     checkWebAuthnSupport, | ||||||
|     transformAssertionForServer, |     transformAssertionForServer, | ||||||
|     transformCredentialRequestOptions, |     transformCredentialRequestOptions, | ||||||
| } from "@goauthentik/common/helpers/webauthn"; | } from "@goauthentik/common/helpers/webauthn"; | ||||||
| import { AuthenticatorValidateStage } from "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStage"; | import "@goauthentik/elements/EmptyState"; | ||||||
| import { BaseStage } from "@goauthentik/flow/stages/base"; |  | ||||||
|  |  | ||||||
| import { msg, str } from "@lit/localize"; | import { msg } from "@lit/localize"; | ||||||
| import { CSSResult, TemplateResult, html } from "lit"; | import { TemplateResult, html, nothing } from "lit"; | ||||||
| import { customElement, property } from "lit/decorators.js"; | import { customElement, property, state } from "lit/decorators.js"; | ||||||
|  |  | ||||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; |  | ||||||
| import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css"; |  | ||||||
| import PFForm from "@patternfly/patternfly/components/Form/form.css"; |  | ||||||
| import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css"; |  | ||||||
| import PFLogin from "@patternfly/patternfly/components/Login/login.css"; |  | ||||||
| import PFTitle from "@patternfly/patternfly/components/Title/title.css"; |  | ||||||
| import PFBullseye from "@patternfly/patternfly/layouts/Bullseye/bullseye.css"; |  | ||||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; |  | ||||||
|  |  | ||||||
| import { | import { | ||||||
|     AuthenticatorValidationChallenge, |     AuthenticatorValidationChallenge, | ||||||
| @ -26,7 +17,7 @@ import { | |||||||
| } from "@goauthentik/api"; | } from "@goauthentik/api"; | ||||||
|  |  | ||||||
| @customElement("ak-stage-authenticator-validate-webauthn") | @customElement("ak-stage-authenticator-validate-webauthn") | ||||||
| export class AuthenticatorValidateStageWebAuthn extends BaseStage< | export class AuthenticatorValidateStageWebAuthn extends BaseDeviceStage< | ||||||
|     AuthenticatorValidationChallenge, |     AuthenticatorValidationChallenge, | ||||||
|     AuthenticatorValidationChallengeResponseRequest |     AuthenticatorValidationChallengeResponseRequest | ||||||
| > { | > { | ||||||
| @ -34,25 +25,15 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage< | |||||||
|     deviceChallenge?: DeviceChallenge; |     deviceChallenge?: DeviceChallenge; | ||||||
|  |  | ||||||
|     @property() |     @property() | ||||||
|     authenticateMessage?: string; |     errorMessage?: string; | ||||||
|  |  | ||||||
|     @property({ type: Boolean }) |     @property({ type: Boolean }) | ||||||
|     showBackButton = false; |     showBackButton = false; | ||||||
|  |  | ||||||
|     transformedCredentialRequestOptions?: PublicKeyCredentialRequestOptions; |     @state() | ||||||
|  |     authenticating = false; | ||||||
|  |  | ||||||
|     static get styles(): CSSResult[] { |     transformedCredentialRequestOptions?: PublicKeyCredentialRequestOptions; | ||||||
|         return [ |  | ||||||
|             PFBase, |  | ||||||
|             PFLogin, |  | ||||||
|             PFEmptyState, |  | ||||||
|             PFBullseye, |  | ||||||
|             PFForm, |  | ||||||
|             PFFormControl, |  | ||||||
|             PFTitle, |  | ||||||
|             PFButton, |  | ||||||
|         ]; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async authenticate(): Promise<void> { |     async authenticate(): Promise<void> { | ||||||
|         // request the authenticator to create an assertion signature using the |         // request the authenticator to create an assertion signature using the | ||||||
| @ -64,10 +45,10 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage< | |||||||
|                 publicKey: this.transformedCredentialRequestOptions, |                 publicKey: this.transformedCredentialRequestOptions, | ||||||
|             }); |             }); | ||||||
|             if (!assertion) { |             if (!assertion) { | ||||||
|                 throw new Error(msg("Assertions is empty")); |                 throw new Error("Assertions is empty"); | ||||||
|             } |             } | ||||||
|         } catch (err) { |         } catch (err) { | ||||||
|             throw new Error(msg(str`Error when creating credential: ${err}`)); |             throw new Error(`Error when creating credential: ${err}`); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // we now have an authentication assertion! encode the byte arrays contained |         // we now have an authentication assertion! encode the byte arrays contained | ||||||
| @ -78,11 +59,16 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage< | |||||||
|  |  | ||||||
|         // post the assertion to the server for verification. |         // post the assertion to the server for verification. | ||||||
|         try { |         try { | ||||||
|             await this.host?.submit({ |             await this.host?.submit( | ||||||
|                 webauthn: transformedAssertionForServer, |                 { | ||||||
|             }); |                     webauthn: transformedAssertionForServer, | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     invisible: true, | ||||||
|  |                 }, | ||||||
|  |             ); | ||||||
|         } catch (err) { |         } catch (err) { | ||||||
|             throw new Error(msg(str`Error when validating assertion on server: ${err}`)); |             throw new Error(`Error when validating assertion on server: ${err}`); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @ -97,58 +83,46 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage< | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     async authenticateWrapper(): Promise<void> { |     async authenticateWrapper(): Promise<void> { | ||||||
|         if (this.host.loading) { |         if (this.authenticating) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|         this.host.loading = true; |         this.authenticating = true; | ||||||
|         this.authenticate() |         this.authenticate() | ||||||
|             .catch((e) => { |             .catch((e: Error) => { | ||||||
|                 console.error(e); |                 console.warn(`authentik/flows/authenticator_validate/webauthn: ${e.toString()}`); | ||||||
|                 this.authenticateMessage = e.toString(); |                 this.errorMessage = msg("Authentication failed."); | ||||||
|             }) |             }) | ||||||
|             .finally(() => { |             .finally(() => { | ||||||
|                 this.host.loading = false; |                 this.authenticating = false; | ||||||
|             }); |             }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     render(): TemplateResult { |     render(): TemplateResult { | ||||||
|         return html`<div class="pf-c-login__main-body"> |         return html`<div class="pf-c-login__main-body"> | ||||||
|                 ${this.authenticateMessage |             <form class="pf-c-form"> | ||||||
|                     ? html`<div class="pf-c-form__group pf-m-action"> |                 ${this.renderUserInfo()} | ||||||
|                           <p class="pf-m-block">${this.authenticateMessage}</p> |                 <ak-empty-state | ||||||
|                           <button |                     ?loading="${this.authenticating}" | ||||||
|  |                     header=${this.authenticating | ||||||
|  |                         ? msg("Authenticating...") | ||||||
|  |                         : this.errorMessage || msg("Failed to authenticate")} | ||||||
|  |                     icon="fa-times" | ||||||
|  |                 > | ||||||
|  |                 </ak-empty-state> | ||||||
|  |                 <div class="pf-c-form__group pf-m-action"> | ||||||
|  |                     ${this.errorMessage | ||||||
|  |                         ? html` <button | ||||||
|                               class="pf-c-button pf-m-primary pf-m-block" |                               class="pf-c-button pf-m-primary pf-m-block" | ||||||
|                               @click=${() => { |                               @click=${() => { | ||||||
|                                   this.authenticateWrapper(); |                                   this.authenticateWrapper(); | ||||||
|                               }} |                               }} | ||||||
|                           > |                           > | ||||||
|                               ${msg("Retry authentication")} |                               ${msg("Retry authentication")} | ||||||
|                           </button> |                           </button>` | ||||||
|                       </div>` |                         : nothing} | ||||||
|                     : html`<div class="pf-c-form__group pf-m-action"> |                     ${this.renderReturnToDevicePicker()} | ||||||
|                           <p class="pf-m-block"> </p> |                 </div> | ||||||
|                           <p class="pf-m-block"> </p> |             </form> | ||||||
|                           <p class="pf-m-block"> </p> |         </div>`; | ||||||
|                       </div> `} |  | ||||||
|             </div> |  | ||||||
|             <footer class="pf-c-login__main-footer"> |  | ||||||
|                 <ul class="pf-c-login__main-footer-links"> |  | ||||||
|                     ${this.showBackButton |  | ||||||
|                         ? html`<li class="pf-c-login__main-footer-links-item"> |  | ||||||
|                               <button |  | ||||||
|                                   class="pf-c-button pf-m-secondary pf-m-block" |  | ||||||
|                                   @click=${() => { |  | ||||||
|                                       if (!this.host) return; |  | ||||||
|                                       ( |  | ||||||
|                                           this.host as AuthenticatorValidateStage |  | ||||||
|                                       ).selectedDeviceChallenge = undefined; |  | ||||||
|                                   }} |  | ||||||
|                               > |  | ||||||
|                                   ${msg("Return to device picker")} |  | ||||||
|                               </button> |  | ||||||
|                           </li>` |  | ||||||
|                         : html``} |  | ||||||
|                 </ul> |  | ||||||
|             </footer>`; |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										69
									
								
								web/src/flow/stages/authenticator_validate/base.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								web/src/flow/stages/authenticator_validate/base.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,69 @@ | |||||||
|  | import { | ||||||
|  |     BaseStage, | ||||||
|  |     FlowInfoChallenge, | ||||||
|  |     PendingUserChallenge, | ||||||
|  | } from "@goauthentik/app/flow/stages/base"; | ||||||
|  | import { AuthenticatorValidateStage } from "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStage"; | ||||||
|  |  | ||||||
|  | import { msg } from "@lit/localize"; | ||||||
|  | import { CSSResult, TemplateResult, css, html } from "lit"; | ||||||
|  | import { property } from "lit/decorators.js"; | ||||||
|  |  | ||||||
|  | import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||||
|  | import PFForm from "@patternfly/patternfly/components/Form/form.css"; | ||||||
|  | import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css"; | ||||||
|  | import PFLogin from "@patternfly/patternfly/components/Login/login.css"; | ||||||
|  | import PFTitle from "@patternfly/patternfly/components/Title/title.css"; | ||||||
|  | import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||||
|  |  | ||||||
|  | import { DeviceChallenge } from "@goauthentik/api"; | ||||||
|  |  | ||||||
|  | export class BaseDeviceStage< | ||||||
|  |     Tin extends FlowInfoChallenge & PendingUserChallenge, | ||||||
|  |     Tout, | ||||||
|  | > extends BaseStage<Tin, Tout> { | ||||||
|  |     @property({ attribute: false }) | ||||||
|  |     deviceChallenge?: DeviceChallenge; | ||||||
|  |  | ||||||
|  |     @property({ type: Boolean }) | ||||||
|  |     showBackButton = false; | ||||||
|  |  | ||||||
|  |     static get styles(): CSSResult[] { | ||||||
|  |         return [ | ||||||
|  |             PFBase, | ||||||
|  |             PFLogin, | ||||||
|  |             PFForm, | ||||||
|  |             PFFormControl, | ||||||
|  |             PFTitle, | ||||||
|  |             PFButton, | ||||||
|  |             css` | ||||||
|  |                 .pf-c-form__group.pf-m-action { | ||||||
|  |                     display: flex; | ||||||
|  |                     gap: 16px; | ||||||
|  |                     margin-top: 0; | ||||||
|  |                     margin-bottom: calc(var(--pf-c-form__group--m-action--MarginTop) / 2); | ||||||
|  |                     flex-direction: column; | ||||||
|  |                 } | ||||||
|  |             `, | ||||||
|  |         ]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     submit(payload: Tin): Promise<boolean> { | ||||||
|  |         return this.host?.submit(payload) || Promise.resolve(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     renderReturnToDevicePicker(): TemplateResult { | ||||||
|  |         if (!this.showBackButton) { | ||||||
|  |             return html``; | ||||||
|  |         } | ||||||
|  |         return html`<button | ||||||
|  |             class="pf-c-button pf-m-secondary pf-m-block" | ||||||
|  |             @click=${() => { | ||||||
|  |                 if (!this.host) return; | ||||||
|  |                 (this.host as AuthenticatorValidateStage).selectedDeviceChallenge = undefined; | ||||||
|  |             }} | ||||||
|  |         > | ||||||
|  |             ${msg("Return to device picker")} | ||||||
|  |         </button>`; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -1,16 +1,22 @@ | |||||||
| import { AKElement } from "@goauthentik/elements/Base"; | import { AKElement } from "@goauthentik/elements/Base"; | ||||||
| import { KeyUnknown } from "@goauthentik/elements/forms/Form"; | import { KeyUnknown } from "@goauthentik/elements/forms/Form"; | ||||||
|  |  | ||||||
|  | import { msg } from "@lit/localize"; | ||||||
| import { TemplateResult, html } from "lit"; | import { TemplateResult, html } from "lit"; | ||||||
| import { property } from "lit/decorators.js"; | import { property } from "lit/decorators.js"; | ||||||
|  | import { ifDefined } from "lit/directives/if-defined.js"; | ||||||
|  |  | ||||||
| import { CurrentBrand, ErrorDetail } from "@goauthentik/api"; | import { ContextualFlowInfo, CurrentBrand, ErrorDetail } from "@goauthentik/api"; | ||||||
|  |  | ||||||
|  | export interface SubmitOptions { | ||||||
|  |     invisible: boolean; | ||||||
|  | } | ||||||
|  |  | ||||||
| export interface StageHost { | export interface StageHost { | ||||||
|     challenge?: unknown; |     challenge?: unknown; | ||||||
|     flowSlug?: string; |     flowSlug?: string; | ||||||
|     loading: boolean; |     loading: boolean; | ||||||
|     submit(payload: unknown): Promise<boolean>; |     submit(payload: unknown, options?: SubmitOptions): Promise<boolean>; | ||||||
|  |  | ||||||
|     readonly brand?: CurrentBrand; |     readonly brand?: CurrentBrand; | ||||||
| } | } | ||||||
| @ -26,7 +32,21 @@ export function readFileAsync(file: Blob) { | |||||||
|     }); |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
| export class BaseStage<Tin, Tout> extends AKElement { | // Challenge which contains flow info | ||||||
|  | export interface FlowInfoChallenge { | ||||||
|  |     flowInfo?: ContextualFlowInfo; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Challenge which has a pending user | ||||||
|  | export interface PendingUserChallenge { | ||||||
|  |     pendingUser?: string; | ||||||
|  |     pendingUserAvatar?: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export class BaseStage< | ||||||
|  |     Tin extends FlowInfoChallenge & PendingUserChallenge, | ||||||
|  |     Tout, | ||||||
|  | > extends AKElement { | ||||||
|     host!: StageHost; |     host!: StageHost; | ||||||
|  |  | ||||||
|     @property({ attribute: false }) |     @property({ attribute: false }) | ||||||
| @ -68,6 +88,31 @@ export class BaseStage<Tin, Tout> extends AKElement { | |||||||
|         </div>`; |         </div>`; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     renderUserInfo(): TemplateResult { | ||||||
|  |         if (!this.challenge.pendingUser || !this.challenge.pendingUserAvatar) { | ||||||
|  |             return html``; | ||||||
|  |         } | ||||||
|  |         return html` | ||||||
|  |             <ak-form-static | ||||||
|  |                 class="pf-c-form__group" | ||||||
|  |                 userAvatar="${this.challenge.pendingUserAvatar}" | ||||||
|  |                 user=${this.challenge.pendingUser} | ||||||
|  |             > | ||||||
|  |                 <div slot="link"> | ||||||
|  |                     <a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}" | ||||||
|  |                         >${msg("Not you?")}</a | ||||||
|  |                     > | ||||||
|  |                 </div> | ||||||
|  |             </ak-form-static> | ||||||
|  |             <input | ||||||
|  |                 name="username" | ||||||
|  |                 autocomplete="username" | ||||||
|  |                 type="hidden" | ||||||
|  |                 value="${this.challenge.pendingUser}" | ||||||
|  |             /> | ||||||
|  |         `; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     cleanup(): void { |     cleanup(): void { | ||||||
|         // Method that can be overridden by stages |         // Method that can be overridden by stages | ||||||
|         return; |         return; | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	