Merge branch 'master' into version-2021.3

This commit is contained in:
Jens Langhammer 2021-03-03 20:48:02 +01:00
commit dd31191845
65 changed files with 2287 additions and 1200 deletions

24
Pipfile.lock generated
View File

@ -95,10 +95,10 @@
},
"autobahn": {
"hashes": [
"sha256:884f79c50fdc55ade2c315946a9caa145e8b10075eee9d2c2594ea5e8f5226aa",
"sha256:bf7a9d302a34d0f719d43c57f65ca1f2f5c982dd6ea0c11e1e190ef6f43710fe"
"sha256:9195df8af03b0ff29ccd4b7f5abbde957ee90273465942205f9a1bad6c3f07ac",
"sha256:e126c1f583e872fb59e79d36977cfa1f2d0a8a79f90ae31f406faae7664b8e03"
],
"version": "==21.2.2"
"version": "==21.3.1"
},
"automat": {
"hashes": [
@ -116,18 +116,18 @@
},
"boto3": {
"hashes": [
"sha256:3570a3c0fbd80bcb30449f87cf9d2f7abb67fac2a5e317d002f9921c59be9b17",
"sha256:ceff2f32ba05acc9ee35a6dd82e29ea285d63e889bed39a6ba7a700146f43749"
"sha256:c9513a9ea00f8d17ecdc02c391ae956bf0f990aa07deec11c421607c09b294e1",
"sha256:f84ca60e9605af69022f039c035b33d519531eeaac52724b9223a5465f4a8b6b"
],
"index": "pypi",
"version": "==1.17.18"
"version": "==1.17.19"
},
"botocore": {
"hashes": [
"sha256:51900b10da4ae45be4b16045e5b2ff7d1158a7955d9d7cc5e5a9ba3170f10586",
"sha256:b181f32d9075e5419a89fa9636ce95946c15459c9bfadfabb53ca902fc8072b8"
"sha256:135b5f30e6662b46d804f993bf31d9c7769c6c0848321ed0aa0393f5b9c19a94",
"sha256:8e42c78d2eb888551635309158c04ef2648a96d8c2c70dbce7712c6ce8629759"
],
"version": "==1.20.18"
"version": "==1.20.19"
},
"cachetools": {
"hashes": [
@ -1249,10 +1249,10 @@
},
"websocket-client": {
"hashes": [
"sha256:0fc45c961324d79c781bab301359d5a1b00b13ad1b10415a4780229ef71a5549",
"sha256:d735b91d6d1692a6a181f2a8c9e0238e5f6373356f561bb9dc4c7af36f452010"
"sha256:44b5df8f08c74c3d82d28100fdc81f4536809ce98a17f0757557813275fbb663",
"sha256:63509b41d158ae5b7f67eb4ad20fecbb4eee99434e73e140354dc3ff8e09716f"
],
"version": "==0.57.0"
"version": "==0.58.0"
},
"websockets": {
"hashes": [

View File

@ -7,14 +7,14 @@ from django.http.response import Http404
from django.utils.translation import gettext_lazy as _
from drf_yasg2.utils import swagger_auto_schema
from rest_framework.decorators import action
from rest_framework.fields import CharField, DateTimeField, IntegerField, ListField
from rest_framework.fields import CharField, ChoiceField, DateTimeField, ListField
from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import Serializer
from rest_framework.viewsets import ViewSet
from authentik.events.monitored_tasks import TaskInfo
from authentik.events.monitored_tasks import TaskInfo, TaskResultStatus
class TaskSerializer(Serializer):
@ -24,7 +24,10 @@ class TaskSerializer(Serializer):
task_description = CharField()
task_finish_timestamp = DateTimeField(source="finish_timestamp")
status = IntegerField(source="result.status.value")
status = ChoiceField(
source="result.status.name",
choices=[(x.name, x.name) for x in TaskResultStatus],
)
messages = ListField(source="result.messages")
def create(self, validated_data: dict) -> Model:

View File

@ -55,7 +55,7 @@ class VersionViewSet(ListModelMixin, GenericViewSet):
def get_queryset(self): # pragma: no cover
return None
@swagger_auto_schema(responses={200: VersionSerializer(many=True)})
@swagger_auto_schema(responses={200: VersionSerializer(many=False)})
def list(self, request: Request) -> Response:
"""Get running and latest version."""
return Response(VersionSerializer(True).data)

View File

@ -0,0 +1,97 @@
"""Swagger Pagination Schema class"""
from typing import OrderedDict
from drf_yasg2 import openapi
from drf_yasg2.inspectors import PaginatorInspector
class PaginationInspector(PaginatorInspector):
"""Swagger Pagination Schema class"""
def get_paginated_response(self, paginator, response_schema):
"""
:param BasePagination paginator: the paginator
:param openapi.Schema response_schema: the response schema that must be paged.
:rtype: openapi.Schema
"""
return openapi.Schema(
type=openapi.TYPE_OBJECT,
properties=OrderedDict(
(
(
"pagination",
openapi.Schema(
type=openapi.TYPE_OBJECT,
properties=OrderedDict(
(
("next", openapi.Schema(type=openapi.TYPE_NUMBER)),
(
"previous",
openapi.Schema(type=openapi.TYPE_NUMBER),
),
("count", openapi.Schema(type=openapi.TYPE_NUMBER)),
(
"current",
openapi.Schema(type=openapi.TYPE_NUMBER),
),
(
"total_pages",
openapi.Schema(type=openapi.TYPE_NUMBER),
),
(
"start_index",
openapi.Schema(type=openapi.TYPE_NUMBER),
),
(
"end_index",
openapi.Schema(type=openapi.TYPE_NUMBER),
),
)
),
required=[
"next",
"previous",
"count",
"current",
"total_pages",
"start_index",
"end_index",
],
),
),
("results", response_schema),
)
),
required=["results", "pagination"],
)
def get_paginator_parameters(self, paginator):
"""
Get the pagination parameters for a single paginator **instance**.
Should return :data:`.NotHandled` if this inspector
does not know how to handle the given `paginator`.
:param BasePagination paginator: the paginator
:rtype: list[openapi.Parameter]
"""
return [
openapi.Parameter(
"page",
openapi.IN_QUERY,
"Page Index",
False,
None,
openapi.TYPE_INTEGER,
),
openapi.Parameter(
"page_size",
openapi.IN_QUERY,
"Page Size",
False,
None,
openapi.TYPE_INTEGER,
),
]

View File

@ -1,10 +1,11 @@
"""core Configs API"""
from django.db.models import Model
from drf_yasg2.utils import swagger_auto_schema
from rest_framework.fields import BooleanField, CharField
from rest_framework.permissions import AllowAny
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ReadOnlyField, Serializer
from rest_framework.serializers import Serializer
from rest_framework.viewsets import ViewSet
from authentik.lib.config import CONFIG
@ -13,12 +14,12 @@ from authentik.lib.config import CONFIG
class ConfigSerializer(Serializer):
"""Serialize authentik Config into DRF Object"""
branding_logo = ReadOnlyField()
branding_title = ReadOnlyField()
branding_logo = CharField(read_only=True)
branding_title = CharField(read_only=True)
error_reporting_enabled = ReadOnlyField()
error_reporting_environment = ReadOnlyField()
error_reporting_send_pii = ReadOnlyField()
error_reporting_enabled = BooleanField(read_only=True)
error_reporting_environment = CharField(read_only=True)
error_reporting_send_pii = BooleanField(read_only=True)
def create(self, validated_data: dict) -> Model:
raise NotImplementedError
@ -32,7 +33,7 @@ class ConfigsViewSet(ViewSet):
permission_classes = [AllowAny]
@swagger_auto_schema(responses={200: ConfigSerializer(many=True)})
@swagger_auto_schema(responses={200: ConfigSerializer(many=False)})
def list(self, request: Request) -> Response:
"""Retrive public configuration options"""
config = ConfigSerializer(

View File

@ -1,37 +0,0 @@
"""core messages API"""
from django.contrib.messages import get_messages
from django.db.models import Model
from drf_yasg2.utils import swagger_auto_schema
from rest_framework.permissions import AllowAny
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ReadOnlyField, Serializer
from rest_framework.viewsets import ViewSet
class MessageSerializer(Serializer):
"""Serialize Django Message into DRF Object"""
message = ReadOnlyField()
level = ReadOnlyField()
tags = ReadOnlyField()
extra_tags = ReadOnlyField()
level_tag = ReadOnlyField()
def create(self, validated_data: dict) -> Model:
raise NotImplementedError
def update(self, instance: Model, validated_data: dict) -> Model:
raise NotImplementedError
class MessagesViewSet(ViewSet):
"""Read-only view set that returns the current session's messages"""
permission_classes = [AllowAny]
@swagger_auto_schema(responses={200: MessageSerializer(many=True)})
def list(self, request: Request) -> Response:
"""List current messages and pass into Serializer"""
all_messages = list(get_messages(request))
return Response(MessageSerializer(all_messages, many=True).data)

View File

@ -10,7 +10,6 @@ from authentik.admin.api.tasks import TaskViewSet
from authentik.admin.api.version import VersionViewSet
from authentik.admin.api.workers import WorkerViewSet
from authentik.api.v2.config import ConfigsViewSet
from authentik.api.v2.messages import MessagesViewSet
from authentik.core.api.applications import ApplicationViewSet
from authentik.core.api.groups import GroupViewSet
from authentik.core.api.propertymappings import PropertyMappingViewSet
@ -77,7 +76,6 @@ from authentik.stages.user_write.api import UserWriteStageViewSet
router = routers.DefaultRouter()
router.register("root/messages", MessagesViewSet, basename="messages")
router.register("root/config", ConfigsViewSet, basename="configs")
router.register("admin/version", VersionViewSet, basename="admin_version")

View File

@ -46,8 +46,7 @@ def backup_database(self: MonitoredTask): # pragma: no cover
TaskResult(
TaskResultStatus.SUCCESSFUL,
[
f"Successfully finished database backup {naturaltime(start)}",
out.getvalue(),
f"Successfully finished database backup {naturaltime(start)} {out.getvalue()}",
],
)
)

View File

@ -13,7 +13,7 @@
<p>{% trans "Configure settings relevant to your user profile." %}</p>
</div>
</section>
<ak-tabs>
<ak-tabs vertical="true">
<section slot="page-1" data-tab-title="{% trans 'User details' %}" class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-u-display-flex pf-u-justify-content-center">
<div class="pf-u-w-75">

View File

@ -29,7 +29,7 @@ class EventSerializer(ModelSerializer):
]
class EventTopPerUserSerialier(Serializer):
class EventTopPerUserSerializer(Serializer):
"""Response object of Event's top_per_user"""
application = DictField()
@ -60,7 +60,7 @@ class EventViewSet(ReadOnlyModelViewSet):
filterset_fields = ["action"]
@swagger_auto_schema(
method="GET", responses={200: EventTopPerUserSerialier(many=True)}
method="GET", responses={200: EventTopPerUserSerializer(many=True)}
)
@action(detail=False, methods=["GET"])
def top_per_user(self, request: Request):

View File

@ -5,6 +5,7 @@ from typing import Type
from kubernetes.client import OpenApiException
from kubernetes.client.api_client import ApiClient
from structlog.testing import capture_logs
from urllib3.exceptions import HTTPError
from yaml import dump_all
from authentik.outposts.controllers.base import BaseController, ControllerException
@ -42,7 +43,7 @@ class KubernetesController(BaseController):
reconciler = self.reconcilers[reconcile_key](self)
reconciler.up()
except OpenApiException as exc:
except (OpenApiException, HTTPError) as exc:
raise ControllerException from exc
def up_with_logs(self) -> list[str]:
@ -54,7 +55,7 @@ class KubernetesController(BaseController):
reconciler.up()
all_logs += [f"{reconcile_key.title()}: {x['event']}" for x in logs]
return all_logs
except OpenApiException as exc:
except (OpenApiException, HTTPError) as exc:
raise ControllerException from exc
def down(self):

View File

@ -139,6 +139,9 @@ GUARDIAN_MONKEY_PATCH = False
SWAGGER_SETTINGS = {
"DEFAULT_INFO": "authentik.api.v2.urls.info",
"DEFAULT_PAGINATOR_INSPECTORS": [
"authentik.api.pagination_schema.PaginationInspector",
],
"SECURITY_DEFINITIONS": {
"token": {"type": "apiKey", "name": "Authorization", "in": "header"}
},
@ -147,7 +150,6 @@ SWAGGER_SETTINGS = {
REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": "authentik.api.pagination.Pagination",
"PAGE_SIZE": 100,
"DATETIME_FORMAT": "%s",
"DEFAULT_FILTER_BACKENDS": [
"rest_framework_guardian.filters.ObjectPermissionsFilter",
"django_filters.rest_framework.DjangoFilterBackend",

View File

@ -1,9 +1,6 @@
"""OTP Validate stage forms"""
from django import forms
from django.utils.translation import gettext_lazy as _
from django_otp import match_token
from authentik.core.models import User
from authentik.flows.models import NotConfiguredAction
from authentik.stages.authenticator_validate.models import (
AuthenticatorValidateStage,
@ -11,35 +8,6 @@ from authentik.stages.authenticator_validate.models import (
)
class ValidationForm(forms.Form):
"""OTP Validate stage forms"""
user: User
code = forms.CharField(
label=_("Please enter the token from your device."),
widget=forms.TextInput(
attrs={
"autocomplete": "one-time-code",
"placeholder": "123456",
"autofocus": "autofocus",
}
),
)
def __init__(self, user, *args, **kwargs):
super().__init__(*args, **kwargs)
self.user = user
def clean_code(self):
"""Validate code against all confirmed devices"""
code = self.cleaned_data.get("code")
device = match_token(self.user, code)
if not device:
raise forms.ValidationError(_("Invalid Token"))
return code
class AuthenticatorValidateStageForm(forms.ModelForm):
"""OTP Validate stage forms"""

View File

@ -378,8 +378,8 @@ stages:
python ./scripts/az_do_set_branch.py
- task: Docker@2
inputs:
containerRegistry: 'GHCR'
repository: 'beryju/authentik'
containerRegistry: 'beryjuorg-harbor'
repository: 'authentik/server'
command: 'buildAndPush'
Dockerfile: 'Dockerfile'
tags: "gh-$(branchName)"

View File

@ -98,8 +98,8 @@ stages:
python ./scripts/az_do_set_branch.py
- task: Docker@2
inputs:
containerRegistry: 'GHCR'
repository: 'beryju/authentik-proxy'
containerRegistry: 'beryjuorg-harbor'
repository: 'authentik/proxy'
command: 'buildAndPush'
Dockerfile: 'outpost/proxy.Dockerfile'
buildContext: 'outpost/'

File diff suppressed because it is too large Load Diff

View File

@ -78,8 +78,8 @@ stages:
python ./scripts/az_do_set_branch.py
- task: Docker@2
inputs:
containerRegistry: 'GHCR'
repository: 'beryju/authentik-static'
containerRegistry: 'beryjuorg-harbor'
repository: 'authentik/static'
command: 'buildAndPush'
Dockerfile: 'web/Dockerfile'
tags: "gh-$(branchName)"

View File

@ -1,3 +1,5 @@
import { gettext } from "django";
import { showMessage } from "../elements/messages/MessageContainer";
import { getCookie } from "../utils";
import { NotFoundError, RequestError } from "./Error";
@ -47,6 +49,13 @@ export class Client {
}
return r;
})
.catch((e) => {
showMessage({
level_tag: "error",
message: gettext(`Unexpected error while fetching: ${e.toString()}`),
});
return e;
})
.then((r) => r.json())
.then((r) => <T>r);
}

View File

@ -1,16 +1,21 @@
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import Chart from "chart.js";
import { showMessage } from "./messages/MessageContainer";
import { DefaultClient } from "../api/Client";
interface TickValue {
value: number;
major: boolean;
}
export interface LoginMetrics {
logins_failed_per_1h: { x: number, y: number }[];
logins_per_1h: { x: number, y: number }[];
}
@customElement("ak-admin-logins-chart")
export class AdminLoginsChart extends LitElement {
@property()
url = "";
@property({type: Array})
url: string[] = [];
chart?: Chart;
@ -40,15 +45,7 @@ export class AdminLoginsChart extends LitElement {
}
firstUpdated(): void {
fetch(this.url)
.then((r) => r.json())
.catch((e) => {
showMessage({
level_tag: "error",
message: "Unexpected error"
});
console.error(e);
})
DefaultClient.fetch<LoginMetrics>(this.url)
.then((r) => {
const canvas = <HTMLCanvasElement>this.shadowRoot?.querySelector("canvas");
if (!canvas) {

View File

@ -12,10 +12,17 @@ export class Tabs extends LitElement {
@property()
currentPage?: string;
@property({type: Boolean})
vertical = false;
static get styles(): CSSResult[] {
return [GlobalsStyle, TabsStyle, css`
::slotted(*) {
height: 100%;
flex-grow: 2;
}
:host([vertical]) {
display: flex;
}
`];
}
@ -39,7 +46,7 @@ export class Tabs extends LitElement {
}
this.currentPage = pages[0].attributes.getNamedItem("slot")?.value;
}
return html`<div class="pf-c-tabs">
return html`<div class="pf-c-tabs ${this.vertical ? "pf-m-vertical pf-m-box" : ""}">
<ul class="pf-c-tabs__list">
${pages.map((page) => this.renderTab(page))}
</ul>

View File

@ -1,6 +1,5 @@
import { gettext } from "django";
import { LitElement, html, customElement, TemplateResult, property } from "lit-element";
import { DefaultClient } from "../../api/Client";
import "./Message";
import { APIMessage } from "./Message";
@ -15,7 +14,6 @@ export function showMessage(message: APIMessage): void {
@customElement("ak-message-container")
export class MessageContainer extends LitElement {
url = DefaultClient.makeUrl(["root", "messages"]);
@property({attribute: false})
messages: APIMessage[] = [];
@ -36,10 +34,6 @@ export class MessageContainer extends LitElement {
}
}
firstUpdated(): void {
this.fetchMessages();
}
connect(): void {
const wsUrl = `${window.location.protocol.replace("http", "ws")}//${
window.location.host
@ -74,21 +68,6 @@ export class MessageContainer extends LitElement {
});
}
/* Fetch messages which were stored in the session.
* This mostly gets messages which were created when the user arrives/leaves the site
* and especially the login flow */
fetchMessages(): Promise<void> {
console.debug("authentik/messages: fetching messages over direct api");
return fetch(this.url)
.then((r) => r.json())
.then((r: APIMessage[]) => {
r.forEach((m: APIMessage) => {
this.messages.push(m);
this.requestUpdate();
});
});
}
render(): TemplateResult {
return html`<ul class="pf-c-alert-group pf-m-toast">
${this.messages.map((m) => {

View File

@ -119,13 +119,15 @@ export class AuthenticatorValidateStage extends BaseStage implements StageHost {
return html`<ak-stage-authenticator-validate-code
.host=${this}
.challenge=${this.challenge}
.deviceChallenge=${this.selectedDeviceChallenge}>
.deviceChallenge=${this.selectedDeviceChallenge}
.showBackButton=${(this.challenge?.device_challenges.length || []) > 1}>
</ak-stage-authenticator-validate-code>`;
case DeviceClasses.WEBAUTHN:
return html`<ak-stage-authenticator-validate-webauthn
.host=${this}
.challenge=${this.challenge}
.deviceChallenge=${this.selectedDeviceChallenge}>
.deviceChallenge=${this.selectedDeviceChallenge}
.showBackButton=${(this.challenge?.device_challenges.length || []) > 1}>
</ak-stage-authenticator-validate-webauthn>`;
}
}

View File

@ -14,6 +14,9 @@ export class AuthenticatorValidateStageWebCode extends BaseStage {
@property({ attribute: false })
deviceChallenge?: DeviceChallenge;
@property({ type: Boolean })
showBackButton = false;
static get styles(): CSSResult[] {
return COMMON_STYLES;
}
@ -61,14 +64,16 @@ export class AuthenticatorValidateStageWebCode extends BaseStage {
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
<li class="pf-c-login__main-footer-links-item">
${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;
}}>
${gettext("Return to device picker")}
</button>
</li>
</li>`:
html``}
</ul>
</footer>`;
}

View File

@ -21,6 +21,9 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage {
@property()
authenticateMessage = "";
@property({type: Boolean})
showBackButton = false;
static get styles(): CSSResult[] {
return COMMON_STYLES;
}
@ -98,14 +101,16 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage {
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
<li class="pf-c-login__main-footer-links-item">
${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;
}}>
${gettext("Return to device picker")}
</button>
</li>
</li>`:
html``}
</ul>
</footer>`;
}

View File

@ -1,6 +1,5 @@
import { gettext } from "django";
import { CSSResult, customElement, html, LitElement, TemplateResult } from "lit-element";
import { DefaultClient } from "../../api/Client";
import { COMMON_STYLES } from "../../common/styles";
import "../../elements/AdminLoginsChart";
@ -31,7 +30,7 @@ export class AdminOverviewPage extends LitElement {
<section class="pf-c-page__main-section">
<div class="pf-l-gallery pf-m-gutter">
<ak-aggregate-card class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-server" header="Logins over the last 24 hours" style="grid-column-end: span 3;grid-row-end: span 2;">
<ak-admin-logins-chart url="${DefaultClient.makeUrl(["admin", "metrics"])}"></ak-admin-logins-chart>
<ak-admin-logins-chart .url="${["admin", "metrics"]}"></ak-admin-logins-chart>
</ak-aggregate-card>
<ak-aggregate-card class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-server" header="Apps with most usage" style="grid-column-end: span 2;grid-row-end: span 3;">
<ak-top-applications-table></ak-top-applications-table>

View File

@ -1,7 +1,6 @@
import { gettext } from "django";
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import { Application } from "../../api/Applications";
import { DefaultClient } from "../../api/Client";
import { COMMON_STYLES } from "../../common/styles";
import "../../elements/Tabs";
@ -71,7 +70,7 @@ export class ApplicationViewPage extends LitElement {
<div class="pf-c-card__body">
${this.application ? html`
<ak-admin-logins-chart
url="${DefaultClient.makeUrl(["core", "applications", this.application?.slug, "metrics"])}">
.url="${["core", "applications", this.application?.slug, "metrics"]}">
</ak-admin-logins-chart>`: ""}
</div>
</div>

View File

@ -1,5 +1,5 @@
---
title: Static Authenticator stage
title: Static Authentication Setup stage
---
This stage configures static OTP Tokens, which can be used as a backup method to time-based OTP tokens.

View File

@ -1,7 +1,7 @@
---
title: TOTP stage
title: TOTP Authentication Setup stage
---
This stage configures a time-based OTP Device, such as Google Authenticator or Authy.
You can configure how many digest should be used for the OTP Token.
You can configure how many digits should be used for the OTP Token.

View File

@ -2,7 +2,16 @@
title: Authenticator Validation Stage
---
This stage validates an already configured OTP Device. This device has to be configured using any of the other authenticator stages:
This stage validates an already configured Authenticator Device. This device has to be configured using any of the other authenticator stages:
- [TOTP authenticator stage](../authenticator_totp/index.md)
- [Static authenticator stage](../authenticator_static/index.md).
- [WebAuth authenticator stage](../authenticator_webauthn/index.md).
You can select which type of device classes are allowed.
Using the `Not configured action`, you can choose what happens when a user does not have any matching devices.
- Skip: Validation is skipped and the flow continues
- Deny: Access is denied, the flow execution ends
- Configure: This option requires a *Configuration stage* to be set. The validation stage will be marked as successful, and the configuration stage will be injected into the flow.

View File

@ -0,0 +1,7 @@
---
title: WebAuthn Authentication Setup stage
---
This stage configures a WebAuthn-based Authenticator. This can either be a browser, biometrics or a Security stick like a YubiKey.
There are no stage-specific settings.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 115 KiB

View File

@ -0,0 +1,10 @@
---
title: Deny stage
---
This stage stops the execution of a flow. This can be used to conditionally deny users access to a flow,
even if they are not signed in (and permissions can't be checked via groups).
:::caution
To effectively use this stage, make sure to **disable** *Evaluate on plan* on the Stage binding.
:::

View File

@ -0,0 +1,62 @@
---
title: Apache Guacamole™
---
## What is Apache Guacamole™
From https://guacamole.apache.org/
:::note
Apache Guacamole is a clientless remote desktop gateway. It supports standard protocols like VNC, RDP, and SSH.
:::
## Preparation
The following placeholders will be used:
- `guacamole.company` is the FQDN of the Guacamole install.
- `authentik.company` is the FQDN of the authentik install.
Create an OAuth2/OpenID provider with the following parameters:
- Client Type: `Confidential`
- JWT Algorithm: `RS256`
- Redirect URIs: `https://guacamole.company/` (depending on your Tomcat setup, you might have to add `/guacamole/` if the application runs in a subfolder)
- Scopes: OpenID, Email and Profile
Note the Client ID value. Create an application, using the provider you've created above.
## Guacamole
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
<Tabs
defaultValue="docker"
values={[
{label: 'Docker', value: 'docker'},
{label: 'Standalone', value: 'standalone'},
]}>
<TabItem value="docker">
The docker containers are configured via environment variables. The following variables are required:
```yaml
OPENID_AUTHORIZATION_ENDPOINT: https://authentik.company/application/o/authorize/
OPENID_CLIENT_ID: # client ID from above
OPENID_ISSUER: https://authentik.company/application/o/apache-guacamole/
OPENID_JWKS_ENDPOINT: https://authentik.company/application/o/apache-guacamole/jwks/
OPENID_REDIRECT_URI: https://guacamole.company/ # This must match the redirect URI above
```
</TabItem>
<TabItem value="standalone">
Standalone Guacamole is configured using the `guacamole.properties` file. Add the following settings:
```
openid-authorization-endpoint=https://authentik.company/application/o/authorize/
openid-client-id=# client ID from above
openid-issuer=https://authentik.company/application/o/apache-guacamole/
openid-jwks-endpoint=https://authentik.company/application/o/apache-guacamole/jwks/
openid-redirect-uri=https://guacamole.company/ # This must match the redirect URI above
```
</TabItem>
</Tabs>

View File

@ -29,6 +29,16 @@ Note the Client ID and Client Secret values. Create an application, using the pr
## Grafana
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
<Tabs
defaultValue="docker"
values={[
{label: 'Docker', value: 'docker'},
{label: 'Standalone', value: 'standalone'},
]}>
<TabItem value="docker">
If your Grafana is running in docker, set the following environment variables:
```yaml
@ -45,7 +55,8 @@ environment:
# Optionally enable auto-login
GF_AUTH_OAUTH_AUTO_LOGIN: "true"
```
</TabItem>
<TabItem value="standalone">
If you are using a config-file instead, you have to set these options:
```ini
@ -64,3 +75,5 @@ auth_url = https://authentik.company/application/o/authorize/
token_url = https://authentik.company/application/o/token/
api_url = https://authentik.company/application/o/userinfo/
```
</TabItem>
</Tabs>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

View File

@ -0,0 +1,72 @@
---
title: Wiki.js
---
## What is Wiki.js
From https://en.wikipedia.org/wiki/Wiki.js
:::note
Wiki.js is a wiki engine running on Node.js and written in JavaScript. It is free software released under the Affero GNU General Public License. It is available as a self-hosted solution or using "single-click" install on the DigitalOcean and AWS marketplace.
:::
:::note
This is based on authentik 2021.3 and Wiki.js 2.5. Instructions may differ between versions.
:::
## Preparation
The following placeholders will be used:
- `wiki.company` is the FQDN of Wiki.js.
- `authentik.company` is the FQDN of authentik.
### Step 1
In Wiki.js, navigate to the _Authentication_ section in the _Administration_ interface.
Add a _Generic OpenID Connect / OAuth2_ strategy and note the _Callback URL / Redirect URI_ in the _Configuration Reference_ section at the bottom.
### Step 2
In authentik, under _Providers_, create an _OAuth2/OpenID Provider_ with these settings:
- Client Type: Confidential
- JWT Algorithm: RS256
- Redirect URI: The _Callback URL / Redirect URI_ you noted from the previous step.
- Scopes: Default OAUth mappings for: OpenID, email, profile.
- RSA Key: Choose a certificate.
- Sub Mode: Based on username.
Note the _client ID_ and _client secret_, then save the provider. If you need to retrieve these values, you can do so by editing the provider.
![](./authentik_provider.png)
### Step 3
In Wiki.js, configure the authentication strategy with these settings:
- Client ID: Client ID from the authentik provider.
- Client Secret: Client Secret from the authentik provider.
- Authorization Endpoint URL: https://authentik.company/application/o/authorize/
- Token Endpoint URL: https://authentik.company/application/o/token/
- User Info Endpont URL: https://authentik.company/application/o/userinfo/
- Issuer: https://authentik.company/application/o/wikijs/
- Logout URL: https://authentik.company/application/o/wikijs/end-session/
- Allow self-registration: Enabled
- Assign to group: The group to which new users logging in from authentik should be assigned.
![](./wiki-js_strategy.png)
:::note
You do not have to enable "Allow self-registration" and select a group to which new users should be assigned, but if you don't you will have to manually provision users in Wiki.js and ensure that their usernames match the username they have in authentik.
:::
### Step 5
In authentik, create an application which uses this provider. Optionally apply access restrictions to the application using policy bindings.
Set the Launch URL to the _Callback URL / Redirect URI_ without the `/callback` at the end, as shown below. This will skip Wiki.js' login prompt and log you in directly.
![](./authentik_application.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

View File

@ -12,7 +12,6 @@ Scopes can be configured using Scope Mappings, a type of [Property Mappings](../
| Token | `/application/o/token/` |
| User Info | `/application/o/userinfo/` |
| End Session | `/application/o/end-session/` |
| Introspect | `/application/o/end-session/` |
| JWKS | `/application/o/<application slug>/jwks/` |
| OpenID Configuration | `/application/o/<application slug>/.well-known/openid-configuration` |

View File

@ -49,7 +49,9 @@ module.exports = {
"flow/stages/authenticator_static/index",
"flow/stages/authenticator_totp/index",
"flow/stages/authenticator_validate/index",
"flow/stages/authenticator_webauthn/index",
"flow/stages/captcha/index",
"flow/stages/deny",
"flow/stages/email/index",
"flow/stages/identification/index",
"flow/stages/invitation/index",
@ -106,6 +108,7 @@ module.exports = {
type: "category",
label: "as Provider",
items: [
"integrations/services/apache-guacamole/index",
"integrations/services/aws/index",
"integrations/services/awx-tower/index",
"integrations/services/gitlab/index",
@ -120,6 +123,7 @@ module.exports = {
"integrations/services/ubuntu-landscape/index",
"integrations/services/veeam-enterprise-manager/index",
"integrations/services/vmware-vcenter/index",
"integrations/services/wiki-js/index",
],
},
],
@ -141,6 +145,7 @@ module.exports = {
"releases/0.14",
"releases/2021.1",
"releases/2021.2",
"releases/2021.3",
],
},
{