chore: use interfaces for all other services

This commit is contained in:
Christoph Haas
2025-03-23 23:09:47 +01:00
parent 02ed7b19df
commit 7d0da4e7ad
40 changed files with 1337 additions and 406 deletions

View File

@@ -14,25 +14,78 @@ import (
"time"
"github.com/coreos/go-oidc/v3/oidc"
evbus "github.com/vardius/message-bus"
"golang.org/x/oauth2"
"github.com/h44z/wg-portal/internal/app"
"github.com/h44z/wg-portal/internal/config"
"github.com/h44z/wg-portal/internal/domain"
)
// region dependencies
type UserManager interface {
// GetUser returns a user by its identifier.
GetUser(context.Context, domain.UserIdentifier) (*domain.User, error)
// RegisterUser creates a new user in the database.
RegisterUser(ctx context.Context, user *domain.User) error
// UpdateUser updates an existing user in the database.
UpdateUser(ctx context.Context, user *domain.User) (*domain.User, error)
}
type EventBus interface {
// Publish sends a message to the message bus.
Publish(topic string, args ...any)
}
// endregion dependencies
type AuthenticatorType string
const (
AuthenticatorTypeOAuth AuthenticatorType = "oauth"
AuthenticatorTypeOidc AuthenticatorType = "oidc"
)
// AuthenticatorOauth is the interface for all OAuth authenticators.
type AuthenticatorOauth interface {
// GetName returns the name of the authenticator.
GetName() string
// GetType returns the type of the authenticator. It can be either AuthenticatorTypeOAuth or AuthenticatorTypeOidc.
GetType() AuthenticatorType
// AuthCodeURL returns the URL for the authentication flow.
AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string
// Exchange exchanges the OAuth code for an access token.
Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error)
// GetUserInfo fetches the user information from the OAuth or OIDC provider.
GetUserInfo(ctx context.Context, token *oauth2.Token, nonce string) (map[string]any, error)
// ParseUserInfo parses the raw user information into a domain.AuthenticatorUserInfo struct.
ParseUserInfo(raw map[string]any) (*domain.AuthenticatorUserInfo, error)
// RegistrationEnabled returns whether registration is enabled for the OAuth authenticator.
RegistrationEnabled() bool
}
// AuthenticatorLdap is the interface for all LDAP authenticators.
type AuthenticatorLdap interface {
// GetName returns the name of the authenticator.
GetName() string
// PlaintextAuthentication performs a plaintext authentication against the LDAP server.
PlaintextAuthentication(userId domain.UserIdentifier, plainPassword string) error
// GetUserInfo fetches the user information from the LDAP server.
GetUserInfo(ctx context.Context, username domain.UserIdentifier) (map[string]any, error)
// ParseUserInfo parses the raw user information into a domain.AuthenticatorUserInfo struct.
ParseUserInfo(raw map[string]any) (*domain.AuthenticatorUserInfo, error)
// RegistrationEnabled returns whether registration is enabled for the LDAP authenticator.
RegistrationEnabled() bool
}
// Authenticator is the main entry point for all authentication related tasks.
// This includes password authentication and external authentication providers (OIDC, OAuth, LDAP).
type Authenticator struct {
cfg *config.Auth
bus evbus.MessageBus
bus EventBus
oauthAuthenticators map[string]domain.OauthAuthenticator
ldapAuthenticators map[string]domain.LdapAuthenticator
oauthAuthenticators map[string]AuthenticatorOauth
ldapAuthenticators map[string]AuthenticatorLdap
// URL prefix for the callback endpoints, this is a combination of the external URL and the API prefix
callbackUrlPrefix string
@@ -40,7 +93,8 @@ type Authenticator struct {
users UserManager
}
func NewAuthenticator(cfg *config.Auth, extUrl string, bus evbus.MessageBus, users UserManager) (
// NewAuthenticator creates a new Authenticator instance.
func NewAuthenticator(cfg *config.Auth, extUrl string, bus EventBus, users UserManager) (
*Authenticator,
error,
) {
@@ -68,8 +122,8 @@ func (a *Authenticator) setupExternalAuthProviders(ctx context.Context) error {
return fmt.Errorf("failed to parse external url: %w", err)
}
a.oauthAuthenticators = make(map[string]domain.OauthAuthenticator, len(a.cfg.OpenIDConnect)+len(a.cfg.OAuth))
a.ldapAuthenticators = make(map[string]domain.LdapAuthenticator, len(a.cfg.Ldap))
a.oauthAuthenticators = make(map[string]AuthenticatorOauth, len(a.cfg.OpenIDConnect)+len(a.cfg.OAuth))
a.ldapAuthenticators = make(map[string]AuthenticatorLdap, len(a.cfg.Ldap))
for i := range a.cfg.OpenIDConnect { // OIDC
providerCfg := &a.cfg.OpenIDConnect[i]
@@ -123,6 +177,7 @@ func (a *Authenticator) setupExternalAuthProviders(ctx context.Context) error {
return nil
}
// GetExternalLoginProviders returns a list of all available external login providers.
func (a *Authenticator) GetExternalLoginProviders(_ context.Context) []domain.LoginProviderInfo {
authProviders := make([]domain.LoginProviderInfo, 0, len(a.cfg.OAuth)+len(a.cfg.OpenIDConnect))
@@ -157,6 +212,7 @@ func (a *Authenticator) GetExternalLoginProviders(_ context.Context) []domain.Lo
return authProviders
}
// IsUserValid checks if a user is valid and not locked or disabled.
func (a *Authenticator) IsUserValid(ctx context.Context, id domain.UserIdentifier) bool {
ctx = domain.SetUserInfo(ctx, domain.SystemAdminContextUserInfo()) // switch to admin user context
user, err := a.users.GetUser(ctx, id)
@@ -177,6 +233,8 @@ func (a *Authenticator) IsUserValid(ctx context.Context, id domain.UserIdentifie
// region password authentication
// PlainLogin performs a password authentication for a user. The username and password are trimmed before usage.
// If the login is successful, the user is returned, otherwise an error.
func (a *Authenticator) PlainLogin(ctx context.Context, username, password string) (*domain.User, error) {
// Validate form input
username = strings.TrimSpace(username)
@@ -204,7 +262,7 @@ func (a *Authenticator) passwordAuthentication(
domain.SystemAdminContextUserInfo()) // switch to admin user context to check if user exists
var ldapUserInfo *domain.AuthenticatorUserInfo
var ldapProvider domain.LdapAuthenticator
var ldapProvider AuthenticatorLdap
var userInDatabase = false
var userSource domain.UserSource
@@ -280,6 +338,7 @@ func (a *Authenticator) passwordAuthentication(
// region oauth authentication
// OauthLoginStep1 starts the oauth authentication flow by returning the authentication URL, state and nonce.
func (a *Authenticator) OauthLoginStep1(_ context.Context, providerId string) (
authCodeUrl, state, nonce string,
err error,
@@ -296,9 +355,9 @@ func (a *Authenticator) OauthLoginStep1(_ context.Context, providerId string) (
}
switch oauthProvider.GetType() {
case domain.AuthenticatorTypeOAuth:
case AuthenticatorTypeOAuth:
authCodeUrl = oauthProvider.AuthCodeURL(state)
case domain.AuthenticatorTypeOidc:
case AuthenticatorTypeOidc:
nonce, err = a.randString(16)
if err != nil {
return "", "", "", fmt.Errorf("failed to generate nonce: %w", err)
@@ -318,6 +377,8 @@ func (a *Authenticator) randString(nByte int) (string, error) {
return base64.RawURLEncoding.EncodeToString(b), nil
}
// OauthLoginStep2 finishes the oauth authentication flow by exchanging the code for an access token and
// fetching the user information.
func (a *Authenticator) OauthLoginStep2(ctx context.Context, providerId, nonce, code string) (*domain.User, error) {
oauthProvider, ok := a.oauthAuthenticators[providerId]
if !ok {

View File

@@ -14,6 +14,7 @@ import (
"github.com/h44z/wg-portal/internal/domain"
)
// LdapAuthenticator is an authenticator that uses LDAP for authentication.
type LdapAuthenticator struct {
cfg *config.LdapProvider
}
@@ -33,14 +34,17 @@ func newLdapAuthenticator(_ context.Context, cfg *config.LdapProvider) (*LdapAut
return provider, nil
}
// GetName returns the name of the LDAP authenticator.
func (l LdapAuthenticator) GetName() string {
return l.cfg.ProviderName
}
// RegistrationEnabled returns whether registration is enabled for the LDAP authenticator.
func (l LdapAuthenticator) RegistrationEnabled() bool {
return l.cfg.RegistrationEnabled
}
// PlaintextAuthentication performs a plaintext authentication against the LDAP server.
func (l LdapAuthenticator) PlaintextAuthentication(userId domain.UserIdentifier, plainPassword string) error {
conn, err := internal.LdapConnect(l.cfg)
if err != nil {
@@ -81,6 +85,9 @@ func (l LdapAuthenticator) PlaintextAuthentication(userId domain.UserIdentifier,
return nil
}
// GetUserInfo retrieves user information from the LDAP server.
// If the user is not found, domain.ErrNotFound is returned.
// If multiple users are found, domain.ErrNotUnique is returned.
func (l LdapAuthenticator) GetUserInfo(_ context.Context, userId domain.UserIdentifier) (
map[string]any,
error,
@@ -126,6 +133,7 @@ func (l LdapAuthenticator) GetUserInfo(_ context.Context, userId domain.UserIden
return users[0], nil
}
// ParseUserInfo parses the user information from the LDAP server into a domain.AuthenticatorUserInfo struct.
func (l LdapAuthenticator) ParseUserInfo(raw map[string]any) (*domain.AuthenticatorUserInfo, error) {
isAdmin, err := internal.LdapIsMemberOf(raw[l.cfg.FieldMap.GroupMembership].([][]byte), l.cfg.ParsedAdminGroupDN)
if err != nil {

View File

@@ -16,6 +16,8 @@ import (
"github.com/h44z/wg-portal/internal/domain"
)
// PlainOauthAuthenticator is an authenticator that uses OAuth for authentication.
// User information is retrieved from the specified user info endpoint.
type PlainOauthAuthenticator struct {
name string
cfg *oauth2.Config
@@ -58,22 +60,27 @@ func newPlainOauthAuthenticator(
return provider, nil
}
// GetName returns the name of the OAuth authenticator.
func (p PlainOauthAuthenticator) GetName() string {
return p.name
}
// RegistrationEnabled returns whether registration is enabled for the OAuth authenticator.
func (p PlainOauthAuthenticator) RegistrationEnabled() bool {
return p.registrationEnabled
}
func (p PlainOauthAuthenticator) GetType() domain.AuthenticatorType {
return domain.AuthenticatorTypeOAuth
// GetType returns the type of the authenticator.
func (p PlainOauthAuthenticator) GetType() AuthenticatorType {
return AuthenticatorTypeOAuth
}
// AuthCodeURL returns the URL to redirect the user to for authentication.
func (p PlainOauthAuthenticator) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string {
return p.cfg.AuthCodeURL(state, opts...)
}
// Exchange exchanges the OAuth code for a token.
func (p PlainOauthAuthenticator) Exchange(
ctx context.Context,
code string,
@@ -82,6 +89,7 @@ func (p PlainOauthAuthenticator) Exchange(
return p.cfg.Exchange(ctx, code, opts...)
}
// GetUserInfo retrieves the user information from the user info endpoint.
func (p PlainOauthAuthenticator) GetUserInfo(
ctx context.Context,
token *oauth2.Token,
@@ -119,6 +127,7 @@ func (p PlainOauthAuthenticator) GetUserInfo(
return userFields, nil
}
// ParseUserInfo parses the user information from the raw data.
func (p PlainOauthAuthenticator) ParseUserInfo(raw map[string]any) (*domain.AuthenticatorUserInfo, error) {
return parseOauthUserInfo(p.userInfoMapping, p.userAdminMapping, raw)
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/h44z/wg-portal/internal/domain"
)
// OidcAuthenticator is an authenticator for OpenID Connect providers.
type OidcAuthenticator struct {
name string
provider *oidc.Provider
@@ -60,22 +61,27 @@ func newOidcAuthenticator(
return provider, nil
}
// GetName returns the name of the authenticator.
func (o OidcAuthenticator) GetName() string {
return o.name
}
// RegistrationEnabled returns whether registration is enabled for this authenticator.
func (o OidcAuthenticator) RegistrationEnabled() bool {
return o.registrationEnabled
}
func (o OidcAuthenticator) GetType() domain.AuthenticatorType {
return domain.AuthenticatorTypeOidc
// GetType returns the type of the authenticator.
func (o OidcAuthenticator) GetType() AuthenticatorType {
return AuthenticatorTypeOidc
}
// AuthCodeURL returns the URL for the OAuth2 flow.
func (o OidcAuthenticator) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string {
return o.cfg.AuthCodeURL(state, opts...)
}
// Exchange exchanges the code for a token.
func (o OidcAuthenticator) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (
*oauth2.Token,
error,
@@ -83,6 +89,7 @@ func (o OidcAuthenticator) Exchange(ctx context.Context, code string, opts ...oa
return o.cfg.Exchange(ctx, code, opts...)
}
// GetUserInfo retrieves the user info from the token.
func (o OidcAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.Token, nonce string) (
map[string]any,
error,
@@ -114,6 +121,7 @@ func (o OidcAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.Token,
return tokenFields, nil
}
// ParseUserInfo parses the user info.
func (o OidcAuthenticator) ParseUserInfo(raw map[string]any) (*domain.AuthenticatorUserInfo, error) {
return parseOauthUserInfo(o.userInfoMapping, o.userAdminMapping, raw)
}