providers: Add ability to choose a default authentication flow (#5070)

* core: add ability to choose a default authentication flow for a provider

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* update web to use correct ak-search-select

I don't think this element existed when the PR was initially created, lol

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

* only use provider authentication flow for authentication designation

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

* add tests

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

* fix tests

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

---------

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
risson 2023-03-24 13:26:00 +01:00 committed by GitHub
parent 94a93adb4b
commit 1957717160
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 299 additions and 7 deletions

View file

@ -35,6 +35,7 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
fields = [
"pk",
"name",
"authentication_flow",
"authorization_flow",
"property_mappings",
"component",

View file

@ -0,0 +1,25 @@
# Generated by Django 4.1.7 on 2023-03-23 21:44
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_flows", "0025_alter_flowstagebinding_evaluate_on_plan_and_more"),
("authentik_core", "0027_alter_user_uuid"),
]
operations = [
migrations.AddField(
model_name="provider",
name="authentication_flow",
field=models.ForeignKey(
help_text="Flow used for authentication when the associated application is accessed by an un-authenticated user.",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="provider_authentication",
to="authentik_flows.flow",
),
),
]

View file

@ -249,6 +249,17 @@ class Provider(SerializerModel):
name = models.TextField(unique=True)
authentication_flow = models.ForeignKey(
"authentik_flows.Flow",
null=True,
on_delete=models.SET_NULL,
help_text=_(
"Flow used for authentication when the associated application is accessed by an "
"un-authenticated user."
),
related_name="provider_authentication",
)
authorization_flow = models.ForeignKey(
"authentik_flows.Flow",
on_delete=models.CASCADE,

View file

@ -129,6 +129,7 @@ class TestApplicationsAPI(APITestCase):
"provider_obj": {
"assigned_application_name": "allowed",
"assigned_application_slug": "allowed",
"authentication_flow": None,
"authorization_flow": str(self.provider.authorization_flow.pk),
"component": "ak-provider-oauth2-form",
"meta_model_name": "authentik_providers_oauth2.oauth2provider",
@ -178,6 +179,7 @@ class TestApplicationsAPI(APITestCase):
"provider_obj": {
"assigned_application_name": "allowed",
"assigned_application_slug": "allowed",
"authentication_flow": None,
"authorization_flow": str(self.provider.authorization_flow.pk),
"component": "ak-provider-oauth2-form",
"meta_model_name": "authentik_providers_oauth2.oauth2provider",

View file

@ -2,10 +2,13 @@
from django.test import TestCase
from django.urls import reverse
from authentik.core.models import Application
from authentik.core.tests.utils import create_test_flow
from authentik.flows.models import Flow, FlowDesignation
from authentik.flows.planner import FlowPlan
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_KEY_PLAN
from authentik.lib.generators import generate_id
from authentik.providers.oauth2.models import OAuth2Provider
class TestHelperView(TestCase):
@ -22,6 +25,41 @@ class TestHelperView(TestCase):
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, expected_url)
def test_default_view_app(self):
"""Test that ToDefaultFlow returns the expected URL (when accessing an application)"""
Flow.objects.filter(designation=FlowDesignation.AUTHENTICATION).delete()
flow = create_test_flow(FlowDesignation.AUTHENTICATION)
self.client.session[SESSION_KEY_APPLICATION_PRE] = Application(
name=generate_id(),
slug=generate_id(),
provider=OAuth2Provider(
name=generate_id(),
authentication_flow=flow,
),
)
response = self.client.get(
reverse("authentik_flows:default-authentication"),
)
expected_url = reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, expected_url)
def test_default_view_app_no_provider(self):
"""Test that ToDefaultFlow returns the expected URL
(when accessing an application, without a provider)"""
Flow.objects.filter(designation=FlowDesignation.AUTHENTICATION).delete()
flow = create_test_flow(FlowDesignation.AUTHENTICATION)
self.client.session[SESSION_KEY_APPLICATION_PRE] = Application(
name=generate_id(),
slug=generate_id(),
)
response = self.client.get(
reverse("authentik_flows:default-authentication"),
)
expected_url = reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, expected_url)
def test_default_view_invalid_plan(self):
"""Test that ToDefaultFlow returns the expected URL (with an invalid plan)"""
Flow.objects.filter(designation=FlowDesignation.INVALIDATION).delete()

View file

@ -22,6 +22,7 @@ from sentry_sdk.api import set_tag
from sentry_sdk.hub import Hub
from structlog.stdlib import BoundLogger, get_logger
from authentik.core.models import Application
from authentik.events.models import Event, EventAction, cleanse_dict
from authentik.flows.challenge import (
Challenge,
@ -480,8 +481,14 @@ class ToDefaultFlow(View):
flow = None
# First, attempt to get default flow from tenant
if self.designation == FlowDesignation.AUTHENTICATION:
flow = tenant.flow_authentication
if self.designation == FlowDesignation.INVALIDATION:
# Attempt to get default flow from application
if SESSION_KEY_APPLICATION_PRE in self.request.session:
application: Application = self.request.session[SESSION_KEY_APPLICATION_PRE]
if application.provider:
flow = application.provider.authentication_flow
else:
flow = tenant.flow_authentication
elif self.designation == FlowDesignation.INVALIDATION:
flow = tenant.flow_invalidation
# If no flow was set, get the first based on slug and policy
if not flow:

View file

@ -15821,6 +15821,11 @@ paths:
name: audience
schema:
type: string
- in: query
name: authentication_flow
schema:
type: string
format: uuid
- in: query
name: authorization_flow
schema:
@ -30582,6 +30587,12 @@ components:
title: ID
name:
type: string
authentication_flow:
type: string
format: uuid
nullable: true
description: Flow used for authentication when the associated application
is accessed by an un-authenticated user.
authorization_flow:
type: string
format: uuid
@ -30672,6 +30683,12 @@ components:
name:
type: string
minLength: 1
authentication_flow:
type: string
format: uuid
nullable: true
description: Flow used for authentication when the associated application
is accessed by an un-authenticated user.
authorization_flow:
type: string
format: uuid
@ -31314,6 +31331,12 @@ components:
title: ID
name:
type: string
authentication_flow:
type: string
format: uuid
nullable: true
description: Flow used for authentication when the associated application
is accessed by an un-authenticated user.
authorization_flow:
type: string
format: uuid
@ -31430,6 +31453,12 @@ components:
name:
type: string
minLength: 1
authentication_flow:
type: string
format: uuid
nullable: true
description: Flow used for authentication when the associated application
is accessed by an un-authenticated user.
authorization_flow:
type: string
format: uuid
@ -36068,6 +36097,12 @@ components:
name:
type: string
minLength: 1
authentication_flow:
type: string
format: uuid
nullable: true
description: Flow used for authentication when the associated application
is accessed by an un-authenticated user.
authorization_flow:
type: string
format: uuid
@ -36297,6 +36332,12 @@ components:
name:
type: string
minLength: 1
authentication_flow:
type: string
format: uuid
nullable: true
description: Flow used for authentication when the associated application
is accessed by an un-authenticated user.
authorization_flow:
type: string
format: uuid
@ -36759,6 +36800,12 @@ components:
name:
type: string
minLength: 1
authentication_flow:
type: string
format: uuid
nullable: true
description: Flow used for authentication when the associated application
is accessed by an un-authenticated user.
authorization_flow:
type: string
format: uuid
@ -36841,6 +36888,12 @@ components:
name:
type: string
minLength: 1
authentication_flow:
type: string
format: uuid
nullable: true
description: Flow used for authentication when the associated application
is accessed by an un-authenticated user.
authorization_flow:
type: string
format: uuid
@ -36853,7 +36906,7 @@ components:
client_networks:
type: string
minLength: 1
description: List of CIDRs (comma-seperated) that clients can connect from.
description: List of CIDRs (comma-separated) that clients can connect from.
A more specific CIDR will match before a looser one. Clients connecting
from a non-specified CIDR will be dropped.
shared_secret:
@ -36911,6 +36964,12 @@ components:
name:
type: string
minLength: 1
authentication_flow:
type: string
format: uuid
nullable: true
description: Flow used for authentication when the associated application
is accessed by an un-authenticated user.
authorization_flow:
type: string
format: uuid
@ -38157,6 +38216,12 @@ components:
title: ID
name:
type: string
authentication_flow:
type: string
format: uuid
nullable: true
description: Flow used for authentication when the associated application
is accessed by an un-authenticated user.
authorization_flow:
type: string
format: uuid
@ -38215,6 +38280,12 @@ components:
name:
type: string
minLength: 1
authentication_flow:
type: string
format: uuid
nullable: true
description: Flow used for authentication when the associated application
is accessed by an un-authenticated user.
authorization_flow:
type: string
format: uuid
@ -38375,6 +38446,12 @@ components:
title: ID
name:
type: string
authentication_flow:
type: string
format: uuid
nullable: true
description: Flow used for authentication when the associated application
is accessed by an un-authenticated user.
authorization_flow:
type: string
format: uuid
@ -38503,6 +38580,12 @@ components:
name:
type: string
minLength: 1
authentication_flow:
type: string
format: uuid
nullable: true
description: Flow used for authentication when the associated application
is accessed by an un-authenticated user.
authorization_flow:
type: string
format: uuid
@ -38598,7 +38681,7 @@ components:
type: string
client_networks:
type: string
description: List of CIDRs (comma-seperated) that clients can connect from.
description: List of CIDRs (comma-separated) that clients can connect from.
A more specific CIDR will match before a looser one. Clients connecting
from a non-specified CIDR will be dropped.
shared_secret:
@ -38619,6 +38702,12 @@ components:
title: ID
name:
type: string
authentication_flow:
type: string
format: uuid
nullable: true
description: Flow used for authentication when the associated application
is accessed by an un-authenticated user.
authorization_flow:
type: string
format: uuid
@ -38654,7 +38743,7 @@ components:
readOnly: true
client_networks:
type: string
description: List of CIDRs (comma-seperated) that clients can connect from.
description: List of CIDRs (comma-separated) that clients can connect from.
A more specific CIDR will match before a looser one. Clients connecting
from a non-specified CIDR will be dropped.
shared_secret:
@ -38677,6 +38766,12 @@ components:
name:
type: string
minLength: 1
authentication_flow:
type: string
format: uuid
nullable: true
description: Flow used for authentication when the associated application
is accessed by an un-authenticated user.
authorization_flow:
type: string
format: uuid
@ -38689,7 +38784,7 @@ components:
client_networks:
type: string
minLength: 1
description: List of CIDRs (comma-seperated) that clients can connect from.
description: List of CIDRs (comma-separated) that clients can connect from.
A more specific CIDR will match before a looser one. Clients connecting
from a non-specified CIDR will be dropped.
shared_secret:
@ -38934,6 +39029,12 @@ components:
title: ID
name:
type: string
authentication_flow:
type: string
format: uuid
nullable: true
description: Flow used for authentication when the associated application
is accessed by an un-authenticated user.
authorization_flow:
type: string
format: uuid
@ -39090,6 +39191,12 @@ components:
name:
type: string
minLength: 1
authentication_flow:
type: string
format: uuid
nullable: true
description: Flow used for authentication when the associated application
is accessed by an un-authenticated user.
authorization_flow:
type: string
format: uuid

View file

@ -92,6 +92,37 @@ export class OAuth2ProviderFormPage extends ModelForm<OAuth2Provider, number> {
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Authentication flow`} name="authenticationFlow">
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
const args: FlowsInstancesListRequest = {
ordering: "slug",
designation: FlowsInstancesListDesignationEnum.Authentication,
};
if (query !== undefined) {
args.search = query;
}
const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList(args);
return flows.results;
}}
.renderElement=${(flow: Flow): string => {
return RenderFlowOption(flow);
}}
.renderDescription=${(flow: Flow): TemplateResult => {
return html`${flow.name}`;
}}
.value=${(flow: Flow | undefined): string | undefined => {
return flow?.pk;
}}
.selected=${(flow: Flow): boolean => {
return flow.pk === this.instance?.authenticationFlow;
}}
>
</ak-search-select>
<p class="pf-c-form__helper-text">
${t`Flow used when a user access this provider and is not authenticated.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Authorization flow`}
?required=${true}

View file

@ -314,6 +314,41 @@ export class ProxyProviderFormPage extends ModelForm<ProxyProvider, number> {
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Authentication flow`}
?required=${false}
name="authenticationFlow"
>
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
const args: FlowsInstancesListRequest = {
ordering: "slug",
designation: FlowsInstancesListDesignationEnum.Authentication,
};
if (query !== undefined) {
args.search = query;
}
const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList(args);
return flows.results;
}}
.renderElement=${(flow: Flow): string => {
return RenderFlowOption(flow);
}}
.renderDescription=${(flow: Flow): TemplateResult => {
return html`${flow.name}`;
}}
.value=${(flow: Flow | undefined): string | undefined => {
return flow?.pk;
}}
.selected=${(flow: Flow): boolean => {
return flow.pk === this.instance?.authenticationFlow;
}}
>
</ak-search-select>
<p class="pf-c-form__helper-text">
${t`Flow used when a user access this provider and is not authenticated.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Authorization flow`}
?required=${true}

View file

@ -81,6 +81,41 @@ export class SAMLProviderFormPage extends ModelForm<SAMLProvider, number> {
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Authentication flow`}
?required=${false}
name="authenticationFlow"
>
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
const args: FlowsInstancesListRequest = {
ordering: "slug",
designation: FlowsInstancesListDesignationEnum.Authentication,
};
if (query !== undefined) {
args.search = query;
}
const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList(args);
return flows.results;
}}
.renderElement=${(flow: Flow): string => {
return RenderFlowOption(flow);
}}
.renderDescription=${(flow: Flow): TemplateResult => {
return html`${flow.name}`;
}}
.value=${(flow: Flow | undefined): string | undefined => {
return flow?.pk;
}}
.selected=${(flow: Flow): boolean => {
return flow.pk === this.instance?.authenticationFlow;
}}
>
</ak-search-select>
<p class="pf-c-form__helper-text">
${t`Flow used when a user access this provider and is not authenticated.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Authorization flow`}
?required=${true}