Skip to content

Commit

Permalink
Enhance Custom Authorization Abilities
Browse files Browse the repository at this point in the history
  • Loading branch information
adrianosela committed Oct 14, 2024
1 parent cc03474 commit c4e36b1
Show file tree
Hide file tree
Showing 9 changed files with 160 additions and 22 deletions.
22 changes: 22 additions & 0 deletions internal/server/authz/authorizer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package authz

Check failure on line 1 in internal/server/authz/authorizer.go

View workflow job for this annotation

GitHub Actions / lint / Go

package-comments: should have a package comment (revive)

import (
"crypto/tls"
"net"
)

// RequestAttributes represents attributes of a TURN request which
// may be useful for authorizing the underlying request.
type RequestAttributes struct {
Username string
Realm string
SrcAddr net.Addr
TLS *tls.ConnectionState

// extend as needed
}

// Authorizer represents functionality required to authorize a request.
type Authorizer interface {
Authorize(ra *RequestAttributes) (key []byte, ok bool)
}
22 changes: 22 additions & 0 deletions internal/server/authz/legacy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package authz

Check failure on line 1 in internal/server/authz/legacy.go

View workflow job for this annotation

GitHub Actions / lint / Go

ST1000: at least one file in a package should have a package comment (stylecheck)

import "net"

// LegacyAuthFunc is a function used to authorize requests compatible with legacy authorization.
type LegacyAuthFunc func(username, realm string, srcAddr net.Addr) (key []byte, ok bool)

// legacyAuthorizer is the an Authorizer implementation
// which wraps an AuthFunc in order to authorize requests.
type legacyAuthorizer struct {
authFunc LegacyAuthFunc
}

// NewLegacy returns a new legacy authorizer.
func NewLegacy(fn LegacyAuthFunc) Authorizer {
return &legacyAuthorizer{authFunc: fn}
}

// Authorize authorizes a request given request attributes.
func (a *legacyAuthorizer) Authorize(ra *RequestAttributes) (key []byte, ok bool) {
return a.authFunc(ra.Username, ra.Realm, ra.SrcAddr)
}
63 changes: 63 additions & 0 deletions internal/server/authz/tls.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package authz

Check failure on line 1 in internal/server/authz/tls.go

View workflow job for this annotation

GitHub Actions / lint / Go

ST1000: at least one file in a package should have a package comment (stylecheck)

import (
"crypto/x509"
)

// tlsAuthorizer is the an Authorizer implementation which verifies
// client TLS certificate metadata in order to to authorize requests.
type tlsAuthorizer struct {
verifyOpts x509.VerifyOptions
getKeyForUserFunc func(string) ([]byte, bool)
}

// NewTLS returns a new client tls certificate authorizer.
//
// This authorizer ensures that the client presents a valid TLS certificate
// for which the CommonName must match the TURN request's username attribute.
func NewTLS(
verifyOpts x509.VerifyOptions,
getKeyForUserFunc func(string) ([]byte, bool),
) Authorizer {
return &tlsAuthorizer{
verifyOpts: verifyOpts,
getKeyForUserFunc: getKeyForUserFunc,

Check warning on line 24 in internal/server/authz/tls.go

View check run for this annotation

Codecov / codecov/patch

internal/server/authz/tls.go#L21-L24

Added lines #L21 - L24 were not covered by tests
}
}

