providers/oauth2: add device flow (#3334)

* start device flow

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* web: fix inconsistent app filtering

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* add tenant device code flow

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* add throttling to device code view

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* somewhat unrelated changes

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* add initial device code entry flow

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* add finish stage

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* it works

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* add support for verification_uri_complete

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* add some tests

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* add more tests

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* add docs

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens L
2022-10-11 13:42:10 +03:00
committed by GitHub
parent 64a7e35950
commit 8ed2f7fe9e
36 changed files with 1084 additions and 46 deletions

View File

@ -314,6 +314,40 @@ export class TenantForm extends ModelForm<Tenant, string> {
${t`If set, users are able to configure details of their profile.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Device code flow`} name="flowDeviceCode">
<select class="pf-c-form-control">
<option
value=""
?selected=${this.instance?.flowDeviceCode === undefined}
>
---------
</option>
${until(
new FlowsApi(DEFAULT_CONFIG)
.flowsInstancesList({
ordering: "slug",
designation:
FlowsInstancesListDesignationEnum.StageConfiguration,
})
.then((flows) => {
return flows.results.map((flow) => {
const selected =
this.instance?.flowDeviceCode === flow.pk;
return html`<option
value=${flow.pk}
?selected=${selected}
>
${flow.name} (${flow.slug})
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select>
<p class="pf-c-form__helper-text">
${t`If set, the OAuth Device Code profile can be used, and the selected flow will be used to enter the code.`}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>

View File

@ -357,18 +357,32 @@ export class FlowExecutor extends AKElement implements StageHost {
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-authenticator-validate>`;
case "ak-flow-sources-plex":
// Sources
case "ak-source-plex":
await import("@goauthentik/flow/sources/plex/PlexLoginInit");
return html`<ak-flow-sources-plex
return html`<ak-flow-source-plex
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-flow-sources-plex>`;
case "ak-flow-sources-oauth-apple":
></ak-flow-source-plex>`;
case "ak-source-oauth-apple":
await import("@goauthentik/flow/sources/apple/AppleLoginInit");
return html`<ak-flow-sources-oauth-apple
return html`<ak-flow-source-oauth-apple
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-flow-sources-oauth-apple>`;
></ak-flow-source-oauth-apple>`;
// Providers
case "ak-provider-oauth2-device-code":
await import("@goauthentik/flow/providers/oauth2/DeviceCode");
return html`<ak-flow-provider-oauth2-code
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-flow-provider-oauth2-code>`;
case "ak-provider-oauth2-device-code-finish":
await import("@goauthentik/flow/providers/oauth2/DeviceCodeFinish");
return html`<ak-flow-provider-oauth2-code-finish
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-flow-provider-oauth2-code-finish>`;
default:
break;
}

View File

@ -0,0 +1,80 @@
import "@goauthentik/elements/EmptyState";
import "@goauthentik/elements/forms/FormElement";
import "@goauthentik/flow/FormStatic";
import { BaseStage } from "@goauthentik/flow/stages/base";
import { t } from "@lingui/macro";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import AKGlobal from "@goauthentik/common/styles/authentik.css";
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 {
OAuthDeviceCodeChallenge,
OAuthDeviceCodeChallengeResponseRequest,
} from "@goauthentik/api";
@customElement("ak-flow-provider-oauth2-code")
export class OAuth2DeviceCode extends BaseStage<
OAuthDeviceCodeChallenge,
OAuthDeviceCodeChallengeResponseRequest
> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
}
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
}
return html`<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
</header>
<div class="pf-c-login__main-body">
<form
class="pf-c-form"
@submit=${(e: Event) => {
this.submitForm(e);
}}
>
<p>${t`Enter the code shown on your device.`}</p>
<ak-form-element
label="${t`Code`}"
?required="${true}"
class="pf-c-form__group"
.errors=${(this.challenge?.responseErrors || {})["code"]}
>
<!-- @ts-ignore -->
<input
type="text"
name="code"
inputmode="numeric"
pattern="[0-9]*"
placeholder="${t`Please enter your Code`}"
autofocus=""
autocomplete="off"
class="pf-c-form-control"
value=""
required
/>
</ak-form-element>
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
${t`Continue`}
</button>
</div>
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`;
}
}

View File

@ -0,0 +1,55 @@
import "@goauthentik/elements/EmptyState";
import "@goauthentik/flow/FormStatic";
import { BaseStage } from "@goauthentik/flow/stages/base";
import { t } from "@lingui/macro";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import AKGlobal from "@goauthentik/common/styles/authentik.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFList from "@patternfly/patternfly/components/List/list.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 {
OAuthDeviceCodeFinishChallenge,
OAuthDeviceCodeFinishChallengeResponseRequest,
} from "@goauthentik/api";
@customElement("ak-flow-provider-oauth2-code-finish")
export class DeviceCodeFinish extends BaseStage<
OAuthDeviceCodeFinishChallenge,
OAuthDeviceCodeFinishChallengeResponseRequest
> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFList, PFFormControl, PFTitle, AKGlobal];
}
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
}
return html`<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form">
<div class="pf-c-form__group">
<p>
<i class="pf-icon pf-icon-ok"></i>
${t`You've successfully authenticated your device.`}
</p>
<hr />
<p>${t`You can close this tab now.`}</p>
</div>
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`;
}
}

View File

@ -16,7 +16,7 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { AppleChallengeResponseRequest, AppleLoginChallenge } from "@goauthentik/api";
@customElement("ak-flow-sources-oauth-apple")
@customElement("ak-flow-source-oauth-apple")
export class AppleLoginInit extends BaseStage<AppleLoginChallenge, AppleChallengeResponseRequest> {
@property({ type: Boolean })
isModalShown = false;

View File

@ -25,7 +25,7 @@ import {
} from "@goauthentik/api";
import { SourcesApi } from "@goauthentik/api";
@customElement("ak-flow-sources-plex")
@customElement("ak-flow-source-plex")
export class PlexLoginInit extends BaseStage<
PlexAuthenticationChallenge,
PlexAuthenticationChallengeResponseRequest

View File

@ -112,10 +112,10 @@ export class LibraryPage extends AKElement {
</div>`;
}
getApps(): [string, Application[]][] {
return groupBy(
filterApps(): Application[] {
return (
this.apps?.results.filter((app) => {
if (app.launchUrl) {
if (app.launchUrl && app.launchUrl !== "") {
// If the launch URL is a full URL, only show with http or https
if (app.launchUrl.indexOf("://") !== -1) {
return app.launchUrl.startsWith("http");
@ -124,11 +124,14 @@ export class LibraryPage extends AKElement {
return true;
}
return false;
}) || [],
(app) => app.group || "",
}) || []
);
}
getApps(): [string, Application[]][] {
return groupBy(this.filterApps(), (app) => app.group || "");
}
renderApps(config: UIConfig): TemplateResult {
let groupClass = "";
let groupGrid = "";
@ -215,9 +218,7 @@ export class LibraryPage extends AKElement {
<section class="pf-c-page__main-section">
${loading(
this.apps,
html`${(this.apps?.results || []).filter((app) => {
return app.launchUrl !== null;
}).length > 0
html`${this.filterApps().length > 0
? this.renderApps(config)
: this.renderEmptyState()}`,
)}