add support for some basic authentication methods

This commit is contained in:
Alisdair MacLeod
2026-01-29 16:34:52 +00:00
parent 0d480071b6
commit e95cfa1a00
12 changed files with 867 additions and 449 deletions

View File

@@ -1,4 +1,14 @@
<!doctype html>
{{ range . }}
<p>{{ . }}</p>
{{ if eq .Key "pin" }}
<form>
<input name={{ . }} />
<button type=submit></button>
</form>
{{ else if eq .Key "password" }}
<form>
<input name={{ . }} />
<button type=submit></button>
</form>
{{ end }}
{{ end }}

View File

@@ -0,0 +1,57 @@
package auth
import (
"net/http"
"github.com/netbirdio/netbird/shared/management/proto"
)
const linkFormId = "email"
type Link struct {
id, accountId string
client authenticator
}
func NewLink(client authenticator, id, accountId string) Link {
return Link{
id: id,
accountId: accountId,
client: client,
}
}
func (Link) Type() Method {
return MethodLink
}
func (l Link) Authenticate(r *http.Request) (string, bool, any) {
email := r.FormValue(linkFormId)
res, err := l.client.Authenticate(r.Context(), &proto.AuthenticateRequest{
Id: l.id,
AccountId: l.accountId,
Request: &proto.AuthenticateRequest_Link{
Link: &proto.LinkRequest{
Email: email,
Redirect: "", // TODO: calculate this.
},
},
})
if err != nil {
// TODO: log error here
return "", false, linkFormId
}
if res.GetSuccess() {
// Use the email address as the user identifier.
return email, true, nil
}
return "", false, linkFormId
}
func (l Link) Middleware(next http.Handler) http.Handler {
// TODO: handle magic link redirects, should be similar to OIDC.
return next
}

View File

