Compare commits

..

7 Commits

Author SHA1 Message Date
f05f440c78 stages/email: don't require pending user
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-05-07 20:40:11 +02:00
7df0e88b9d events: cleanse http query string in events (#5508)
* events: cleanse http query string in events

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add more tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-05-07 20:11:36 +02:00
53f827b54f blueprints: specify schema for blueprint metadata (#5509)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-05-07 20:11:18 +02:00
395dc08f05 web/flows: don't autoclose in redirect stage if redirecting to non-http protocol (#5506)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-05-07 12:58:14 +02:00
080f2ab5e7 web: bump API Client version (#5505)
Signed-off-by: GitHub <noreply@github.com>
2023-05-07 10:44:33 +00:00
2a2e159a0d blueprints: improve schema generation by including model schema (#5503)
* blueprints: improve schema generation by including model schema

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* unset required

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add deps

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-05-07 12:32:01 +02:00
564b2874a9 providers/oauth2: use simpler charset for refresh tokens (#5502)
various implementations might have issues with the special chars

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-05-07 00:19:11 +02:00
23 changed files with 8496 additions and 399 deletions

View File

@ -1,7 +0,0 @@
FROM ghcr.io/goauthentik/server:latest
USER root
HEALTHCHECK --interval=10s CMD exit 0
RUN pip install --no-cache-dir -r /app-root/requirements-dev.txt

View File

@ -1,34 +0,0 @@
{
"name": "authentik",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/app-root",
"features": {
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {},
"ghcr.io/devcontainers/features/go:1": {},
"ghcr.io/devcontainers/features/node:1": {}
},
"forwardPorts": [9000],
"customizations": {
"vscode": {
"extensions": [
"EditorConfig.EditorConfig",
"bashmish.es6-string-css",
"bpruitt-goddard.mermaid-markdown-syntax-highlighting",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"golang.go",
"Gruntfuggly.todo-tree",
"mechatroner.rainbow-csv",
"ms-python.black-formatter",
"ms-python.isort",
"ms-python.pylint",
"ms-python.python",
"ms-python.vscode-pylance",
"redhat.vscode-yaml",
"Tobermory.es6-string-html",
"unifiedjs.vscode-mdx"
]
}
}
}

View File

@ -1,32 +0,0 @@
version: '3.8'
services:
app:
build:
context: ..
dockerfile: .devcontainer/Dockerfile
volumes:
- ../:/app-root:cached
command: debug
environment:
AUTHENTIK_POSTGRESQL__USER: postgres
AUTHENTIK_POSTGRESQL__PASSWORD: postgres
AUTHENTIK_BOOTSTRAP_PASSWORD: akadmin
AUTHENTIK_BOOTSTRAP_TOKEN: akadmin
db:
image: docker.io/library/postgres:15
restart: unless-stopped
volumes:
- postgres-data:/var/lib/postgresql/data
network_mode: service:app
environment:
POSTGRES_USER: postgres
POSTGRES_DB: authentik
POSTGRES_PASSWORD: postgres
redis:
image: docker.io/redis/redis-stack-server
restart: unless-stopped
network_mode: service:app
volumes:
postgres-data:

18
.vscode/tasks.json vendored
View File

@ -3,23 +3,26 @@
"tasks": [
{
"label": "authentik[core]: format & test",
"command": "make",
"command": "poetry",
"args": [
"run",
"make"
],
"group": "build",
},
{
"label": "authentik[core]: run",
"command": "ak",
"command": "poetry",
"args": [
"server",
"run",
"make",
"run",
],
"group": "build",
"presentation": {
"panel": "dedicated",
"group": "running"
},
"runOptions": {
"runOn": "folderOpen"
}
},
{
"label": "authentik[web]: format",
@ -36,9 +39,6 @@
"panel": "dedicated",
"group": "running"
},
"runOptions": {
"runOn": "folderOpen"
}
},
{
"label": "authentik: install",

View File

@ -68,14 +68,14 @@ LABEL org.opencontainers.image.url https://goauthentik.io
LABEL org.opencontainers.image.description goauthentik.io Main server image, see https://goauthentik.io for more info.
LABEL org.opencontainers.image.source https://github.com/goauthentik/authentik
WORKDIR /app-root
WORKDIR /
ARG GIT_BUILD_HASH
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
COPY --from=poetry-locker /work/requirements.txt /app-root
COPY --from=poetry-locker /work/requirements-dev.txt /app-root
COPY --from=geoip /usr/share/GeoIP /app-root/geoip
COPY --from=poetry-locker /work/requirements.txt /
COPY --from=poetry-locker /work/requirements-dev.txt /
COPY --from=geoip /usr/share/GeoIP /geoip
RUN apt-get update && \
# Required for installing pip packages
@ -84,35 +84,35 @@ RUN apt-get update && \
apt-get install -y --no-install-recommends libxmlsec1-openssl libmaxminddb0 && \
# Required for bootstrap & healtcheck
apt-get install -y --no-install-recommends runit && \
pip install --no-cache-dir -r /app-root/requirements.txt && \
pip install --no-cache-dir -r /requirements.txt && \
apt-get remove --purge -y build-essential pkg-config libxmlsec1-dev && \
apt-get autoremove --purge -y && \
apt-get clean && \
rm -rf /tmp/* /var/lib/apt/lists/* /var/tmp/ && \
adduser --system --no-create-home --uid 1000 --group --home /app-root authentik && \
mkdir -p /app-root /app-root/.ssh && \
adduser --system --no-create-home --uid 1000 --group --home /authentik authentik && \
mkdir -p /certs /media /blueprints && \
chown -R authentik:authentik /certs /media /app-root/
mkdir -p /authentik/.ssh && \
chown authentik:authentik /certs /media /authentik/.ssh
COPY ./authentik/ /app-root/authentik
COPY ./pyproject.toml /app-root/
COPY ./schemas /app-root/schemas
COPY ./locale /app-root/locale
COPY ./tests /app-root/tests
COPY ./manage.py /app-root/
COPY ./authentik/ /authentik
COPY ./pyproject.toml /
COPY ./schemas /schemas
COPY ./locale /locale
COPY ./tests /tests
COPY ./manage.py /
COPY ./blueprints /blueprints
COPY ./lifecycle/ /app-root/lifecycle
COPY ./lifecycle/ /lifecycle
COPY --from=go-builder /work/authentik /bin/authentik
COPY --from=web-builder /work/web/dist/ /app-root/web/dist/
COPY --from=web-builder /work/web/authentik/ /app-root/web/authentik/
COPY --from=website-builder /work/website/help/ /app-root/website/help/
COPY --from=web-builder /work/web/dist/ /web/dist/
COPY --from=web-builder /work/web/authentik/ /web/authentik/
COPY --from=website-builder /work/website/help/ /website/help/
USER 1000
ENV TMPDIR /dev/shm/
ENV PYTHONUNBUFFERED 1
ENV PATH "/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/app-root/lifecycle"
ENV PATH "/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/lifecycle"
HEALTHCHECK --interval=30s --timeout=30s --start-period=60s --retries=3 CMD [ "ak", "healthcheck" ]
HEALTHCHECK --interval=30s --timeout=30s --start-period=60s --retries=3 CMD [ "/lifecycle/ak", "healthcheck" ]
ENTRYPOINT [ "/usr/local/bin/dumb-init", "--", "ak" ]
ENTRYPOINT [ "/usr/local/bin/dumb-init", "--", "/lifecycle/ak" ]

View File

@ -1,12 +1,17 @@
"""Generate JSON Schema for blueprints"""
from json import dumps, loads
from pathlib import Path
from json import dumps
from typing import Any
from django.core.management.base import BaseCommand, no_translations
from django.db.models import Model
from drf_jsonschema_serializer.convert import field_to_converter
from rest_framework.fields import Field, JSONField, UUIDField
from rest_framework.serializers import Serializer
from structlog.stdlib import get_logger
from authentik.blueprints.v1.importer import is_model_allowed
from authentik.blueprints.v1.meta.registry import registry
from authentik.lib.models import SerializerModel
LOGGER = get_logger()
@ -16,21 +21,138 @@ class Command(BaseCommand):
schema: dict
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.schema = {
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://goauthentik.io/blueprints/schema.json",
"type": "object",
"title": "authentik Blueprint schema",
"required": ["version", "entries"],
"properties": {
"version": {
"$id": "#/properties/version",
"type": "integer",
"title": "Blueprint version",
"default": 1,
},
"metadata": {
"$id": "#/properties/metadata",
"type": "object",
"required": ["name"],
"properties": {
"name": {"type": "string"},
"labels": {"type": "object", "additionalProperties": {"type": "string"}},
},
},
"context": {
"$id": "#/properties/context",
"type": "object",
"additionalProperties": True,
},
"entries": {
"type": "array",
"items": {
"oneOf": [],
},
},
},
"$defs": {},
}
@no_translations
def handle(self, *args, **options):
"""Generate JSON Schema for blueprints"""
path = Path(__file__).parent.joinpath("./schema_template.json")
with open(path, "r", encoding="utf-8") as _template_file:
self.schema = loads(_template_file.read())
self.set_model_allowed()
self.stdout.write(dumps(self.schema, indent=4))
self.build()
self.stdout.write(dumps(self.schema, indent=4, default=Command.json_default))
def set_model_allowed(self):
"""Set model enum"""
model_names = []
@staticmethod
def json_default(value: Any) -> Any:
"""Helper that handles gettext_lazy strings that JSON doesn't handle"""
return str(value)
def build(self):
"""Build all models into the schema"""
for model in registry.get_models():
if model._meta.abstract:
continue
if not is_model_allowed(model):
continue
model_names.append(f"{model._meta.app_label}.{model._meta.model_name}")
model_names.sort()
self.schema["properties"]["entries"]["items"]["properties"]["model"]["enum"] = model_names
model_instance: Model = model()
if not isinstance(model_instance, SerializerModel):
continue
serializer = model_instance.serializer()
model_path = f"{model._meta.app_label}.{model._meta.model_name}"
self.schema["properties"]["entries"]["items"]["oneOf"].append(
self.template_entry(model_path, serializer)
)
def template_entry(self, model_path: str, serializer: Serializer) -> dict:
"""Template entry for a single model"""
model_schema = self.to_jsonschema(serializer)
model_schema["required"] = []
def_name = f"model_{model_path}"
def_path = f"#/$defs/{def_name}"
self.schema["$defs"][def_name] = model_schema
return {
"type": "object",
"required": ["model", "attrs"],
"properties": {
"model": {"const": model_path},
"id": {"type": "string"},
"state": {
"type": "string",
"enum": ["absent", "present", "created"],
"default": "present",
},
"conditions": {"type": "array", "items": {"type": "boolean"}},
"attrs": {"$ref": def_path},
"identifiers": {"$ref": def_path},
},
}
def field_to_jsonschema(self, field: Field) -> dict:
"""Convert a single field to json schema"""
if isinstance(field, Serializer):
result = self.to_jsonschema(field)
else:
try:
converter = field_to_converter[field]
result = converter.convert(field)
except KeyError:
if isinstance(field, JSONField):
result = {"type": "object", "additionalProperties": True}
elif isinstance(field, UUIDField):
result = {"type": "string", "format": "uuid"}
else:
raise
if field.label:
result["title"] = field.label
if field.help_text:
result["description"] = field.help_text
return self.clean_result(result)
def clean_result(self, result: dict) -> dict:
"""Remove enumNames from result, recursively"""
result.pop("enumNames", None)
for key, value in result.items():
if isinstance(value, dict):
result[key] = self.clean_result(value)
return result
def to_jsonschema(self, serializer: Serializer) -> dict:
"""Convert serializer to json schema"""
properties = {}
required = []
for name, field in serializer.fields.items():
if field.read_only:
continue
sub_schema = self.field_to_jsonschema(field)
if field.required:
required.append(name)
properties[name] = sub_schema
result = {"type": "object", "properties": properties}
if required:
result["required"] = required
return result

View File

@ -1,105 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "http://example.com/example.json",
"type": "object",
"title": "authentik Blueprint schema",
"default": {},
"required": [
"version",
"entries"
],
"properties": {
"version": {
"$id": "#/properties/version",
"type": "integer",
"title": "Blueprint version",
"default": 1
},
"metadata": {
"$id": "#/properties/metadata",
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "string"
},
"labels": {
"type": "object"
}
}
},
"context": {
"$id": "#/properties/context",
"type": "object",
"additionalProperties": true
},
"entries": {
"type": "array",
"items": {
"$id": "#entry",
"type": "object",
"required": [
"model"
],
"properties": {
"model": {
"type": "string",
"enum": [
"placeholder"
]
},
"id": {
"type": "string"
},
"state": {
"type": "string",
"enum": [
"absent",
"present",
"created"
],
"default": "present"
},
"conditions": {
"type": "array",
"items": {
"type": "boolean"
}
},
"attrs": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Commonly available field, may not exist on all models"
}
},
"default": {},
"additionalProperties": true
},
"identifiers": {
"type": "object",
"default": {},
"properties": {
"pk": {
"description": "Commonly available field, may not exist on all models",
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"format": "uuid"
}
]
}
},
"additionalProperties": true
}
}
}
}
}
}

View File

@ -160,6 +160,7 @@ class CertificateKeyPairSerializer(ModelSerializer):
"managed",
]
extra_kwargs = {
"managed": {"read_only": True},
"key_data": {"write_only": True},
"certificate_data": {"write_only": True},
}

View File

@ -219,13 +219,13 @@ class Event(SerializerModel, ExpiringModel):
self.context["http_request"] = {
"path": request.path,
"method": request.method,
"args": QueryDict(request.META.get("QUERY_STRING", "")),
"args": cleanse_dict(QueryDict(request.META.get("QUERY_STRING", ""))),
}
# Special case for events created during flow execution
# since they keep the http query within a wrapped query
if QS_QUERY in self.context["http_request"]["args"]:
wrapped = self.context["http_request"]["args"][QS_QUERY]
self.context["http_request"]["args"] = QueryDict(wrapped)
self.context["http_request"]["args"] = cleanse_dict(QueryDict(wrapped))
if hasattr(request, "tenant"):
tenant: Tenant = request.tenant
# Because self.created only gets set on save, we can't use it's value here

View File

@ -1,17 +1,25 @@
"""event tests"""
from urllib.parse import urlencode
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from django.test import RequestFactory, TestCase
from django.views.debug import SafeExceptionReporterFilter
from guardian.shortcuts import get_anonymous_user
from authentik.core.models import Group
from authentik.events.models import Event
from authentik.flows.views.executor import QS_QUERY
from authentik.lib.generators import generate_id
from authentik.policies.dummy.models import DummyPolicy
from authentik.tenants.models import Tenant
class TestEvents(TestCase):
"""Test Event"""
def setUp(self) -> None:
self.factory = RequestFactory()
def test_new_with_model(self):
"""Create a new Event passing a model as kwarg"""
test_model = Group.objects.create(name="test")
@ -40,3 +48,58 @@ class TestEvents(TestCase):
model_content_type = ContentType.objects.get_for_model(temp_model)
self.assertEqual(event.context.get("model").get("app"), model_content_type.app_label)
self.assertEqual(event.context.get("model").get("pk"), temp_model.pk.hex)
def test_from_http_basic(self):
"""Test plain from_http"""
event = Event.new("unittest").from_http(self.factory.get("/"))
self.assertEqual(
event.context, {"http_request": {"args": {}, "method": "GET", "path": "/"}}
)
def test_from_http_clean_querystring(self):
"""Test cleansing query string"""
request = self.factory.get(f"/?token={generate_id()}")
event = Event.new("unittest").from_http(request)
self.assertEqual(
event.context,
{
"http_request": {
"args": {"token": SafeExceptionReporterFilter.cleansed_substitute},
"method": "GET",
"path": "/",
}
},
)
def test_from_http_clean_querystring_flow(self):
"""Test cleansing query string (nested query string like flow executor)"""
nested_qs = {"token": generate_id()}
request = self.factory.get(f"/?{QS_QUERY}={urlencode(nested_qs)}")
event = Event.new("unittest").from_http(request)
self.assertEqual(
event.context,
{
"http_request": {
"args": {"token": SafeExceptionReporterFilter.cleansed_substitute},
"method": "GET",
"path": "/",
}
},
)
def test_from_http_tenant(self):
"""Test from_http tenant"""
# Test tenant
request = self.factory.get("/")
tenant = Tenant(domain="test-tenant")
setattr(request, "tenant", tenant)
event = Event.new("unittest").from_http(request)
self.assertEqual(
event.tenant,
{
"app": "authentik_tenants",
"model_name": "tenant",
"name": "Tenant test-tenant",
"pk": tenant.pk.hex,
},
)

View File

@ -72,7 +72,7 @@ cookie_domain: null
disable_update_check: false
disable_startup_analytics: false
avatars: env://AUTHENTIK_AUTHENTIK__AVATARS?gravatar,initials
geoip: "/app-root/geoip/GeoLite2-City.mmdb"
geoip: "/geoip/GeoLite2-City.mmdb"
footer_links: []

View File

@ -0,0 +1,24 @@
# Generated by Django 4.1.7 on 2023-05-06 16:18
from django.db import migrations, models
import authentik.providers.oauth2.models
class Migration(migrations.Migration):
dependencies = [
(
"authentik_providers_oauth2",
"0015_accesstoken_auth_time_authorizationcode_auth_time_and_more",
),
]
operations = [
migrations.AlterField(
model_name="refreshtoken",
name="token",
field=models.TextField(
default=authentik.providers.oauth2.models.generate_client_secret
),
),
]

View File

@ -382,7 +382,7 @@ class AccessToken(SerializerModel, ExpiringModel, BaseGrantModel):
class RefreshToken(SerializerModel, ExpiringModel, BaseGrantModel):
"""OAuth2 Refresh Token, opaque"""
token = models.TextField(default=generate_key)
token = models.TextField(default=generate_client_secret)
_id_token = models.TextField(verbose_name=_("ID Token"))
@property

View File

@ -114,9 +114,10 @@ class EmailStageView(ChallengeStageView):
user.is_active = True
user.save()
return self.executor.stage_ok()
if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context:
self.logger.debug("No pending user")
messages.error(self.request, _("No pending user."))
if not user.is_authenticated:
# We'll only get here if there's no user in the flow plan context
# and no authenticated user either
self.logger.debug("Unauthenticated user", user=user)
return self.executor.stage_invalid()
# Check if we've already sent the initial e-mail
if PLAN_CONTEXT_EMAIL_SENT not in self.executor.plan.context:

File diff suppressed because it is too large Load Diff

28
poetry.lock generated
View File

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry and should not be changed by hand.
# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand.
[[package]]
name = "aiohttp"
@ -1272,6 +1272,30 @@ websocket-client = ">=0.32.0"
[package.extras]
ssh = ["paramiko (>=2.4.3)"]
[[package]]
name = "drf-jsonschema-serializer"
version = "1.0.0"
description = "JSON Schema support for Django REST Framework"
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "drf-jsonschema-serializer-1.0.0.tar.gz", hash = "sha256:aa58d03deba5a936bc0b0dbca4b69ee902886b7a0be130797f1d5e741b92e42b"},
{file = "drf_jsonschema_serializer-1.0.0-py3-none-any.whl", hash = "sha256:06401c94f1a2610797a26c390b701504b90b6b44683932daccbc250ea2aad3b1"},
]
[package.dependencies]
django = ">=3.2"
djangorestframework = ">=3.13"
jsonschema = ">=4.0.0"
[package.extras]
all-format-validators = ["fqdn", "idna", "isoduration", "jsonpointer", "rfc3339-validator", "rfc3987", "uri-template", "webcolors"]
coverage = ["pytest-cov"]
docs = ["sphinx", "sphinx-rtd-theme"]
release = ["bump2version", "twine"]
tests = ["black", "django-stubs[compatible-mypy]", "djangorestframework-stubs[compatible-mypy]", "flake8", "fqdn", "idna", "isoduration", "isort", "jsonpointer", "mypy", "pytest", "pytest-django", "rfc3339-validator", "rfc3987", "tox", "types-jsonschema", "uri-template", "webcolors"]
[[package]]
name = "drf-spectacular"
version = "0.26.2"
@ -4152,4 +4176,4 @@ files = [
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "82fc267d6041997d1410a951033cdb9f6c57d91df7d48acaecdbab320daab58e"
content-hash = "da0f14183137ec5d4fcd7df877f1488860bc26f795f8aaa19c78655f77e3f409"

View File

@ -179,6 +179,7 @@ bump2version = "*"
colorama = "*"
coverage = { extras = ["toml"], version = "*" }
django-silk = "*"
drf-jsonschema-serializer = "*"
importlib-metadata = "*"
pylint = "*"
pylint-django = "*"

View File

@ -27912,6 +27912,7 @@ components:
readOnly: true
managed:
type: string
readOnly: true
nullable: true
title: Managed by authentik
description: Objects which are managed by authentik. These objects are created
@ -27924,6 +27925,7 @@ components:
- certificate_download_url
- fingerprint_sha1
- fingerprint_sha256
- managed
- name
- pk
- private_key_available
@ -27946,15 +27948,6 @@ components:
writeOnly: true
description: Optional Private Key. If this is set, you can use this keypair
for encryption.
managed:
type: string
nullable: true
minLength: 1
title: Managed by authentik
description: Objects which are managed by authentik. These objects are created
and updated automatically. This is flag only indicates that an object
can be overwritten by migrations. You can still modify the objects via
the API, but expect changes to be overwritten in a later update.
required:
- certificate_data
- name
@ -35649,15 +35642,6 @@ components:
writeOnly: true
description: Optional Private Key. If this is set, you can use this keypair
for encryption.
managed:
type: string
nullable: true
minLength: 1
title: Managed by authentik
description: Objects which are managed by authentik. These objects are created
and updated automatically. This is flag only indicates that an object
can be overwritten by migrations. You can still modify the objects via
the API, but expect changes to be overwritten in a later update.
PatchedConsentStageRequest:
type: object
description: ConsentStage Serializer

8
web/package-lock.json generated
View File

@ -22,7 +22,7 @@
"@codemirror/theme-one-dark": "^6.1.2",
"@formatjs/intl-listformat": "^7.2.2",
"@fortawesome/fontawesome-free": "^6.4.0",
"@goauthentik/api": "^2023.4.1-1683127005",
"@goauthentik/api": "^2023.4.1-1683455546",
"@hcaptcha/types": "^1.0.3",
"@jackfranklin/rollup-plugin-markdown": "^0.4.0",
"@lingui/cli": "^4.0.0",
@ -2347,9 +2347,9 @@
}
},
"node_modules/@goauthentik/api": {
"version": "2023.4.1-1683127005",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2023.4.1-1683127005.tgz",
"integrity": "sha512-+qSD+LlaBvGKrN6qXK7peabIbT2CAIsXbgqFvHjOD6/9t98fwKUX6jDMJqFrGV4xdANZ14a5wYLV6m9Wm3rqDA=="
"version": "2023.4.1-1683455546",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2023.4.1-1683455546.tgz",
"integrity": "sha512-RpHngzXzuSi4OKzQRCbRUpbmHzAR4eHUghUiMm15fVVMYSYkjYs2t6b9iEUwpf7xENhKPgrKSNptfv8U+3B++Q=="
},
"node_modules/@hcaptcha/types": {
"version": "1.0.3",

View File

@ -29,7 +29,7 @@
"@codemirror/theme-one-dark": "^6.1.2",
"@formatjs/intl-listformat": "^7.2.2",
"@fortawesome/fontawesome-free": "^6.4.0",
"@goauthentik/api": "^2023.4.1-1683127005",
"@goauthentik/api": "^2023.4.1-1683455546",
"@hcaptcha/types": "^1.0.3",
"@jackfranklin/rollup-plugin-markdown": "^0.4.0",
"@lingui/cli": "^4.0.0",

View File

@ -1,37 +1,25 @@
import "@goauthentik/elements/EmptyState";
import "@goauthentik/flow/FormStatic";
import { AccessDeniedStage } from "@goauthentik/flow/stages/access_denied/AccessDeniedStage";
import { BaseStage } from "@goauthentik/flow/stages/base";
import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { OAuthDeviceCodeFinishChallenge } from "@goauthentik/api";
@customElement("ak-flow-provider-oauth2-code-finish")
export class DeviceCodeFinish extends AccessDeniedStage {
export class DeviceCodeFinish extends BaseStage<
OAuthDeviceCodeFinishChallenge,
OAuthDeviceCodeFinishChallenge
> {
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 class="big-icon">
<i class="pf-icon pf-icon-ok"></i>
</p>
<h3 class="pf-c-title pf-m-3xl reason">
${t`You've successfully authenticated your device.`}
</h3>
<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>`;
return html`<ak-empty-state icon="fas fa-check" header=${t`You may close this page now.`}>
<span slot="body"> ${t`You've successfully authenticated your device.`} </span>
</ak-empty-state>`;
}
}

View File

@ -64,9 +64,6 @@ export class RedirectStage extends BaseStage<RedirectChallenge, FlowChallengeRes
// As this wouldn't really be a redirect, show a message that the page can be closed
// and try to close it ourselves
if (!url.protocol.startsWith("http")) {
setTimeout(() => {
window.close();
}, 500);
return html`<ak-empty-state
icon="fas fa-check"
header=${t`You may close this page now.`}

View File

@ -5,6 +5,7 @@ Blueprints are YAML files, which can use some additional tags to ease blueprint
## Structure
```yaml
# yaml-language-server: $schema=https://goauthentik.io/blueprints/schema.json
# The version of this blueprint, currently 1
version: 1
# Optional block of metadata, name is required if metadata is set