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,
|
||||
expires=expiry,
|
||||
path="/",
|
||||
path=settings.SESSION_COOKIE_PATH,
|
||||
domain=settings.SESSION_COOKIE_DOMAIN,
|
||||
samesite="Lax",
|
||||
samesite=settings.SESSION_COOKIE_SAMESITE,
|
||||
)
|
||||
return response
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ class UserLoginStageSerializer(StageSerializer):
|
|||
fields = StageSerializer.Meta.fields + [
|
||||
"session_duration",
|
||||
"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(
|
||||
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
|
||||
def serializer(self) -> type[BaseSerializer]:
|
||||
|
|
|
@ -3,23 +3,61 @@ from django.contrib import messages
|
|||
from django.contrib.auth import login
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.utils.translation import gettext as _
|
||||
from rest_framework.fields import BooleanField, CharField
|
||||
|
||||
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.stage import StageView
|
||||
from authentik.flows.stage import ChallengeStageView
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.stages.password import BACKEND_INBUILT
|
||||
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"""
|
||||
|
||||
def post(self, request: HttpRequest) -> HttpResponse:
|
||||
"""Wrapper for post requests"""
|
||||
return self.get(request)
|
||||
response_class = UserLoginChallengeResponse
|
||||
|
||||
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"""
|
||||
if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context:
|
||||
message = _("No Pending user to login.")
|
||||
|
@ -33,6 +71,9 @@ class UserLoginStageView(StageView):
|
|||
if not user.is_active:
|
||||
self.logger.warning("User is not active, login will not work.")
|
||||
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:
|
||||
self.request.session.set_expiry(0)
|
||||
else:
|
||||
|
@ -47,7 +88,7 @@ class UserLoginStageView(StageView):
|
|||
backend=backend,
|
||||
user=user.username,
|
||||
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
|
||||
# as sources show their own success messages
|
||||
|
|
|
@ -116,6 +116,36 @@ class TestUserLoginStage(FlowTestCase):
|
|||
self.client.session.clear_expired()
|
||||
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(
|
||||
"authentik.flows.views.executor.to_stage_response",
|
||||
TO_STAGE_RESPONSE_MOCK,
|
||||
|
|
|
@ -3,10 +3,11 @@ package flow
|
|||
type StageComponent string
|
||||
|
||||
const (
|
||||
StageAccessDenied = StageComponent("ak-stage-access-denied")
|
||||
StageAuthenticatorValidate = StageComponent("ak-stage-authenticator-validate")
|
||||
StageIdentification = StageComponent("ak-stage-identification")
|
||||
StagePassword = StageComponent("ak-stage-password")
|
||||
StageAuthenticatorValidate = StageComponent("ak-stage-authenticator-validate")
|
||||
StageAccessDenied = StageComponent("ak-stage-access-denied")
|
||||
StageUserLogin = StageComponent("ak-stage-user-login")
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
@ -75,6 +75,7 @@ func NewFlowExecutor(ctx context.Context, flowSlug string, refConfig *api.Config
|
|||
StageIdentification: fe.solveChallenge_Identification,
|
||||
StagePassword: fe.solveChallenge_Password,
|
||||
StageAuthenticatorValidate: fe.solveChallenge_AuthenticatorValidate,
|
||||
StageUserLogin: fe.solveChallenge_UserLogin,
|
||||
}
|
||||
// Create new http client that also sets the correct ip
|
||||
config := api.NewConfiguration()
|
||||
|
|
|
@ -30,6 +30,11 @@ func (fe *FlowExecutor) solveChallenge_Password(challenge *api.ChallengeTypes, r
|
|||
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) {
|
||||
// We only support duo and code-based authenticators, check if that's allowed
|
||||
var deviceChallenge *api.DeviceChallenge
|
||||
|
|
62
schema.yml
62
schema.yml
|
@ -24994,6 +24994,10 @@ paths:
|
|||
description: Number of results to return per page.
|
||||
schema:
|
||||
type: integer
|
||||
- in: query
|
||||
name: remember_me_offset
|
||||
schema:
|
||||
type: string
|
||||
- name: search
|
||||
required: false
|
||||
in: query
|
||||
|
@ -27515,6 +27519,7 @@ components:
|
|||
- $ref: '#/components/schemas/PromptChallenge'
|
||||
- $ref: '#/components/schemas/RedirectChallenge'
|
||||
- $ref: '#/components/schemas/ShellChallenge'
|
||||
- $ref: '#/components/schemas/UserLoginChallenge'
|
||||
discriminator:
|
||||
propertyName: component
|
||||
mapping:
|
||||
|
@ -27540,6 +27545,7 @@ components:
|
|||
ak-stage-prompt: '#/components/schemas/PromptChallenge'
|
||||
xak-flow-redirect: '#/components/schemas/RedirectChallenge'
|
||||
xak-flow-shell: '#/components/schemas/ShellChallenge'
|
||||
ak-stage-user-login: '#/components/schemas/UserLoginChallenge'
|
||||
ClientTypeEnum:
|
||||
enum:
|
||||
- confidential
|
||||
|
@ -29023,6 +29029,7 @@ components:
|
|||
- $ref: '#/components/schemas/PasswordChallengeResponseRequest'
|
||||
- $ref: '#/components/schemas/PlexAuthenticationChallengeResponseRequest'
|
||||
- $ref: '#/components/schemas/PromptChallengeResponseRequest'
|
||||
- $ref: '#/components/schemas/UserLoginChallengeResponseRequest'
|
||||
discriminator:
|
||||
propertyName: component
|
||||
mapping:
|
||||
|
@ -29044,6 +29051,7 @@ components:
|
|||
ak-stage-password: '#/components/schemas/PasswordChallengeResponseRequest'
|
||||
ak-source-plex: '#/components/schemas/PlexAuthenticationChallengeResponseRequest'
|
||||
ak-stage-prompt: '#/components/schemas/PromptChallengeResponseRequest'
|
||||
ak-stage-user-login: '#/components/schemas/UserLoginChallengeResponseRequest'
|
||||
FlowDesignationEnum:
|
||||
enum:
|
||||
- authentication
|
||||
|
@ -36807,6 +36815,12 @@ components:
|
|||
terminate_other_sessions:
|
||||
type: boolean
|
||||
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:
|
||||
type: object
|
||||
description: UserLogoutStage Serializer
|
||||
|
@ -40170,6 +40184,43 @@ components:
|
|||
additionalProperties: {}
|
||||
required:
|
||||
- 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:
|
||||
type: object
|
||||
description: UserLoginStage Serializer
|
||||
|
@ -40208,6 +40259,11 @@ components:
|
|||
terminate_other_sessions:
|
||||
type: boolean
|
||||
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:
|
||||
- component
|
||||
- meta_model_name
|
||||
|
@ -40234,6 +40290,12 @@ components:
|
|||
terminate_other_sessions:
|
||||
type: boolean
|
||||
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:
|
||||
- name
|
||||
UserLogoutStage:
|
||||
|
|
|
@ -81,6 +81,22 @@ export class UserLoginStageForm extends ModelForm<UserLoginStage, string> {
|
|||
</a>
|
||||
</ak-alert>
|
||||
</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">
|
||||
<label class="pf-c-switch">
|
||||
<input
|
||||
|
|
|
@ -372,6 +372,12 @@ export class FlowExecutor extends Interface implements StageHost {
|
|||
.host=${this as StageHost}
|
||||
.challenge=${this.challenge}
|
||||
></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
|
||||
case "ak-source-plex":
|
||||
await import("@goauthentik/flow/sources/plex/PlexLoginInit");
|
||||
|
|
|
@ -32,7 +32,7 @@ export class BaseStage<Tin, Tout> extends AKElement {
|
|||
@property({ attribute: false })
|
||||
challenge!: Tin;
|
||||
|
||||
async submitForm(e: Event, defaults?: KeyUnknown): Promise<boolean> {
|
||||
async submitForm(e: Event, defaults?: Tout): Promise<boolean> {
|
||||
e.preventDefault();
|
||||
const object: KeyUnknown = defaults || {};
|
||||
const form = new FormData(this.shadowRoot?.querySelector("form") || undefined);
|
||||
|
|
|
@ -41,7 +41,9 @@ export class ConsentStage extends BaseStage<ConsentChallenge, ConsentChallengeRe
|
|||
renderNoPrevious(): TemplateResult {
|
||||
return html`
|
||||
<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
|
||||
? html`
|
||||
<p class="pf-u-mb-sm">
|
||||
|
@ -59,7 +61,9 @@ export class ConsentStage extends BaseStage<ConsentChallenge, ConsentChallengeRe
|
|||
renderAdditional(): TemplateResult {
|
||||
return html`
|
||||
<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
|
||||
? html`
|
||||
<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.
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
## 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
|
||||
|
||||
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
|
||||
---
|
||||
|
||||
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.
|
||||
|
||||
- [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)
|
||||
|
||||
#### Direct bind
|
||||
|
|
|
@ -171,7 +171,7 @@ module.exports = {
|
|||
"flow/stages/password/index",
|
||||
"flow/stages/prompt/index",
|
||||
"flow/stages/user_delete",
|
||||
"flow/stages/user_login",
|
||||
"flow/stages/user_login/index",
|
||||
"flow/stages/user_logout",
|
||||
"flow/stages/user_write",
|
||||
],
|
||||
|
|
Reference in New Issue