@@ -1,6 +1,7 @@
package auth
import (
"context"
"crypto/rand"
_ "embed"
"encoding/base64"
@@ -8,6 +9,10 @@ import (
"net/http"
"sync"
"time"
"google.golang.org/grpc"
"github.com/netbirdio/netbird/shared/management/proto"
)
//go:embed auth.gohtml
@@ -16,9 +21,10 @@ var authTemplate string
type Method string
var (
MethodBasicAuth Method = "basic"
MethodPIN Method = "pin"
MethodBearer Method = "bearer"
MethodPassword Method = "password"
MethodPIN Method = "pin"
MethodOIDC Method = "oidc"
MethodLink Method = "link"
)
func (m Method) String() string {
@@ -36,6 +42,10 @@ type session struct {
CreatedAt time.Time
}
type authenticator interface {
Authenticate(ctx context.Context, in *proto.AuthenticateRequest, opts ...grpc.CallOption) (*proto.AuthenticateResponse, error)
}
type Scheme interface {
Type() Method
// Authenticate should check the passed request and determine whether

View File

@@ -35,14 +35,15 @@ type oidcState struct {
// OIDC implements the Scheme interface for JWT/OIDC authentication
type OIDC struct {
verifier *oidc.IDTokenVerifier
oauthConfig *oauth2.Config
states map[string]*oidcState
statesMux sync.RWMutex
id, accountId string
verifier *oidc.IDTokenVerifier
oauthConfig *oauth2.Config
states map[string]*oidcState
statesMux sync.RWMutex
}
// NewOIDC creates a new OIDC authentication scheme
func NewOIDC(ctx context.Context, cfg OIDCConfig) (*OIDC, error) {
func NewOIDC(ctx context.Context, id, accountId string, cfg OIDCConfig) (*OIDC, error) {
if cfg.OIDCProviderURL == "" || cfg.OIDCClientID == "" {
return nil, fmt.Errorf("OIDC provider URL and client ID are required")
}
@@ -58,6 +59,8 @@ func NewOIDC(ctx context.Context, cfg OIDCConfig) (*OIDC, error) {
}
o := &OIDC{
id: id,
accountId: accountId,
verifier: provider.Verifier(&oidc.Config{
ClientID: cfg.OIDCClientID,
}),
@@ -77,7 +80,7 @@ func NewOIDC(ctx context.Context, cfg OIDCConfig) (*OIDC, error) {
}
func (*OIDC) Type() Method {
return MethodBearer
return MethodOIDC
}
func (o *OIDC) Authenticate(r *http.Request) (string, bool, any) {

View File

@@ -0,0 +1,62 @@
package auth
import (
"net/http"
"github.com/netbirdio/netbird/shared/management/proto"
)
const (
passwordUserId = "password-user"
passwordFormId = "password"
)
type Password struct {
id, accountId string
client authenticator
}
func NewPassword(client authenticator, id, accountId string) Password {
return Password{
id: id,
accountId: accountId,
client: client,
}
}
func (Password) Type() Method {
return MethodPassword
}
// Authenticate attempts to authenticate the request using a form
// value passed in the request.
// If authentication fails, the required HTTP form ID is returned
// so that it can be injected into a request from the UI so that
// authentication may be successful.
func (p Password) Authenticate(r *http.Request) (string, bool, any) {
password := r.FormValue(passwordFormId)
res, err := p.client.Authenticate(r.Context(), &proto.AuthenticateRequest{
Id: p.id,
AccountId: p.accountId,
Request: &proto.AuthenticateRequest_Password{
Password: &proto.PasswordRequest{
Password: password,
},
},
})
if err != nil {
// TODO: log error here
return "", false, passwordFormId
}
if res.GetSuccess() {
return passwordUserId, true, nil
}
return "", false, passwordFormId
}
func (p Password) Middleware(next http.Handler) http.Handler {
return next
}

View File

@@ -1,22 +1,26 @@
package auth
import (
"crypto/subtle"
"net/http"
"github.com/netbirdio/netbird/shared/management/proto"
)
const (
userId = "pin-user"
formId = "pin"
pinUserId = "pin-user"
pinFormId = "pin"
)
type Pin struct {
pin string
id, accountId string
client authenticator
}
func NewPin(pin string) Pin {
func NewPin(client authenticator, id, accountId string) Pin {
return Pin{
pin: pin,
id: id,
accountId: accountId,
client: client,
}
}
@@ -30,14 +34,27 @@ func (Pin) Type() Method {
// so that it can be injected into a request from the UI so that
// authentication may be successful.
func (p Pin) Authenticate(r *http.Request) (string, bool, any) {
pin := r.FormValue(formId)
pin := r.FormValue(pinFormId)
// Compare the passed pin with the expected pin.
if subtle.ConstantTimeCompare([]byte(pin), []byte(p.pin)) == 1 {
return userId, false, nil
res, err := p.client.Authenticate(r.Context(), &proto.AuthenticateRequest{
Id: p.id,
AccountId: p.accountId,
Request: &proto.AuthenticateRequest_Pin{
Pin: &proto.PinRequest{
Pin: pin,
},
},
})
if err != nil {
// TODO: log error here
return "", false, pinFormId
}
return "", false, formId
if res.GetSuccess() {
return pinUserId, true, nil
}
return "", false, pinFormId
}
func (p Pin) Middleware(next http.Handler) http.Handler {

View File

@@ -287,15 +287,17 @@ func (s *Server) updateMapping(ctx context.Context, mapping *proto.ProxyMapping)
// the auth and proxy mappings.
// Note: this does require the management server to always send a
// full mapping rather than deltas during a modification.
mgmtClient := proto.NewProxyServiceClient(s.mgmtConn)
var schemes []auth.Scheme
if mapping.GetAuth().GetPin().GetEnabled() {
schemes = append(schemes, auth.NewPin(
mapping.GetAuth().GetPin().GetPin(),
))
if mapping.GetAuth().GetPassword() {
schemes = append(schemes, auth.NewPassword(mgmtClient, mapping.GetId(), mapping.GetAccountId()))
}
if mapping.GetAuth().GetOidc().GetEnabled() {
if mapping.GetAuth().GetPin() {
schemes = append(schemes, auth.NewPin(mgmtClient, mapping.GetId(), mapping.GetAccountId()))
}
if mapping.GetAuth().GetOidc() != nil {
oidc := mapping.GetAuth().GetOidc()
scheme, err := auth.NewOIDC(ctx, auth.OIDCConfig{
scheme, err := auth.NewOIDC(ctx, mapping.GetId(), mapping.GetAccountId(), auth.OIDCConfig{
OIDCProviderURL: oidc.GetOidcProviderUrl(),
OIDCClientID: oidc.GetOidcClientId(),
OIDCClientSecret: oidc.GetOidcClientSecret(),
@@ -308,6 +310,9 @@ func (s *Server) updateMapping(ctx context.Context, mapping *proto.ProxyMapping)
schemes = append(schemes, scheme)
}
}
if mapping.GetAuth().GetLink() {
schemes = append(schemes, auth.NewLink(mgmtClient, mapping.GetId(), mapping.GetAccountId()))
}
s.auth.AddDomain(mapping.GetDomain(), schemes)
s.proxy.AddMapping(s.protoToMapping(mapping))
}