Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

providers/radius: TOTP MFA support #7217

Merged
merged 2 commits into from
Oct 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -37474,6 +37474,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 @@ -39369,6 +39376,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 @@ -39444,6 +39458,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 @@ -39489,6 +39510,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