providers/radius: TOTP MFA support (#7217)

* move CheckPasswordMFA to flow executor

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

* add mfa support field to radius

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L 2023-10-18 19:43:36 +02:00 committed by GitHub
parent 3c734da86a
commit 8aafa06259
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 145 additions and 50 deletions

View File

@ -21,6 +21,7 @@ class RadiusProviderSerializer(ProviderSerializer):
# an admin might have to view it # an admin might have to view it
"shared_secret", "shared_secret",
"outpost_set", "outpost_set",
"mfa_support",
] ]
extra_kwargs = ProviderSerializer.Meta.extra_kwargs extra_kwargs = ProviderSerializer.Meta.extra_kwargs
@ -55,6 +56,7 @@ class RadiusOutpostConfigSerializer(ModelSerializer):
"auth_flow_slug", "auth_flow_slug",
"client_networks", "client_networks",
"shared_secret", "shared_secret",
"mfa_support",
] ]

View File

@ -0,0 +1,21 @@
# Generated by Django 4.2.6 on 2023-10-18 15:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_radius", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="radiusprovider",
name="mfa_support",
field=models.BooleanField(
default=True,
help_text="When enabled, code-based multi-factor authentication can be used by appending a semicolon and the TOTP code to the password. This should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon.",
verbose_name="MFA Support",
),
),
]

View File

@ -27,6 +27,17 @@ class RadiusProvider(OutpostModel, Provider):
), ),
) )
mfa_support = models.BooleanField(
default=True,
verbose_name="MFA Support",
help_text=_(
"When enabled, code-based multi-factor authentication can be used by appending a "
"semicolon and the TOTP code to the password. This should only be enabled if all "
"users that will bind to this provider have a TOTP device configured, as otherwise "
"a password may incorrectly be rejected if it contains a semicolon."
),
)
@property @property
def launch_url(self) -> Optional[str]: def launch_url(self) -> Optional[str]:
"""Radius never has a launch URL""" """Radius never has a launch URL"""

View File

@ -4794,6 +4794,11 @@
"minLength": 1, "minLength": 1,
"title": "Shared secret", "title": "Shared secret",
"description": "Shared secret between clients and server to hash packets." "description": "Shared secret between clients and server to hash packets."
},
"mfa_support": {
"type": "boolean",
"title": "MFA Support",
"description": "When enabled, code-based multi-factor authentication can be used by appending a semicolon and the TOTP code to the password. This should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon."
} }
}, },
"required": [] "required": []

View File

@ -0,0 +1,48 @@
package flow
import (
"regexp"
"strconv"
"strings"
)
const CodePasswordSeparator = ";"
var alphaNum = regexp.MustCompile(`^[a-zA-Z0-9]*$`)
// CheckPasswordInlineMFA For protocols that only support username/password, check if the password
// contains the TOTP code
func (fe *FlowExecutor) CheckPasswordInlineMFA() {
password := fe.Answers[StagePassword]
// We already have an authenticator answer
if fe.Answers[StageAuthenticatorValidate] != "" {
return
}
// password doesn't contain the separator
if !strings.Contains(password, CodePasswordSeparator) {
return
}
// password ends with the separator, so it won't contain an answer
if strings.HasSuffix(password, CodePasswordSeparator) {
return
}
idx := strings.LastIndex(password, CodePasswordSeparator)
authenticator := password[idx+1:]
// Authenticator is either 6 chars (totp code) or 8 chars (long totp or static)
if len(authenticator) == 6 {
// authenticator answer isn't purely numerical, so won't be value
if _, err := strconv.Atoi(authenticator); err != nil {
return
}
} else if len(authenticator) == 8 {
// 8 chars can be a long totp or static token, so it needs to be alphanumerical
if !alphaNum.MatchString(authenticator) {
return
}
} else {
// Any other length, doesn't contain an answer
return
}
fe.Answers[StagePassword] = password[:idx]
fe.Answers[StageAuthenticatorValidate] = authenticator
}

View File

