Compare commits
1 Commits
eap-but-ac
...
website/do
Author | SHA1 | Date | |
---|---|---|---|
cddc1c0478 |
@ -44,7 +44,6 @@ class RadiusProviderSerializer(ProviderSerializer):
|
||||
"shared_secret",
|
||||
"outpost_set",
|
||||
"mfa_support",
|
||||
"certificate",
|
||||
]
|
||||
extra_kwargs = ProviderSerializer.Meta.extra_kwargs
|
||||
|
||||
@ -80,7 +79,6 @@ class RadiusOutpostConfigSerializer(ModelSerializer):
|
||||
"client_networks",
|
||||
"shared_secret",
|
||||
"mfa_support",
|
||||
"certificate",
|
||||
]
|
||||
|
||||
|
||||
|
@ -1,25 +0,0 @@
|
||||
# Generated by Django 5.1.9 on 2025-05-16 13:53
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_crypto", "0004_alter_certificatekeypair_name"),
|
||||
("authentik_providers_radius", "0004_alter_radiusproviderpropertymapping_options"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="radiusprovider",
|
||||
name="certificate",
|
||||
field=models.ForeignKey(
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="authentik_crypto.certificatekeypair",
|
||||
),
|
||||
),
|
||||
]
|
@ -1,14 +1,11 @@
|
||||
"""Radius Provider"""
|
||||
|
||||
from collections.abc import Iterable
|
||||
|
||||
from django.db import models
|
||||
from django.templatetags.static import static
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
from authentik.core.models import PropertyMapping, Provider
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.outposts.models import OutpostModel
|
||||
|
||||
@ -41,10 +38,6 @@ class RadiusProvider(OutpostModel, Provider):
|
||||
),
|
||||
)
|
||||
|
||||
certificate = models.ForeignKey(
|
||||
CertificateKeyPair, on_delete=models.CASCADE, default=None, null=True
|
||||
)
|
||||
|
||||
@property
|
||||
def launch_url(self) -> str | None:
|
||||
"""Radius never has a launch URL"""
|
||||
@ -64,12 +57,6 @@ class RadiusProvider(OutpostModel, Provider):
|
||||
|
||||
return RadiusProviderSerializer
|
||||
|
||||
def get_required_objects(self) -> Iterable[models.Model | str]:
|
||||
required_models = [self, "authentik_stages_mtls.pass_outpost_certificate"]
|
||||
if self.certificate is not None:
|
||||
required_models.append(self.certificate)
|
||||
return required_models
|
||||
|
||||
def __str__(self):
|
||||
return f"Radius Provider {self.name}"
|
||||
|
||||
|
@ -8953,11 +8953,6 @@
|
||||
"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."
|
||||
},
|
||||
"certificate": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Certificate"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
|
@ -34,10 +34,9 @@ var (
|
||||
type SolverFunction func(*api.ChallengeTypes, api.ApiFlowsExecutorSolveRequest) (api.FlowChallengeResponseRequest, error)
|
||||
|
||||
type FlowExecutor struct {
|
||||
Params url.Values
|
||||
Answers map[StageComponent]string
|
||||
Context context.Context
|
||||
InteractiveSolver SolverFunction
|
||||
Params url.Values
|
||||
Answers map[StageComponent]string
|
||||
Context context.Context
|
||||
|
||||
solvers map[StageComponent]SolverFunction
|
||||
|
||||
@ -95,10 +94,6 @@ func NewFlowExecutor(ctx context.Context, flowSlug string, refConfig *api.Config
|
||||
return fe
|
||||
}
|
||||
|
||||
func (fe *FlowExecutor) AddHeader(name string, value string) {
|
||||
fe.api.GetConfig().AddDefaultHeader(name, value)
|
||||
}
|
||||
|
||||
func (fe *FlowExecutor) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
res, err := fe.transport.RoundTrip(req)
|
||||
if res != nil {
|
||||
@ -115,7 +110,7 @@ func (fe *FlowExecutor) ApiClient() *api.APIClient {
|
||||
return fe.api
|
||||
}
|
||||
|
||||
type ChallengeCommon interface {
|
||||
type challengeCommon interface {
|
||||
GetComponent() string
|
||||
GetResponseErrors() map[string][]api.ErrorDetail
|
||||
}
|
||||
@ -170,7 +165,7 @@ func (fe *FlowExecutor) getInitialChallenge() (*api.ChallengeTypes, error) {
|
||||
if i == nil {
|
||||
return nil, errors.New("response instance was null")
|
||||
}
|
||||
ch := i.(ChallengeCommon)
|
||||
ch := i.(challengeCommon)
|
||||
fe.log.WithField("component", ch.GetComponent()).Debug("Got challenge")
|
||||
gcsp.SetTag("authentik.flow.component", ch.GetComponent())
|
||||
gcsp.Finish()
|
||||
@ -189,7 +184,7 @@ func (fe *FlowExecutor) solveFlowChallenge(challenge *api.ChallengeTypes, depth
|
||||
if i == nil {
|
||||
return false, errors.New("response request instance was null")
|
||||
}
|
||||
ch := i.(ChallengeCommon)
|
||||
ch := i.(challengeCommon)
|
||||
|
||||
// Check for any validation errors that we might've gotten
|
||||
if len(ch.GetResponseErrors()) > 0 {
|
||||
@ -206,17 +201,11 @@ func (fe *FlowExecutor) solveFlowChallenge(challenge *api.ChallengeTypes, depth
|
||||
case string(StageRedirect):
|
||||
return true, nil
|
||||
default:
|
||||
var err error
|
||||
var rr api.FlowChallengeResponseRequest
|
||||
if fe.InteractiveSolver != nil {
|
||||
rr, err = fe.InteractiveSolver(challenge, responseReq)
|
||||
} else {
|
||||
solver, ok := fe.solvers[StageComponent(ch.GetComponent())]
|
||||
if !ok {
|
||||
return false, fmt.Errorf("unsupported challenge type %s", ch.GetComponent())
|
||||
}
|
||||
rr, err = solver(challenge, responseReq)
|
||||
solver, ok := fe.solvers[StageComponent(ch.GetComponent())]
|
||||
if !ok {
|
||||
return false, fmt.Errorf("unsupported challenge type %s", ch.GetComponent())
|
||||
}
|
||||
rr, err := solver(challenge, responseReq)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -231,7 +220,7 @@ func (fe *FlowExecutor) solveFlowChallenge(challenge *api.ChallengeTypes, depth
|
||||
if i == nil {
|
||||
return false, errors.New("response instance was null")
|
||||
}
|
||||
ch = i.(ChallengeCommon)
|
||||
ch = i.(challengeCommon)
|
||||
fe.log.WithField("component", ch.GetComponent()).Debug("Got response")
|
||||
scsp.SetTag("authentik.flow.component", ch.GetComponent())
|
||||
scsp.Finish()
|
||||
|
@ -8,6 +8,6 @@ import (
|
||||
)
|
||||
|
||||
func TestConvert(t *testing.T) {
|
||||
var a ChallengeCommon = api.NewIdentificationChallengeWithDefaults()
|
||||
var a challengeCommon = api.NewIdentificationChallengeWithDefaults()
|
||||
assert.NotNil(t, a)
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ import (
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/outpost/ak"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
func parseCIDRs(raw string) []*net.IPNet {
|
||||
@ -42,28 +41,26 @@ func (rs *RadiusServer) Refresh() error {
|
||||
if len(apiProviders) < 1 {
|
||||
return errors.New("no radius provider defined")
|
||||
}
|
||||
providers := make(map[int32]*ProviderInstance)
|
||||
for _, provider := range apiProviders {
|
||||
existing, ok := rs.providers[provider.Pk]
|
||||
state := map[string]*protocol.State{}
|
||||
if ok {
|
||||
state = existing.eapState
|
||||
}
|
||||
providers := make([]*ProviderInstance, len(apiProviders))
|
||||
for idx, provider := range apiProviders {
|
||||
logger := log.WithField("logger", "authentik.outpost.radius").WithField("provider", provider.Name)
|
||||
providers[provider.Pk] = &ProviderInstance{
|
||||
providers[idx] = &ProviderInstance{
|
||||
SharedSecret: []byte(provider.GetSharedSecret()),
|
||||
ClientNetworks: parseCIDRs(provider.GetClientNetworks()),
|
||||
MFASupport: provider.GetMfaSupport(),
|
||||
appSlug: provider.ApplicationSlug,
|
||||
flowSlug: provider.AuthFlowSlug,
|
||||
certId: provider.GetCertificate(),
|
||||
providerId: provider.Pk,
|
||||
s: rs,
|
||||
log: logger,
|
||||
eapState: state,
|
||||
}
|
||||
}
|
||||
rs.providers = providers
|
||||
rs.log.Info("Update providers")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rs *RadiusServer) StartRadiusServer() error {
|
||||
rs.log.WithField("listen", rs.s.Addr).Info("Starting radius server")
|
||||
return rs.s.ListenAndServe()
|
||||
}
|
||||
|
@ -1,44 +0,0 @@
|
||||
# EAP protocol implementation
|
||||
|
||||
Install `eapol_test` (`sudo apt install eapoltest`)
|
||||
|
||||
Both PEAP and EAP-TLS require a minimal PKI setup. A CA, a certificate for the server and for EAP-TLS a client certificate need to be provided.
|
||||
|
||||
Save either of the config files below and run eapoltest like so:
|
||||
|
||||
```
|
||||
# peap.conf is the config file under the PEAP testing section
|
||||
# foo is the shared RADIUS secret
|
||||
# 1.2.3.4 is the IP of the RADIUS server
|
||||
eapol_test -c peap.conf -s foo -a 1.2.3.4
|
||||
```
|
||||
|
||||
### PEAP testing
|
||||
|
||||
```
|
||||
network={
|
||||
ssid="DoesNotMatterForThisTest"
|
||||
key_mgmt=WPA-EAP
|
||||
eap=PEAP
|
||||
identity="foo"
|
||||
password="bar"
|
||||
ca_cert="ca.pem"
|
||||
phase2="auth=MSCHAPV2"
|
||||
}
|
||||
```
|
||||
|
||||
### EAP-TLS testing
|
||||
|
||||
```
|
||||
network={
|
||||
ssid="DoesNotMatterForThisTest"
|
||||
key_mgmt=WPA-EAP
|
||||
eap=TLS
|
||||
identity="foo"
|
||||
ca_cert="ca.pem"
|
||||
client_cert="cert_client.pem"
|
||||
private_key="cert_client.key"
|
||||
eapol_flags=3
|
||||
eap_workaround=0
|
||||
}
|
||||
```
|
@ -1,55 +0,0 @@
|
||||
package eap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
"layeh.com/radius"
|
||||
)
|
||||
|
||||
type context struct {
|
||||
req *radius.Request
|
||||
rootPayload protocol.Payload
|
||||
typeState map[protocol.Type]any
|
||||
log *log.Entry
|
||||
settings interface{}
|
||||
parent *context
|
||||
endStatus protocol.Status
|
||||
handleInner func(protocol.Payload, protocol.StateManager, protocol.Context) (protocol.Payload, error)
|
||||
}
|
||||
|
||||
func (ctx *context) RootPayload() protocol.Payload { return ctx.rootPayload }
|
||||
func (ctx *context) Packet() *radius.Request { return ctx.req }
|
||||
func (ctx *context) ProtocolSettings() any { return ctx.settings }
|
||||
func (ctx *context) GetProtocolState(p protocol.Type) any { return ctx.typeState[p] }
|
||||
func (ctx *context) SetProtocolState(p protocol.Type, st any) { ctx.typeState[p] = st }
|
||||
func (ctx *context) IsProtocolStart(p protocol.Type) bool { return ctx.typeState[p] == nil }
|
||||
func (ctx *context) Log() *log.Entry { return ctx.log }
|
||||
func (ctx *context) HandleInnerEAP(p protocol.Payload, st protocol.StateManager) (protocol.Payload, error) {
|
||||
return ctx.handleInner(p, st, ctx)
|
||||
}
|
||||
func (ctx *context) Inner(p protocol.Payload, t protocol.Type) protocol.Context {
|
||||
nctx := &context{
|
||||
req: ctx.req,
|
||||
rootPayload: ctx.rootPayload,
|
||||
typeState: ctx.typeState,
|
||||
log: ctx.log.WithField("type", fmt.Sprintf("%T", p)).WithField("code", t),
|
||||
settings: ctx.settings,
|
||||
parent: ctx,
|
||||
handleInner: ctx.handleInner,
|
||||
}
|
||||
nctx.log.Debug("Creating inner context")
|
||||
return nctx
|
||||
}
|
||||
func (ctx *context) EndInnerProtocol(st protocol.Status) {
|
||||
ctx.log.Info("Ending protocol")
|
||||
if ctx.parent != nil {
|
||||
ctx.parent.EndInnerProtocol(st)
|
||||
return
|
||||
}
|
||||
if ctx.endStatus != protocol.StatusUnknown {
|
||||
return
|
||||
}
|
||||
ctx.endStatus = st
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func FormatBytes(d []byte) string {
|
||||
b := d
|
||||
if len(b) > 32 {
|
||||
b = b[:32]
|
||||
}
|
||||
return fmt.Sprintf("% x", b)
|
||||
}
|
@ -1,182 +0,0 @@
|
||||
package eap
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/gorilla/securecookie"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/eap"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/legacy_nak"
|
||||
"layeh.com/radius"
|
||||
"layeh.com/radius/rfc2865"
|
||||
"layeh.com/radius/rfc2869"
|
||||
)
|
||||
|
||||
func sendErrorResponse(w radius.ResponseWriter, r *radius.Request) {
|
||||
rres := r.Response(radius.CodeAccessReject)
|
||||
err := w.Write(rres)
|
||||
if err != nil {
|
||||
log.WithError(err).Warning("failed to send response")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Packet) HandleRadiusPacket(w radius.ResponseWriter, r *radius.Request) {
|
||||
p.r = r
|
||||
rst := rfc2865.State_GetString(r.Packet)
|
||||
if rst == "" {
|
||||
rst = base64.StdEncoding.EncodeToString(securecookie.GenerateRandomKey(12))
|
||||
}
|
||||
p.state = rst
|
||||
|
||||
rp := &Packet{r: r}
|
||||
rep, err := p.handleEAP(p.eap, p.stm, nil)
|
||||
rp.eap = rep
|
||||
|
||||
rres := r.Response(radius.CodeAccessReject)
|
||||
if err == nil {
|
||||
switch rp.eap.Code {
|
||||
case protocol.CodeRequest:
|
||||
rres.Code = radius.CodeAccessChallenge
|
||||
case protocol.CodeFailure:
|
||||
rres.Code = radius.CodeAccessReject
|
||||
case protocol.CodeSuccess:
|
||||
rres.Code = radius.CodeAccessAccept
|
||||
}
|
||||
} else {
|
||||
rres.Code = radius.CodeAccessReject
|
||||
log.WithError(err).Debug("Rejecting request")
|
||||
}
|
||||
for _, mod := range p.responseModifiers {
|
||||
err := mod.ModifyRADIUSResponse(rres, r.Packet)
|
||||
if err != nil {
|
||||
log.WithError(err).Warning("Root-EAP: failed to modify response packet")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
rfc2865.State_SetString(rres, p.state)
|
||||
eapEncoded, err := rp.Encode()
|
||||
if err != nil {
|
||||
log.WithError(err).Warning("failed to encode response")
|
||||
sendErrorResponse(w, r)
|
||||
return
|
||||
}
|
||||
log.WithField("length", len(eapEncoded)).WithField("type", fmt.Sprintf("%T", rp.eap.Payload)).Debug("Root-EAP: encapsulated challenge")
|
||||
rfc2869.EAPMessage_Set(rres, eapEncoded)
|
||||
err = p.setMessageAuthenticator(rres)
|
||||
if err != nil {
|
||||
log.WithError(err).Warning("failed to send message authenticator")
|
||||
sendErrorResponse(w, r)
|
||||
return
|
||||
}
|
||||
err = w.Write(rres)
|
||||
if err != nil {
|
||||
log.WithError(err).Warning("failed to send response")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Packet) handleEAP(pp protocol.Payload, stm protocol.StateManager, parentContext *context) (*eap.Payload, error) {
|
||||
st := stm.GetEAPState(p.state)
|
||||
if st == nil {
|
||||
log.Debug("Root-EAP: blank state")
|
||||
st = protocol.BlankState(stm.GetEAPSettings())
|
||||
}
|
||||
|
||||
nextChallengeToOffer, err := st.GetNextProtocol()
|
||||
if err != nil {
|
||||
return &eap.Payload{
|
||||
Code: protocol.CodeFailure,
|
||||
ID: p.eap.ID,
|
||||
}, err
|
||||
}
|
||||
|
||||
next := func() (*eap.Payload, error) {
|
||||
st.ProtocolIndex += 1
|
||||
st.TypeState = map[protocol.Type]any{}
|
||||
stm.SetEAPState(p.state, st)
|
||||
return p.handleEAP(pp, stm, nil)
|
||||
}
|
||||
|
||||
if n, ok := pp.(*eap.Payload).Payload.(*legacy_nak.Payload); ok {
|
||||
log.WithField("desired", n.DesiredType).Debug("Root-EAP: received NAK, trying next protocol")
|
||||
pp.(*eap.Payload).Payload = nil
|
||||
return next()
|
||||
}
|
||||
|
||||
np, t, _ := eap.EmptyPayload(stm.GetEAPSettings(), nextChallengeToOffer)
|
||||
|
||||
var ctx *context
|
||||
if parentContext != nil {
|
||||
ctx = parentContext.Inner(np, t).(*context)
|
||||
ctx.settings = stm.GetEAPSettings().ProtocolSettings[np.Type()]
|
||||
} else {
|
||||
ctx = &context{
|
||||
req: p.r,
|
||||
rootPayload: p.eap,
|
||||
typeState: st.TypeState,
|
||||
log: log.WithField("type", fmt.Sprintf("%T", np)).WithField("code", t),
|
||||
settings: stm.GetEAPSettings().ProtocolSettings[t],
|
||||
}
|
||||
ctx.handleInner = func(pp protocol.Payload, sm protocol.StateManager, ctx protocol.Context) (protocol.Payload, error) {
|
||||
// cctx := ctx.Inner(np, np.Type(), nil).(*context)
|
||||
return p.handleEAP(pp, sm, ctx.(*context))
|
||||
}
|
||||
}
|
||||
if !np.Offerable() {
|
||||
ctx.Log().Debug("Root-EAP: protocol not offerable, skipping")
|
||||
return next()
|
||||
}
|
||||
ctx.Log().Debug("Root-EAP: Passing to protocol")
|
||||
|
||||
res := &eap.Payload{
|
||||
Code: protocol.CodeRequest,
|
||||
ID: p.eap.ID + 1,
|
||||
MsgType: t,
|
||||
}
|
||||
var payload any
|
||||
if reflect.TypeOf(pp.(*eap.Payload).Payload) == reflect.TypeOf(np) {
|
||||
np.Decode(pp.(*eap.Payload).RawPayload)
|
||||
}
|
||||
payload = np.Handle(ctx)
|
||||
if payload != nil {
|
||||
res.Payload = payload.(protocol.Payload)
|
||||
}
|
||||
|
||||
stm.SetEAPState(p.state, st)
|
||||
|
||||
if rm, ok := np.(protocol.ResponseModifier); ok {
|
||||
ctx.log.Debug("Root-EAP: Registered response modifier")
|
||||
p.responseModifiers = append(p.responseModifiers, rm)
|
||||
}
|
||||
|
||||
switch ctx.endStatus {
|
||||
case protocol.StatusSuccess:
|
||||
res.Code = protocol.CodeSuccess
|
||||
res.ID -= 1
|
||||
case protocol.StatusError:
|
||||
res.Code = protocol.CodeFailure
|
||||
res.ID -= 1
|
||||
case protocol.StatusNextProtocol:
|
||||
ctx.log.Debug("Root-EAP: Protocol ended, starting next protocol")
|
||||
return next()
|
||||
case protocol.StatusUnknown:
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (p *Packet) setMessageAuthenticator(rp *radius.Packet) error {
|
||||
_ = rfc2869.MessageAuthenticator_Set(rp, make([]byte, 16))
|
||||
hash := hmac.New(md5.New, rp.Secret)
|
||||
encode, err := rp.MarshalBinary()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hash.Write(encode)
|
||||
_ = rfc2869.MessageAuthenticator_Set(rp, hash.Sum(nil))
|
||||
return nil
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
package eap
|
||||
|
||||
import (
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/eap"
|
||||
"layeh.com/radius"
|
||||
)
|
||||
|
||||
type Packet struct {
|
||||
r *radius.Request
|
||||
eap *eap.Payload
|
||||
stm protocol.StateManager
|
||||
state string
|
||||
responseModifiers []protocol.ResponseModifier
|
||||
}
|
||||
|
||||
func Decode(stm protocol.StateManager, raw []byte) (*Packet, error) {
|
||||
packet := &Packet{
|
||||
eap: &eap.Payload{
|
||||
Settings: stm.GetEAPSettings(),
|
||||
},
|
||||
stm: stm,
|
||||
responseModifiers: []protocol.ResponseModifier{},
|
||||
}
|
||||
err := packet.eap.Decode(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return packet, nil
|
||||
}
|
||||
|
||||
func (p *Packet) Encode() ([]byte, error) {
|
||||
return p.eap.Encode()
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"layeh.com/radius"
|
||||
)
|
||||
|
||||
type Status int
|
||||
|
||||
const (
|
||||
StatusUnknown Status = iota
|
||||
StatusSuccess
|
||||
StatusError
|
||||
StatusNextProtocol
|
||||
)
|
||||
|
||||
type Context interface {
|
||||
Packet() *radius.Request
|
||||
RootPayload() Payload
|
||||
|
||||
ProtocolSettings() interface{}
|
||||
|
||||
GetProtocolState(p Type) interface{}
|
||||
SetProtocolState(p Type, s interface{})
|
||||
IsProtocolStart(p Type) bool
|
||||
|
||||
HandleInnerEAP(Payload, StateManager) (Payload, error)
|
||||
Inner(Payload, Type) Context
|
||||
EndInnerProtocol(Status)
|
||||
|
||||
Log() *log.Entry
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package eap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
func EmptyPayload(settings protocol.Settings, t protocol.Type) (protocol.Payload, protocol.Type, error) {
|
||||
for _, cons := range settings.Protocols {
|
||||
np := cons()
|
||||
if np.Type() == t {
|
||||
return np, np.Type(), nil
|
||||
}
|
||||
// If the protocol has an inner protocol, return the original type but the code for the inner protocol
|
||||
if i, ok := np.(protocol.Inner); ok {
|
||||
if ii := i.HasInner(); ii != nil {
|
||||
return np, ii.Type(), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, protocol.Type(0), fmt.Errorf("unsupported EAP type %d", t)
|
||||
}
|
@ -1,96 +0,0 @@
|
||||
package eap
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/outpost/radius/eap/debug"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
const TypeEAP protocol.Type = 0
|
||||
|
||||
func Protocol() protocol.Payload {
|
||||
return &Payload{}
|
||||
}
|
||||
|
||||
type Payload struct {
|
||||
Code protocol.Code
|
||||
ID uint8
|
||||
Length uint16
|
||||
MsgType protocol.Type
|
||||
Payload protocol.Payload
|
||||
RawPayload []byte
|
||||
|
||||
Settings protocol.Settings
|
||||
}
|
||||
|
||||
func (p *Payload) Type() protocol.Type {
|
||||
return TypeEAP
|
||||
}
|
||||
|
||||
func (p *Payload) Offerable() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Payload) Decode(raw []byte) error {
|
||||
p.Code = protocol.Code(raw[0])
|
||||
p.ID = raw[1]
|
||||
p.Length = binary.BigEndian.Uint16(raw[2:])
|
||||
if p.Length != uint16(len(raw)) {
|
||||
return fmt.Errorf("mismatched packet length; got %d, expected %d", p.Length, uint16(len(raw)))
|
||||
}
|
||||
if len(raw) > 4 && (p.Code == protocol.CodeRequest || p.Code == protocol.CodeResponse) {
|
||||
p.MsgType = protocol.Type(raw[4])
|
||||
}
|
||||
log.WithField("raw", debug.FormatBytes(raw)).Trace("EAP: decode raw")
|
||||
p.RawPayload = raw[5:]
|
||||
if p.Payload == nil {
|
||||
pp, _, err := EmptyPayload(p.Settings, p.MsgType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Payload = pp
|
||||
}
|
||||
err := p.Payload.Decode(raw[5:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) Encode() ([]byte, error) {
|
||||
buff := make([]byte, 4)
|
||||
buff[0] = uint8(p.Code)
|
||||
buff[1] = uint8(p.ID)
|
||||
|
||||
if p.Payload != nil {
|
||||
payloadBuffer, err := p.Payload.Encode()
|
||||
if err != nil {
|
||||
return buff, err
|
||||
}
|
||||
if p.Code == protocol.CodeRequest || p.Code == protocol.CodeResponse {
|
||||
buff = append(buff, uint8(p.MsgType))
|
||||
}
|
||||
buff = append(buff, payloadBuffer...)
|
||||
}
|
||||
binary.BigEndian.PutUint16(buff[2:], uint16(len(buff)))
|
||||
return buff, nil
|
||||
}
|
||||
|
||||
func (p *Payload) Handle(ctx protocol.Context) protocol.Payload {
|
||||
ctx.Log().Debug("EAP: Handle")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) String() string {
|
||||
return fmt.Sprintf(
|
||||
"<EAP Packet Code=%d, ID=%d, Type=%d, Length=%d, Payload=%T>",
|
||||
p.Code,
|
||||
p.ID,
|
||||
p.MsgType,
|
||||
p.Length,
|
||||
p.Payload,
|
||||
)
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
package eap
|
||||
|
||||
type State struct {
|
||||
PacketID uint8
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
package gtc
|
||||
|
||||
import (
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
const TypeGTC protocol.Type = 6
|
||||
|
||||
func Protocol() protocol.Payload {
|
||||
return &Payload{}
|
||||
}
|
||||
|
||||
type Payload struct {
|
||||
Challenge []byte
|
||||
|
||||
st *State
|
||||
raw []byte
|
||||
}
|
||||
|
||||
func (p *Payload) Type() protocol.Type {
|
||||
return TypeGTC
|
||||
}
|
||||
|
||||
func (p *Payload) Decode(raw []byte) error {
|
||||
p.raw = raw
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) Encode() ([]byte, error) {
|
||||
return p.Challenge, nil
|
||||
}
|
||||
|
||||
func (p *Payload) Handle(ctx protocol.Context) protocol.Payload {
|
||||
defer func() {
|
||||
ctx.SetProtocolState(TypeGTC, p.st)
|
||||
}()
|
||||
settings := ctx.ProtocolSettings().(Settings)
|
||||
if ctx.IsProtocolStart(TypeGTC) {
|
||||
g, v := settings.ChallengeHandler(ctx)
|
||||
p.st = &State{
|
||||
getChallenge: g,
|
||||
validateResponse: v,
|
||||
}
|
||||
return &Payload{
|
||||
Challenge: p.st.getChallenge(),
|
||||
}
|
||||
}
|
||||
p.st = ctx.GetProtocolState(TypeGTC).(*State)
|
||||
p.st.validateResponse(p.raw)
|
||||
return &Payload{
|
||||
Challenge: p.st.getChallenge(),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Payload) Offerable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *Payload) String() string {
|
||||
return "<GTC Packet>"
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
package gtc
|
||||
|
||||
import "goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
|
||||
type GetChallenge func() []byte
|
||||
type ValidateResponse func(answer []byte)
|
||||
|
||||
type Settings struct {
|
||||
ChallengeHandler func(ctx protocol.Context) (GetChallenge, ValidateResponse)
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
package gtc
|
||||
|
||||
type State struct {
|
||||
getChallenge GetChallenge
|
||||
validateResponse ValidateResponse
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
package identity
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
const TypeIdentity protocol.Type = 1
|
||||
|
||||
func Protocol() protocol.Payload {
|
||||
return &Payload{}
|
||||
}
|
||||
|
||||
type Payload struct {
|
||||
Identity string
|
||||
}
|
||||
|
||||
func (p *Payload) Type() protocol.Type {
|
||||
return TypeIdentity
|
||||
}
|
||||
|
||||
func (p *Payload) Decode(raw []byte) error {
|
||||
p.Identity = string(raw)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) Encode() ([]byte, error) {
|
||||
return []byte{}, nil
|
||||
}
|
||||
|
||||
func (p *Payload) Handle(ctx protocol.Context) protocol.Payload {
|
||||
if ctx.IsProtocolStart(TypeIdentity) {
|
||||
ctx.EndInnerProtocol(protocol.StatusNextProtocol)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) Offerable() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Payload) String() string {
|
||||
return fmt.Sprintf(
|
||||
"<Identity Packet Identity=%s>",
|
||||
p.Identity,
|
||||
)
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
package legacy_nak
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
const TypeLegacyNAK protocol.Type = 3
|
||||
|
||||
func Protocol() protocol.Payload {
|
||||
return &Payload{}
|
||||
}
|
||||
|
||||
type Payload struct {
|
||||
DesiredType protocol.Type
|
||||
}
|
||||
|
||||
func (p *Payload) Type() protocol.Type {
|
||||
return TypeLegacyNAK
|
||||
}
|
||||
|
||||
func (p *Payload) Decode(raw []byte) error {
|
||||
p.DesiredType = protocol.Type(raw[0])
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) Encode() ([]byte, error) {
|
||||
return []byte{byte(p.DesiredType)}, nil
|
||||
}
|
||||
|
||||
func (p *Payload) Handle(ctx protocol.Context) protocol.Payload {
|
||||
if ctx.IsProtocolStart(TypeLegacyNAK) {
|
||||
ctx.EndInnerProtocol(protocol.StatusError)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) Offerable() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Payload) String() string {
|
||||
return fmt.Sprintf(
|
||||
"<Legacy NAK Packet DesiredType=%d>",
|
||||
p.DesiredType,
|
||||
)
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package mschapv2
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
Challenge []byte
|
||||
NTResponse []byte
|
||||
Flags uint8
|
||||
}
|
||||
|
||||
func ParseResponse(raw []byte) (*Response, error) {
|
||||
res := &Response{}
|
||||
res.Challenge = raw[:challengeValueSize]
|
||||
if !bytes.Equal(raw[challengeValueSize:challengeValueSize+responseReservedSize], make([]byte, 8)) {
|
||||
return nil, errors.New("MSCHAPv2: Reserved bytes not empty?")
|
||||
}
|
||||
res.NTResponse = raw[challengeValueSize+responseReservedSize : challengeValueSize+responseReservedSize+responseNTResponseSize]
|
||||
res.Flags = (raw[challengeValueSize+responseReservedSize+responseNTResponseSize])
|
||||
return res, nil
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package mschapv2
|
||||
|
||||
import "encoding/binary"
|
||||
|
||||
type SuccessRequest struct {
|
||||
*Payload
|
||||
Authenticator []byte
|
||||
}
|
||||
|
||||
// A success request is encoded slightly differently, it doesn't have a challenge and as such
|
||||
// doesn't need to encode the length of it
|
||||
func (sr *SuccessRequest) Encode() ([]byte, error) {
|
||||
encoded := []byte{
|
||||
byte(sr.OpCode),
|
||||
sr.MSCHAPv2ID,
|
||||
0,
|
||||
0,
|
||||
}
|
||||
encoded = append(encoded, sr.Authenticator...)
|
||||
sr.MSLength = uint16(len(encoded))
|
||||
binary.BigEndian.PutUint16(encoded[2:], sr.MSLength)
|
||||
return encoded, nil
|
||||
}
|
@ -1,196 +0,0 @@
|
||||
package mschapv2
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
"github.com/gorilla/securecookie"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/outpost/radius/eap/debug"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/eap"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/peap"
|
||||
"layeh.com/radius"
|
||||
"layeh.com/radius/vendors/microsoft"
|
||||
)
|
||||
|
||||
const TypeMSCHAPv2 protocol.Type = 26
|
||||
|
||||
func Protocol() protocol.Payload {
|
||||
return &Payload{}
|
||||
}
|
||||
|
||||
const (
|
||||
challengeValueSize = 16
|
||||
responseValueSize = 49
|
||||
responseReservedSize = 8
|
||||
responseNTResponseSize = 24
|
||||
)
|
||||
|
||||
type OpCode uint8
|
||||
|
||||
const (
|
||||
OpChallenge OpCode = 1
|
||||
OpResponse OpCode = 2
|
||||
OpSuccess OpCode = 3
|
||||
)
|
||||
|
||||
type Payload struct {
|
||||
OpCode OpCode
|
||||
MSCHAPv2ID uint8
|
||||
MSLength uint16
|
||||
ValueSize uint8
|
||||
|
||||
Challenge []byte
|
||||
Response []byte
|
||||
|
||||
Name []byte
|
||||
|
||||
st *State
|
||||
}
|
||||
|
||||
func (p *Payload) Type() protocol.Type {
|
||||
return TypeMSCHAPv2
|
||||
}
|
||||
|
||||
func (p *Payload) Decode(raw []byte) error {
|
||||
log.WithField("raw", debug.FormatBytes(raw)).Debugf("MSCHAPv2: decode raw")
|
||||
p.OpCode = OpCode(raw[0])
|
||||
if p.OpCode == OpSuccess {
|
||||
return nil
|
||||
}
|
||||
// TODO: Validate against root EAP packet
|
||||
p.MSCHAPv2ID = raw[1]
|
||||
p.MSLength = binary.BigEndian.Uint16(raw[2:])
|
||||
|
||||
p.ValueSize = raw[4]
|
||||
if p.ValueSize != responseValueSize {
|
||||
return fmt.Errorf("MSCHAPv2: incorrect value size: %d", p.ValueSize)
|
||||
}
|
||||
p.Response = raw[5 : p.ValueSize+5]
|
||||
p.Name = raw[5+p.ValueSize:]
|
||||
if int(p.MSLength) != len(raw) {
|
||||
return fmt.Errorf("MSCHAPv2: incorrect MS-Length: %d, should be %d", p.MSLength, len(raw))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) Encode() ([]byte, error) {
|
||||
encoded := []byte{
|
||||
byte(p.OpCode),
|
||||
p.MSCHAPv2ID,
|
||||
0,
|
||||
0,
|
||||
byte(len(p.Challenge)),
|
||||
}
|
||||
encoded = append(encoded, p.Challenge...)
|
||||
encoded = append(encoded, p.Name...)
|
||||
p.MSLength = uint16(len(encoded))
|
||||
binary.BigEndian.PutUint16(encoded[2:], p.MSLength)
|
||||
return encoded, nil
|
||||
}
|
||||
|
||||
func (p *Payload) Handle(ctx protocol.Context) protocol.Payload {
|
||||
defer func() {
|
||||
ctx.SetProtocolState(TypeMSCHAPv2, p.st)
|
||||
}()
|
||||
|
||||
rootEap := ctx.RootPayload().(*eap.Payload)
|
||||
|
||||
if ctx.IsProtocolStart(TypeMSCHAPv2) {
|
||||
ctx.Log().Debug("MSCHAPv2: Empty state, starting")
|
||||
p.st = &State{
|
||||
Challenge: securecookie.GenerateRandomKey(challengeValueSize),
|
||||
}
|
||||
return &Payload{
|
||||
OpCode: OpChallenge,
|
||||
MSCHAPv2ID: rootEap.ID + 1,
|
||||
Challenge: p.st.Challenge,
|
||||
Name: []byte("authentik"),
|
||||
}
|
||||
}
|
||||
p.st = ctx.GetProtocolState(TypeMSCHAPv2).(*State)
|
||||
|
||||
response := &Payload{
|
||||
MSCHAPv2ID: rootEap.ID + 1,
|
||||
}
|
||||
|
||||
settings := ctx.ProtocolSettings().(Settings)
|
||||
|
||||
ctx.Log().Debugf("MSCHAPv2: OpCode: %d", p.OpCode)
|
||||
if p.OpCode == OpResponse {
|
||||
res, err := ParseResponse(p.Response)
|
||||
if err != nil {
|
||||
ctx.Log().WithError(err).Warning("MSCHAPv2: failed to parse response")
|
||||
return nil
|
||||
}
|
||||
p.st.PeerChallenge = res.Challenge
|
||||
auth, err := settings.AuthenticateRequest(AuthRequest{
|
||||
Challenge: p.st.Challenge,
|
||||
PeerChallenge: p.st.PeerChallenge,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.Log().WithError(err).Warning("MSCHAPv2: failed to check password")
|
||||
return nil
|
||||
}
|
||||
if !bytes.Equal(auth.NTResponse, res.NTResponse) {
|
||||
ctx.Log().Warning("MSCHAPv2: NT response mismatch")
|
||||
return nil
|
||||
}
|
||||
ctx.Log().Info("MSCHAPv2: Successfully checked password")
|
||||
p.st.AuthResponse = auth
|
||||
succ := &SuccessRequest{
|
||||
Payload: &Payload{
|
||||
OpCode: OpSuccess,
|
||||
},
|
||||
Authenticator: []byte(auth.AuthenticatorResponse),
|
||||
}
|
||||
return succ
|
||||
} else if p.OpCode == OpSuccess && p.st.AuthResponse != nil {
|
||||
ep := &peap.ExtensionPayload{
|
||||
AVPs: []peap.ExtensionAVP{
|
||||
{
|
||||
Mandatory: true,
|
||||
Type: peap.AVPAckResult,
|
||||
Value: []byte{0, 1},
|
||||
},
|
||||
},
|
||||
}
|
||||
p.st.IsProtocolEnded = true
|
||||
return ep
|
||||
} else if p.st.IsProtocolEnded {
|
||||
ctx.EndInnerProtocol(protocol.StatusSuccess)
|
||||
return &Payload{}
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
func (p *Payload) ModifyRADIUSResponse(r *radius.Packet, q *radius.Packet) error {
|
||||
if p.st == nil || p.st.AuthResponse == nil {
|
||||
return nil
|
||||
}
|
||||
if r.Code != radius.CodeAccessAccept {
|
||||
return nil
|
||||
}
|
||||
log.Debug("MSCHAPv2: Radius modifier")
|
||||
if len(microsoft.MSMPPERecvKey_Get(r, q)) < 1 {
|
||||
microsoft.MSMPPERecvKey_Set(r, p.st.AuthResponse.RecvKey)
|
||||
}
|
||||
if len(microsoft.MSMPPESendKey_Get(r, q)) < 1 {
|
||||
microsoft.MSMPPESendKey_Set(r, p.st.AuthResponse.SendKey)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) Offerable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *Payload) String() string {
|
||||
return fmt.Sprintf(
|
||||
"<MSCHAPv2 Packet OpCode=%d, MSCHAPv2ID=%d>",
|
||||
p.OpCode,
|
||||
p.MSCHAPv2ID,
|
||||
)
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
package mschapv2
|
||||
|
||||
import (
|
||||
"layeh.com/radius/rfc2759"
|
||||
"layeh.com/radius/rfc3079"
|
||||
)
|
||||
|
||||
type Settings struct {
|
||||
AuthenticateRequest func(req AuthRequest) (*AuthResponse, error)
|
||||
}
|
||||
|
||||
type AuthRequest struct {
|
||||
Challenge []byte
|
||||
PeerChallenge []byte
|
||||
}
|
||||
|
||||
type AuthResponse struct {
|
||||
NTResponse []byte
|
||||
RecvKey []byte
|
||||
SendKey []byte
|
||||
AuthenticatorResponse string
|
||||
}
|
||||
|
||||
func DebugStaticCredentials(user, password []byte) func(req AuthRequest) (*AuthResponse, error) {
|
||||
return func(req AuthRequest) (*AuthResponse, error) {
|
||||
res := &AuthResponse{}
|
||||
ntResponse, err := rfc2759.GenerateNTResponse(req.Challenge, req.PeerChallenge, user, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.NTResponse = ntResponse
|
||||
|
||||
res.RecvKey, err = rfc3079.MakeKey(ntResponse, password, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res.SendKey, err = rfc3079.MakeKey(ntResponse, password, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res.AuthenticatorResponse, err = rfc2759.GenerateAuthenticatorResponse(req.Challenge, req.PeerChallenge, ntResponse, user, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
package mschapv2
|
||||
|
||||
type State struct {
|
||||
Challenge []byte
|
||||
PeerChallenge []byte
|
||||
IsProtocolEnded bool
|
||||
AuthResponse *AuthResponse
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
package protocol
|
||||
|
||||
import "layeh.com/radius"
|
||||
|
||||
type Type uint8
|
||||
|
||||
type Code uint8
|
||||
|
||||
const (
|
||||
CodeRequest Code = 1
|
||||
CodeResponse Code = 2
|
||||
CodeSuccess Code = 3
|
||||
CodeFailure Code = 4
|
||||
)
|
||||
|
||||
type Payload interface {
|
||||
Decode(raw []byte) error
|
||||
Encode() ([]byte, error)
|
||||
Handle(ctx Context) Payload
|
||||
Type() Type
|
||||
Offerable() bool
|
||||
String() string
|
||||
}
|
||||
|
||||
type Inner interface {
|
||||
HasInner() Payload
|
||||
}
|
||||
|
||||
type ResponseModifier interface {
|
||||
ModifyRADIUSResponse(r *radius.Packet, q *radius.Packet) error
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
package peap
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/outpost/radius/eap/debug"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
const TypePEAPExtension protocol.Type = 33
|
||||
|
||||
type ExtensionPayload struct {
|
||||
AVPs []ExtensionAVP
|
||||
}
|
||||
|
||||
func (ep *ExtensionPayload) Decode(raw []byte) error {
|
||||
log.WithField("raw", debug.FormatBytes(raw)).Debugf("PEAP-Extension: decode raw")
|
||||
ep.AVPs = []ExtensionAVP{}
|
||||
offset := 0
|
||||
for {
|
||||
if len(raw[offset:]) < 4 {
|
||||
return nil
|
||||
}
|
||||
len := binary.BigEndian.Uint16(raw[offset+2:offset+2+2]) + ExtensionHeaderSize
|
||||
avp := &ExtensionAVP{}
|
||||
err := avp.Decode(raw[offset : offset+int(len)])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ep.AVPs = append(ep.AVPs, *avp)
|
||||
offset = offset + int(len)
|
||||
}
|
||||
}
|
||||
|
||||
func (ep *ExtensionPayload) Encode() ([]byte, error) {
|
||||
log.Debug("PEAP-Extension: encode")
|
||||
buff := []byte{}
|
||||
for _, avp := range ep.AVPs {
|
||||
buff = append(buff, avp.Encode()...)
|
||||
}
|
||||
return buff, nil
|
||||
}
|
||||
|
||||
func (ep *ExtensionPayload) Handle(protocol.Context) protocol.Payload {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ep *ExtensionPayload) Offerable() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (ep *ExtensionPayload) String() string {
|
||||
return "<PEAP Extension Payload>"
|
||||
}
|
||||
|
||||
func (ep *ExtensionPayload) Type() protocol.Type {
|
||||
return TypePEAPExtension
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
package peap
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type AVPType uint16
|
||||
|
||||
const (
|
||||
AVPAckResult AVPType = 3
|
||||
)
|
||||
|
||||
const ExtensionHeaderSize = 4
|
||||
|
||||
type ExtensionAVP struct {
|
||||
Mandatory bool
|
||||
Type AVPType // 14-bit field
|
||||
Length uint16
|
||||
Value []byte
|
||||
}
|
||||
|
||||
var (
|
||||
ErrorReservedBitSet = errors.New("PEAP-Extension: Reserved bit is not 0")
|
||||
)
|
||||
|
||||
func (eavp *ExtensionAVP) Decode(raw []byte) error {
|
||||
typ := binary.BigEndian.Uint16(raw[:2])
|
||||
if typ>>15 == 1 {
|
||||
eavp.Mandatory = true
|
||||
}
|
||||
if typ>>14&1 != 0 {
|
||||
return ErrorReservedBitSet
|
||||
}
|
||||
eavp.Type = AVPType(typ & 0b0011111111111111)
|
||||
eavp.Length = binary.BigEndian.Uint16(raw[2:4])
|
||||
val := raw[4:]
|
||||
if eavp.Length != uint16(len(val)) {
|
||||
return fmt.Errorf("PEAP-Extension: Invalid length: %d, should be %d", eavp.Length, len(val))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (eavp ExtensionAVP) Encode() []byte {
|
||||
buff := []byte{
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
}
|
||||
t := uint16(eavp.Type)
|
||||
// Type is a 14-bit number, the highest bit is the mandatory flag
|
||||
if eavp.Mandatory {
|
||||
t = t | 0b1000000000000000
|
||||
}
|
||||
// The next bit is reserved and should always be set to 0
|
||||
t = t & 0b1011111111111111
|
||||
binary.BigEndian.PutUint16(buff[0:], t)
|
||||
binary.BigEndian.PutUint16(buff[2:], uint16(len(eavp.Value)))
|
||||
return append(buff, eavp.Value...)
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
package peap_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/peap"
|
||||
)
|
||||
|
||||
func TestEncode(t *testing.T) {
|
||||
eavp := peap.ExtensionAVP{
|
||||
Mandatory: true,
|
||||
Type: peap.AVPType(3),
|
||||
}
|
||||
assert.Equal(t, []byte{0x80, 0x3, 0x0, 0x0}, eavp.Encode())
|
||||
}
|
||||
|
||||
func TestDecode(t *testing.T) {
|
||||
eavp := peap.ExtensionAVP{}
|
||||
err := eavp.Decode([]byte{0x80, 0x3, 0x0, 0x0})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, eavp.Mandatory)
|
||||
assert.Equal(t, peap.AVPType(3), eavp.Type)
|
||||
}
|
||||
|
||||
func TestDecode_Invalid_ReservedBitSet(t *testing.T) {
|
||||
eavp := peap.ExtensionAVP{}
|
||||
err := eavp.Decode([]byte{0xc0, 0x3, 0x0, 0x0})
|
||||
assert.ErrorIs(t, err, peap.ErrorReservedBitSet)
|
||||
}
|
||||
|
||||
func TestDecode_Invalid_Length(t *testing.T) {
|
||||
eavp := peap.ExtensionAVP{}
|
||||
err := eavp.Decode([]byte{0x80, 0x3, 0x0, 0x0, 0x0})
|
||||
assert.NotNil(t, err)
|
||||
}
|
@ -1,167 +0,0 @@
|
||||
package peap
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/outpost/radius/eap/debug"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/eap"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/identity"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/tls"
|
||||
)
|
||||
|
||||
const TypePEAP protocol.Type = 25
|
||||
|
||||
func Protocol() protocol.Payload {
|
||||
return &tls.Payload{
|
||||
Inner: &Payload{
|
||||
Inner: &eap.Payload{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type Payload struct {
|
||||
Inner protocol.Payload
|
||||
|
||||
eap *eap.Payload
|
||||
st *State
|
||||
settings Settings
|
||||
raw []byte
|
||||
}
|
||||
|
||||
func (p *Payload) Type() protocol.Type {
|
||||
return TypePEAP
|
||||
}
|
||||
|
||||
func (p *Payload) HasInner() protocol.Payload {
|
||||
return p.Inner
|
||||
}
|
||||
|
||||
func (p *Payload) Decode(raw []byte) error {
|
||||
log.WithField("raw", debug.FormatBytes(raw)).Debug("PEAP: Decode")
|
||||
p.raw = raw
|
||||
return nil
|
||||
}
|
||||
|
||||
// Inner EAP packets in PEAP may not include the header, hence we need a custom decoder
|
||||
// https://datatracker.ietf.org/doc/html/draft-kamath-pppext-peapv0-00.txt#section-1.1
|
||||
func (p *Payload) Encode() ([]byte, error) {
|
||||
log.Debug("PEAP: Encoding inner EAP")
|
||||
if p.eap.Payload == nil {
|
||||
return []byte{}, errors.New("PEAP: no payload in response eap packet")
|
||||
}
|
||||
payload, err := p.eap.Payload.Encode()
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
encoded := []byte{
|
||||
byte(p.eap.MsgType),
|
||||
}
|
||||
return append(encoded, payload...), nil
|
||||
}
|
||||
|
||||
// Inner EAP packets in PEAP may not include the header, hence we need a custom decoder
|
||||
// https://datatracker.ietf.org/doc/html/draft-kamath-pppext-peapv0-00.txt#section-1.1
|
||||
func (p *Payload) eapInnerDecode(ctx protocol.Context) (*eap.Payload, error) {
|
||||
ep := &eap.Payload{
|
||||
Settings: p.GetEAPSettings(),
|
||||
}
|
||||
rootEap := ctx.RootPayload().(*eap.Payload)
|
||||
fixedRaw := []byte{
|
||||
byte(rootEap.Code),
|
||||
rootEap.ID,
|
||||
// 2 byte space for length
|
||||
0,
|
||||
0,
|
||||
}
|
||||
fullLength := len(p.raw) + len(fixedRaw)
|
||||
binary.BigEndian.PutUint16(fixedRaw[2:], uint16(fullLength))
|
||||
fixedRaw = append(fixedRaw, p.raw...)
|
||||
// If the raw data has a msgtype set to type 33 (EAP extension), decode differently
|
||||
if len(p.raw) > 5 && p.raw[4] == byte(TypePEAPExtension) {
|
||||
ep.Payload = &ExtensionPayload{}
|
||||
// Pass original raw data to EAP as extension payloads are encoded like normal EAP packets
|
||||
fixedRaw = p.raw
|
||||
}
|
||||
err := ep.Decode(fixedRaw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ep, nil
|
||||
}
|
||||
|
||||
func (p *Payload) Handle(ctx protocol.Context) protocol.Payload {
|
||||
defer func() {
|
||||
ctx.SetProtocolState(TypePEAP, p.st)
|
||||
}()
|
||||
p.settings = ctx.ProtocolSettings().(Settings)
|
||||
|
||||
rootEap := ctx.RootPayload().(*eap.Payload)
|
||||
|
||||
if ctx.IsProtocolStart(TypePEAP) {
|
||||
ctx.Log().Debug("PEAP: Protocol start")
|
||||
p.st = &State{
|
||||
SubState: make(map[string]*protocol.State),
|
||||
}
|
||||
return &eap.Payload{
|
||||
Code: protocol.CodeRequest,
|
||||
ID: rootEap.ID + 1,
|
||||
MsgType: identity.TypeIdentity,
|
||||
Payload: &identity.Payload{},
|
||||
}
|
||||
}
|
||||
p.st = ctx.GetProtocolState(TypePEAP).(*State)
|
||||
|
||||
ep, err := p.eapInnerDecode(ctx)
|
||||
if err != nil {
|
||||
ctx.Log().WithError(err).Warning("PEAP: failed to decode inner EAP")
|
||||
return &eap.Payload{
|
||||
Code: protocol.CodeFailure,
|
||||
ID: rootEap.ID + 1,
|
||||
}
|
||||
}
|
||||
p.eap = ep
|
||||
ctx.Log().Debugf("PEAP: Decoded inner EAP to %s", ep.String())
|
||||
|
||||
res, err := ctx.HandleInnerEAP(ep, p)
|
||||
if err != nil {
|
||||
ctx.Log().WithError(err).Warning("PEAP: failed to handle inner EAP")
|
||||
return nil
|
||||
}
|
||||
// Normal payloads need to be wrapped in PEAP to use the correct encoding (see Encode() above)
|
||||
// Extension payloads handle encoding differently
|
||||
pres := res.(*eap.Payload)
|
||||
if _, ok := pres.Payload.(*ExtensionPayload); ok {
|
||||
// HandleInnerEAP will set the MsgType to the PEAP type, however we need to override that
|
||||
pres.MsgType = TypePEAPExtension
|
||||
ctx.Log().Debug("PEAP: Encoding response as extension")
|
||||
return res
|
||||
}
|
||||
return &Payload{eap: pres}
|
||||
}
|
||||
|
||||
func (p *Payload) GetEAPSettings() protocol.Settings {
|
||||
return p.settings.InnerProtocols
|
||||
}
|
||||
|
||||
func (p *Payload) GetEAPState(key string) *protocol.State {
|
||||
return p.st.SubState[key]
|
||||
}
|
||||
|
||||
func (p *Payload) SetEAPState(key string, st *protocol.State) {
|
||||
p.st.SubState[key] = st
|
||||
}
|
||||
|
||||
func (p *Payload) Offerable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *Payload) String() string {
|
||||
return fmt.Sprintf(
|
||||
"<PEAP Packet Wrapping=%s>",
|
||||
p.eap.String(),
|
||||
)
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
package peap
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
type Settings struct {
|
||||
Config *tls.Config
|
||||
InnerProtocols protocol.Settings
|
||||
}
|
||||
|
||||
func (s Settings) TLSConfig() *tls.Config {
|
||||
return s.Config
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
package peap
|
||||
|
||||
import "goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
|
||||
type State struct {
|
||||
SubState map[string]*protocol.State
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"slices"
|
||||
)
|
||||
|
||||
type StateManager interface {
|
||||
GetEAPSettings() Settings
|
||||
GetEAPState(string) *State
|
||||
SetEAPState(string, *State)
|
||||
}
|
||||
|
||||
type ProtocolConstructor func() Payload
|
||||
|
||||
type Settings struct {
|
||||
Protocols []ProtocolConstructor
|
||||
ProtocolPriority []Type
|
||||
ProtocolSettings map[Type]interface{}
|
||||
}
|
||||
|
||||
type State struct {
|
||||
Protocols []ProtocolConstructor
|
||||
ProtocolIndex int
|
||||
ProtocolPriority []Type
|
||||
TypeState map[Type]any
|
||||
}
|
||||
|
||||
func (st *State) GetNextProtocol() (Type, error) {
|
||||
if st.ProtocolIndex >= len(st.ProtocolPriority) {
|
||||
return Type(0), errors.New("no more protocols to offer")
|
||||
}
|
||||
return st.ProtocolPriority[st.ProtocolIndex], nil
|
||||
}
|
||||
|
||||
func BlankState(settings Settings) *State {
|
||||
return &State{
|
||||
Protocols: slices.Clone(settings.Protocols),
|
||||
ProtocolPriority: slices.Clone(settings.ProtocolPriority),
|
||||
TypeState: map[Type]any{},
|
||||
}
|
||||
}
|
@ -1,111 +0,0 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/avast/retry-go/v4"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type BuffConn struct {
|
||||
reader *bytes.Buffer
|
||||
writer *bytes.Buffer
|
||||
|
||||
ctx context.Context
|
||||
|
||||
expectedWriterByteCount int
|
||||
writtenByteCount int
|
||||
|
||||
retryOptions []retry.Option
|
||||
}
|
||||
|
||||
func NewBuffConn(initialData []byte, ctx context.Context) *BuffConn {
|
||||
c := &BuffConn{
|
||||
reader: bytes.NewBuffer(initialData),
|
||||
writer: bytes.NewBuffer([]byte{}),
|
||||
ctx: ctx,
|
||||
retryOptions: []retry.Option{
|
||||
retry.Context(ctx),
|
||||
retry.Delay(10 * time.Microsecond),
|
||||
retry.DelayType(retry.BackOffDelay),
|
||||
retry.MaxDelay(100 * time.Millisecond),
|
||||
retry.Attempts(0),
|
||||
},
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
var errStall = errors.New("Stall")
|
||||
|
||||
func (conn BuffConn) OutboundData() []byte {
|
||||
d, _ := retry.DoWithData(
|
||||
func() ([]byte, error) {
|
||||
b := conn.writer.Bytes()
|
||||
if len(b) < 1 {
|
||||
return nil, errStall
|
||||
}
|
||||
return b, nil
|
||||
},
|
||||
conn.retryOptions...,
|
||||
)
|
||||
return d
|
||||
}
|
||||
|
||||
func (conn *BuffConn) UpdateData(data []byte) {
|
||||
conn.reader.Write(data)
|
||||
conn.writtenByteCount += len(data)
|
||||
log.Debugf("TLS(buffcon): Appending new data %d (total %d, expecting %d)", len(data), conn.writtenByteCount, conn.expectedWriterByteCount)
|
||||
}
|
||||
|
||||
func (conn BuffConn) NeedsMoreData() bool {
|
||||
if conn.expectedWriterByteCount > 0 {
|
||||
return conn.reader.Len() < int(conn.expectedWriterByteCount)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (conn *BuffConn) Read(p []byte) (int, error) {
|
||||
d, err := retry.DoWithData(
|
||||
func() (int, error) {
|
||||
if conn.reader.Len() == 0 {
|
||||
log.Debugf("TLS(buffcon): Attempted read %d from empty buffer, stalling...", len(p))
|
||||
return 0, errStall
|
||||
}
|
||||
if conn.expectedWriterByteCount > 0 {
|
||||
// If we're waiting for more data, we need to stall
|
||||
if conn.writtenByteCount < int(conn.expectedWriterByteCount) {
|
||||
log.Debugf("TLS(buffcon): Attempted read %d while waiting for bytes %d, stalling...", len(p), conn.expectedWriterByteCount-conn.reader.Len())
|
||||
return 0, errStall
|
||||
}
|
||||
// If we have all the data, reset how much we're expecting to still get
|
||||
if conn.writtenByteCount == int(conn.expectedWriterByteCount) {
|
||||
conn.expectedWriterByteCount = 0
|
||||
}
|
||||
}
|
||||
if conn.reader.Len() == 0 {
|
||||
conn.writtenByteCount = 0
|
||||
}
|
||||
n, err := conn.reader.Read(p)
|
||||
log.Debugf("TLS(buffcon): Read: %d into %d (total %d)", n, len(p), conn.reader.Len())
|
||||
return n, err
|
||||
},
|
||||
conn.retryOptions...,
|
||||
)
|
||||
return d, err
|
||||
}
|
||||
|
||||
func (conn BuffConn) Write(p []byte) (int, error) {
|
||||
log.Debugf("TLS(buffcon): Write: %d", len(p))
|
||||
return conn.writer.Write(p)
|
||||
}
|
||||
|
||||
func (conn BuffConn) Close() error { return nil }
|
||||
func (conn BuffConn) LocalAddr() net.Addr { return nil }
|
||||
func (conn BuffConn) RemoteAddr() net.Addr { return nil }
|
||||
func (conn BuffConn) SetDeadline(t time.Time) error { return nil }
|
||||
func (conn BuffConn) SetReadDeadline(t time.Time) error { return nil }
|
||||
func (conn BuffConn) SetWriteDeadline(t time.Time) error { return nil }
|
@ -1,10 +0,0 @@
|
||||
package tls
|
||||
|
||||
type Flag byte
|
||||
|
||||
const (
|
||||
FlagLengthIncluded Flag = 1 << 7
|
||||
FlagMoreFragments Flag = 1 << 6
|
||||
FlagTLSStart Flag = 1 << 5
|
||||
FlagNone Flag = 0
|
||||
)
|
@ -1,39 +0,0 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
func (p *Payload) innerHandler(ctx protocol.Context) {
|
||||
d := make([]byte, 1024)
|
||||
if !ctx.IsProtocolStart(p.Inner.Type()) {
|
||||
ctx.Log().Debug("TLS: Reading from TLS for inner protocol")
|
||||
n, err := p.st.TLS.Read(d)
|
||||
if err != nil {
|
||||
ctx.Log().WithError(err).Warning("TLS: Failed to read from TLS connection")
|
||||
ctx.EndInnerProtocol(protocol.StatusError)
|
||||
return
|
||||
}
|
||||
// Truncate data to the size we read
|
||||
d = d[:n]
|
||||
}
|
||||
err := p.Inner.Decode(d)
|
||||
if err != nil {
|
||||
ctx.Log().WithError(err).Warning("TLS: failed to decode inner protocol")
|
||||
ctx.EndInnerProtocol(protocol.StatusError)
|
||||
return
|
||||
}
|
||||
pl := p.Inner.Handle(ctx.Inner(p.Inner, p.Inner.Type()))
|
||||
enc, err := pl.Encode()
|
||||
if err != nil {
|
||||
ctx.Log().WithError(err).Warning("TLS: failed to encode inner protocol")
|
||||
ctx.EndInnerProtocol(protocol.StatusError)
|
||||
return
|
||||
}
|
||||
_, err = p.st.TLS.Write(enc)
|
||||
if err != nil {
|
||||
ctx.Log().WithError(err).Warning("TLS: failed to write to TLS")
|
||||
ctx.EndInnerProtocol(protocol.StatusError)
|
||||
return
|
||||
}
|
||||
}
|
@ -1,279 +0,0 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/avast/retry-go/v4"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/outpost/radius/eap/debug"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
"layeh.com/radius"
|
||||
"layeh.com/radius/vendors/microsoft"
|
||||
)
|
||||
|
||||
const maxChunkSize = 1000
|
||||
const staleConnectionTimeout = 10
|
||||
|
||||
const TypeTLS protocol.Type = 13
|
||||
|
||||
func Protocol() protocol.Payload {
|
||||
return &Payload{}
|
||||
}
|
||||
|
||||
type Payload struct {
|
||||
Flags Flag
|
||||
Length uint32
|
||||
Data []byte
|
||||
|
||||
st *State
|
||||
Inner protocol.Payload
|
||||
}
|
||||
|
||||
func (p *Payload) Type() protocol.Type {
|
||||
return TypeTLS
|
||||
}
|
||||
|
||||
func (p *Payload) HasInner() protocol.Payload {
|
||||
return p.Inner
|
||||
}
|
||||
|
||||
func (p *Payload) Offerable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *Payload) Decode(raw []byte) error {
|
||||
p.Flags = Flag(raw[0])
|
||||
raw = raw[1:]
|
||||
if p.Flags&FlagLengthIncluded != 0 {
|
||||
if len(raw) < 4 {
|
||||
return errors.New("invalid size")
|
||||
}
|
||||
p.Length = binary.BigEndian.Uint32(raw)
|
||||
p.Data = raw[4:]
|
||||
} else {
|
||||
p.Data = raw[0:]
|
||||
}
|
||||
log.WithField("raw", debug.FormatBytes(p.Data)).WithField("size", len(p.Data)).WithField("flags", p.Flags).Trace("TLS: decode raw")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) Encode() ([]byte, error) {
|
||||
l := 1
|
||||
if p.Flags&FlagLengthIncluded != 0 {
|
||||
l += 4
|
||||
}
|
||||
buff := make([]byte, len(p.Data)+l)
|
||||
buff[0] = byte(p.Flags)
|
||||
if p.Flags&FlagLengthIncluded != 0 {
|
||||
buff[1] = byte(p.Length >> 24)
|
||||
buff[2] = byte(p.Length >> 16)
|
||||
buff[3] = byte(p.Length >> 8)
|
||||
buff[4] = byte(p.Length)
|
||||
}
|
||||
if len(p.Data) > 0 {
|
||||
copy(buff[5:], p.Data)
|
||||
}
|
||||
return buff, nil
|
||||
}
|
||||
|
||||
func (p *Payload) Handle(ctx protocol.Context) protocol.Payload {
|
||||
defer func() {
|
||||
ctx.SetProtocolState(TypeTLS, p.st)
|
||||
}()
|
||||
if ctx.IsProtocolStart(TypeTLS) {
|
||||
p.st = NewState(ctx).(*State)
|
||||
return &Payload{
|
||||
Flags: FlagTLSStart,
|
||||
}
|
||||
}
|
||||
p.st = ctx.GetProtocolState(TypeTLS).(*State)
|
||||
|
||||
if p.st.TLS == nil {
|
||||
p.tlsInit(ctx)
|
||||
} else if len(p.Data) > 0 {
|
||||
ctx.Log().Debug("TLS: Updating buffer with new TLS data from packet")
|
||||
if p.Flags&FlagLengthIncluded != 0 && p.st.Conn.expectedWriterByteCount == 0 {
|
||||
ctx.Log().Debugf("TLS: Expecting %d total bytes, will buffer", p.Length)
|
||||
p.st.Conn.expectedWriterByteCount = int(p.Length)
|
||||
} else if p.Flags&FlagLengthIncluded != 0 {
|
||||
ctx.Log().Debug("TLS: No length included, not buffering")
|
||||
p.st.Conn.expectedWriterByteCount = 0
|
||||
}
|
||||
p.st.Conn.UpdateData(p.Data)
|
||||
if !p.st.Conn.NeedsMoreData() && !p.st.HandshakeDone {
|
||||
// Wait for outbound data to be available
|
||||
p.st.Conn.OutboundData()
|
||||
}
|
||||
}
|
||||
// If we need more data, send the client the go-ahead
|
||||
if p.st.Conn.NeedsMoreData() {
|
||||
return &Payload{
|
||||
Flags: FlagNone,
|
||||
Length: 0,
|
||||
Data: []byte{},
|
||||
}
|
||||
}
|
||||
if p.st.HasMore() {
|
||||
return p.sendNextChunk()
|
||||
}
|
||||
if p.st.Conn.writer.Len() == 0 && p.st.HandshakeDone {
|
||||
if p.Inner != nil {
|
||||
ctx.Log().Debug("TLS: Handshake is done, delegating to inner protocol")
|
||||
p.innerHandler(ctx)
|
||||
return p.startChunkedTransfer(p.st.Conn.OutboundData())
|
||||
}
|
||||
defer p.st.ContextCancel()
|
||||
// If we don't have a final status from the handshake finished function, stall for time
|
||||
pst, _ := retry.DoWithData(
|
||||
func() (protocol.Status, error) {
|
||||
if p.st.FinalStatus == protocol.StatusUnknown {
|
||||
return p.st.FinalStatus, errStall
|
||||
}
|
||||
return p.st.FinalStatus, nil
|
||||
},
|
||||
retry.Context(p.st.Context),
|
||||
retry.Delay(10*time.Microsecond),
|
||||
retry.DelayType(retry.BackOffDelay),
|
||||
retry.MaxDelay(100*time.Millisecond),
|
||||
retry.Attempts(0),
|
||||
)
|
||||
ctx.EndInnerProtocol(pst)
|
||||
return nil
|
||||
}
|
||||
return p.startChunkedTransfer(p.st.Conn.OutboundData())
|
||||
}
|
||||
|
||||
func (p *Payload) ModifyRADIUSResponse(r *radius.Packet, q *radius.Packet) error {
|
||||
if r.Code != radius.CodeAccessAccept {
|
||||
return nil
|
||||
}
|
||||
if p.st == nil || !p.st.HandshakeDone {
|
||||
return nil
|
||||
}
|
||||
log.Debug("TLS: Adding MPPE Keys")
|
||||
// TLS overrides other protocols' MPPE keys
|
||||
if len(microsoft.MSMPPERecvKey_Get(r, q)) > 0 {
|
||||
microsoft.MSMPPERecvKey_Del(r)
|
||||
}
|
||||
if len(microsoft.MSMPPESendKey_Get(r, q)) > 0 {
|
||||
microsoft.MSMPPESendKey_Del(r)
|
||||
}
|
||||
microsoft.MSMPPERecvKey_Set(r, p.st.MPPEKey[:32])
|
||||
microsoft.MSMPPESendKey_Set(r, p.st.MPPEKey[64:64+32])
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) tlsInit(ctx protocol.Context) {
|
||||
ctx.Log().Debug("TLS: no TLS connection in state yet, starting connection")
|
||||
p.st.Context, p.st.ContextCancel = context.WithTimeout(context.Background(), staleConnectionTimeout*time.Second)
|
||||
p.st.Conn = NewBuffConn(p.Data, p.st.Context)
|
||||
cfg := ctx.ProtocolSettings().(TLSConfig).TLSConfig().Clone()
|
||||
|
||||
if klp, ok := os.LookupEnv("SSLKEYLOGFILE"); ok {
|
||||
kl, err := os.OpenFile(klp, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
cfg.KeyLogWriter = kl
|
||||
}
|
||||
|
||||
cfg.GetConfigForClient = func(chi *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
ctx.Log().Debugf("TLS: ClientHello: %+v\n", chi)
|
||||
p.st.ClientHello = chi
|
||||
return nil, nil
|
||||
}
|
||||
p.st.TLS = tls.Server(p.st.Conn, cfg)
|
||||
p.st.TLS.SetDeadline(time.Now().Add(staleConnectionTimeout * time.Second))
|
||||
go func() {
|
||||
err := p.st.TLS.HandshakeContext(p.st.Context)
|
||||
if err != nil {
|
||||
ctx.Log().WithError(err).Debug("TLS: Handshake error")
|
||||
p.st.FinalStatus = protocol.StatusError
|
||||
ctx.EndInnerProtocol(protocol.StatusError)
|
||||
return
|
||||
}
|
||||
ctx.Log().Debug("TLS: handshake done")
|
||||
p.tlsHandshakeFinished(ctx)
|
||||
}()
|
||||
}
|
||||
|
||||
func (p *Payload) tlsHandshakeFinished(ctx protocol.Context) {
|
||||
cs := p.st.TLS.ConnectionState()
|
||||
label := "client EAP encryption"
|
||||
var context []byte
|
||||
switch cs.Version {
|
||||
case tls.VersionTLS10:
|
||||
ctx.Log().Debugf("TLS: Version %d (1.0)", cs.Version)
|
||||
case tls.VersionTLS11:
|
||||
ctx.Log().Debugf("TLS: Version %d (1.1)", cs.Version)
|
||||
case tls.VersionTLS12:
|
||||
ctx.Log().Debugf("TLS: Version %d (1.2)", cs.Version)
|
||||
case tls.VersionTLS13:
|
||||
ctx.Log().Debugf("TLS: Version %d (1.3)", cs.Version)
|
||||
label = "EXPORTER_EAP_TLS_Key_Material"
|
||||
context = []byte{byte(TypeTLS)}
|
||||
}
|
||||
ksm, err := cs.ExportKeyingMaterial(label, context, 64+64)
|
||||
ctx.Log().Debugf("TLS: ksm % x %v", ksm, err)
|
||||
p.st.MPPEKey = ksm
|
||||
p.st.HandshakeDone = true
|
||||
if p.Inner == nil {
|
||||
p.st.FinalStatus = ctx.ProtocolSettings().(Settings).HandshakeSuccessful(ctx, cs.PeerCertificates)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Payload) startChunkedTransfer(data []byte) *Payload {
|
||||
if len(data) > maxChunkSize {
|
||||
log.WithField("length", len(data)).Debug("TLS: Data needs to be chunked")
|
||||
p.st.RemainingChunks = append(p.st.RemainingChunks, slices.Collect(slices.Chunk(data, maxChunkSize))...)
|
||||
p.st.TotalPayloadSize = len(data)
|
||||
return p.sendNextChunk()
|
||||
}
|
||||
log.WithField("length", len(data)).Debug("TLS: Sending data un-chunked")
|
||||
p.st.Conn.writer.Reset()
|
||||
return &Payload{
|
||||
Flags: FlagLengthIncluded,
|
||||
Length: uint32(len(data)),
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Payload) sendNextChunk() *Payload {
|
||||
nextChunk := p.st.RemainingChunks[0]
|
||||
log.WithField("raw", debug.FormatBytes(nextChunk)).Debug("TLS: Sending next chunk")
|
||||
p.st.RemainingChunks = p.st.RemainingChunks[1:]
|
||||
flags := FlagLengthIncluded
|
||||
if p.st.HasMore() {
|
||||
log.WithField("chunks", len(p.st.RemainingChunks)).Debug("TLS: More chunks left")
|
||||
flags += FlagMoreFragments
|
||||
} else {
|
||||
// Last chunk, reset the connection buffers and pending payload size
|
||||
defer func() {
|
||||
log.Debug("TLS: Sent last chunk")
|
||||
p.st.Conn.writer.Reset()
|
||||
p.st.TotalPayloadSize = 0
|
||||
}()
|
||||
}
|
||||
log.WithField("length", p.st.TotalPayloadSize).Debug("TLS: Total payload size")
|
||||
return &Payload{
|
||||
Flags: flags,
|
||||
Length: uint32(p.st.TotalPayloadSize),
|
||||
Data: nextChunk,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Payload) String() string {
|
||||
return fmt.Sprintf(
|
||||
"<TLS Packet HandshakeDone=%t, FinalStatus=%d, ClientHello=%v>",
|
||||
p.st.HandshakeDone,
|
||||
p.st.FinalStatus,
|
||||
p.st.ClientHello,
|
||||
)
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
type TLSConfig interface {
|
||||
TLSConfig() *tls.Config
|
||||
}
|
||||
|
||||
type Settings struct {
|
||||
Config *tls.Config
|
||||
HandshakeSuccessful func(ctx protocol.Context, certs []*x509.Certificate) protocol.Status
|
||||
}
|
||||
|
||||
func (s Settings) TLSConfig() *tls.Config {
|
||||
return s.Config
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
type State struct {
|
||||
RemainingChunks [][]byte
|
||||
HandshakeDone bool
|
||||
FinalStatus protocol.Status
|
||||
ClientHello *tls.ClientHelloInfo
|
||||
MPPEKey []byte
|
||||
TotalPayloadSize int
|
||||
TLS *tls.Conn
|
||||
Conn *BuffConn
|
||||
Context context.Context
|
||||
ContextCancel context.CancelFunc
|
||||
}
|
||||
|
||||
func NewState(c protocol.Context) interface{} {
|
||||
c.Log().Debug("TLS: new state")
|
||||
return &State{
|
||||
RemainingChunks: make([][]byte, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (s State) HasMore() bool {
|
||||
return len(s.RemainingChunks) > 0
|
||||
}
|
@ -1,44 +1,17 @@
|
||||
package radius
|
||||
|
||||
import (
|
||||
"context"
|
||||
ttls "crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"net/url"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/api/v3"
|
||||
"goauthentik.io/internal/outpost/flow"
|
||||
"goauthentik.io/internal/outpost/radius/eap"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/gtc"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/identity"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/legacy_nak"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/mschapv2"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/peap"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/tls"
|
||||
"goauthentik.io/internal/outpost/radius/metrics"
|
||||
"goauthentik.io/internal/utils"
|
||||
"layeh.com/radius"
|
||||
"layeh.com/radius/rfc2865"
|
||||
"layeh.com/radius/rfc2869"
|
||||
)
|
||||
|
||||
func (rs *RadiusServer) Handle_AccessRequest(w radius.ResponseWriter, r *RadiusRequest) {
|
||||
eap := rfc2869.EAPMessage_Get(r.Packet)
|
||||
if len(eap) > 0 {
|
||||
rs.log.Trace("EAP request")
|
||||
rs.Handle_AccessRequest_EAP(w, r)
|
||||
} else {
|
||||
rs.log.Trace("PAP request")
|
||||
rs.Handle_AccessRequest_PAP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (rs *RadiusServer) Handle_AccessRequest_PAP(w radius.ResponseWriter, r *RadiusRequest) {
|
||||
username := rfc2865.UserName_GetString(r.Packet)
|
||||
|
||||
fe := flow.NewFlowExecutor(r.Context(), r.pi.flowSlug, r.pi.s.ac.Client.GetConfig(), log.Fields{
|
||||
@ -114,164 +87,3 @@ func (rs *RadiusServer) Handle_AccessRequest_PAP(w radius.ResponseWriter, r *Rad
|
||||
res.Add(attr.Type, attr.Attribute)
|
||||
}
|
||||
}
|
||||
|
||||
func (rs *RadiusServer) Handle_AccessRequest_EAP(w radius.ResponseWriter, r *RadiusRequest) {
|
||||
er := rfc2869.EAPMessage_Get(r.Packet)
|
||||
ep, err := eap.Decode(r.pi, er)
|
||||
if err != nil {
|
||||
rs.log.WithError(err).Warning("failed to parse EAP packet")
|
||||
return
|
||||
}
|
||||
ep.HandleRadiusPacket(w, r.Request)
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) GetEAPState(key string) *protocol.State {
|
||||
return pi.eapState[key]
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) SetEAPState(key string, state *protocol.State) {
|
||||
pi.eapState[key] = state
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) GetEAPSettings() protocol.Settings {
|
||||
protocols := []protocol.ProtocolConstructor{
|
||||
identity.Protocol,
|
||||
legacy_nak.Protocol,
|
||||
}
|
||||
|
||||
certId := pi.certId
|
||||
if certId == "" {
|
||||
return protocol.Settings{
|
||||
Protocols: protocols,
|
||||
}
|
||||
}
|
||||
|
||||
cert := pi.s.cryptoStore.Get(certId)
|
||||
if cert == nil {
|
||||
return protocol.Settings{
|
||||
Protocols: protocols,
|
||||
}
|
||||
}
|
||||
|
||||
return protocol.Settings{
|
||||
Protocols: append(protocols, tls.Protocol, peap.Protocol),
|
||||
ProtocolPriority: []protocol.Type{tls.TypeTLS, peap.TypePEAP},
|
||||
ProtocolSettings: map[protocol.Type]interface{}{
|
||||
tls.TypeTLS: tls.Settings{
|
||||
Config: &ttls.Config{
|
||||
Certificates: []ttls.Certificate{*cert},
|
||||
ClientAuth: ttls.RequireAnyClientCert,
|
||||
},
|
||||
HandshakeSuccessful: func(ctx protocol.Context, certs []*x509.Certificate) protocol.Status {
|
||||
ctx.Log().Debug("Starting authn flow")
|
||||
pem := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: certs[0].Raw,
|
||||
})
|
||||
|
||||
fe := flow.NewFlowExecutor(context.Background(), pi.flowSlug, pi.s.ac.Client.GetConfig(), log.Fields{
|
||||
"client": utils.GetIP(ctx.Packet().RemoteAddr),
|
||||
})
|
||||
fe.DelegateClientIP(utils.GetIP(ctx.Packet().RemoteAddr))
|
||||
fe.Params.Add("goauthentik.io/outpost/radius", "true")
|
||||
fe.AddHeader("X-Authentik-Outpost-Certificate", url.QueryEscape(string(pem)))
|
||||
|
||||
passed, err := fe.Execute()
|
||||
if err != nil {
|
||||
ctx.Log().WithError(err).Warning("failed to execute flow")
|
||||
return protocol.StatusError
|
||||
}
|
||||
ctx.Log().WithField("passed", passed).Debug("Finished flow")
|
||||
if passed {
|
||||
return protocol.StatusSuccess
|
||||
} else {
|
||||
return protocol.StatusError
|
||||
}
|
||||
},
|
||||
},
|
||||
peap.TypePEAP: peap.Settings{
|
||||
Config: &ttls.Config{
|
||||
Certificates: []ttls.Certificate{*cert},
|
||||
},
|
||||
InnerProtocols: protocol.Settings{
|
||||
Protocols: append(protocols, gtc.Protocol, mschapv2.Protocol),
|
||||
ProtocolPriority: []protocol.Type{gtc.TypeGTC, mschapv2.TypeMSCHAPv2},
|
||||
ProtocolSettings: map[protocol.Type]interface{}{
|
||||
mschapv2.TypeMSCHAPv2: mschapv2.Settings{
|
||||
AuthenticateRequest: mschapv2.DebugStaticCredentials(
|
||||
[]byte("foo"), []byte("bar"),
|
||||
),
|
||||
},
|
||||
gtc.TypeGTC: gtc.Settings{
|
||||
ChallengeHandler: func(ctx protocol.Context) (gtc.GetChallenge, gtc.ValidateResponse) {
|
||||
fe := flow.NewFlowExecutor(context.Background(), pi.flowSlug, pi.s.ac.Client.GetConfig(), log.Fields{
|
||||
"client": utils.GetIP(ctx.Packet().RemoteAddr),
|
||||
})
|
||||
fe.DelegateClientIP(utils.GetIP(ctx.Packet().RemoteAddr))
|
||||
fe.Params.Add("goauthentik.io/outpost/radius", "true")
|
||||
var ch []byte = nil
|
||||
var ans []byte = nil
|
||||
fe.InteractiveSolver = func(ct *api.ChallengeTypes, afesr api.ApiFlowsExecutorSolveRequest) (api.FlowChallengeResponseRequest, error) {
|
||||
comp := ct.GetActualInstance().(flow.ChallengeCommon).GetComponent()
|
||||
ch = []byte(comp)
|
||||
for {
|
||||
if ans == nil {
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
switch comp {
|
||||
case string(flow.StageIdentification):
|
||||
r := api.NewIdentificationChallengeResponseRequest(string(ans))
|
||||
return api.IdentificationChallengeResponseRequestAsFlowChallengeResponseRequest(r), nil
|
||||
case string(flow.StagePassword):
|
||||
r := api.NewPasswordChallengeResponseRequest(string(ans))
|
||||
return api.PasswordChallengeResponseRequestAsFlowChallengeResponseRequest(r), nil
|
||||
}
|
||||
panic(comp)
|
||||
}
|
||||
passed := false
|
||||
done := false
|
||||
go func() {
|
||||
var err error
|
||||
passed, err = fe.Execute()
|
||||
done = true
|
||||
if err != nil {
|
||||
ctx.Log().WithError(err).Warning("failed to execute flow")
|
||||
// return protocol.StatusError
|
||||
}
|
||||
// ctx.Log().WithField("passed", passed).Debug("Finished flow")
|
||||
// if passed {
|
||||
// return protocol.StatusSuccess
|
||||
// } else {
|
||||
// return protocol.StatusError
|
||||
// }
|
||||
}()
|
||||
return func() []byte {
|
||||
if done {
|
||||
status := protocol.StatusError
|
||||
if passed {
|
||||
status = protocol.StatusSuccess
|
||||
}
|
||||
ctx.EndInnerProtocol(status)
|
||||
}
|
||||
for {
|
||||
if ch == nil {
|
||||
continue
|
||||
}
|
||||
defer func() {
|
||||
ch = nil
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
}, func(answer []byte) {
|
||||
ans = answer
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ package radius
|
||||
import (
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
@ -36,32 +35,12 @@ func (r *RadiusRequest) ID() string {
|
||||
return r.id
|
||||
}
|
||||
|
||||
type LogWriter struct {
|
||||
w radius.ResponseWriter
|
||||
l *log.Entry
|
||||
}
|
||||
|
||||
func (lw LogWriter) Write(packet *radius.Packet) error {
|
||||
lw.l.WithField("code", packet.Code.String()).Info("Radius Response")
|
||||
return lw.w.Write(packet)
|
||||
}
|
||||
|
||||
func (rs *RadiusServer) ServeRADIUS(w radius.ResponseWriter, r *radius.Request) {
|
||||
span := sentry.StartSpan(r.Context(), "authentik.providers.radius.connect",
|
||||
sentry.WithTransactionName("authentik.providers.radius.connect"))
|
||||
rid := uuid.New().String()
|
||||
span.SetTag("request_uid", rid)
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr.String())
|
||||
if err != nil {
|
||||
rs.log.WithError(err).Warning("Failed to get remote IP")
|
||||
return
|
||||
}
|
||||
rl := rs.log.WithFields(log.Fields{
|
||||
"code": r.Code.String(),
|
||||
"request": rid,
|
||||
"ip": host,
|
||||
"id": r.Identifier,
|
||||
})
|
||||
rl := rs.log.WithField("code", r.Code.String()).WithField("request", rid)
|
||||
selectedApp := ""
|
||||
defer func() {
|
||||
span.Finish()
|
||||
@ -79,7 +58,6 @@ func (rs *RadiusServer) ServeRADIUS(w radius.ResponseWriter, r *radius.Request)
|
||||
}
|
||||
|
||||
rl.Info("Radius Request")
|
||||
ww := LogWriter{w, rl}
|
||||
|
||||
// Lookup provider by shared secret
|
||||
var pi *ProviderInstance
|
||||
@ -94,12 +72,12 @@ func (rs *RadiusServer) ServeRADIUS(w radius.ResponseWriter, r *radius.Request)
|
||||
hs := sha512.Sum512([]byte(r.Secret))
|
||||
bs := hex.EncodeToString(hs[:])
|
||||
nr.Log().WithField("hashed_secret", bs).Warning("No provider found")
|
||||
_ = ww.Write(r.Response(radius.CodeAccessReject))
|
||||
_ = w.Write(r.Response(radius.CodeAccessReject))
|
||||
return
|
||||
}
|
||||
nr.pi = pi
|
||||
|
||||
if nr.Code == radius.CodeAccessRequest {
|
||||
rs.Handle_AccessRequest(ww, nr)
|
||||
rs.Handle_AccessRequest(w, nr)
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/config"
|
||||
"goauthentik.io/internal/outpost/ak"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
"goauthentik.io/internal/outpost/radius/metrics"
|
||||
|
||||
"layeh.com/radius"
|
||||
@ -23,27 +22,23 @@ type ProviderInstance struct {
|
||||
appSlug string
|
||||
flowSlug string
|
||||
providerId int32
|
||||
certId string
|
||||
s *RadiusServer
|
||||
log *log.Entry
|
||||
eapState map[string]*protocol.State
|
||||
}
|
||||
|
||||
type RadiusServer struct {
|
||||
s radius.PacketServer
|
||||
log *log.Entry
|
||||
ac *ak.APIController
|
||||
cryptoStore *ak.CryptoStore
|
||||
s radius.PacketServer
|
||||
log *log.Entry
|
||||
ac *ak.APIController
|
||||
|
||||
providers map[int32]*ProviderInstance
|
||||
providers []*ProviderInstance
|
||||
}
|
||||
|
||||
func NewServer(ac *ak.APIController) ak.Outpost {
|
||||
rs := &RadiusServer{
|
||||
log: log.WithField("logger", "authentik.outpost.radius"),
|
||||
ac: ac,
|
||||
providers: map[int32]*ProviderInstance{},
|
||||
cryptoStore: ak.NewCryptoStore(ac.Client.CryptoApi),
|
||||
log: log.WithField("logger", "authentik.outpost.radius"),
|
||||
ac: ac,
|
||||
providers: []*ProviderInstance{},
|
||||
}
|
||||
rs.s = radius.PacketServer{
|
||||
Handler: rs,
|
||||
@ -90,7 +85,7 @@ func (rs *RadiusServer) RADIUSSecret(ctx context.Context, remoteAddr net.Addr) (
|
||||
return bi < bj
|
||||
})
|
||||
candidate := matchedPrefixes[0]
|
||||
rs.log.WithField("ip", ip.String()).WithField("cidr", candidate.c.String()).WithField("instance", candidate.p.appSlug).Debug("Matched CIDR")
|
||||
rs.log.WithField("ip", ip.String()).WithField("cidr", candidate.c.String()).Debug("Matched CIDR")
|
||||
return candidate.p.SharedSecret, nil
|
||||
}
|
||||
|
||||
@ -103,8 +98,7 @@ func (rs *RadiusServer) Start() error {
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
rs.log.WithField("listen", rs.s.Addr).Info("Starting radius server")
|
||||
err := rs.s.ListenAndServe()
|
||||
err := rs.StartRadiusServer()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
4
notes.md
4
notes.md
@ -1,4 +0,0 @@
|
||||
|
||||
eapol_test -s foo -a 192.168.68.1 -c config
|
||||
|
||||
sudo tcpdump -i bridge100 port 1812 -w eap.pcap
|
16
schema.yml
16
schema.yml
@ -54849,10 +54849,6 @@ components:
|
||||
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.
|
||||
certificate:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
PatchedRedirectStageRequest:
|
||||
type: object
|
||||
description: RedirectStage Serializer
|
||||
@ -57306,10 +57302,6 @@ components:
|
||||
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.
|
||||
certificate:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
required:
|
||||
- application_slug
|
||||
- auth_flow_slug
|
||||
@ -57396,10 +57388,6 @@ components:
|
||||
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.
|
||||
certificate:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
required:
|
||||
- assigned_application_name
|
||||
- assigned_application_slug
|
||||
@ -57524,10 +57512,6 @@ components:
|
||||
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.
|
||||
certificate:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
required:
|
||||
- authorization_flow
|
||||
- invalidation_flow
|
||||
|
@ -1,4 +1,3 @@
|
||||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
|
||||
import { ascii_letters, digits, randomString } from "@goauthentik/common/utils";
|
||||
@ -94,14 +93,6 @@ export function renderForm(
|
||||
help=${clientNetworksHelp}
|
||||
input-hint="code"
|
||||
></ak-text-input>
|
||||
<ak-form-element-horizontal label=${msg("Certificate")} name="certificate">
|
||||
<!-- NOTE: 'null' cast to 'undefined' on signingKey to satisfy Lit requirements -->
|
||||
<ak-crypto-certificate-search
|
||||
certificate=${ifDefined(provider?.certificate ?? undefined)}
|
||||
singleton
|
||||
></ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">${msg("Certificate used for EAP-TLS.")}</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Property mappings")}
|
||||
name="propertyMappings"
|
||||
|
@ -45,11 +45,11 @@ Configuration details such as credentials can be specified through _settings_, w
|
||||
|
||||
### Connection settings
|
||||
|
||||
Each connection is authorized through authentik policy objects that are bound to the application and the endpoint. Additional verification can be done with the authorization flow.
|
||||
Each connection is authorized through authentik Policy objects that are bound to the application and the endpoint. Additional verification can be done with the authorization flow.
|
||||
|
||||
A new connection is created every time an endpoint is selected in the [User Interface](../../../customize/interfaces/user). After the user's authentik session expires, the connection is terminated. Additionally, the connection timeout can be specified in the provider, which applies even if the user is still authenticated. The connection can also be terminated manually from the **Connections** tab of the RAC provider.
|
||||
A new connection is created every time an endpoint is selected in the [User Interface](../../../customize/interfaces/user/customization.mdx). Once the user's authentik session expires, the connection is terminated. Additionally, the connection timeout can be specified in the provider, which applies even if the user is still authenticated. The connection can also be terminated manually.
|
||||
|
||||
Additionally, it is possible to modify the connection settings through the authorization flow. Configuration set in `connection_settings` in the flow plan context will be merged with other settings as shown above.
|
||||
Additionally it is possible to modify the connection settings through the authorization flow. Configuration set in `connection_settings` in the flow plan context will be merged with other settings as shown above.
|
||||
|
||||
The RAC provider utilises [Apache Guacamole](https://guacamole.apache.org/) for establishing SSH, RDP and VNC connections. RAC supports the use of Apache Guacamole connection configurations.
|
||||
|
||||
|
@ -6,8 +6,8 @@ You can customize the behaviour, look, and available resources for your authenti
|
||||
|
||||
- [Policies](./policies/working_with_policies.md)
|
||||
- Interfaces:
|
||||
- [Flow interface](./interfaces/flow)
|
||||
- [User interface](./interfaces/user)
|
||||
- [Admin interface](./interfaces/admin)
|
||||
- [Flows](./interfaces/flow/customization.mdx)
|
||||
- [User interface](./interfaces/user/customization.mdx)
|
||||
- [Admin interface](./interfaces/admin/customization.mdx)
|
||||
- [Blueprints](./blueprints/index.mdx)
|
||||
- [Branding](./branding.md)
|
||||
|
@ -1,19 +0,0 @@
|
||||
### Enabling/disabling features
|
||||
|
||||
The features listed below can be enabled or disabled through attributes set on the Brand. By default, all of the listed features are enabled. To disable a specific feature, set its value to `false`.
|
||||
|
||||
#### `settings.enabledFeatures.apiDrawer`
|
||||
|
||||
Display the API Request drawer in the upper tool bar.
|
||||
|
||||
#### `settings.enabledFeatures.notificationDrawer`
|
||||
|
||||
Display the Notification drawer in the upper tool bar.
|
||||
|
||||
#### `settings.enabledFeatures.settings`
|
||||
|
||||
Display the Settings link in the upper tool bar.
|
||||
|
||||
#### `settings.enabledFeatures.search`
|
||||
|
||||
Display the Search bar in the upper tool bar.
|
@ -1,36 +0,0 @@
|
||||
### General settings (both Admin and User interfaces)
|
||||
|
||||
#### `settings.navbar.userDisplay`
|
||||
|
||||
Configure what is shown in the top right corner. Defaults to `username`. Available options: `username`, `name`, `email`
|
||||
|
||||
#### `settings.theme.base`
|
||||
|
||||
Configure the base color scheme or toggle between dark and light modes. The default setting is `automatic`, which adapts based on the user’s browser preference. Available options: `automatic`, `dark`, `light`.
|
||||
|
||||
**Example**:
|
||||
|
||||
```
|
||||
settings:
|
||||
theme:
|
||||
base: dark
|
||||
```
|
||||
|
||||
#### `settings.theme.background`
|
||||
|
||||
Optional CSS that is applied to the background of the User interface, for example to set a custom background color, gradient, or image.
|
||||
|
||||
```yaml
|
||||
settings:
|
||||
theme:
|
||||
background: >
|
||||
background: url('https://picsum.photos/1920/1080');
|
||||
filter: blur(8px);
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
```
|
||||
|
||||
#### `settings.locale`
|
||||
|
||||
The locale which can be configured in the user settings by default. This can be used to preset locales for groups of users, but still let them choose their own preferred locale.
|
@ -1,11 +1,3 @@
|
||||
## Global customization
|
||||
### Global customization
|
||||
|
||||
To customize the following brand settings, log in to the Admin interface and navigate to **System > Brands > Brand settings**.
|
||||
|
||||
- Title
|
||||
- Logo
|
||||
- Favicon
|
||||
- Default flow background image
|
||||
- Custom CSS
|
||||
|
||||
For more details, see the [Brand settings](../../../sys-mgmt/brands.md#branding-settings) documentation.
|
||||
See [Brand Settings](../../../sys-mgmt/brands.md#branding-settings)
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 96 KiB |
17
website/docs/customize/interfaces/admin/customization.mdx
Normal file
17
website/docs/customize/interfaces/admin/customization.mdx
Normal file
@ -0,0 +1,17 @@
|
||||
# Customization
|
||||
|
||||
### `settings.pagination.perPage`
|
||||
|
||||
How many items should be retrieved per page. Defaults to 20.
|
||||
|
||||
### `settings.defaults.userPath`
|
||||
|
||||
Default user path which is opened when opening the user list. Defaults to `users`.
|
||||
|
||||
### `settings.theme.base`
|
||||
|
||||
Configure the base color scheme. Defaults to `automatic`, which switches between dark and light mode based on the users' browsers' preference. Choices: `automatic`, `dark`, `light`.
|
||||
|
||||
import Global from "../_global/global.mdx";
|
||||
|
||||
<Global />
|
@ -1,46 +0,0 @@
|
||||
---
|
||||
title: Customize the Admin interface
|
||||
sidebar_label: Admin interface
|
||||
---
|
||||
|
||||
The Admin interface can be customized using attributes configured in [Brands](../../../sys-mgmt/brands.md)
|
||||
|
||||
To add, remove, or modify attributes for a brand, log in to the Admin interface and navigate to **System > Brands > Other global settings > Attributes**.
|
||||
|
||||
Most attributes defined in a brand apply to _both_ the User and Admin interfaces. However, any settings that are specific to only the Admin interface are explicitly noted as such below.
|
||||
|
||||
The following screenshot shows the syntax for setting several attributes for a brand: dark mode, a 3-column display of applications on **My applications** page of the User interface, and hiding the API and Notifications drawers from the Admin interface tool bar.
|
||||
|
||||

|
||||
|
||||
## Custom settings
|
||||
|
||||
The following settings for attributes are grouped by:
|
||||
|
||||
- `enabledFeatures` settings
|
||||
- General settings (used on both the Admin interface and the User interface)
|
||||
- Admin interface only
|
||||
|
||||
import Enabledfeatureslist from "../\_enabledfeatureslist.mdx";
|
||||
|
||||
<Enabledfeatureslist />
|
||||
|
||||
import Generalattributes from "../\_generalattributes.mdx";
|
||||
|
||||
<Generalattributes />
|
||||
|
||||
### Settings for the Admin interface only
|
||||
|
||||
The following settings can only be used to customize the Admin interface, not the User interface.
|
||||
|
||||
#### `settings.pagination.perPage`
|
||||
|
||||
How many items should be retrieved per page. Defaults to 20.
|
||||
|
||||
#### `settings.defaults.userPath`
|
||||
|
||||
Default user path which is used when opening the user list. Defaults to `users`.
|
||||
|
||||
import Global from "../_global/global.mdx";
|
||||
|
||||
<Global />
|
11
website/docs/customize/interfaces/flow/customization.mdx
Normal file
11
website/docs/customize/interfaces/flow/customization.mdx
Normal file
@ -0,0 +1,11 @@
|
||||
# Customization
|
||||
|
||||
Since flows can be executed authenticated or unauthenticated, the default settings can be set via brands _attributes_.
|
||||
|
||||
### `settings.theme.base`
|
||||
|
||||
Configure the base color scheme. Defaults to `automatic`, which switches between dark and light mode based on the users' browsers' preference. Choices: `automatic`, `dark`, `light`.
|
||||
|
||||
import Global from "../_global/global.mdx";
|
||||
|
||||
<Global />
|
@ -1,19 +0,0 @@
|
||||
---
|
||||
title: Customize a flow
|
||||
sidebar_label: Flow interface
|
||||
---
|
||||
|
||||
Typically, settings for flows are defined as defaults in the [Brand settings](../../../sys-mgmt/brands.md). However, it’s important to note that some flows are executed before the specific user is authenticated and thus before authentik can determine which user is viewing the flow (for example, the `default-authentication-flow`!). Consequently, using default settings for all flows ensures a more consistent user experience.
|
||||
|
||||
Two settings that you can configure per flow are the _background image_ for the flow, and the _layout_.
|
||||
|
||||
## Customize a flow's background image
|
||||
|
||||
You can define a:
|
||||
|
||||
- Default background image for all flows, set in the instance's [brand](../../../sys-mgmt/brands.md)
|
||||
- A background image for [one or more specific flows](../../../add-secure-apps/flows-stages/flow/index.md#flow-configuration-options) (overrides the default)
|
||||
|
||||
## Set the layout for a flow
|
||||
|
||||
To define the layout for a flow, edit the flow and under **Appearance settings > Layout** select how the UI displays the flow when it is executed; with stacked elements, content left or right, and sidebar left or right.
|
64
website/docs/customize/interfaces/user/customization.mdx
Normal file
64
website/docs/customize/interfaces/user/customization.mdx
Normal file
@ -0,0 +1,64 @@
|
||||
# Customization
|
||||
|
||||
The user interface can be customized through attributes, and will be inherited from a users' groups.
|
||||
|
||||
## Enabling/disabling features
|
||||
|
||||
The following features can be enabled/disabled. By default, all of them are enabled:
|
||||
|
||||
- `settings.enabledFeatures.apiDrawer`
|
||||
|
||||
API Request drawer in navbar
|
||||
|
||||
- `settings.enabledFeatures.notificationDrawer`
|
||||
|
||||
Notification drawer in navbar
|
||||
|
||||
- `settings.enabledFeatures.settings`
|
||||
|
||||
Settings link in navbar
|
||||
|
||||
- `settings.enabledFeatures.applicationEdit`
|
||||
|
||||
Application edit in library (only shown when user is superuser)
|
||||
|
||||
- `settings.enabledFeatures.search`
|
||||
|
||||
Search bar
|
||||
|
||||
## Other configuration
|
||||
|
||||
### `settings.navbar.userDisplay`
|
||||
|
||||
Configure what is shown in the top right corner. Defaults to `username`. Choices: `username`, `name`, `email`
|
||||
|
||||
### `settings.theme.base`
|
||||
|
||||
Configure the base color scheme. Defaults to `automatic`, which switches between dark and light mode based on the users' browsers' preference. Choices: `automatic`, `dark`, `light`.
|
||||
|
||||
### `settings.theme.background`
|
||||
|
||||
Optional CSS which is applied in the background of the background of the user interface; for example
|
||||
|
||||
```yaml
|
||||
settings:
|
||||
theme:
|
||||
background: >
|
||||
background: url('https://picsum.photos/1920/1080');
|
||||
filter: blur(8px);
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
```
|
||||
|
||||
### `settings.layout.type`
|
||||
|
||||
Which layout to use for the _My applications_ view. Defaults to `row`. Choices: `row`, `2-column`, `3-column`
|
||||
|
||||
### `settings.locale`
|
||||
|
||||
The locale which can be configured in the user settings by default. This can be used to preset locales for groups of users, but still let them choose their own preferred locale
|
||||
|
||||
import Global from "../_global/global.mdx";
|
||||
|
||||
<Global />
|
@ -1,44 +0,0 @@
|
||||
---
|
||||
title: Customize the User interface
|
||||
sidebar_label: User interface
|
||||
---
|
||||
|
||||
The User interface can be customized using attributes configured in [Brands](../../../sys-mgmt/brands.md).
|
||||
|
||||
To add, remove, or modify attributes for a brand, log in as an administrator and navigate to **System > Brands > Other global settings > Attributes**.
|
||||
|
||||
Most attributes defined in a brand apply to _both_ the User and Admin interfaces. However, any settings that are specific to only one interface are explicitly noted as such below.
|
||||
|
||||
The following screenshot shows the syntax for setting several attributes for a brand: light mode, a 3-column display of applications on **My applications** page, hiding the API drawer and the Notification drawer from the tool bar, and disallowing users to edit the applications on **My applications** page.
|
||||
|
||||

|
||||
|
||||
## Custom settings
|
||||
|
||||
The following settings for attributes are grouped by:
|
||||
|
||||
- `enabledFeatures` settings
|
||||
- General attributes (used on both the Admin interface and the User interface)
|
||||
- User interface only
|
||||
|
||||
import Enabledfeatureslist from "../\_enabledfeatureslist.mdx";
|
||||
|
||||
<Enabledfeatureslist />
|
||||
|
||||
#### `settings.enabledFeatures.applicationEdit` (User interface only)
|
||||
|
||||
Display the Edit option for each application on the **My applications** page (only shown when user is superuser).
|
||||
|
||||
import Generalattributes from "../\_generalattributes.mdx";
|
||||
|
||||
<Generalattributes />
|
||||
|
||||
### Settings for the User interface only
|
||||
|
||||
#### `settings.layout.type`
|
||||
|
||||
Which layout to use for the **My applications** page. Defaults to `row`. Choices: `row`, `2-column`, `3-column`
|
||||
|
||||
import Global from "../_global/global.mdx";
|
||||
|
||||
<Global />
|
Binary file not shown.
Before Width: | Height: | Size: 106 KiB |
@ -16,8 +16,8 @@ slug: "/releases/2023.3"
|
||||
Documentation: [SCIM Provider](../../add-secure-apps/providers/scim/index.md)
|
||||
|
||||
- Theming improvements
|
||||
- The custom.css file is now loaded in ShadowDOMs, allowing for much greater customization, as previously it was only possible to style elements outside of the ShadowDOM. See docs for the [User interface](../../customize/interfaces/user/index.mdx) and [Admin interface](../../customize/interfaces/admin/index.mdx).
|
||||
- Previously, authentik would automatically switch between dark and light theme based on the users' browsers' settings. This can now be overridden to either force the light or dark theme, per user/group/brand. See docs for the [User interface](../../customize/interfaces/user/index.mdx) and [Admin interface](../../customize/interfaces/admin/index.mdx).
|
||||
- The custom.css file is now loaded in ShadowDOMs, allowing for much greater customization, as previously it was only possible to style elements outside of the ShadowDOM. See docs for [Flow](../../customize/interfaces/flow/customization.mdx), [User](../../customize/interfaces/user/customization.mdx) and [Admin](../../customize/interfaces/admin/customization.mdx) interfaces.
|
||||
- Previously, authentik would automatically switch between dark and light theme based on the users' browsers' settings. This can now be overridden to either force the light or dark theme, per user/group/tenant. See docs for [Flow](../../customize/interfaces/flow/customization.mdx), [User](../../customize/interfaces/user/customization.mdx) and [Admin](../../customize/interfaces/admin/customization.mdx) interfaces.
|
||||
|
||||
## Upgrading
|
||||
|
||||
|
@ -3,7 +3,7 @@ title: Brands
|
||||
slug: /brands
|
||||
---
|
||||
|
||||
As an authentik administrator, you can customize your instance's appearance and behavior using brands. Brands apply to a single domain, a domain wildcard, or can be set as default, in which case the brand will be applied when no other brand matches the domain.
|
||||
As an authentik admin, you can customize your instance's appearance and behavior using brands. Brands apply to a single domain, a domain wildcard or can be set as default, in which case the brand will be used when no other brand matches the domain.
|
||||
|
||||
For an overview of branding and other customization options in authentik refer to [Customize your instance](../customize/index.md).
|
||||
|
||||
@ -71,4 +71,4 @@ When using the [Mutual TLS Stage](../add-secure-apps/flows-stages/stages/mtls/in
|
||||
|
||||
#### Attributes
|
||||
|
||||
Attributes such as locale, theme settings (light/dark mode), and custom attributes can be set to a per-brand default value here. Any custom attributes can be retrieved via [`group_attributes()`](../users-sources/user/user_ref.mdx#object-properties).
|
||||
Attributes such as locale, theme settings and custom attributes can be set to a per-brand default value here. Any custom attributes can be retrieved via [`group_attributes()`](../users-sources/user/user_ref.mdx#object-properties).
|
||||
|
@ -0,0 +1,172 @@
|
||||
---
|
||||
title: Notification Rule Expression Policies
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
Notification rules with bound expression policies are very powerful. The following are examples of what can be achieved.
|
||||
|
||||
### Change user attributes upon account deactivation
|
||||
|
||||
This example code is triggered when a user account with the `sshPublicKey` attribute set is deactivated. It saves the `sshPublicKey` attribute to a new `inactivesshPublicKey` attribute, and subsequently nullifies the `sshPublicKey` attribute.
|
||||
|
||||
```python
|
||||
from authentik.core.models import User
|
||||
|
||||
# Check if an event has occurred
|
||||
event = request.context.get("event", None)
|
||||
if not event:
|
||||
ak_logger.info("no event")
|
||||
return False
|
||||
|
||||
# Check if the event action includes updating a model
|
||||
if event.action != "model_updated":
|
||||
ak_logger.info("event action does not match")
|
||||
return False
|
||||
|
||||
model_app = event.context["model"]["app"]
|
||||
model_name = event.context["model"]["model_name"]
|
||||
|
||||
# Check if the model that was updated is the user model
|
||||
if model_app != "authentik_core" or model_name != "user":
|
||||
ak_logger.info("model does not match")
|
||||
|
||||
user_pk = event.context["model"]["pk"]
|
||||
user = User.objects.filter(pk=user_pk).first()
|
||||
|
||||
# Check if an user object was found
|
||||
if not user:
|
||||
ak_logger.info("user not found")
|
||||
return False
|
||||
|
||||
# Check if user is active
|
||||
if user.is_active:
|
||||
ak_logger.info("user is active, not changing")
|
||||
return False
|
||||
|
||||
# Check if user has the `sshPublicKey` attribute set
|
||||
if not user.attributes.get("sshPublicKey"):
|
||||
ak_logger.info("no public keys to remove")
|
||||
return False
|
||||
|
||||
# Save the `sshPublicKey` attribute to a new `inactiveSSHPublicKey` attribute
|
||||
user.attributes["inactiveSSHPublicKey"] = user.attributes["sshPublicKey"]
|
||||
|
||||
# Nullify the `sshPublicKey` attribute
|
||||
user.attributes["sshPublicKey"] = []
|
||||
|
||||
# Save the changes made to the user
|
||||
user.save()
|
||||
|
||||
return False
|
||||
```
|
||||
|
||||
### Alert when application is created without binding
|
||||
|
||||
This code is triggered when a new application is created without any user, group, or policy bound to it. The notification rule can then be configured to alert an administrator. This feature is useful for ensuring limited access to applications, as by default, an application without any users, groups, or policies bound to it can be accessed by all users.
|
||||
|
||||
```python
|
||||
from authentik.core.models import Application
|
||||
from authentik.policies.models import PolicyBinding
|
||||
|
||||
# Check if an event has occurred
|
||||
event = request.context.get("event", None)
|
||||
if not event:
|
||||
ak_logger.info("no event")
|
||||
return False
|
||||
|
||||
# Check if the event action includes creating a model
|
||||
if event.action != "model_created":
|
||||
ak_logger.info("event action does not match")
|
||||
return False
|
||||
|
||||
model_app = event.context["model"]["app"]
|
||||
model_name = event.context["model"]["model_name"]
|
||||
|
||||
# Check if the model that was created is the application model
|
||||
if model_app != "authentik_core" or model_name != "application":
|
||||
ak_logger.info("model does not match")
|
||||
|
||||
application_pk = event.context["model"]["pk"]
|
||||
application = Application.objects.filter(pk=application_pk).first()
|
||||
|
||||
# Check if an application object was found
|
||||
if not application:
|
||||
ak_logger.info("application not found")
|
||||
return False
|
||||
|
||||
# Check if application has binding
|
||||
if PolicyBinding.objects.filter(target=application).exists():
|
||||
output = PolicyBinding.objects.filter(target=application)
|
||||
ak_logger.info("application has bindings, returning true")
|
||||
return True
|
||||
|
||||
return False
|
||||
```
|
||||
|
||||
### Append user addition history to group attributes
|
||||
|
||||
This code is triggered when a user is added to a group. It then creates and updates a `UserAddedHistory` attribute to the group with a date/time stamp and the username of the added user. This functionality is already available within the changelog of a group, but this code can be used as a template to trigger alerts or other events.
|
||||
|
||||
:::note
|
||||
This policy interacts with the `diff` event output. This filed is only available with an enterprise license.
|
||||
:::
|
||||
|
||||
```python
|
||||
from authentik.core.models import User
|
||||
from authentik.core.models import Group
|
||||
from datetime import datetime
|
||||
|
||||
# Check if an event has occurred
|
||||
event = request.context.get("event", None)
|
||||
if not event:
|
||||
ak_logger.info("no event")
|
||||
return False
|
||||
|
||||
# Check if the event action includes updating a model
|
||||
if event.action != "model_updated":
|
||||
ak_logger.info("event action does not match")
|
||||
return False
|
||||
|
||||
model_app = event.context["model"]["app"]
|
||||
model_name = event.context["model"]["model_name"]
|
||||
|
||||
# Check if the model that was updated is the group model
|
||||
if model_app != "authentik_core" or model_name != "group":
|
||||
ak_logger.info("model does not match")
|
||||
|
||||
group_pk = event.context["model"]["pk"]
|
||||
group = Group.objects.filter(pk=group_pk).first()
|
||||
|
||||
# If user was added to group, get user object, else return false
|
||||
if "add" in event.context["diff"]["users"]:
|
||||
ak_logger.info("user added to group")
|
||||
|
||||
user_pk = event.context["diff"]["users"]["add"][0]
|
||||
user = User.objects.filter(pk=user_pk).first()
|
||||
else:
|
||||
ak_logger.info("user not added to group")
|
||||
return False
|
||||
|
||||
# Check if a group object was found
|
||||
if not group:
|
||||
ak_logger.info("group not found")
|
||||
return False
|
||||
|
||||
# Check if an user object was found
|
||||
if not user:
|
||||
ak_logger.info("user not found")
|
||||
return False
|
||||
|
||||
if not group.attributes.get("UserAddedHistory"):
|
||||
group.attributes["UserAddedHistory"] = []
|
||||
|
||||
current_date_time = datetime.now().isoformat(timespec='seconds')
|
||||
|
||||
group.attributes["UserAddedHistory"].append(current_date_time + " - Added user: " + user.username)
|
||||
|
||||
# Save the changes made to the group
|
||||
group.save()
|
||||
|
||||
return False
|
||||
```
|
@ -131,10 +131,6 @@ const config = createDocusaurusConfig({
|
||||
],
|
||||
],
|
||||
},
|
||||
gtag: {
|
||||
trackingID: ["G-9MVR9WZFZH"],
|
||||
anonymizeIP: true,
|
||||
},
|
||||
theme: {
|
||||
customCss: require.resolve("@goauthentik/docusaurus-config/css/index.css"),
|
||||
},
|
||||
|
@ -128,10 +128,6 @@ const config = createDocusaurusConfig({
|
||||
],
|
||||
],
|
||||
},
|
||||
gtag: {
|
||||
trackingID: ["G-9MVR9WZFZH"],
|
||||
anonymizeIP: true,
|
||||
},
|
||||
theme: {
|
||||
customCss: require.resolve("@goauthentik/docusaurus-config/css/index.css"),
|
||||
},
|
||||
|
@ -86,20 +86,6 @@ package = "netlify-plugin-debug-cache"
|
||||
to = "/docs/customize/branding"
|
||||
status = 302
|
||||
|
||||
[[redirects]]
|
||||
from = "/docs/customize/interfaces/admin/customization"
|
||||
to = "/docs/customize/interfaces/admin"
|
||||
status = 302
|
||||
|
||||
[[redirects]]
|
||||
from = "/docs/customize/interfaces/user/customization"
|
||||
to = "/docs/customize/interfaces/user"
|
||||
status = 302
|
||||
|
||||
[[redirects]]
|
||||
from = "/docs/customize/interfaces/flow/customization"
|
||||
to = "/docs/customize/interfaces/flow"
|
||||
status = 302
|
||||
|
||||
# Migration to new structure with script Sept 2025
|
||||
[[redirects]]
|
||||
|
80
website/package-lock.json
generated
80
website/package-lock.json
generated
@ -19,7 +19,7 @@
|
||||
"@goauthentik/docusaurus-config": "^1.1.0",
|
||||
"@goauthentik/tsconfig": "^1.0.4",
|
||||
"@mdx-js/react": "^3.1.0",
|
||||
"@swc/html-linux-x64-gnu": "1.12.9",
|
||||
"@rspack/binding-linux-x64-gnu": "1.4.1",
|
||||
"clsx": "^2.1.1",
|
||||
"docusaurus-plugin-openapi-docs": "^4.4.0",
|
||||
"docusaurus-theme-openapi-docs": "^4.4.0",
|
||||
@ -62,15 +62,15 @@
|
||||
"node": ">=22.14.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rspack/binding-darwin-arm64": "1.4.2",
|
||||
"@rspack/binding-linux-arm64-gnu": "1.4.2",
|
||||
"@rspack/binding-linux-x64-gnu": "1.4.2",
|
||||
"@swc/core-darwin-arm64": "1.12.9",
|
||||
"@swc/core-linux-arm64-gnu": "1.12.9",
|
||||
"@swc/core-linux-x64-gnu": "1.12.9",
|
||||
"@swc/html-darwin-arm64": "1.12.9",
|
||||
"@swc/html-linux-arm64-gnu": "1.12.9",
|
||||
"@swc/html-linux-x64-gnu": "1.12.9",
|
||||
"@rspack/binding-darwin-arm64": "1.4.1",
|
||||
"@rspack/binding-linux-arm64-gnu": "1.4.1",
|
||||
"@rspack/binding-linux-x64-gnu": "1.4.1",
|
||||
"@swc/core-darwin-arm64": "1.12.7",
|
||||
"@swc/core-linux-arm64-gnu": "1.12.7",
|
||||
"@swc/core-linux-x64-gnu": "1.12.7",
|
||||
"@swc/html-darwin-arm64": "1.12.7",
|
||||
"@swc/html-linux-arm64-gnu": "1.12.7",
|
||||
"@swc/html-linux-x64-gnu": "1.12.7",
|
||||
"lightningcss-darwin-arm64": "1.30.1",
|
||||
"lightningcss-linux-arm64-gnu": "1.30.1",
|
||||
"lightningcss-linux-x64-gnu": "1.30.1"
|
||||
@ -5034,9 +5034,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rspack/binding-darwin-arm64": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.4.2.tgz",
|
||||
"integrity": "sha512-0fPOew7D0l/x6qFZYdyUqutbw15K98VLvES2/7x2LPssTgypE4rVmnQSmVBnge3Nr8Qs/9qASPRpMWXBaqMfOA==",
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.4.1.tgz",
|
||||
"integrity": "sha512-enh5DYbpaexdEmjbcxj3BJDauP3w+20jFKWvKROtAQV350PUw0bf2b4WOgngIH9hBzlfjpXNYAk6T5AhVAlY3Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -5060,9 +5060,9 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rspack/binding-linux-arm64-gnu": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.4.2.tgz",
|
||||
"integrity": "sha512-UHAzggS8Mc7b3Xguhj82HwujLqBZquCeo8qJj5XreNaMKGb6YRw/91dJOVmkNiLCB0bj71CRE1Cocd+Peq3N9A==",
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.4.1.tgz",
|
||||
"integrity": "sha512-PJ5cHqvrj1bK7jH5DVrdKoR8Fy+p6l9baxXajq/6xWTxP+4YTdEtLsRZnpLMS1Ho2RRpkxDWJn+gdlKuleNioQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -5086,9 +5086,9 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rspack/binding-linux-x64-gnu": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.4.2.tgz",
|
||||
"integrity": "sha512-ucCCWdtH1tekZadrsYj6GNJ8EP21BM2uSE7MootbwLw8aBtgVTKUuRDQEps1h/rtrdthzd9XBX6Lc2N926gM+g==",
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.4.1.tgz",
|
||||
"integrity": "sha512-jjTx53CpiYWK7fAv5qS8xHEytFK6gLfZRk+0kt2YII6uqez/xQ3SRcboreH8XbJcBoxINBzMNMf5/SeMBZ939A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -5586,12 +5586,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-darwin-arm64": {
|
||||
"version": "1.12.9",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.12.9.tgz",
|
||||
"integrity": "sha512-GACFEp4nD6V+TZNR2JwbMZRHB+Yyvp14FrcmB6UCUYmhuNWjkxi+CLnEvdbuiKyQYv0zA+TRpCHZ+whEs6gwfA==",
|
||||
"version": "1.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.12.7.tgz",
|
||||
"integrity": "sha512-w6BBT0hBRS56yS+LbReVym0h+iB7/PpCddqrn1ha94ra4rZ4R/A91A/rkv+LnQlPqU/+fhqdlXtCJU9mrhCBtA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
@ -5633,12 +5634,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm64-gnu": {
|
||||
"version": "1.12.9",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.12.9.tgz",
|
||||
"integrity": "sha512-6qx1ka9LHcLzxIgn2Mros+CZLkHK2TawlXzi/h7DJeNnzi8F1Hw0Yzjp8WimxNCg6s2n+o3jnmin1oXB7gg8rw==",
|
||||
"version": "1.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.12.7.tgz",
|
||||
"integrity": "sha512-N15hKizSSh+hkZ2x3TDVrxq0TDcbvDbkQJi2ZrLb9fK+NdFUV/x+XF16ZDPlbxtrGXl1CT7VD439SNaMN9F7qw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@ -5664,12 +5666,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-x64-gnu": {
|
||||
"version": "1.12.9",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.12.9.tgz",
|
||||
"integrity": "sha512-SFUxyhWLZRNL8QmgGNqdi2Q43PNyFVkRZ2zIif30SOGFSxnxcf2JNeSeBgKIGVgaLSuk6xFVVCtJ3KIeaStgRg==",
|
||||
"version": "1.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.12.7.tgz",
|
||||
"integrity": "sha512-PR4tPVwU1BQBfFDk2XfzXxsEIjF3x/bOV1BzZpYvrlkU0TKUDbR4t2wzvsYwD/coW7/yoQmlL70/qnuPtTp1Zw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@ -5821,12 +5824,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/html-darwin-arm64": {
|
||||
"version": "1.12.9",
|
||||
"resolved": "https://registry.npmjs.org/@swc/html-darwin-arm64/-/html-darwin-arm64-1.12.9.tgz",
|
||||
"integrity": "sha512-uQl0y9uOgqnYR6t+TgcwFeGv1TC48xHGBqw3MrOIQLc+tqavqhQsLkVEEz1yd1J0WW3cVAsNSQlbERiwQcXQXA==",
|
||||
"version": "1.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@swc/html-darwin-arm64/-/html-darwin-arm64-1.12.7.tgz",
|
||||
"integrity": "sha512-4rHV4lW8PXSc7YfJ/c9Cj0xZWSJArkD/Yuax4plH6f4VtEcEAluZI3ryBG3Vh4VawQ1RMkytPQ2S65BbCyDIXg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
@ -5868,12 +5872,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/html-linux-arm64-gnu": {
|
||||
"version": "1.12.9",
|
||||
"resolved": "https://registry.npmjs.org/@swc/html-linux-arm64-gnu/-/html-linux-arm64-gnu-1.12.9.tgz",
|
||||
"integrity": "sha512-xX/S0galaqXMNc1olt1UOMcHXybDYGogGP90WheI6XD5zKVmbHdz9yU/nVeddZNUf5gZ011NCc5QSMB+2fh8EA==",
|
||||
"version": "1.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@swc/html-linux-arm64-gnu/-/html-linux-arm64-gnu-1.12.7.tgz",
|
||||
"integrity": "sha512-z66ejXsSwI0mKyDhLimG74+xZyvSQCrceSZv9jLHa23sn/di+07M9njZrj3SQKGfHoJqXsN1iPqDpvkVajNb9Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@ -5899,12 +5904,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/html-linux-x64-gnu": {
|
||||
"version": "1.12.9",
|
||||
"resolved": "https://registry.npmjs.org/@swc/html-linux-x64-gnu/-/html-linux-x64-gnu-1.12.9.tgz",
|
||||
"integrity": "sha512-9tRAsVsjjyEUFMH5uNrcLxb+5q0l2PCgTH7pe48hjcshKFoZamp1aiwvNnJMMBan3Ny9vFG5jKMJKG3ZkYPYxg==",
|
||||
"version": "1.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@swc/html-linux-x64-gnu/-/html-linux-x64-gnu-1.12.7.tgz",
|
||||
"integrity": "sha512-5KFLil4ELKzCLjjvKpt+SMEU6uBDR/EL4e7eleybtYi1cU8Jzv0xnTvabsVDfpT8fsvJF3Mvach4F/ggH5+CDQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
|
@ -75,15 +75,15 @@
|
||||
"typescript-eslint": "^8.35.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rspack/binding-darwin-arm64": "1.4.2",
|
||||
"@rspack/binding-linux-arm64-gnu": "1.4.2",
|
||||
"@rspack/binding-linux-x64-gnu": "1.4.2",
|
||||
"@swc/core-darwin-arm64": "1.12.9",
|
||||
"@swc/core-linux-arm64-gnu": "1.12.9",
|
||||
"@swc/core-linux-x64-gnu": "1.12.9",
|
||||
"@swc/html-darwin-arm64": "1.12.9",
|
||||
"@swc/html-linux-arm64-gnu": "1.12.9",
|
||||
"@swc/html-linux-x64-gnu": "1.12.9",
|
||||
"@rspack/binding-darwin-arm64": "1.4.1",
|
||||
"@rspack/binding-linux-arm64-gnu": "1.4.1",
|
||||
"@rspack/binding-linux-x64-gnu": "1.4.1",
|
||||
"@swc/core-darwin-arm64": "1.12.7",
|
||||
"@swc/core-linux-arm64-gnu": "1.12.7",
|
||||
"@swc/core-linux-x64-gnu": "1.12.7",
|
||||
"@swc/html-darwin-arm64": "1.12.7",
|
||||
"@swc/html-linux-arm64-gnu": "1.12.7",
|
||||
"@swc/html-linux-x64-gnu": "1.12.7",
|
||||
"lightningcss-darwin-arm64": "1.30.1",
|
||||
"lightningcss-linux-arm64-gnu": "1.30.1",
|
||||
"lightningcss-linux-x64-gnu": "1.30.1"
|
||||
|
@ -408,9 +408,21 @@ const items = [
|
||||
type: "category",
|
||||
label: "Interfaces",
|
||||
items: [
|
||||
"customize/interfaces/flow/index",
|
||||
"customize/interfaces/user/index",
|
||||
"customize/interfaces/admin/index",
|
||||
{
|
||||
type: "category",
|
||||
label: "Flow",
|
||||
items: ["customize/interfaces/flow/customization"],
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "User",
|
||||
items: ["customize/interfaces/user/customization"],
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "Admin",
|
||||
items: ["customize/interfaces/admin/customization"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
Reference in New Issue
Block a user