mirror of
https://github.com/h44z/wg-portal.git
synced 2025-09-14 06:51:15 +00:00
Improve admin privilege handling for OAuth. Update documentation.
This commit is contained in:
@@ -2,6 +2,7 @@ package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
"github.com/h44z/wg-portal/internal"
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type LdapAuthenticator struct {
|
||||
@@ -78,7 +80,10 @@ func (l LdapAuthenticator) PlaintextAuthentication(userId domain.UserIdentifier,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l LdapAuthenticator) GetUserInfo(_ context.Context, userId domain.UserIdentifier) (map[string]interface{}, error) {
|
||||
func (l LdapAuthenticator) GetUserInfo(_ context.Context, userId domain.UserIdentifier) (
|
||||
map[string]interface{},
|
||||
error,
|
||||
) {
|
||||
conn, err := internal.LdapConnect(l.cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to setup connection: %w", err)
|
||||
@@ -109,6 +114,11 @@ func (l LdapAuthenticator) GetUserInfo(_ context.Context, userId domain.UserIden
|
||||
|
||||
users := internal.LdapConvertEntries(sr, &l.cfg.FieldMap)
|
||||
|
||||
if l.cfg.LogUserInfo {
|
||||
contents, _ := json.Marshal(users[0])
|
||||
logrus.Tracef("User info from LDAP source %s for %s: %v", l.GetName(), userId, string(contents))
|
||||
}
|
||||
|
||||
return users[0], nil
|
||||
}
|
||||
|
||||
|
@@ -6,12 +6,11 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/h44z/wg-portal/internal"
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
@@ -21,10 +20,16 @@ type PlainOauthAuthenticator struct {
|
||||
userInfoEndpoint string
|
||||
client *http.Client
|
||||
userInfoMapping config.OauthFields
|
||||
userAdminMapping *config.OauthAdminMapping
|
||||
registrationEnabled bool
|
||||
userInfoLogging bool
|
||||
}
|
||||
|
||||
func newPlainOauthAuthenticator(_ context.Context, callbackUrl string, cfg *config.OAuthProvider) (*PlainOauthAuthenticator, error) {
|
||||
func newPlainOauthAuthenticator(
|
||||
_ context.Context,
|
||||
callbackUrl string,
|
||||
cfg *config.OAuthProvider,
|
||||
) (*PlainOauthAuthenticator, error) {
|
||||
var provider = &PlainOauthAuthenticator{}
|
||||
|
||||
provider.name = cfg.ProviderName
|
||||
@@ -44,7 +49,9 @@ func newPlainOauthAuthenticator(_ context.Context, callbackUrl string, cfg *conf
|
||||
}
|
||||
provider.userInfoEndpoint = cfg.UserInfoURL
|
||||
provider.userInfoMapping = getOauthFieldMapping(cfg.FieldMap)
|
||||
provider.userAdminMapping = &cfg.AdminMapping
|
||||
provider.registrationEnabled = cfg.RegistrationEnabled
|
||||
provider.userInfoLogging = cfg.LogUserInfo
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
@@ -65,11 +72,19 @@ func (p PlainOauthAuthenticator) AuthCodeURL(state string, opts ...oauth2.AuthCo
|
||||
return p.cfg.AuthCodeURL(state, opts...)
|
||||
}
|
||||
|
||||
func (p PlainOauthAuthenticator) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
|
||||
func (p PlainOauthAuthenticator) Exchange(
|
||||
ctx context.Context,
|
||||
code string,
|
||||
opts ...oauth2.AuthCodeOption,
|
||||
) (*oauth2.Token, error) {
|
||||
return p.cfg.Exchange(ctx, code, opts...)
|
||||
}
|
||||
|
||||
func (p PlainOauthAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.Token, _ string) (map[string]interface{}, error) {
|
||||
func (p PlainOauthAuthenticator) GetUserInfo(
|
||||
ctx context.Context,
|
||||
token *oauth2.Token,
|
||||
_ string,
|
||||
) (map[string]interface{}, error) {
|
||||
req, err := http.NewRequest("GET", p.userInfoEndpoint, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create user info get request: %w", err)
|
||||
@@ -93,57 +108,13 @@ func (p PlainOauthAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.
|
||||
return nil, fmt.Errorf("failed to parse user info: %w", err)
|
||||
}
|
||||
|
||||
if p.userInfoLogging {
|
||||
logrus.Tracef("User info from OAuth source %s: %v", p.name, string(contents))
|
||||
}
|
||||
|
||||
return userFields, nil
|
||||
}
|
||||
|
||||
func (p PlainOauthAuthenticator) ParseUserInfo(raw map[string]interface{}) (*domain.AuthenticatorUserInfo, error) {
|
||||
isAdmin, _ := strconv.ParseBool(internal.MapDefaultString(raw, p.userInfoMapping.IsAdmin, ""))
|
||||
userInfo := &domain.AuthenticatorUserInfo{
|
||||
Identifier: domain.UserIdentifier(internal.MapDefaultString(raw, p.userInfoMapping.UserIdentifier, "")),
|
||||
Email: internal.MapDefaultString(raw, p.userInfoMapping.Email, ""),
|
||||
Firstname: internal.MapDefaultString(raw, p.userInfoMapping.Firstname, ""),
|
||||
Lastname: internal.MapDefaultString(raw, p.userInfoMapping.Lastname, ""),
|
||||
Phone: internal.MapDefaultString(raw, p.userInfoMapping.Phone, ""),
|
||||
Department: internal.MapDefaultString(raw, p.userInfoMapping.Department, ""),
|
||||
IsAdmin: isAdmin,
|
||||
}
|
||||
|
||||
return userInfo, nil
|
||||
}
|
||||
|
||||
func getOauthFieldMapping(f config.OauthFields) config.OauthFields {
|
||||
defaultMap := config.OauthFields{
|
||||
BaseFields: config.BaseFields{
|
||||
UserIdentifier: "sub",
|
||||
Email: "email",
|
||||
Firstname: "given_name",
|
||||
Lastname: "family_name",
|
||||
Phone: "phone",
|
||||
Department: "department",
|
||||
},
|
||||
IsAdmin: "admin_flag",
|
||||
}
|
||||
if f.UserIdentifier != "" {
|
||||
defaultMap.UserIdentifier = f.UserIdentifier
|
||||
}
|
||||
if f.Email != "" {
|
||||
defaultMap.Email = f.Email
|
||||
}
|
||||
if f.Firstname != "" {
|
||||
defaultMap.Firstname = f.Firstname
|
||||
}
|
||||
if f.Lastname != "" {
|
||||
defaultMap.Lastname = f.Lastname
|
||||
}
|
||||
if f.Phone != "" {
|
||||
defaultMap.Phone = f.Phone
|
||||
}
|
||||
if f.Department != "" {
|
||||
defaultMap.Department = f.Department
|
||||
}
|
||||
if f.IsAdmin != "" {
|
||||
defaultMap.IsAdmin = f.IsAdmin
|
||||
}
|
||||
|
||||
return defaultMap
|
||||
return parseOauthUserInfo(p.userInfoMapping, p.userAdminMapping, raw)
|
||||
}
|
||||
|
@@ -2,14 +2,14 @@ package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/h44z/wg-portal/internal"
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
@@ -19,15 +19,22 @@ type OidcAuthenticator struct {
|
||||
verifier *oidc.IDTokenVerifier
|
||||
cfg *oauth2.Config
|
||||
userInfoMapping config.OauthFields
|
||||
userAdminMapping *config.OauthAdminMapping
|
||||
registrationEnabled bool
|
||||
userInfoLogging bool
|
||||
}
|
||||
|
||||
func newOidcAuthenticator(ctx context.Context, callbackUrl string, cfg *config.OpenIDConnectProvider) (*OidcAuthenticator, error) {
|
||||
func newOidcAuthenticator(
|
||||
_ context.Context,
|
||||
callbackUrl string,
|
||||
cfg *config.OpenIDConnectProvider,
|
||||
) (*OidcAuthenticator, error) {
|
||||
var err error
|
||||
var provider = &OidcAuthenticator{}
|
||||
|
||||
provider.name = cfg.ProviderName
|
||||
provider.provider, err = oidc.NewProvider(context.Background(), cfg.BaseUrl) // use new context here, see https://github.com/coreos/go-oidc/issues/339
|
||||
provider.provider, err = oidc.NewProvider(context.Background(),
|
||||
cfg.BaseUrl) // use new context here, see https://github.com/coreos/go-oidc/issues/339
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create new oidc provider: %w", err)
|
||||
}
|
||||
@@ -45,7 +52,9 @@ func newOidcAuthenticator(ctx context.Context, callbackUrl string, cfg *config.O
|
||||
Scopes: scopes,
|
||||
}
|
||||
provider.userInfoMapping = getOauthFieldMapping(cfg.FieldMap)
|
||||
provider.userAdminMapping = &cfg.AdminMapping
|
||||
provider.registrationEnabled = cfg.RegistrationEnabled
|
||||
provider.userInfoLogging = cfg.LogUserInfo
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
@@ -66,11 +75,17 @@ func (o OidcAuthenticator) AuthCodeURL(state string, opts ...oauth2.AuthCodeOpti
|
||||
return o.cfg.AuthCodeURL(state, opts...)
|
||||
}
|
||||
|
||||
func (o OidcAuthenticator) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
|
||||
func (o OidcAuthenticator) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (
|
||||
*oauth2.Token,
|
||||
error,
|
||||
) {
|
||||
return o.cfg.Exchange(ctx, code, opts...)
|
||||
}
|
||||
|
||||
func (o OidcAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.Token, nonce string) (map[string]interface{}, error) {
|
||||
func (o OidcAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.Token, nonce string) (
|
||||
map[string]interface{},
|
||||
error,
|
||||
) {
|
||||
rawIDToken, ok := token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
return nil, errors.New("token does not contain id_token")
|
||||
@@ -88,20 +103,14 @@ func (o OidcAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.Token,
|
||||
return nil, fmt.Errorf("failed to parse extra claims: %w", err)
|
||||
}
|
||||
|
||||
if o.userInfoLogging {
|
||||
contents, _ := json.Marshal(tokenFields)
|
||||
logrus.Tracef("User info from OIDC source %s: %v", o.name, string(contents))
|
||||
}
|
||||
|
||||
return tokenFields, nil
|
||||
}
|
||||
|
||||
func (o OidcAuthenticator) ParseUserInfo(raw map[string]interface{}) (*domain.AuthenticatorUserInfo, error) {
|
||||
isAdmin, _ := strconv.ParseBool(internal.MapDefaultString(raw, o.userInfoMapping.IsAdmin, ""))
|
||||
userInfo := &domain.AuthenticatorUserInfo{
|
||||
Identifier: domain.UserIdentifier(internal.MapDefaultString(raw, o.userInfoMapping.UserIdentifier, "")),
|
||||
Email: internal.MapDefaultString(raw, o.userInfoMapping.Email, ""),
|
||||
Firstname: internal.MapDefaultString(raw, o.userInfoMapping.Firstname, ""),
|
||||
Lastname: internal.MapDefaultString(raw, o.userInfoMapping.Lastname, ""),
|
||||
Phone: internal.MapDefaultString(raw, o.userInfoMapping.Phone, ""),
|
||||
Department: internal.MapDefaultString(raw, o.userInfoMapping.Department, ""),
|
||||
IsAdmin: isAdmin,
|
||||
}
|
||||
|
||||
return userInfo, nil
|
||||
return parseOauthUserInfo(o.userInfoMapping, o.userAdminMapping, raw)
|
||||
}
|
||||
|
88
internal/app/auth/oauth_common.go
Normal file
88
internal/app/auth/oauth_common.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/h44z/wg-portal/internal"
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
// parseOauthUserInfo parses the raw user info from the oauth provider and maps it to the internal user info struct
|
||||
func parseOauthUserInfo(
|
||||
mapping config.OauthFields,
|
||||
adminMapping *config.OauthAdminMapping,
|
||||
raw map[string]interface{},
|
||||
) (*domain.AuthenticatorUserInfo, error) {
|
||||
var isAdmin bool
|
||||
|
||||
// first try to match the is_admin field against the given regex
|
||||
if mapping.IsAdmin != "" {
|
||||
re := adminMapping.GetAdminValueRegex()
|
||||
if re.MatchString(strings.TrimSpace(internal.MapDefaultString(raw, mapping.IsAdmin, ""))) {
|
||||
isAdmin = true
|
||||
}
|
||||
}
|
||||
|
||||
// next try to parse the user's groups
|
||||
if !isAdmin && mapping.UserGroups != "" && adminMapping.AdminGroupRegex != "" {
|
||||
userGroups := internal.MapDefaultStringSlice(raw, mapping.UserGroups, nil)
|
||||
re := adminMapping.GetAdminGroupRegex()
|
||||
for _, group := range userGroups {
|
||||
if re.MatchString(strings.TrimSpace(group)) {
|
||||
isAdmin = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
userInfo := &domain.AuthenticatorUserInfo{
|
||||
Identifier: domain.UserIdentifier(internal.MapDefaultString(raw, mapping.UserIdentifier, "")),
|
||||
Email: internal.MapDefaultString(raw, mapping.Email, ""),
|
||||
Firstname: internal.MapDefaultString(raw, mapping.Firstname, ""),
|
||||
Lastname: internal.MapDefaultString(raw, mapping.Lastname, ""),
|
||||
Phone: internal.MapDefaultString(raw, mapping.Phone, ""),
|
||||
Department: internal.MapDefaultString(raw, mapping.Department, ""),
|
||||
IsAdmin: isAdmin,
|
||||
}
|
||||
|
||||
return userInfo, nil
|
||||
}
|
||||
|
||||
// getOauthFieldMapping returns the default field mapping for the oauth provider
|
||||
func getOauthFieldMapping(f config.OauthFields) config.OauthFields {
|
||||
defaultMap := config.OauthFields{
|
||||
BaseFields: config.BaseFields{
|
||||
UserIdentifier: "sub",
|
||||
Email: "email",
|
||||
Firstname: "given_name",
|
||||
Lastname: "family_name",
|
||||
Phone: "phone",
|
||||
Department: "department",
|
||||
},
|
||||
IsAdmin: "admin_flag",
|
||||
}
|
||||
if f.UserIdentifier != "" {
|
||||
defaultMap.UserIdentifier = f.UserIdentifier
|
||||
}
|
||||
if f.Email != "" {
|
||||
defaultMap.Email = f.Email
|
||||
}
|
||||
if f.Firstname != "" {
|
||||
defaultMap.Firstname = f.Firstname
|
||||
}
|
||||
if f.Lastname != "" {
|
||||
defaultMap.Lastname = f.Lastname
|
||||
}
|
||||
if f.Phone != "" {
|
||||
defaultMap.Phone = f.Phone
|
||||
}
|
||||
if f.Department != "" {
|
||||
defaultMap.Department = f.Department
|
||||
}
|
||||
if f.IsAdmin != "" {
|
||||
defaultMap.IsAdmin = f.IsAdmin
|
||||
}
|
||||
|
||||
return defaultMap
|
||||
}
|
Reference in New Issue
Block a user