Skip to content

Commit

Permalink
providers/radius: TOTP MFA support (#7217)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
BeryJu committed Oct 18, 2023
1 parent 3c734da commit 8aafa06
Show file tree
Hide file tree
Showing 11 changed files with 145 additions and 50 deletions.
2 changes: 2 additions & 0 deletions authentik/providers/radius/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class Meta:
# an admin might have to view it
"shared_secret",
"outpost_set",
"mfa_support",
]
extra_kwargs = ProviderSerializer.Meta.extra_kwargs

Expand Down Expand Up @@ -55,6 +56,7 @@ class Meta:
"auth_flow_slug",
"client_networks",
"shared_secret",
"mfa_support",
]


Expand Down
Original file line number Diff line number Diff line change
@@ -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",
),
),
]
11 changes: 11 additions & 0 deletions authentik/providers/radius/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
def launch_url(self) -> Optional[str]:
"""Radius never has a launch URL"""
Expand Down
5 changes: 5 additions & 0 deletions blueprints/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -4794,6 +4794,11 @@
"minLength": 1,
"title": "Shared secret",
"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": []
Expand Down
48 changes: 48 additions & 0 deletions internal/outpost/flow/solvers_mfa.go
Original file line number Diff line number Diff line change
@@ -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
}
49 changes: 3 additions & 46 deletions internal/outpost/ldap/bind/direct/bind.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@ package direct

import (
"context"
"regexp"
"strconv"
"strings"

"beryju.io/ldap"
"github.com/getsentry/sentry-go"
Expand All @@ -16,10 +13,6 @@ import (
"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) {
fe := flow.NewFlowExecutor(req.Context(), db.si.GetAuthenticationFlowSlug(), db.si.GetAPIClient().GetConfig(), log.Fields{
"bindDN": req.BindDN,
Expand All @@ -31,7 +24,9 @@ func (db *DirectBinder) Bind(username string, req *bind.Request) (ldap.LDAPResul

fe.Answers[flow.StageIdentification] = username
fe.Answers[flow.StagePassword] = req.BindPW
db.CheckPasswordMFA(fe)
if db.si.GetMFASupport() {
fe.CheckPasswordInlineMFA()
}

passed, err := fe.Execute()
flags := flags.UserFlags{
Expand Down Expand Up @@ -141,41 +136,3 @@ func (db *DirectBinder) Bind(username string, req *bind.Request) (ldap.LDAPResul
uisp.Finish()
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
}
7 changes: 3 additions & 4 deletions internal/outpost/radius/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,10 @@ func (rs *RadiusServer) Refresh() error {
providers := make([]*ProviderInstance, len(outposts.Results))
for idx, provider := range outposts.Results {
logger := log.WithField("logger", "authentik.outpost.radius").WithField("provider", provider.Name)
s := *provider.SharedSecret
c := *provider.ClientNetworks
providers[idx] = &ProviderInstance{
SharedSecret: []byte(s),
ClientNetworks: parseCIDRs(c),
SharedSecret: []byte(provider.GetSharedSecret()),
ClientNetworks: parseCIDRs(provider.GetClientNetworks()),
MFASupport: provider.GetMfaSupport(),
appSlug: provider.ApplicationSlug,
flowSlug: provider.AuthFlowSlug,
s: rs,
Expand Down
3 changes: 3 additions & 0 deletions internal/outpost/radius/handle_access_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ func (rs *RadiusServer) Handle_AccessRequest(w radius.ResponseWriter, r *RadiusR

fe.Answers[flow.StageIdentification] = username
fe.Answers[flow.StagePassword] = rfc2865.UserPassword_GetString(r.Packet)
if r.pi.MFASupport {
fe.CheckPasswordInlineMFA()
}

passed, err := fe.Execute()

Expand Down
1 change: 1 addition & 0 deletions internal/outpost/radius/radius.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
type ProviderInstance struct {
ClientNetworks []*net.IPNet
SharedSecret []byte
MFASupport bool

appSlug string
flowSlug string
Expand Down
28 changes: 28 additions & 0 deletions schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37484,6 +37484,13 @@ components:
type: string
minLength: 1
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:
type: object
description: Reputation Policy Serializer
Expand Down Expand Up @@ -39379,6 +39386,13 @@ components:
shared_secret:
type: string
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:
- application_slug
- auth_flow_slug
Expand Down Expand Up @@ -39454,6 +39468,13 @@ components:
items:
type: string
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:
- assigned_application_name
- assigned_application_slug
Expand Down Expand Up @@ -39499,6 +39520,13 @@ components:
type: string
minLength: 1
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:
- authorization_flow
- name
Expand Down
20 changes: 20 additions & 0 deletions web/src/admin/providers/radius/RadiusProviderForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,26 @@ export class RadiusProviderFormPage extends ModelForm<RadiusProvider, number> {
></ak-tenanted-flow-search>
<p class="pf-c-form__helper-text">${msg("Flow used for users to authenticate.")}</p>
</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}>
<span slot="header"> ${msg("Protocol settings")} </span>
Expand Down

0 comments on commit 8aafa06

Please sign in to comment.