This commit is contained in:
Stavros
2026-04-29 19:21:07 +03:00
parent 956d2f55c3
commit eec75a6f49
17 changed files with 667 additions and 561 deletions
+9 -9
View File
@@ -4,20 +4,20 @@ import (
"errors"
"strings"
"github.com/tinyauthapp/tinyauth/internal/config"
"github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
)
type LabelProvider interface {
GetLabels(appDomain string) (config.App, error)
GetLabels(appDomain string) (*model.App, error)
}
type AccessControlsService struct {
labelProvider LabelProvider
static map[string]config.App
static map[string]model.App
}
func NewAccessControlsService(labelProvider LabelProvider, static map[string]config.App) *AccessControlsService {
func NewAccessControlsService(labelProvider LabelProvider, static map[string]model.App) *AccessControlsService {
return &AccessControlsService{
labelProvider: labelProvider,
static: static,
@@ -28,22 +28,22 @@ func (acls *AccessControlsService) Init() error {
return nil // No initialization needed
}
func (acls *AccessControlsService) lookupStaticACLs(domain string) (config.App, error) {
func (acls *AccessControlsService) lookupStaticACLs(domain string) (*model.App, error) {
for app, config := range acls.static {
if config.Config.Domain == domain {
tlog.App.Debug().Str("name", app).Msg("Found matching container by domain")
return config, nil
return &config, nil
}
if strings.SplitN(domain, ".", 2)[0] == app {
tlog.App.Debug().Str("name", app).Msg("Found matching container by app name")
return config, nil
return &config, nil
}
}
return config.App{}, errors.New("no results")
return nil, errors.New("no results")
}
func (acls *AccessControlsService) GetAccessControls(domain string) (config.App, error) {
func (acls *AccessControlsService) GetAccessControls(domain string) (*model.App, error) {
// First check in the static config
app, err := acls.lookupStaticACLs(domain)
+130 -138
View File
@@ -5,12 +5,13 @@ import (
"database/sql"
"errors"
"fmt"
"net/http"
"regexp"
"strings"
"sync"
"time"
"github.com/tinyauthapp/tinyauth/internal/config"
"github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/repository"
"github.com/tinyauthapp/tinyauth/internal/utils"
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
@@ -68,7 +69,7 @@ type Lockdown struct {
}
type AuthServiceConfig struct {
Users []config.User
LocalUsers []model.LocalUser
OauthWhitelist []string
SessionExpiry int
SessionMaxLifetime int
@@ -77,7 +78,7 @@ type AuthServiceConfig struct {
LoginTimeout int
LoginMaxRetries int
SessionCookieName string
IP config.IPConfig
IP model.IPConfig
LDAPGroupsCacheTTL int
}
@@ -106,7 +107,7 @@ func NewAuthService(config AuthServiceConfig, ldap *LdapService, queries *reposi
ldap: ldap,
queries: queries,
oauthBroker: oauthBroker,
}
}
}
func (auth *AuthService) Init() error {
@@ -114,79 +115,67 @@ func (auth *AuthService) Init() error {
return nil
}
func (auth *AuthService) SearchUser(username string) config.UserSearch {
func (auth *AuthService) SearchUser(username string) (*model.UserSearch, error) {
if auth.GetLocalUser(username).Username != "" {
return config.UserSearch{
return &model.UserSearch{
Username: username,
Type: "local",
}
Type: model.UserLocal,
}, nil
}
if auth.ldap.IsConfigured() {
userDN, err := auth.ldap.GetUserDN(username)
if err != nil {
tlog.App.Warn().Err(err).Str("username", username).Msg("Failed to search for user in LDAP")
return config.UserSearch{
Type: "unknown",
}
return nil, fmt.Errorf("failed to get ldap user: %w", err)
}
return config.UserSearch{
return &model.UserSearch{
Username: userDN,
Type: "ldap",
}
Type: model.UserLDAP,
}, nil
}
return config.UserSearch{
Type: "unknown",
}
return nil, fmt.Errorf("user not found")
}
func (auth *AuthService) VerifyUser(search config.UserSearch, password string) bool {
func (auth *AuthService) CheckUserPassword(search model.UserSearch, password string) error {
switch search.Type {
case "local":
case model.UserLocal:
user := auth.GetLocalUser(search.Username)
return auth.CheckPassword(user, password)
case "ldap":
return bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
case model.UserLDAP:
if auth.ldap.IsConfigured() {
err := auth.ldap.Bind(search.Username, password)
if err != nil {
tlog.App.Warn().Err(err).Str("username", search.Username).Msg("Failed to bind to LDAP")
return false
return fmt.Errorf("failed to bind to ldap user: %w", err)
}
err = auth.ldap.BindService(true)
if err != nil {
tlog.App.Error().Err(err).Msg("Failed to rebind with service account after user authentication")
return false
return fmt.Errorf("failed to bind to ldap service account: %w", err)
}
return true
return nil
}
default:
tlog.App.Debug().Str("type", search.Type).Msg("Unknown user type for authentication")
return false
return errors.New("unknown user search type")
}
tlog.App.Warn().Str("username", search.Username).Msg("User authentication failed")
return false
return errors.New("user authentication failed")
}
func (auth *AuthService) GetLocalUser(username string) config.User {
for _, user := range auth.config.Users {
func (auth *AuthService) GetLocalUser(username string) *model.LocalUser {
for _, user := range auth.config.LocalUsers {
if user.Username == username {
return user
return &user
}
}
tlog.App.Warn().Str("username", username).Msg("Local user not found")
return config.User{}
return nil
}
func (auth *AuthService) GetLdapUser(userDN string) (config.LdapUser, error) {
func (auth *AuthService) GetLDAPUser(userDN string) (*model.LDAPUser, error) {
if !auth.ldap.IsConfigured() {
return config.LdapUser{}, errors.New("LDAP service not initialized")
return nil, errors.New("ldap service not configured")
}
auth.ldapGroupsMutex.RLock()
@@ -194,7 +183,7 @@ func (auth *AuthService) GetLdapUser(userDN string) (config.LdapUser, error) {
auth.ldapGroupsMutex.RUnlock()
if exists && time.Now().Before(entry.Expires) {
return config.LdapUser{
return &model.LDAPUser{
DN: userDN,
Groups: entry.Groups,
}, nil
@@ -203,7 +192,7 @@ func (auth *AuthService) GetLdapUser(userDN string) (config.LdapUser, error) {
groups, err := auth.ldap.GetUserGroups(userDN)
if err != nil {
return config.LdapUser{}, err
return nil, fmt.Errorf("failed to get ldap groups: %w", err)
}
auth.ldapGroupsMutex.Lock()
@@ -213,16 +202,12 @@ func (auth *AuthService) GetLdapUser(userDN string) (config.LdapUser, error) {
}
auth.ldapGroupsMutex.Unlock()
return config.LdapUser{
return &model.LDAPUser{
DN: userDN,
Groups: groups,
}, nil
}
func (auth *AuthService) CheckPassword(user config.User, password string) bool {
return bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) == nil
}
func (auth *AuthService) IsAccountLocked(identifier string) (bool, int) {
auth.loginMutex.RLock()
defer auth.loginMutex.RUnlock()
@@ -291,11 +276,11 @@ func (auth *AuthService) IsEmailWhitelisted(email string) bool {
return utils.CheckFilter(strings.Join(auth.config.OauthWhitelist, ","), email)
}
func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *repository.Session) error {
func (auth *AuthService) CreateSession(ctx context.Context, data repository.Session) (*http.Cookie, error) {
uuid, err := uuid.NewRandom()
if err != nil {
return err
return nil, fmt.Errorf("failed to generate session uuid: %w", err)
}
var expiry int
@@ -320,28 +305,30 @@ func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *repository.Se
OAuthSub: data.OAuthSub,
}
_, err = auth.queries.CreateSession(c, session)
_, err = auth.queries.CreateSession(ctx, session)
if err != nil {
return err
return nil, fmt.Errorf("failed to create session entry: %w", err)
}
c.SetCookie(auth.config.SessionCookieName, session.UUID, expiry, "/", fmt.Sprintf(".%s", auth.config.CookieDomain), auth.config.SecureCookie, true)
return nil
return &http.Cookie{
Name: auth.config.SessionCookieName,
Value: session.UUID,
Path: "/",
Domain: fmt.Sprintf(".%s", auth.config.CookieDomain),
Expires: time.Now().Add(time.Duration(expiry) * time.Second),
MaxAge: expiry,
Secure: auth.config.SecureCookie,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
}, nil
}
func (auth *AuthService) RefreshSessionCookie(c *gin.Context) error {
cookie, err := c.Cookie(auth.config.SessionCookieName)
func (auth *AuthService) RefreshSession(ctx context.Context, uuid string) (*http.Cookie, error) {
session, err := auth.queries.GetSession(ctx, uuid)
if err != nil {
return err
}
session, err := auth.queries.GetSession(c, cookie)
if err != nil {
return err
return nil, fmt.Errorf("failed to retrieve session: %w", err)
}
currentTime := time.Now().Unix()
@@ -355,12 +342,12 @@ func (auth *AuthService) RefreshSessionCookie(c *gin.Context) error {
}
if session.Expiry-currentTime > refreshThreshold {
return nil
return nil, fmt.Errorf("session not eligible for refresh yet")
}
newExpiry := session.Expiry + refreshThreshold
_, err = auth.queries.UpdateSession(c, repository.UpdateSessionParams{
_, err = auth.queries.UpdateSession(ctx, repository.UpdateSessionParams{
Username: session.Username,
Email: session.Email,
Name: session.Name,
@@ -374,120 +361,117 @@ func (auth *AuthService) RefreshSessionCookie(c *gin.Context) error {
})
if err != nil {
return err
return nil, fmt.Errorf("failed to update session expiry: %w", err)
}
c.SetCookie(auth.config.SessionCookieName, cookie, int(newExpiry-currentTime), "/", fmt.Sprintf(".%s", auth.config.CookieDomain), auth.config.SecureCookie, true)
tlog.App.Trace().Str("username", session.Username).Msg("Session cookie refreshed")
return &http.Cookie{
Name: auth.config.SessionCookieName,
Value: session.UUID,
Path: "/",
Domain: fmt.Sprintf(".%s", auth.config.CookieDomain),
Expires: time.Now().Add(time.Duration(newExpiry-currentTime) * time.Second),
MaxAge: auth.config.SessionExpiry,
Secure: auth.config.SecureCookie,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
}, nil
return nil
}
func (auth *AuthService) DeleteSessionCookie(c *gin.Context) error {
cookie, err := c.Cookie(auth.config.SessionCookieName)
func (auth *AuthService) DeleteSession(ctx context.Context, uuid string) (*http.Cookie, error) {
err := auth.queries.DeleteSession(ctx, uuid)
if err != nil {
return err
tlog.App.Warn().Err(err).Msg("Failed to delete session from database, proceeding to clear cookie anyway")
}
err = auth.queries.DeleteSession(c, cookie)
if err != nil {
return err
}
c.SetCookie(auth.config.SessionCookieName, "", -1, "/", fmt.Sprintf(".%s", auth.config.CookieDomain), auth.config.SecureCookie, true)
return nil
return &http.Cookie{
Name: auth.config.SessionCookieName,
Value: "",
Path: "/",
Domain: fmt.Sprintf(".%s", auth.config.CookieDomain),
Expires: time.Now(),
MaxAge: -1,
Secure: auth.config.SecureCookie,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
}, nil
}
func (auth *AuthService) GetSessionCookie(c *gin.Context) (repository.Session, error) {
cookie, err := c.Cookie(auth.config.SessionCookieName)
if err != nil {
return repository.Session{}, err
}
session, err := auth.queries.GetSession(c, cookie)
func (auth *AuthService) GetSession(ctx context.Context, uuid string) (*repository.Session, error) {
session, err := auth.queries.GetSession(ctx, uuid)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return repository.Session{}, fmt.Errorf("session not found")
return nil, errors.New("session not found")
}
return repository.Session{}, err
return nil, err
}
currentTime := time.Now().Unix()
if auth.config.SessionMaxLifetime != 0 && session.CreatedAt != 0 {
if currentTime-session.CreatedAt > int64(auth.config.SessionMaxLifetime) {
err = auth.queries.DeleteSession(c, cookie)
err = auth.queries.DeleteSession(ctx, uuid)
if err != nil {
tlog.App.Error().Err(err).Msg("Failed to delete session exceeding max lifetime")
return nil, fmt.Errorf("failed to delete expired session: %w", err)
}
return repository.Session{}, fmt.Errorf("session expired due to max lifetime exceeded")
return nil, fmt.Errorf("session max lifetime exceeded")
}
}
if currentTime > session.Expiry {
err = auth.queries.DeleteSession(c, cookie)
err = auth.queries.DeleteSession(ctx, uuid)
if err != nil {
tlog.App.Error().Err(err).Msg("Failed to delete expired session")
return nil, fmt.Errorf("failed to delete expired session: %w", err)
}
return repository.Session{}, fmt.Errorf("session expired")
return nil, fmt.Errorf("session expired")
}
return repository.Session{
UUID: session.UUID,
Username: session.Username,
Email: session.Email,
Name: session.Name,
Provider: session.Provider,
TotpPending: session.TotpPending,
OAuthGroups: session.OAuthGroups,
OAuthName: session.OAuthName,
OAuthSub: session.OAuthSub,
}, nil
return &session, nil
}
func (auth *AuthService) LocalAuthConfigured() bool {
return len(auth.config.Users) > 0
return len(auth.config.LocalUsers) > 0
}
func (auth *AuthService) LdapAuthConfigured() bool {
func (auth *AuthService) LDAPAuthConfigured() bool {
return auth.ldap.IsConfigured()
}
func (auth *AuthService) IsUserAllowed(c *gin.Context, context config.UserContext, acls config.App) bool {
if context.OAuth {
func (auth *AuthService) IsUserAllowed(c *gin.Context, context model.UserContext, acls model.App) bool {
if context.Provider == model.ProviderOAuth {
tlog.App.Debug().Msg("Checking OAuth whitelist")
return utils.CheckFilter(acls.OAuth.Whitelist, context.Email)
return utils.CheckFilter(acls.OAuth.Whitelist, context.OAuth.Email)
}
if acls.Users.Block != "" {
tlog.App.Debug().Msg("Checking blocked users")
if utils.CheckFilter(acls.Users.Block, context.Username) {
if utils.CheckFilter(acls.Users.Block, context.GetUsername()) {
return false
}
}
tlog.App.Debug().Msg("Checking users")
return utils.CheckFilter(acls.Users.Allow, context.Username)
return utils.CheckFilter(acls.Users.Allow, context.GetUsername())
}
func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context config.UserContext, requiredGroups string) bool {
func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context model.UserContext, requiredGroups string) bool {
if requiredGroups == "" {
return true
}
for id := range config.OverrideProviders {
if context.Provider == id {
tlog.App.Info().Str("provider", id).Msg("OAuth groups not supported for this provider")
return true
}
if !context.IsOAuth() {
tlog.App.Debug().Msg("User is not an OAuth user, skipping OAuth group check")
return false
}
for userGroup := range strings.SplitSeq(context.OAuthGroups, ",") {
if _, ok := model.OverrideProviders[context.OAuth.ID]; ok {
tlog.App.Debug().Msg("Provider override for OAuth groups enabled, skipping group check")
return true
}
for _, userGroup := range context.OAuth.Groups {
if utils.CheckFilter(requiredGroups, strings.TrimSpace(userGroup)) {
tlog.App.Trace().Str("group", userGroup).Str("required", requiredGroups).Msg("User group matched")
return true
@@ -498,12 +482,17 @@ func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context config.UserConte
return false
}
func (auth *AuthService) IsInLdapGroup(c *gin.Context, context config.UserContext, requiredGroups string) bool {
func (auth *AuthService) IsInLDAPGroup(c *gin.Context, context model.UserContext, requiredGroups string) bool {
if requiredGroups == "" {
return true
}
for userGroup := range strings.SplitSeq(context.LdapGroups, ",") {
if !context.IsLDAP() {
tlog.App.Debug().Msg("User is not an LDAP user, skipping LDAP group check")
return false
}
for _, userGroup := range context.LDAP.Groups {
if utils.CheckFilter(requiredGroups, strings.TrimSpace(userGroup)) {
tlog.App.Trace().Str("group", userGroup).Str("required", requiredGroups).Msg("User group matched")
return true
@@ -514,7 +503,7 @@ func (auth *AuthService) IsInLdapGroup(c *gin.Context, context config.UserContex
return false
}
func (auth *AuthService) IsAuthEnabled(uri string, path config.AppPath) (bool, error) {
func (auth *AuthService) IsAuthEnabled(uri string, path model.AppPath) (bool, error) {
// Check for block list
if path.Block != "" {
regex, err := regexp.Compile(path.Block)
@@ -544,19 +533,22 @@ func (auth *AuthService) IsAuthEnabled(uri string, path config.AppPath) (bool, e
return true, nil
}
func (auth *AuthService) GetBasicAuth(c *gin.Context) *config.User {
username, password, ok := c.Request.BasicAuth()
if !ok {
tlog.App.Debug().Msg("No basic auth provided")
return nil
// local user is used only as a medium to pass the basic auth credentials, user can be ldap too
func (auth *AuthService) GetBasicAuth(req *http.Request) (*model.LocalUser, error) {
if req == nil {
return nil, errors.New("request is nil")
}
return &config.User{
username, password, ok := req.BasicAuth()
if !ok {
return nil, errors.New("no basic auth credentials provided")
}
return &model.LocalUser{
Username: username,
Password: password,
}
}, nil
}
func (auth *AuthService) CheckIP(acls config.AppIP, ip string) bool {
func (auth *AuthService) CheckIP(acls model.AppIP, ip string) bool {
// Merge the global and app IP filter
blockedIps := append(auth.config.IP.Block, acls.Block...)
allowedIPs := append(auth.config.IP.Allow, acls.Allow...)
@@ -594,7 +586,7 @@ func (auth *AuthService) CheckIP(acls config.AppIP, ip string) bool {
return true
}
func (auth *AuthService) IsBypassedIP(acls config.AppIP, ip string) bool {
func (auth *AuthService) IsBypassedIP(acls model.AppIP, ip string) bool {
for _, bypassed := range acls.Bypass {
res, err := utils.FilterIP(bypassed, ip)
if err != nil {
@@ -674,21 +666,21 @@ func (auth *AuthService) GetOAuthToken(sessionId string, code string) (*oauth2.T
return token, nil
}
func (auth *AuthService) GetOAuthUserinfo(sessionId string) (config.Claims, error) {
func (auth *AuthService) GetOAuthUserinfo(sessionId string) (*model.Claims, error) {
session, err := auth.GetOAuthPendingSession(sessionId)
if err != nil {
return config.Claims{}, err
return nil, err
}
if session.Token == nil {
return config.Claims{}, fmt.Errorf("oauth token not found for session: %s", sessionId)
return nil, fmt.Errorf("oauth token not found for session: %s", sessionId)
}
userinfo, err := (*session.Service).GetUserinfo(session.Token)
if err != nil {
return config.Claims{}, fmt.Errorf("failed to get userinfo: %w", err)
return nil, fmt.Errorf("failed to get userinfo: %w", err)
}
return userinfo, nil
+10 -10
View File
@@ -4,7 +4,7 @@ import (
"context"
"strings"
"github.com/tinyauthapp/tinyauth/internal/config"
"github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/utils/decoders"
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
@@ -66,41 +66,41 @@ func (docker *DockerService) inspectContainer(containerId string) (container.Ins
return inspect, nil
}
func (docker *DockerService) GetLabels(appDomain string) (config.App, error) {
func (docker *DockerService) GetLabels(appDomain string) (*model.App, error) {
if !docker.isConnected {
tlog.App.Debug().Msg("Docker not connected, returning empty labels")
return config.App{}, nil
return nil, nil
}
containers, err := docker.getContainers()
if err != nil {
return config.App{}, err
return nil, err
}
for _, ctr := range containers {
inspect, err := docker.inspectContainer(ctr.ID)
if err != nil {
return config.App{}, err
return nil, err
}
labels, err := decoders.DecodeLabels[config.Apps](inspect.Config.Labels, "apps")
labels, err := decoders.DecodeLabels[model.Apps](inspect.Config.Labels, "apps")
if err != nil {
return config.App{}, err
return nil, err
}
for appName, appLabels := range labels.Apps {
if appLabels.Config.Domain == appDomain {
tlog.App.Debug().Str("id", inspect.ID).Str("name", inspect.Name).Msg("Found matching container by domain")
return appLabels, nil
return &appLabels, nil
}
if strings.SplitN(appDomain, ".", 2)[0] == appName {
tlog.App.Debug().Str("id", inspect.ID).Str("name", inspect.Name).Msg("Found matching container by app name")
return appLabels, nil
return &appLabels, nil
}
}
}
tlog.App.Debug().Msg("No matching container found, returning empty labels")
return config.App{}, nil
return nil, nil
}
+12 -13
View File
@@ -7,7 +7,7 @@ import (
"sync"
"time"
"github.com/tinyauthapp/tinyauth/internal/config"
"github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/utils/decoders"
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
@@ -32,7 +32,7 @@ type ingressAppKey struct {
type ingressApp struct {
domain string
appName string
app config.App
app model.App
}
type KubernetesService struct {
@@ -89,7 +89,7 @@ func (k *KubernetesService) removeIngress(namespace, name string) {
}
}
func (k *KubernetesService) getByDomain(domain string) (config.App, bool) {
func (k *KubernetesService) getByDomain(domain string) (*model.App, bool) {
k.mu.RLock()
defer k.mu.RUnlock()
@@ -97,15 +97,15 @@ func (k *KubernetesService) getByDomain(domain string) (config.App, bool) {
if apps, ok := k.ingressApps[appKey.ingressKey]; ok {
for _, app := range apps {
if app.domain == domain && app.appName == appKey.appName {
return app.app, true
return &app.app, true
}
}
}
}
return config.App{}, false
return nil, false
}
func (k *KubernetesService) getByAppName(appName string) (config.App, bool) {
func (k *KubernetesService) getByAppName(appName string) (*model.App, bool) {
k.mu.RLock()
defer k.mu.RUnlock()
@@ -113,12 +113,12 @@ func (k *KubernetesService) getByAppName(appName string) (config.App, bool) {
if apps, ok := k.ingressApps[appKey.ingressKey]; ok {
for _, app := range apps {
if app.appName == appName {
return app.app, true
return &app.app, true
}
}
}
}
return config.App{}, false
return nil, false
}
func (k *KubernetesService) updateFromItem(item *unstructured.Unstructured) {
@@ -129,7 +129,7 @@ func (k *KubernetesService) updateFromItem(item *unstructured.Unstructured) {
k.removeIngress(namespace, name)
return
}
labels, err := decoders.DecodeLabels[config.Apps](annotations, "apps")
labels, err := decoders.DecodeLabels[model.Apps](annotations, "apps")
if err != nil {
tlog.App.Debug().Err(err).Msg("Failed to decode labels from annotations")
k.removeIngress(namespace, name)
@@ -280,10 +280,10 @@ func (k *KubernetesService) Init() error {
return nil
}
func (k *KubernetesService) GetLabels(appDomain string) (config.App, error) {
func (k *KubernetesService) GetLabels(appDomain string) (*model.App, error) {
if !k.started {
tlog.App.Debug().Msg("Kubernetes not connected, returning empty labels")
return config.App{}, nil
return nil, nil
}
// First check cache
@@ -298,6 +298,5 @@ func (k *KubernetesService) GetLabels(appDomain string) (config.App, error) {
}
tlog.App.Debug().Str("domain", appDomain).Msg("Cache miss, no matching ingress found")
return config.App{}, nil
return nil, nil
}
+5 -5
View File
@@ -1,7 +1,7 @@
package service
import (
"github.com/tinyauthapp/tinyauth/internal/config"
"github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
"slices"
@@ -15,20 +15,20 @@ type OAuthServiceImpl interface {
NewRandom() string
GetAuthURL(state string, verifier string) string
GetToken(code string, verifier string) (*oauth2.Token, error)
GetUserinfo(token *oauth2.Token) (config.Claims, error)
GetUserinfo(token *oauth2.Token) (*model.Claims, error)
}
type OAuthBrokerService struct {
services map[string]OAuthServiceImpl
configs map[string]config.OAuthServiceConfig
configs map[string]model.OAuthServiceConfig
}
var presets = map[string]func(config config.OAuthServiceConfig) *OAuthService{
var presets = map[string]func(config model.OAuthServiceConfig) *OAuthService{
"github": newGitHubOAuthService,
"google": newGoogleOAuthService,
}
func NewOAuthBrokerService(configs map[string]config.OAuthServiceConfig) *OAuthBrokerService {
func NewOAuthBrokerService(configs map[string]model.OAuthServiceConfig) *OAuthBrokerService {
return &OAuthBrokerService{
services: make(map[string]OAuthServiceImpl),
configs: configs,
+19 -19
View File
@@ -8,7 +8,7 @@ import (
"net/http"
"strconv"
"github.com/tinyauthapp/tinyauth/internal/config"
"github.com/tinyauthapp/tinyauth/internal/model"
)
type GithubEmailResponse []struct {
@@ -22,32 +22,32 @@ type GithubUserInfoResponse struct {
ID int `json:"id"`
}
func defaultExtractor(client *http.Client, url string) (config.Claims, error) {
return simpleReq[config.Claims](client, url, nil)
func defaultExtractor(client *http.Client, url string) (*model.Claims, error) {
return simpleReq[model.Claims](client, url, nil)
}
func githubExtractor(client *http.Client, url string) (config.Claims, error) {
var user config.Claims
func githubExtractor(client *http.Client, url string) (*model.Claims, error) {
var user model.Claims
userInfo, err := simpleReq[GithubUserInfoResponse](client, "https://api.github.com/user", map[string]string{
"accept": "application/vnd.github+json",
})
if err != nil {
return config.Claims{}, err
return nil, err
}
userEmails, err := simpleReq[GithubEmailResponse](client, "https://api.github.com/user/emails", map[string]string{
"accept": "application/vnd.github+json",
})
if err != nil {
return config.Claims{}, err
return nil, err
}
if len(userEmails) == 0 {
return user, errors.New("no emails found")
if len(*userEmails) == 0 {
return nil, errors.New("no emails found")
}
for _, email := range userEmails {
for _, email := range *userEmails {
if email.Primary {
user.Email = email.Email
break
@@ -56,22 +56,22 @@ func githubExtractor(client *http.Client, url string) (config.Claims, error) {
// Use first available email if no primary email was found
if user.Email == "" {
user.Email = userEmails[0].Email
user.Email = (*userEmails)[0].Email
}
user.PreferredUsername = userInfo.Login
user.Name = userInfo.Name
user.Sub = strconv.Itoa(userInfo.ID)
return user, nil
return &user, nil
}
func simpleReq[T any](client *http.Client, url string, headers map[string]string) (T, error) {
func simpleReq[T any](client *http.Client, url string, headers map[string]string) (*T, error) {
var decodedRes T
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return decodedRes, err
return nil, err
}
for key, value := range headers {
@@ -80,23 +80,23 @@ func simpleReq[T any](client *http.Client, url string, headers map[string]string
res, err := client.Do(req)
if err != nil {
return decodedRes, err
return nil, err
}
defer res.Body.Close()
if res.StatusCode < 200 || res.StatusCode >= 300 {
return decodedRes, fmt.Errorf("request failed with status: %s", res.Status)
return nil, fmt.Errorf("request failed with status: %s", res.Status)
}
body, err := io.ReadAll(res.Body)
if err != nil {
return decodedRes, err
return nil, err
}
err = json.Unmarshal(body, &decodedRes)
if err != nil {
return decodedRes, err
return nil, err
}
return decodedRes, nil
return &decodedRes, nil
}
+3 -3
View File
@@ -1,11 +1,11 @@
package service
import (
"github.com/tinyauthapp/tinyauth/internal/config"
"github.com/tinyauthapp/tinyauth/internal/model"
"golang.org/x/oauth2/endpoints"
)
func newGoogleOAuthService(config config.OAuthServiceConfig) *OAuthService {
func newGoogleOAuthService(config model.OAuthServiceConfig) *OAuthService {
scopes := []string{"openid", "email", "profile"}
config.Scopes = scopes
config.AuthURL = endpoints.Google.AuthURL
@@ -14,7 +14,7 @@ func newGoogleOAuthService(config config.OAuthServiceConfig) *OAuthService {
return NewOAuthService(config, "google")
}
func newGitHubOAuthService(config config.OAuthServiceConfig) *OAuthService {
func newGitHubOAuthService(config model.OAuthServiceConfig) *OAuthService {
scopes := []string{"read:user", "user:email"}
config.Scopes = scopes
config.AuthURL = endpoints.GitHub.AuthURL
+5 -5
View File
@@ -6,21 +6,21 @@ import (
"net/http"
"time"
"github.com/tinyauthapp/tinyauth/internal/config"
"github.com/tinyauthapp/tinyauth/internal/model"
"golang.org/x/oauth2"
)
type UserinfoExtractor func(client *http.Client, url string) (config.Claims, error)
type UserinfoExtractor func(client *http.Client, url string) (*model.Claims, error)
type OAuthService struct {
serviceCfg config.OAuthServiceConfig
serviceCfg model.OAuthServiceConfig
config *oauth2.Config
ctx context.Context
userinfoExtractor UserinfoExtractor
id string
}
func NewOAuthService(config config.OAuthServiceConfig, id string) *OAuthService {
func NewOAuthService(config model.OAuthServiceConfig, id string) *OAuthService {
httpClient := &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
@@ -78,7 +78,7 @@ func (s *OAuthService) GetToken(code string, verifier string) (*oauth2.Token, er
return s.config.Exchange(s.ctx, code, oauth2.VerifierOption(verifier))
}
func (s *OAuthService) GetUserinfo(token *oauth2.Token) (config.Claims, error) {
func (s *OAuthService) GetUserinfo(token *oauth2.Token) (*model.Claims, error) {
client := oauth2.NewClient(s.ctx, oauth2.StaticTokenSource(token))
return s.userinfoExtractor(client, s.serviceCfg.UserinfoURL)
}
+59 -57
View File
@@ -22,7 +22,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/go-jose/go-jose/v4"
"github.com/tinyauthapp/tinyauth/internal/config"
"github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/repository"
"github.com/tinyauthapp/tinyauth/internal/utils"
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
@@ -68,27 +68,27 @@ type ClaimSet struct {
}
type UserinfoResponse struct {
Sub string `json:"sub"`
Name string `json:"name,omitempty"`
GivenName string `json:"given_name,omitempty"`
FamilyName string `json:"family_name,omitempty"`
MiddleName string `json:"middle_name,omitempty"`
Nickname string `json:"nickname,omitempty"`
Profile string `json:"profile,omitempty"`
Picture string `json:"picture,omitempty"`
Website string `json:"website,omitempty"`
Gender string `json:"gender,omitempty"`
Birthdate string `json:"birthdate,omitempty"`
Zoneinfo string `json:"zoneinfo,omitempty"`
Locale string `json:"locale,omitempty"`
Email string `json:"email,omitempty"`
PreferredUsername string `json:"preferred_username,omitempty"`
Groups []string `json:"groups,omitempty"`
EmailVerified bool `json:"email_verified,omitempty"`
PhoneNumber string `json:"phone_number,omitempty"`
PhoneNumberVerified *bool `json:"phone_number_verified,omitempty"`
Address *config.AddressClaim `json:"address,omitempty"`
UpdatedAt int64 `json:"updated_at"`
Sub string `json:"sub"`
Name string `json:"name,omitempty"`
GivenName string `json:"given_name,omitempty"`
FamilyName string `json:"family_name,omitempty"`
MiddleName string `json:"middle_name,omitempty"`
Nickname string `json:"nickname,omitempty"`
Profile string `json:"profile,omitempty"`
Picture string `json:"picture,omitempty"`
Website string `json:"website,omitempty"`
Gender string `json:"gender,omitempty"`
Birthdate string `json:"birthdate,omitempty"`
Zoneinfo string `json:"zoneinfo,omitempty"`
Locale string `json:"locale,omitempty"`
Email string `json:"email,omitempty"`
PreferredUsername string `json:"preferred_username,omitempty"`
Groups []string `json:"groups,omitempty"`
EmailVerified bool `json:"email_verified,omitempty"`
PhoneNumber string `json:"phone_number,omitempty"`
PhoneNumberVerified *bool `json:"phone_number_verified,omitempty"`
Address *model.AddressClaim `json:"address,omitempty"`
UpdatedAt int64 `json:"updated_at"`
}
type TokenResponse struct {
@@ -112,7 +112,7 @@ type AuthorizeRequest struct {
}
type OIDCServiceConfig struct {
Clients map[string]config.OIDCClientConfig
Clients map[string]model.OIDCClientConfig
PrivateKeyPath string
PublicKeyPath string
Issuer string
@@ -122,7 +122,7 @@ type OIDCServiceConfig struct {
type OIDCService struct {
config OIDCServiceConfig
queries *repository.Queries
clients map[string]config.OIDCClientConfig
clients map[string]model.OIDCClientConfig
privateKey *rsa.PrivateKey
publicKey crypto.PublicKey
issuer string
@@ -255,7 +255,7 @@ func (service *OIDCService) Init() error {
}
// We will reorganize the client into a map with the client ID as the key
service.clients = make(map[string]config.OIDCClientConfig)
service.clients = make(map[string]model.OIDCClientConfig)
for id, client := range service.config.Clients {
client.ID = id
@@ -283,7 +283,7 @@ func (service *OIDCService) GetIssuer() string {
return service.issuer
}
func (service *OIDCService) GetClient(id string) (config.OIDCClientConfig, bool) {
func (service *OIDCService) GetClient(id string) (model.OIDCClientConfig, bool) {
client, ok := service.clients[id]
return client, ok
}
@@ -367,43 +367,45 @@ func (service *OIDCService) StoreCode(c *gin.Context, sub string, code string, r
return err
}
func (service *OIDCService) StoreUserinfo(c *gin.Context, sub string, userContext config.UserContext, req AuthorizeRequest) error {
addressJSON, err := json.Marshal(userContext.Attributes.Address)
if err != nil {
return err
}
func (service *OIDCService) StoreUserinfo(c *gin.Context, sub string, userContext model.UserContext, req AuthorizeRequest) error {
userInfoParams := repository.CreateOidcUserInfoParams{
Sub: sub,
Name: userContext.Name,
Email: userContext.Email,
PreferredUsername: userContext.Username,
Name: userContext.GetName(),
Email: userContext.GetEmail(),
PreferredUsername: userContext.GetUsername(),
UpdatedAt: time.Now().Unix(),
GivenName: userContext.Attributes.GivenName,
FamilyName: userContext.Attributes.FamilyName,
MiddleName: userContext.Attributes.MiddleName,
Nickname: userContext.Attributes.Nickname,
Profile: userContext.Attributes.Profile,
Picture: userContext.Attributes.Picture,
Website: userContext.Attributes.Website,
Gender: userContext.Attributes.Gender,
Birthdate: userContext.Attributes.Birthdate,
Zoneinfo: userContext.Attributes.Zoneinfo,
Locale: userContext.Attributes.Locale,
PhoneNumber: userContext.Attributes.PhoneNumber,
Address: string(addressJSON),
}
if userContext.IsLocal() {
addressJSON, err := json.Marshal(userContext.Local.Attributes.Address)
if err != nil {
return err
}
userInfoParams.GivenName = userContext.Local.Attributes.GivenName
userInfoParams.FamilyName = userContext.Local.Attributes.FamilyName
userInfoParams.MiddleName = userContext.Local.Attributes.MiddleName
userInfoParams.Nickname = userContext.Local.Attributes.Nickname
userInfoParams.Profile = userContext.Local.Attributes.Profile
userInfoParams.Picture = userContext.Local.Attributes.Picture
userInfoParams.Website = userContext.Local.Attributes.Website
userInfoParams.Gender = userContext.Local.Attributes.Gender
userInfoParams.Birthdate = userContext.Local.Attributes.Birthdate
userInfoParams.Zoneinfo = userContext.Local.Attributes.Zoneinfo
userInfoParams.Locale = userContext.Local.Attributes.Locale
userInfoParams.PhoneNumber = userContext.Local.Attributes.PhoneNumber
userInfoParams.Address = string(addressJSON)
}
// Tinyauth will pass through the groups it got from an LDAP or an OIDC server
if userContext.Provider == "ldap" {
userInfoParams.Groups = userContext.LdapGroups
if userContext.IsLDAP() {
userInfoParams.Groups = strings.Join(userContext.LDAP.Groups, ",")
}
if userContext.OAuth && len(userContext.OAuthGroups) > 0 {
userInfoParams.Groups = userContext.OAuthGroups
if userContext.IsOAuth() {
userInfoParams.Groups = strings.Join(userContext.OAuth.Groups, ",")
}
_, err = service.queries.CreateOidcUserInfo(c, userInfoParams)
_, err := service.queries.CreateOidcUserInfo(c, userInfoParams)
return err
}
@@ -445,7 +447,7 @@ func (service *OIDCService) GetCodeEntry(c *gin.Context, codeHash string, client
return oidcCode, nil
}
func (service *OIDCService) generateIDToken(client config.OIDCClientConfig, user repository.OidcUserinfo, scope string, nonce string) (string, error) {
func (service *OIDCService) generateIDToken(client model.OIDCClientConfig, user repository.OidcUserinfo, scope string, nonce string) (string, error) {
createdAt := time.Now().Unix()
expiresAt := time.Now().Add(time.Duration(service.config.SessionExpiry) * time.Second).Unix()
@@ -511,7 +513,7 @@ func (service *OIDCService) generateIDToken(client config.OIDCClientConfig, user
return token, nil
}
func (service *OIDCService) GenerateAccessToken(c *gin.Context, client config.OIDCClientConfig, codeEntry repository.OidcCode) (TokenResponse, error) {
func (service *OIDCService) GenerateAccessToken(c *gin.Context, client model.OIDCClientConfig, codeEntry repository.OidcCode) (TokenResponse, error) {
user, err := service.GetUserinfo(c, codeEntry.Sub)
if err != nil {
@@ -585,7 +587,7 @@ func (service *OIDCService) RefreshAccessToken(c *gin.Context, refreshToken stri
return TokenResponse{}, err
}
idToken, err := service.generateIDToken(config.OIDCClientConfig{
idToken, err := service.generateIDToken(model.OIDCClientConfig{
ClientID: entry.ClientID,
}, user, entry.Scope, entry.Nonce)
@@ -714,7 +716,7 @@ func (service *OIDCService) CompileUserinfo(user repository.OidcUserinfo, scope
}
if slices.Contains(scopes, "address") {
var addr config.AddressClaim
var addr model.AddressClaim
if err := json.Unmarshal([]byte(user.Address), &addr); err == nil {
userInfo.Address = &addr
}