// Authorize authorizes a request given request attributes.
func (a *tlsAuthorizer) Authorize(ra *RequestAttributes) ([]byte, bool) {
if ra.TLS == nil || len(ra.TLS.PeerCertificates) == 0 {

Check warning on line 30 in internal/server/authz/tls.go

View check run for this annotation

Codecov / codecov/patch

internal/server/authz/tls.go#L29-L30

Added lines #L29 - L30 were not covered by tests
// request not allowed due to not having tls state metadata
// TODO: INFO log
return nil, false

Check warning on line 33 in internal/server/authz/tls.go

View check run for this annotation

Codecov / codecov/patch

internal/server/authz/tls.go#L33

Added line #L33 was not covered by tests
}

key, ok := a.getKeyForUserFunc(ra.Username)
if !ok {

Check warning on line 37 in internal/server/authz/tls.go

View check run for this annotation

Codecov / codecov/patch

internal/server/authz/tls.go#L36-L37

Added lines #L36 - L37 were not covered by tests
// request not allowed due to having no key for the TURN request's username
// TODO: INFO log
return nil, false

Check warning on line 40 in internal/server/authz/tls.go

View check run for this annotation

Codecov / codecov/patch

internal/server/authz/tls.go#L40

Added line #L40 was not covered by tests
}

for _, cert := range ra.TLS.PeerCertificates {
if cert.Subject.CommonName != ra.Username {

Check warning on line 44 in internal/server/authz/tls.go

View check run for this annotation

Codecov / codecov/patch

internal/server/authz/tls.go#L43-L44

Added lines #L43 - L44 were not covered by tests
// cert not allowed due to not matching the TURN username
// TODO: DEBUG log
continue

Check warning on line 47 in internal/server/authz/tls.go

View check run for this annotation

Codecov / codecov/patch

internal/server/authz/tls.go#L47

Added line #L47 was not covered by tests
}

if _, err := cert.Verify(a.verifyOpts); err != nil {

Check warning on line 50 in internal/server/authz/tls.go

View check run for this annotation

Codecov / codecov/patch

internal/server/authz/tls.go#L50

Added line #L50 was not covered by tests
// cert not allowed due to failed validation
// TODO: WARN log
continue

Check warning on line 53 in internal/server/authz/tls.go

View check run for this annotation

Codecov / codecov/patch

internal/server/authz/tls.go#L53

Added line #L53 was not covered by tests
}

// a valid certificate was allowed
return key, true

Check warning on line 57 in internal/server/authz/tls.go

View check run for this annotation

Codecov / codecov/patch

internal/server/authz/tls.go#L57

Added line #L57 was not covered by tests
}

// request not allowed due to not having any valid certs
// TODO: INFO log
return nil, false

Check warning on line 62 in internal/server/authz/tls.go

View check run for this annotation

Codecov / codecov/patch

internal/server/authz/tls.go#L62

Added line #L62 was not covered by tests
}
5 changes: 4 additions & 1 deletion internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package server

import (
"crypto/tls"
"fmt"
"net"
"time"
Expand All @@ -13,6 +14,7 @@ import (
"github.com/pion/stun/v3"
"github.com/pion/turn/v4/internal/allocation"
"github.com/pion/turn/v4/internal/proto"
"github.com/pion/turn/v4/internal/server/authz"
)

// Request contains all the state needed to process a single incoming datagram
Expand All @@ -21,13 +23,14 @@ type Request struct {
Conn net.PacketConn
SrcAddr net.Addr
Buff []byte
TLS *tls.ConnectionState

// Server State
AllocationManager *allocation.Manager
NonceHash *NonceHash

// User Configuration
AuthHandler func(username string, realm string, srcAddr net.Addr) (key []byte, ok bool)
Authorizer authz.Authorizer
Log logging.LeveledLogger
Realm string
ChannelBindTimeout time.Duration
Expand Down
5 changes: 3 additions & 2 deletions internal/server/turn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/pion/stun/v3"
"github.com/pion/turn/v4/internal/allocation"
"github.com/pion/turn/v4/internal/proto"
"github.com/pion/turn/v4/internal/server/authz"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -90,9 +91,9 @@ func TestAllocationLifeTime(t *testing.T) {
Conn: l,
SrcAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 5000},
Log: logger,
AuthHandler: func(string, string, net.Addr) (key []byte, ok bool) {
Authorizer: authz.NewLegacy(func(string, string, net.Addr) (key []byte, ok bool) {
return []byte(staticKey), true
},
}),
}

fiveTuple := &allocation.FiveTuple{SrcAddr: r.SrcAddr, DstAddr: r.Conn.LocalAddr(), Protocol: allocation.UDP}
Expand Down
12 changes: 9 additions & 3 deletions internal/server/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/pion/stun/v3"
"github.com/pion/turn/v4/internal/proto"
"github.com/pion/turn/v4/internal/server/authz"
)

