stages/user_login: stay logged in (#4958)
* add initial remember me offset Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add to go executor Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add ui for user login stage Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * update docs Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
parent
fd9293e3e8
commit
eaf56f4f3f
|
@ -368,9 +368,9 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
||||||
COOKIE_NAME_MFA,
|
COOKIE_NAME_MFA,
|
||||||
cookie,
|
cookie,
|
||||||
expires=expiry,
|
expires=expiry,
|
||||||
path="/",
|
path=settings.SESSION_COOKIE_PATH,
|
||||||
domain=settings.SESSION_COOKIE_DOMAIN,
|
domain=settings.SESSION_COOKIE_DOMAIN,
|
||||||
samesite="Lax",
|
samesite=settings.SESSION_COOKIE_SAMESITE,
|
||||||
)
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ class UserLoginStageSerializer(StageSerializer):
|
||||||
fields = StageSerializer.Meta.fields + [
|
fields = StageSerializer.Meta.fields + [
|
||||||
"session_duration",
|
"session_duration",
|
||||||
"terminate_other_sessions",
|
"terminate_other_sessions",
|
||||||
|
"remember_me_offset",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Generated by Django 4.1.7 on 2023-03-15 13:16
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import authentik.lib.utils.time
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("authentik_stages_user_login", "0004_userloginstage_terminate_other_sessions"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="userloginstage",
|
||||||
|
name="remember_me_offset",
|
||||||
|
field=models.TextField(
|
||||||
|
default="seconds=0",
|
||||||
|
help_text="Offset the session will be extended by when the user picks the remember me option. Default of 0 means that the remember me option will not be shown. (Format: hours=-1;minutes=-2;seconds=-3)",
|
||||||
|
validators=[authentik.lib.utils.time.timedelta_string_validator],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -24,6 +24,15 @@ class UserLoginStage(Stage):
|
||||||
terminate_other_sessions = models.BooleanField(
|
terminate_other_sessions = models.BooleanField(
|
||||||
default=False, help_text=_("Terminate all other sessions of the user logging in.")
|
default=False, help_text=_("Terminate all other sessions of the user logging in.")
|
||||||
)
|
)
|
||||||
|
remember_me_offset = models.TextField(
|
||||||
|
default="seconds=0",
|
||||||
|
validators=[timedelta_string_validator],
|
||||||
|
help_text=_(
|
||||||
|
"Offset the session will be extended by when the user picks the remember me option. "
|
||||||
|
"Default of 0 means that the remember me option will not be shown. "
|
||||||
|
"(Format: hours=-1;minutes=-2;seconds=-3)"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def serializer(self) -> type[BaseSerializer]:
|
def serializer(self) -> type[BaseSerializer]:
|
||||||
|
|
|
@ -3,23 +3,61 @@ from django.contrib import messages
|
||||||
from django.contrib.auth import login
|
from django.contrib.auth import login
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
from rest_framework.fields import BooleanField, CharField
|
||||||
|
|
||||||
from authentik.core.models import AuthenticatedSession, User
|
from authentik.core.models import AuthenticatedSession, User
|
||||||
|
from authentik.flows.challenge import ChallengeResponse, ChallengeTypes, WithUserInfoChallenge
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, PLAN_CONTEXT_SOURCE
|
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, PLAN_CONTEXT_SOURCE
|
||||||
from authentik.flows.stage import StageView
|
from authentik.flows.stage import ChallengeStageView
|
||||||
from authentik.lib.utils.time import timedelta_from_string
|
from authentik.lib.utils.time import timedelta_from_string
|
||||||
from authentik.stages.password import BACKEND_INBUILT
|
from authentik.stages.password import BACKEND_INBUILT
|
||||||
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||||
|
from authentik.stages.user_login.models import UserLoginStage
|
||||||
|
|
||||||
|
|
||||||
class UserLoginStageView(StageView):
|
class UserLoginChallenge(WithUserInfoChallenge):
|
||||||
|
"""Empty challenge"""
|
||||||
|
|
||||||
|
component = CharField(default="ak-stage-user-login")
|
||||||
|
|
||||||
|
|
||||||
|
class UserLoginChallengeResponse(ChallengeResponse):
|
||||||
|
"""User login challenge"""
|
||||||
|
|
||||||
|
component = CharField(default="ak-stage-user-login")
|
||||||
|
|
||||||
|
remember_me = BooleanField(required=True)
|
||||||
|
|
||||||
|
|
||||||
|
class UserLoginStageView(ChallengeStageView):
|
||||||
"""Finalise Authentication flow by logging the user in"""
|
"""Finalise Authentication flow by logging the user in"""
|
||||||
|
|
||||||
def post(self, request: HttpRequest) -> HttpResponse:
|
response_class = UserLoginChallengeResponse
|
||||||
"""Wrapper for post requests"""
|
|
||||||
return self.get(request)
|
|
||||||
|
|
||||||
def get(self, request: HttpRequest) -> HttpResponse:
|
def get_challenge(self, *args, **kwargs) -> UserLoginChallenge:
|
||||||
|
return UserLoginChallenge(
|
||||||
|
data={
|
||||||
|
"type": ChallengeTypes.NATIVE.value,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
|
"""Wrapper for post requests"""
|
||||||
|
stage: UserLoginStage = self.executor.current_stage
|
||||||
|
if timedelta_from_string(stage.remember_me_offset).total_seconds() > 0:
|
||||||
|
return super().post(request, *args, **kwargs)
|
||||||
|
return self.do_login(request)
|
||||||
|
|
||||||
|
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
|
stage: UserLoginStage = self.executor.current_stage
|
||||||
|
if timedelta_from_string(stage.remember_me_offset).total_seconds() > 0:
|
||||||
|
return super().get(request, *args, **kwargs)
|
||||||
|
return self.do_login(request)
|
||||||
|
|
||||||
|
def challenge_valid(self, response: UserLoginChallengeResponse) -> HttpResponse:
|
||||||
|
return self.do_login(self.request, response.validated_data["remember_me"])
|
||||||
|
|
||||||
|
def do_login(self, request: HttpRequest, remember: bool = False) -> HttpResponse:
|
||||||
"""Attach the currently pending user to the current session"""
|
"""Attach the currently pending user to the current session"""
|
||||||
if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context:
|
if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context:
|
||||||
message = _("No Pending user to login.")
|
message = _("No Pending user to login.")
|
||||||
|
@ -33,6 +71,9 @@ class UserLoginStageView(StageView):
|
||||||
if not user.is_active:
|
if not user.is_active:
|
||||||
self.logger.warning("User is not active, login will not work.")
|
self.logger.warning("User is not active, login will not work.")
|
||||||
delta = timedelta_from_string(self.executor.current_stage.session_duration)
|
delta = timedelta_from_string(self.executor.current_stage.session_duration)
|
||||||
|
if remember:
|
||||||
|
offset = timedelta_from_string(self.executor.current_stage.remember_me_offset)
|
||||||
|
delta = delta + offset
|
||||||
if delta.total_seconds() == 0:
|
if delta.total_seconds() == 0:
|
||||||
self.request.session.set_expiry(0)
|
self.request.session.set_expiry(0)
|
||||||
else:
|
else:
|
||||||
|
@ -47,7 +88,7 @@ class UserLoginStageView(StageView):
|
||||||
backend=backend,
|
backend=backend,
|
||||||
user=user.username,
|
user=user.username,
|
||||||
flow_slug=self.executor.flow.slug,
|
flow_slug=self.executor.flow.slug,
|
||||||
session_duration=self.executor.current_stage.session_duration,
|
session_duration=delta,
|
||||||
)
|
)
|
||||||
# Only show success message if we don't have a source in the flow
|
# Only show success message if we don't have a source in the flow
|
||||||
# as sources show their own success messages
|
# as sources show their own success messages
|
||||||
|
|
|
@ -116,6 +116,36 @@ class TestUserLoginStage(FlowTestCase):
|
||||||
self.client.session.clear_expired()
|
self.client.session.clear_expired()
|
||||||
self.assertEqual(list(self.client.session.keys()), [])
|
self.assertEqual(list(self.client.session.keys()), [])
|
||||||
|
|
||||||
|
def test_expiry_remember(self):
|
||||||
|
"""Test with expiry"""
|
||||||
|
self.stage.session_duration = "seconds=2"
|
||||||
|
self.stage.remember_me_offset = "seconds=2"
|
||||||
|
self.stage.save()
|
||||||
|
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||||
|
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||||
|
session = self.client.session
|
||||||
|
session[SESSION_KEY_PLAN] = plan
|
||||||
|
session.save()
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||||
|
data={"remember_me": True},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
|
||||||
|
self.assertNotEqual(list(self.client.session.keys()), [])
|
||||||
|
session_key = self.client.session.session_key
|
||||||
|
session = AuthenticatedSession.objects.filter(session_key=session_key).first()
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
session.expires.timestamp() - now().timestamp(),
|
||||||
|
timedelta_from_string(self.stage.session_duration).total_seconds()
|
||||||
|
+ timedelta_from_string(self.stage.remember_me_offset).total_seconds(),
|
||||||
|
delta=1,
|
||||||
|
)
|
||||||
|
sleep(5)
|
||||||
|
self.client.session.clear_expired()
|
||||||
|
self.assertEqual(list(self.client.session.keys()), [])
|
||||||
|
|
||||||
@patch(
|
@patch(
|
||||||
"authentik.flows.views.executor.to_stage_response",
|
"authentik.flows.views.executor.to_stage_response",
|
||||||
TO_STAGE_RESPONSE_MOCK,
|
TO_STAGE_RESPONSE_MOCK,
|
||||||
|
|
|
@ -3,10 +3,11 @@ package flow
|
||||||
type StageComponent string
|
type StageComponent string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
StageAccessDenied = StageComponent("ak-stage-access-denied")
|
||||||
|
StageAuthenticatorValidate = StageComponent("ak-stage-authenticator-validate")
|
||||||
StageIdentification = StageComponent("ak-stage-identification")
|
StageIdentification = StageComponent("ak-stage-identification")
|
||||||
StagePassword = StageComponent("ak-stage-password")
|
StagePassword = StageComponent("ak-stage-password")
|
||||||
StageAuthenticatorValidate = StageComponent("ak-stage-authenticator-validate")
|
StageUserLogin = StageComponent("ak-stage-user-login")
|
||||||
StageAccessDenied = StageComponent("ak-stage-access-denied")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
@ -75,6 +75,7 @@ func NewFlowExecutor(ctx context.Context, flowSlug string, refConfig *api.Config
|
||||||
StageIdentification: fe.solveChallenge_Identification,
|
StageIdentification: fe.solveChallenge_Identification,
|
||||||
StagePassword: fe.solveChallenge_Password,
|
StagePassword: fe.solveChallenge_Password,
|
||||||
StageAuthenticatorValidate: fe.solveChallenge_AuthenticatorValidate,
|
StageAuthenticatorValidate: fe.solveChallenge_AuthenticatorValidate,
|
||||||
|
StageUserLogin: fe.solveChallenge_UserLogin,
|
||||||
}
|
}
|
||||||
// Create new http client that also sets the correct ip
|
// Create new http client that also sets the correct ip
|
||||||
config := api.NewConfiguration()
|
config := api.NewConfiguration()
|
||||||
|
|
|
@ -30,6 +30,11 @@ func (fe *FlowExecutor) solveChallenge_Password(challenge *api.ChallengeTypes, r
|
||||||
return api.PasswordChallengeResponseRequestAsFlowChallengeResponseRequest(r), nil
|
return api.PasswordChallengeResponseRequestAsFlowChallengeResponseRequest(r), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (fe *FlowExecutor) solveChallenge_UserLogin(challenge *api.ChallengeTypes, req api.ApiFlowsExecutorSolveRequest) (api.FlowChallengeResponseRequest, error) {
|
||||||
|
r := api.NewUserLoginChallengeResponseRequest(true)
|
||||||
|
return api.UserLoginChallengeResponseRequestAsFlowChallengeResponseRequest(r), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (fe *FlowExecutor) solveChallenge_AuthenticatorValidate(challenge *api.ChallengeTypes, req api.ApiFlowsExecutorSolveRequest) (api.FlowChallengeResponseRequest, error) {
|
func (fe *FlowExecutor) solveChallenge_AuthenticatorValidate(challenge *api.ChallengeTypes, req api.ApiFlowsExecutorSolveRequest) (api.FlowChallengeResponseRequest, error) {
|
||||||
// We only support duo and code-based authenticators, check if that's allowed
|
// We only support duo and code-based authenticators, check if that's allowed
|
||||||
var deviceChallenge *api.DeviceChallenge
|
var deviceChallenge *api.DeviceChallenge
|
||||||
|
|
62
schema.yml
62
schema.yml
|
@ -24994,6 +24994,10 @@ paths:
|
||||||
description: Number of results to return per page.
|
description: Number of results to return per page.
|
||||||
schema:
|
schema:
|
||||||
type: integer
|
type: integer
|
||||||
|
- in: query
|
||||||
|
name: remember_me_offset
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
- name: search
|
- name: search
|
||||||
required: false
|
required: false
|
||||||
in: query
|
in: query
|
||||||
|
@ -27515,6 +27519,7 @@ components:
|
||||||
- $ref: '#/components/schemas/PromptChallenge'
|
- $ref: '#/components/schemas/PromptChallenge'
|
||||||
- $ref: '#/components/schemas/RedirectChallenge'
|
- $ref: '#/components/schemas/RedirectChallenge'
|
||||||
- $ref: '#/components/schemas/ShellChallenge'
|
- $ref: '#/components/schemas/ShellChallenge'
|
||||||
|
- $ref: '#/components/schemas/UserLoginChallenge'
|
||||||
discriminator:
|
discriminator:
|
||||||
propertyName: component
|
propertyName: component
|
||||||
mapping:
|
mapping:
|
||||||
|
@ -27540,6 +27545,7 @@ components:
|
||||||
ak-stage-prompt: '#/components/schemas/PromptChallenge'
|
ak-stage-prompt: '#/components/schemas/PromptChallenge'
|
||||||
xak-flow-redirect: '#/components/schemas/RedirectChallenge'
|
xak-flow-redirect: '#/components/schemas/RedirectChallenge'
|
||||||
xak-flow-shell: '#/components/schemas/ShellChallenge'
|
xak-flow-shell: '#/components/schemas/ShellChallenge'
|
||||||
|
ak-stage-user-login: '#/components/schemas/UserLoginChallenge'
|
||||||
ClientTypeEnum:
|
ClientTypeEnum:
|
||||||
enum:
|
enum:
|
||||||
- confidential
|
- confidential
|
||||||
|
@ -29023,6 +29029,7 @@ components:
|
||||||
- $ref: '#/components/schemas/PasswordChallengeResponseRequest'
|
- $ref: '#/components/schemas/PasswordChallengeResponseRequest'
|
||||||
- $ref: '#/components/schemas/PlexAuthenticationChallengeResponseRequest'
|
- $ref: '#/components/schemas/PlexAuthenticationChallengeResponseRequest'
|
||||||
- $ref: '#/components/schemas/PromptChallengeResponseRequest'
|
- $ref: '#/components/schemas/PromptChallengeResponseRequest'
|
||||||
|
- $ref: '#/components/schemas/UserLoginChallengeResponseRequest'
|
||||||
discriminator:
|
discriminator:
|
||||||
propertyName: component
|
propertyName: component
|
||||||
mapping:
|
mapping:
|
||||||
|
@ -29044,6 +29051,7 @@ components:
|
||||||
ak-stage-password: '#/components/schemas/PasswordChallengeResponseRequest'
|
ak-stage-password: '#/components/schemas/PasswordChallengeResponseRequest'
|
||||||
ak-source-plex: '#/components/schemas/PlexAuthenticationChallengeResponseRequest'
|
ak-source-plex: '#/components/schemas/PlexAuthenticationChallengeResponseRequest'
|
||||||
ak-stage-prompt: '#/components/schemas/PromptChallengeResponseRequest'
|
ak-stage-prompt: '#/components/schemas/PromptChallengeResponseRequest'
|
||||||
|
ak-stage-user-login: '#/components/schemas/UserLoginChallengeResponseRequest'
|
||||||
FlowDesignationEnum:
|
FlowDesignationEnum:
|
||||||
enum:
|
enum:
|
||||||
- authentication
|
- authentication
|
||||||
|
@ -36807,6 +36815,12 @@ components:
|
||||||
terminate_other_sessions:
|
terminate_other_sessions:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: Terminate all other sessions of the user logging in.
|
description: Terminate all other sessions of the user logging in.
|
||||||
|
remember_me_offset:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
description: 'Offset the session will be extended by when the user picks
|
||||||
|
the remember me option. Default of 0 means that the remember me option
|
||||||
|
will not be shown. (Format: hours=-1;minutes=-2;seconds=-3)'
|
||||||
PatchedUserLogoutStageRequest:
|
PatchedUserLogoutStageRequest:
|
||||||
type: object
|
type: object
|
||||||
description: UserLogoutStage Serializer
|
description: UserLogoutStage Serializer
|
||||||
|
@ -40170,6 +40184,43 @@ components:
|
||||||
additionalProperties: {}
|
additionalProperties: {}
|
||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
|
UserLoginChallenge:
|
||||||
|
type: object
|
||||||
|
description: Empty challenge
|
||||||
|
properties:
|
||||||
|
type:
|
||||||
|
$ref: '#/components/schemas/ChallengeChoices'
|
||||||
|
flow_info:
|
||||||
|
$ref: '#/components/schemas/ContextualFlowInfo'
|
||||||
|
component:
|
||||||
|
type: string
|
||||||
|
default: ak-stage-user-login
|
||||||
|
response_errors:
|
||||||
|
type: object
|
||||||
|
additionalProperties:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/ErrorDetail'
|
||||||
|
pending_user:
|
||||||
|
type: string
|
||||||
|
pending_user_avatar:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- pending_user
|
||||||
|
- pending_user_avatar
|
||||||
|
- type
|
||||||
|
UserLoginChallengeResponseRequest:
|
||||||
|
type: object
|
||||||
|
description: User login challenge
|
||||||
|
properties:
|
||||||
|
component:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
default: ak-stage-user-login
|
||||||
|
remember_me:
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- remember_me
|
||||||
UserLoginStage:
|
UserLoginStage:
|
||||||
type: object
|
type: object
|
||||||
description: UserLoginStage Serializer
|
description: UserLoginStage Serializer
|
||||||
|
@ -40208,6 +40259,11 @@ components:
|
||||||
terminate_other_sessions:
|
terminate_other_sessions:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: Terminate all other sessions of the user logging in.
|
description: Terminate all other sessions of the user logging in.
|
||||||
|
remember_me_offset:
|
||||||
|
type: string
|
||||||
|
description: 'Offset the session will be extended by when the user picks
|
||||||
|
the remember me option. Default of 0 means that the remember me option
|
||||||
|
will not be shown. (Format: hours=-1;minutes=-2;seconds=-3)'
|
||||||
required:
|
required:
|
||||||
- component
|
- component
|
||||||
- meta_model_name
|
- meta_model_name
|
||||||
|
@ -40234,6 +40290,12 @@ components:
|
||||||
terminate_other_sessions:
|
terminate_other_sessions:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: Terminate all other sessions of the user logging in.
|
description: Terminate all other sessions of the user logging in.
|
||||||
|
remember_me_offset:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
description: 'Offset the session will be extended by when the user picks
|
||||||
|
the remember me option. Default of 0 means that the remember me option
|
||||||
|
will not be shown. (Format: hours=-1;minutes=-2;seconds=-3)'
|
||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
UserLogoutStage:
|
UserLogoutStage:
|
||||||
|
|
|
@ -81,6 +81,22 @@ export class UserLoginStageForm extends ModelForm<UserLoginStage, string> {
|
||||||
</a>
|
</a>
|
||||||
</ak-alert>
|
</ak-alert>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${t`Stay signed in offset`}
|
||||||
|
?required=${true}
|
||||||
|
name="rememberMeOffset"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value="${first(this.instance?.rememberMeOffset, "seconds=0")}"
|
||||||
|
class="pf-c-form-control"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<p class="pf-c-form__helper-text">
|
||||||
|
${t`If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here.`}
|
||||||
|
</p>
|
||||||
|
<ak-utils-time-delta-help></ak-utils-time-delta-help>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
<ak-form-element-horizontal name="terminateOtherSessions">
|
<ak-form-element-horizontal name="terminateOtherSessions">
|
||||||
<label class="pf-c-switch">
|
<label class="pf-c-switch">
|
||||||
<input
|
<input
|
||||||
|
|
|
@ -372,6 +372,12 @@ export class FlowExecutor extends Interface implements StageHost {
|
||||||
.host=${this as StageHost}
|
.host=${this as StageHost}
|
||||||
.challenge=${this.challenge}
|
.challenge=${this.challenge}
|
||||||
></ak-stage-authenticator-validate>`;
|
></ak-stage-authenticator-validate>`;
|
||||||
|
case "ak-stage-user-login":
|
||||||
|
await import("@goauthentik/flow/stages/user_login/UserLoginStage");
|
||||||
|
return html`<ak-stage-user-login
|
||||||
|
.host=${this as StageHost}
|
||||||
|
.challenge=${this.challenge}
|
||||||
|
></ak-stage-user-login>`;
|
||||||
// Sources
|
// Sources
|
||||||
case "ak-source-plex":
|
case "ak-source-plex":
|
||||||
await import("@goauthentik/flow/sources/plex/PlexLoginInit");
|
await import("@goauthentik/flow/sources/plex/PlexLoginInit");
|
||||||
|
|
|
@ -32,7 +32,7 @@ export class BaseStage<Tin, Tout> extends AKElement {
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
challenge!: Tin;
|
challenge!: Tin;
|
||||||
|
|
||||||
async submitForm(e: Event, defaults?: KeyUnknown): Promise<boolean> {
|
async submitForm(e: Event, defaults?: Tout): Promise<boolean> {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const object: KeyUnknown = defaults || {};
|
const object: KeyUnknown = defaults || {};
|
||||||
const form = new FormData(this.shadowRoot?.querySelector("form") || undefined);
|
const form = new FormData(this.shadowRoot?.querySelector("form") || undefined);
|
||||||
|
|
|
@ -41,7 +41,9 @@ export class ConsentStage extends BaseStage<ConsentChallenge, ConsentChallengeRe
|
||||||
renderNoPrevious(): TemplateResult {
|
renderNoPrevious(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<div class="pf-c-form__group">
|
<div class="pf-c-form__group">
|
||||||
<p id="header-text" class="pf-u-mb-xl">${this.challenge.headerText}</p>
|
<h3 id="header-text" class="pf-c-title pf-m-xl pf-u-mb-xl">
|
||||||
|
${this.challenge.headerText}
|
||||||
|
</h3>
|
||||||
${this.challenge.permissions.length > 0
|
${this.challenge.permissions.length > 0
|
||||||
? html`
|
? html`
|
||||||
<p class="pf-u-mb-sm">
|
<p class="pf-u-mb-sm">
|
||||||
|
@ -59,7 +61,9 @@ export class ConsentStage extends BaseStage<ConsentChallenge, ConsentChallengeRe
|
||||||
renderAdditional(): TemplateResult {
|
renderAdditional(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<div class="pf-c-form__group">
|
<div class="pf-c-form__group">
|
||||||
<p id="header-text" class="pf-u-mb-xl">${this.challenge.headerText}</p>
|
<h3 id="header-text" class="pf-c-title pf-m-xl pf-u-mb-xl">
|
||||||
|
${this.challenge.headerText}
|
||||||
|
</h3>
|
||||||
${this.challenge.permissions.length > 0
|
${this.challenge.permissions.length > 0
|
||||||
? html`
|
? html`
|
||||||
<p class="pf-u-mb-sm">
|
<p class="pf-u-mb-sm">
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
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 { ifDefined } from "lit/directives/if-defined.js";
|
||||||
|
|
||||||
|
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 PFSpacing from "@patternfly/patternfly/utilities/Spacing/spacing.css";
|
||||||
|
|
||||||
|
import { UserLoginChallenge, UserLoginChallengeResponseRequest } from "@goauthentik/api";
|
||||||
|
|
||||||
|
@customElement("ak-stage-user-login")
|
||||||
|
export class PasswordStage extends BaseStage<
|
||||||
|
UserLoginChallenge,
|
||||||
|
UserLoginChallengeResponseRequest
|
||||||
|
> {
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return [PFBase, PFLogin, PFForm, PFFormControl, PFSpacing, PFButton, PFTitle];
|
||||||
|
}
|
||||||
|
|
||||||
|
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">
|
||||||
|
<ak-form-static
|
||||||
|
class="pf-c-form__group"
|
||||||
|
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||||
|
user=${this.challenge.pendingUser}
|
||||||
|
>
|
||||||
|
<div slot="link">
|
||||||
|
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
|
||||||
|
>${t`Not you?`}</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</ak-form-static>
|
||||||
|
<div class="pf-c-form__group">
|
||||||
|
<h3 id="header-text" class="pf-c-title pf-m-xl pf-u-mb-xl">
|
||||||
|
${t`Stay signed in?`}
|
||||||
|
</h3>
|
||||||
|
<p class="pf-u-mb-sm">
|
||||||
|
${t`Select Yes to reduce the number of times you're asked to sign in.`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pf-c-form__group pf-m-action">
|
||||||
|
<button
|
||||||
|
@click=${(e: Event) => {
|
||||||
|
this.submitForm(e, {
|
||||||
|
rememberMe: true,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
class="pf-c-button pf-m-primary"
|
||||||
|
>
|
||||||
|
${t`Yes`}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click=${(e: Event) => {
|
||||||
|
this.submitForm(e, {
|
||||||
|
rememberMe: false,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
class="pf-c-button pf-m-secondary"
|
||||||
|
>
|
||||||
|
${t`No`}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<footer class="pf-c-login__main-footer">
|
||||||
|
<ul class="pf-c-login__main-footer-links"></ul>
|
||||||
|
</footer>`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -36,7 +36,7 @@ Flows are designated for a single purpose. This designation changes when a flow
|
||||||
|
|
||||||
This is designates a flow to be used for authentication.
|
This is designates a flow to be used for authentication.
|
||||||
|
|
||||||
The authentication flow should always contain a [**User Login**](stages/user_login.md) stage, which attaches the staged user to the current session.
|
The authentication flow should always contain a [**User Login**](stages/user_login/index.md) stage, which attaches the staged user to the current session.
|
||||||
|
|
||||||
#### Invalidation
|
#### Invalidation
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,12 @@ You can set the session to expire after any duration using the syntax of `hours=
|
||||||
|
|
||||||
All values accept floating-point values.
|
All values accept floating-point values.
|
||||||
|
|
||||||
|
## Stay signed in offset
|
||||||
|
|
||||||
|
When this is set to a higher value than the default _seconds=0_, a prompt is shown, allowing the users to choose if their session should be extended or not. The same syntax as for _Session duration_ applies.
|
||||||
|
|
||||||
|
![](./stay_signed_in.png)
|
||||||
|
|
||||||
## Terminate other sessions
|
## Terminate other sessions
|
||||||
|
|
||||||
When enabled, previous sessions of the user logging in will be revoked. This has no affect on OAuth refresh tokens.
|
When enabled, previous sessions of the user logging in will be revoked. This has no affect on OAuth refresh tokens.
|
Binary file not shown.
After Width: | Height: | Size: 142 KiB |
|
@ -2,4 +2,4 @@
|
||||||
title: User logout stage
|
title: User logout stage
|
||||||
---
|
---
|
||||||
|
|
||||||
Opposite stage of [User Login Stages](user_login.md). It removes the user from the current session.
|
Opposite stage of [User Login Stages](user_login/index.md). It removes the user from the current session.
|
||||||
|
|
|
@ -89,7 +89,7 @@ The following stages are supported:
|
||||||
SMS-based authenticators are not supported as they require a code to be sent from authentik, which is not possible during the bind.
|
SMS-based authenticators are not supported as they require a code to be sent from authentik, which is not possible during the bind.
|
||||||
|
|
||||||
- [User Logout](../../flow/stages/user_logout.md)
|
- [User Logout](../../flow/stages/user_logout.md)
|
||||||
- [User Login](../../flow/stages/user_login.md)
|
- [User Login](../../flow/stages/user_login/index.md)
|
||||||
- [Deny](../../flow/stages/deny.md)
|
- [Deny](../../flow/stages/deny.md)
|
||||||
|
|
||||||
#### Direct bind
|
#### Direct bind
|
||||||
|
|
|
@ -171,7 +171,7 @@ module.exports = {
|
||||||
"flow/stages/password/index",
|
"flow/stages/password/index",
|
||||||
"flow/stages/prompt/index",
|
"flow/stages/prompt/index",
|
||||||
"flow/stages/user_delete",
|
"flow/stages/user_delete",
|
||||||
"flow/stages/user_login",
|
"flow/stages/user_login/index",
|
||||||
"flow/stages/user_logout",
|
"flow/stages/user_logout",
|
||||||
"flow/stages/user_write",
|
"flow/stages/user_write",
|
||||||
],
|
],
|
||||||
|
|
Reference in New Issue