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:
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
80
web/src/flow/providers/oauth2/DeviceCode.ts
Normal file
80
web/src/flow/providers/oauth2/DeviceCode.ts
Normal 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>`;
|
||||
}
|
||||
}
|
55
web/src/flow/providers/oauth2/DeviceCodeFinish.ts
Normal file
55
web/src/flow/providers/oauth2/DeviceCodeFinish.ts
Normal 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>`;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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()}`,
|
||||
)}
|
||||
|
Reference in New Issue
Block a user