const (
Expand Down Expand Up @@ -66,9 +67,9 @@ func authenticateRequest(r Request, m *stun.Message, callingMethod stun.Method)
realmAttr := &stun.Realm{}
badRequestMsg := buildMsg(m.TransactionID, stun.NewType(callingMethod, stun.ClassErrorResponse), &stun.ErrorCodeAttribute{Code: stun.CodeBadRequest})

// No Auth handler is set, server is running in STUN only mode
// No Authorizer is set, server is running in STUN only mode
// Respond with 400 so clients don't retry
if r.AuthHandler == nil {
if r.Authorizer == nil {
sendErr := buildAndSend(r.Conn, r.SrcAddr, badRequestMsg...)
return nil, false, sendErr
}
Expand All @@ -88,7 +89,12 @@ func authenticateRequest(r Request, m *stun.Message, callingMethod stun.Method)
return nil, false, buildAndSendErr(r.Conn, r.SrcAddr, err, badRequestMsg...)
}

ourKey, ok := r.AuthHandler(usernameAttr.String(), realmAttr.String(), r.SrcAddr)
ourKey, ok := r.Authorizer.Authorize(&authz.RequestAttributes{
Username: usernameAttr.String(),
Realm: realmAttr.String(),
SrcAddr: r.SrcAddr,
TLS: r.TLS,
})
if !ok {
return nil, false, buildAndSendErr(r.Conn, r.SrcAddr, fmt.Errorf("%w %s", errNoSuchUser, usernameAttr.String()), badRequestMsg...)
}
Expand Down
5 changes: 3 additions & 2 deletions lt_cred.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import ( //nolint:gci
"time"

"github.com/pion/logging"
"github.com/pion/turn/v4/internal/server/authz"
)

// GenerateLongTermCredentials can be used to create credentials valid for [duration] time
Expand Down Expand Up @@ -44,7 +45,7 @@ func longTermCredentials(username string, sharedSecret string) (string, error) {

// NewLongTermAuthHandler returns a turn.AuthAuthHandler used with Long Term (or Time Windowed) Credentials.
// See: https://datatracker.ietf.org/doc/html/rfc8489#section-9.2
func NewLongTermAuthHandler(sharedSecret string, l logging.LeveledLogger) AuthHandler {
func NewLongTermAuthHandler(sharedSecret string, l logging.LeveledLogger) authz.LegacyAuthFunc {
if l == nil {
l = logging.NewDefaultLoggerFactory().NewLogger("turn")
}
Expand Down Expand Up @@ -74,7 +75,7 @@ func NewLongTermAuthHandler(sharedSecret string, l logging.LeveledLogger) AuthHa
//
// The supported format of is timestamp:username, where username is an arbitrary user id and the
// timestamp specifies the expiry of the credential.
func LongTermTURNRESTAuthHandler(sharedSecret string, l logging.LeveledLogger) AuthHandler {
func LongTermTURNRESTAuthHandler(sharedSecret string, l logging.LeveledLogger) authz.LegacyAuthFunc {
if l == nil {
l = logging.NewDefaultLoggerFactory().NewLogger("turn")
}
Expand Down
31 changes: 25 additions & 6 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package turn

import (
"crypto/tls"
"errors"
"fmt"
"net"
Expand All @@ -14,6 +15,7 @@ import (
"github.com/pion/turn/v4/internal/allocation"
"github.com/pion/turn/v4/internal/proto"
"github.com/pion/turn/v4/internal/server"
"github.com/pion/turn/v4/internal/server/authz"
)

const (
Expand All @@ -23,7 +25,7 @@ const (
// Server is an instance of the Pion TURN Server
type Server struct {
log logging.LeveledLogger
authHandler AuthHandler
authorizer authz.Authorizer
realm string
channelBindTimeout time.Duration
nonceHash *server.NonceHash
Expand Down Expand Up @@ -57,9 +59,16 @@ func NewServer(config ServerConfig) (*Server, error) {
return nil, err
}

// determine authorizer, prioritizing the
// (legacy) AuthHandler if it was provided.
authorizer := config.Authorizer
if config.AuthHandler != nil {
authorizer = authz.NewLegacy(config.AuthHandler)
}

s := &Server{
log: loggerFactory.NewLogger("turn"),
authHandler: config.AuthHandler,
authorizer: authorizer,
realm: config.Realm,
channelBindTimeout: config.ChannelBindTimeout,
packetConnConfigs: config.PacketConnConfigs,
Expand All @@ -79,7 +88,7 @@ func NewServer(config ServerConfig) (*Server, error) {
}

go func(cfg PacketConnConfig, am *allocation.Manager) {
s.readLoop(cfg.PacketConn, am)
s.readLoop(cfg.PacketConn, am, nil)

if err := am.Close(); err != nil {
s.log.Errorf("Failed to close AllocationManager: %s", err)
Expand Down Expand Up @@ -151,7 +160,16 @@ func (s *Server) readListener(l net.Listener, am *allocation.Manager) {
}

go func() {
s.readLoop(NewSTUNConn(conn), am)
var tlsConnectionState *tls.ConnectionState

// extract tls connection state if possible
tlsConn, ok := conn.(*tls.Conn)
if ok {
cs := tlsConn.ConnectionState()
tlsConnectionState = &cs

Check warning on line 169 in server.go

View check run for this annotation

Codecov / codecov/patch

server.go#L168-L169

Added lines #L168 - L169 were not covered by tests
}

s.readLoop(NewSTUNConn(conn), am, tlsConnectionState)

// Delete allocation
am.DeleteAllocation(&allocation.FiveTuple{
Expand Down Expand Up @@ -202,7 +220,7 @@ func (s *Server) createAllocationManager(addrGenerator RelayAddressGenerator, ha
return am, err
}

func (s *Server) readLoop(p net.PacketConn, allocationManager *allocation.Manager) {
func (s *Server) readLoop(p net.PacketConn, allocationManager *allocation.Manager, tls *tls.ConnectionState) {
buf := make([]byte, s.inboundMTU)
for {
n, addr, err := p.ReadFrom(buf)
Expand All @@ -219,8 +237,9 @@ func (s *Server) readLoop(p net.PacketConn, allocationManager *allocation.Manage
Conn: p,
SrcAddr: addr,
Buff: buf[:n],
TLS: tls,
Log: s.log,
AuthHandler: s.authHandler,
Authorizer: s.authorizer,
Realm: s.realm,
AllocationManager: allocationManager,
ChannelBindTimeout: s.channelBindTimeout,
Expand Down
17 changes: 9 additions & 8 deletions server_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"time"

"github.com/pion/logging"
"github.com/pion/turn/v4/internal/server/authz"
)

// RelayAddressGenerator is used to generate a RelayAddress when creating an allocation.
Expand Down Expand Up @@ -93,9 +94,6 @@ func (c *ListenerConfig) validate() error {
return c.RelayAddressGenerator.Validate()
}

// AuthHandler is a callback used to handle incoming auth requests, allowing users to customize Pion TURN with custom behavior
type AuthHandler func(username, realm string, srcAddr net.Addr) (key []byte, ok bool)

// GenerateAuthKey is a convenience function to easily generate keys in the format used by AuthHandler
func GenerateAuthKey(username, realm, password string) []byte {
// #nosec
Expand All @@ -106,25 +104,28 @@ func GenerateAuthKey(username, realm, password string) []byte {

// ServerConfig configures the Pion TURN Server
type ServerConfig struct {
// PacketConnConfigs and ListenerConfigs are a list of all the turn listeners
// Each listener can have custom behavior around the creation of Relays
// PacketConnConfigs and ListenerConfigs are a list of all the turn listeners.
// Each listener can have custom behavior around the creation of Relays.
PacketConnConfigs []PacketConnConfig
ListenerConfigs []ListenerConfig

// LoggerFactory must be set for logging from this server.
LoggerFactory logging.LoggerFactory

// Realm sets the realm for this server
// Realm sets the realm for this server.
Realm string

// AuthHandler is a callback used to handle incoming auth requests, allowing users to customize Pion TURN with custom behavior
AuthHandler AuthHandler
// Authorizer is user to handle incoming auth requests, allowing users to customize Pion TURN with custom behavior.
Authorizer authz.Authorizer

// ChannelBindTimeout sets the lifetime of channel binding. Defaults to 10 minutes.
ChannelBindTimeout time.Duration

// Sets the server inbound MTU(Maximum transmition unit). Defaults to 1600 bytes.
InboundMTU int

// AuthHandler is deprecated, use Authorizer instead.
AuthHandler authz.LegacyAuthFunc
}

func (s *ServerConfig) validate() error {
Expand Down

0 comments on commit c4e36b1

Please sign in to comment.