@ -2,9 +2,6 @@ package direct
import ( import (
"context" "context"
"regexp"
"strconv"
"strings"
"beryju.io/ldap" "beryju.io/ldap"
"github.com/getsentry/sentry-go" "github.com/getsentry/sentry-go"
@ -16,10 +13,6 @@ import (
"goauthentik.io/internal/outpost/ldap/metrics" "goauthentik.io/internal/outpost/ldap/metrics"
) )
const CodePasswordSeparator = ";"
var alphaNum = regexp.MustCompile(`^[a-zA-Z0-9]*$`)
func (db *DirectBinder) Bind(username string, req *bind.Request) (ldap.LDAPResultCode, error) { func (db *DirectBinder) Bind(username string, req *bind.Request) (ldap.LDAPResultCode, error) {
fe := flow.NewFlowExecutor(req.Context(), db.si.GetAuthenticationFlowSlug(), db.si.GetAPIClient().GetConfig(), log.Fields{ fe := flow.NewFlowExecutor(req.Context(), db.si.GetAuthenticationFlowSlug(), db.si.GetAPIClient().GetConfig(), log.Fields{
"bindDN": req.BindDN, "bindDN": req.BindDN,
@ -31,7 +24,9 @@ func (db *DirectBinder) Bind(username string, req *bind.Request) (ldap.LDAPResul
fe.Answers[flow.StageIdentification] = username fe.Answers[flow.StageIdentification] = username
fe.Answers[flow.StagePassword] = req.BindPW fe.Answers[flow.StagePassword] = req.BindPW
db.CheckPasswordMFA(fe) if db.si.GetMFASupport() {
fe.CheckPasswordInlineMFA()
}
passed, err := fe.Execute() passed, err := fe.Execute()
flags := flags.UserFlags{ flags := flags.UserFlags{
@ -141,41 +136,3 @@ func (db *DirectBinder) Bind(username string, req *bind.Request) (ldap.LDAPResul
uisp.Finish() uisp.Finish()
return ldap.LDAPResultSuccess, nil return ldap.LDAPResultSuccess, nil
} }
func (db *DirectBinder) CheckPasswordMFA(fe *flow.FlowExecutor) {
if !db.si.GetMFASupport() {
return
}
password := fe.Answers[flow.StagePassword]
// We already have an authenticator answer
if fe.Answers[flow.StageAuthenticatorValidate] != "" {
return
}
// password doesn't contain the separator
if !strings.Contains(password, CodePasswordSeparator) {
return
}
// password ends with the separator, so it won't contain an answer
if strings.HasSuffix(password, CodePasswordSeparator) {
return
}
idx := strings.LastIndex(password, CodePasswordSeparator)
authenticator := password[idx+1:]
// Authenticator is either 6 chars (totp code) or 8 chars (long totp or static)
if len(authenticator) == 6 {
// authenticator answer isn't purely numerical, so won't be value
if _, err := strconv.Atoi(authenticator); err != nil {
return
}
} else if len(authenticator) == 8 {
// 8 chars can be a long totp or static token, so it needs to be alphanumerical
if !alphaNum.MatchString(authenticator) {
return
}
} else {
// Any other length, doesn't contain an answer
return
}
fe.Answers[flow.StagePassword] = password[:idx]
fe.Answers[flow.StageAuthenticatorValidate] = authenticator
}

View File

@ -40,11 +40,10 @@ func (rs *RadiusServer) Refresh() error {
providers := make([]*ProviderInstance, len(outposts.Results)) providers := make([]*ProviderInstance, len(outposts.Results))
for idx, provider := range outposts.Results { for idx, provider := range outposts.Results {
logger := log.WithField("logger", "authentik.outpost.radius").WithField("provider", provider.Name) logger := log.WithField("logger", "authentik.outpost.radius").WithField("provider", provider.Name)
s := *provider.SharedSecret
c := *provider.ClientNetworks
providers[idx] = &ProviderInstance{ providers[idx] = &ProviderInstance{
SharedSecret: []byte(s), SharedSecret: []byte(provider.GetSharedSecret()),
ClientNetworks: parseCIDRs(c), ClientNetworks: parseCIDRs(provider.GetClientNetworks()),
MFASupport: provider.GetMfaSupport(),
appSlug: provider.ApplicationSlug, appSlug: provider.ApplicationSlug,
flowSlug: provider.AuthFlowSlug, flowSlug: provider.AuthFlowSlug,
s: rs, s: rs,

View File

@ -22,6 +22,9 @@ func (rs *RadiusServer) Handle_AccessRequest(w radius.ResponseWriter, r *RadiusR
fe.Answers[flow.StageIdentification] = username fe.Answers[flow.StageIdentification] = username
fe.Answers[flow.StagePassword] = rfc2865.UserPassword_GetString(r.Packet) fe.Answers[flow.StagePassword] = rfc2865.UserPassword_GetString(r.Packet)
if r.pi.MFASupport {
fe.CheckPasswordInlineMFA()
}
passed, err := fe.Execute() passed, err := fe.Execute()

View File

@ -17,6 +17,7 @@ import (
type ProviderInstance struct { type ProviderInstance struct {
ClientNetworks []*net.IPNet ClientNetworks []*net.IPNet
SharedSecret []byte SharedSecret []byte
MFASupport bool
appSlug string appSlug string
flowSlug string flowSlug string

View File

@ -37484,6 +37484,13 @@ components:
type: string type: string
minLength: 1 minLength: 1
description: Shared secret between clients and server to hash packets. description: Shared secret between clients and server to hash packets.
mfa_support:
type: boolean
description: When enabled, code-based multi-factor authentication can be
used by appending a semicolon and the TOTP code to the password. This
should only be enabled if all users that will bind to this provider have
a TOTP device configured, as otherwise a password may incorrectly be rejected
if it contains a semicolon.
PatchedReputationPolicyRequest: PatchedReputationPolicyRequest:
type: object type: object
description: Reputation Policy Serializer description: Reputation Policy Serializer
@ -39379,6 +39386,13 @@ components:
shared_secret: shared_secret:
type: string type: string
description: Shared secret between clients and server to hash packets. description: Shared secret between clients and server to hash packets.
mfa_support:
type: boolean
description: When enabled, code-based multi-factor authentication can be
used by appending a semicolon and the TOTP code to the password. This
should only be enabled if all users that will bind to this provider have
a TOTP device configured, as otherwise a password may incorrectly be rejected
if it contains a semicolon.
required: required:
- application_slug - application_slug
- auth_flow_slug - auth_flow_slug
@ -39454,6 +39468,13 @@ components:
items: items:
type: string type: string
readOnly: true readOnly: true
mfa_support:
type: boolean
description: When enabled, code-based multi-factor authentication can be
used by appending a semicolon and the TOTP code to the password. This
should only be enabled if all users that will bind to this provider have
a TOTP device configured, as otherwise a password may incorrectly be rejected
if it contains a semicolon.
required: required:
- assigned_application_name - assigned_application_name
- assigned_application_slug - assigned_application_slug
@ -39499,6 +39520,13 @@ components:
type: string type: string
minLength: 1 minLength: 1
description: Shared secret between clients and server to hash packets. description: Shared secret between clients and server to hash packets.
mfa_support:
type: boolean
description: When enabled, code-based multi-factor authentication can be
used by appending a semicolon and the TOTP code to the password. This
should only be enabled if all users that will bind to this provider have
a TOTP device configured, as otherwise a password may incorrectly be rejected
if it contains a semicolon.
required: required:
- authorization_flow - authorization_flow
- name - name

View File

@ -70,6 +70,26 @@ export class RadiusProviderFormPage extends ModelForm<RadiusProvider, number> {
></ak-tenanted-flow-search> ></ak-tenanted-flow-search>
<p class="pf-c-form__helper-text">${msg("Flow used for users to authenticate.")}</p> <p class="pf-c-form__helper-text">${msg("Flow used for users to authenticate.")}</p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal name="mfaSupport">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.mfaSupport, true)}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label">${msg("Code-based MFA Support")}</span>
</label>
<p class="pf-c-form__helper-text">
${msg(
"When enabled, code-based multi-factor authentication can be used by appending a semicolon and the TOTP code to the password. This should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-group .expanded=${true}> <ak-form-group .expanded=${true}>
<span slot="header"> ${msg("Protocol settings")} </span> <span slot="header"> ${msg("Protocol settings")} </span>