mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-10-31 06:05:43 +00:00 
			
		
		
		
	feat: map info from OIDC claims to headers
This commit is contained in:
		| @@ -111,6 +111,11 @@ var rootCmd = &cobra.Command{ | ||||
| 			LoginMaxRetries: config.LoginMaxRetries, | ||||
| 		} | ||||
|  | ||||
| 		// Create hooks config | ||||
| 		hooksConfig := types.HooksConfig{ | ||||
| 			Domain: domain, | ||||
| 		} | ||||
|  | ||||
| 		// Create docker service | ||||
| 		docker := docker.NewDocker() | ||||
|  | ||||
| @@ -128,7 +133,7 @@ var rootCmd = &cobra.Command{ | ||||
| 		providers.Init() | ||||
|  | ||||
| 		// Create hooks service | ||||
| 		hooks := hooks.NewHooks(auth, providers) | ||||
| 		hooks := hooks.NewHooks(hooksConfig, auth, providers) | ||||
|  | ||||
| 		// Create handlers | ||||
| 		handlers := handlers.NewHandlers(handlersConfig, auth, hooks, providers, docker) | ||||
|   | ||||
| @@ -10,7 +10,7 @@ import { useAppContext } from "../context/app-context"; | ||||
| import { Trans, useTranslation } from "react-i18next"; | ||||
|  | ||||
| export const LogoutPage = () => { | ||||
|   const { isLoggedIn, username, oauth, provider } = useUserContext(); | ||||
|   const { isLoggedIn, oauth, provider, email } = useUserContext(); | ||||
|   const { genericName } = useAppContext(); | ||||
|   const { t } = useTranslation(); | ||||
|  | ||||
| @@ -56,7 +56,7 @@ export const LogoutPage = () => { | ||||
|               values={{ | ||||
|                 provider: | ||||
|                   provider === "generic" ? genericName : capitalize(provider), | ||||
|                 username: username, | ||||
|                 username: email, | ||||
|               }} | ||||
|             /> | ||||
|           ) : ( | ||||
| @@ -65,7 +65,7 @@ export const LogoutPage = () => { | ||||
|               t={t} | ||||
|               components={{ Code: <Code /> }} | ||||
|               values={{ | ||||
|                 username: username, | ||||
|                 username: email, | ||||
|               }} | ||||
|             /> | ||||
|           )} | ||||
|   | ||||
| @@ -3,6 +3,8 @@ import { z } from "zod"; | ||||
| export const userContextSchema = z.object({ | ||||
|   isLoggedIn: z.boolean(), | ||||
|   username: z.string(), | ||||
|   name: z.string(), | ||||
|   email: z.string(), | ||||
|   oauth: z.boolean(), | ||||
|   provider: z.string(), | ||||
|   totpPending: z.boolean(), | ||||
|   | ||||
| @@ -45,6 +45,11 @@ var authConfig = types.AuthConfig{ | ||||
| 	LoginMaxRetries: 0, | ||||
| } | ||||
|  | ||||
| // Simple hooks config for tests | ||||
| var hooksConfig = types.HooksConfig{ | ||||
| 	Domain: "localhost", | ||||
| } | ||||
|  | ||||
| // Cookie | ||||
| var cookie string | ||||
|  | ||||
| @@ -83,7 +88,7 @@ func getAPI(t *testing.T) *api.API { | ||||
| 	providers.Init() | ||||
|  | ||||
| 	// Create hooks service | ||||
| 	hooks := hooks.NewHooks(auth, providers) | ||||
| 	hooks := hooks.NewHooks(hooksConfig, auth, providers) | ||||
|  | ||||
| 	// Create handlers service | ||||
| 	handlers := handlers.NewHandlers(handlersConfig, auth, hooks, providers, docker) | ||||
|   | ||||
| @@ -160,6 +160,8 @@ func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) | ||||
|  | ||||
| 	// Set data | ||||
| 	session.Values["username"] = data.Username | ||||
| 	session.Values["name"] = data.Name | ||||
| 	session.Values["email"] = data.Email | ||||
| 	session.Values["provider"] = data.Provider | ||||
| 	session.Values["expiry"] = time.Now().Add(time.Duration(sessionExpiry) * time.Second).Unix() | ||||
| 	session.Values["totpPending"] = data.TotpPending | ||||
| @@ -211,14 +213,23 @@ func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error) | ||||
| 		return types.SessionCookie{}, err | ||||
| 	} | ||||
|  | ||||
| 	log.Debug().Interface("session", session).Msg("Got session") | ||||
|  | ||||
| 	// Get data from session | ||||
| 	username, usernameOk := session.Values["username"].(string) | ||||
| 	email, emailOk := session.Values["email"].(string) | ||||
| 	name, nameOk := session.Values["name"].(string) | ||||
| 	provider, providerOK := session.Values["provider"].(string) | ||||
| 	expiry, expiryOk := session.Values["expiry"].(int64) | ||||
| 	totpPending, totpPendingOk := session.Values["totpPending"].(bool) | ||||
|  | ||||
| 	if !usernameOk || !providerOK || !expiryOk || !totpPendingOk { | ||||
| 		log.Warn().Msg("Session cookie is missing data") | ||||
| 	if !usernameOk || !providerOK || !expiryOk || !totpPendingOk || !emailOk || !nameOk { | ||||
| 		log.Warn().Msg("Session cookie is invalid") | ||||
|  | ||||
| 		// If any data is missing, delete the session cookie | ||||
| 		auth.DeleteSessionCookie(c) | ||||
|  | ||||
| 		// Return empty cookie | ||||
| 		return types.SessionCookie{}, nil | ||||
| 	} | ||||
|  | ||||
| @@ -233,11 +244,13 @@ func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error) | ||||
| 		return types.SessionCookie{}, nil | ||||
| 	} | ||||
|  | ||||
| 	log.Debug().Str("username", username).Str("provider", provider).Int64("expiry", expiry).Bool("totpPending", totpPending).Msg("Parsed cookie") | ||||
| 	log.Debug().Str("username", username).Str("provider", provider).Int64("expiry", expiry).Bool("totpPending", totpPending).Str("name", name).Str("email", email).Msg("Parsed cookie") | ||||
|  | ||||
| 	// Return the cookie | ||||
| 	return types.SessionCookie{ | ||||
| 		Username:    username, | ||||
| 		Name:        name, | ||||
| 		Email:       email, | ||||
| 		Provider:    provider, | ||||
| 		TotpPending: totpPending, | ||||
| 	}, nil | ||||
|   | ||||
| @@ -8,13 +8,9 @@ var TinyauthLabels = []string{ | ||||
| 	"tinyauth.headers", | ||||
| } | ||||
|  | ||||
| // Claims are the OIDC supported claims | ||||
| // Claims are the OIDC supported claims (including preferd username for some reason) | ||||
| type Claims struct { | ||||
| 	Name       string `json:"name"` | ||||
| 	FamilyName string `json:"family_name"` | ||||
| 	GivenName  string `json:"given_name"` | ||||
| 	MiddleName string `json:"middle_name"` | ||||
| 	Nickname   string `json:"nickname"` | ||||
| 	Picture    string `json:"picture"` | ||||
| 	Email      string `json:"email"` | ||||
| 	Name              string `json:"name"` | ||||
| 	Email             string `json:"email"` | ||||
| 	PreferredUsername string `json:"preferred_username"` | ||||
| } | ||||
|   | ||||
| @@ -6,8 +6,7 @@ import ( | ||||
| 	"tinyauth/internal/types" | ||||
| 	"tinyauth/internal/utils" | ||||
|  | ||||
| 	apiTypes "github.com/docker/docker/api/types" | ||||
| 	containerTypes "github.com/docker/docker/api/types/container" | ||||
| 	container "github.com/docker/docker/api/types/container" | ||||
| 	"github.com/docker/docker/client" | ||||
| 	"github.com/rs/zerolog/log" | ||||
| ) | ||||
| @@ -38,9 +37,9 @@ func (docker *Docker) Init() error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (docker *Docker) GetContainers() ([]apiTypes.Container, error) { | ||||
| func (docker *Docker) GetContainers() ([]container.Summary, error) { | ||||
| 	// Get the list of containers | ||||
| 	containers, err := docker.Client.ContainerList(docker.Context, containerTypes.ListOptions{}) | ||||
| 	containers, err := docker.Client.ContainerList(docker.Context, container.ListOptions{}) | ||||
|  | ||||
| 	// Check if there was an error | ||||
| 	if err != nil { | ||||
| @@ -51,13 +50,13 @@ func (docker *Docker) GetContainers() ([]apiTypes.Container, error) { | ||||
| 	return containers, nil | ||||
| } | ||||
|  | ||||
| func (docker *Docker) InspectContainer(containerId string) (apiTypes.ContainerJSON, error) { | ||||
| func (docker *Docker) InspectContainer(containerId string) (container.InspectResponse, error) { | ||||
| 	// Inspect the container | ||||
| 	inspect, err := docker.Client.ContainerInspect(docker.Context, containerId) | ||||
|  | ||||
| 	// Check if there was an error | ||||
| 	if err != nil { | ||||
| 		return apiTypes.ContainerJSON{}, err | ||||
| 		return container.InspectResponse{}, err | ||||
| 	} | ||||
|  | ||||
| 	// Return the inspect | ||||
|   | ||||
| @@ -10,6 +10,7 @@ import ( | ||||
| 	"tinyauth/internal/hooks" | ||||
| 	"tinyauth/internal/providers" | ||||
| 	"tinyauth/internal/types" | ||||
| 	"tinyauth/internal/utils" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/google/go-querystring/query" | ||||
| @@ -183,8 +184,9 @@ func (h *Handlers) AuthHandler(c *gin.Context) { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		// Set the user header | ||||
| 		c.Header("Remote-User", userContext.Username) | ||||
| 		c.Header("Remote-Name", userContext.Name) | ||||
| 		c.Header("Remote-Email", userContext.Email) | ||||
|  | ||||
| 		// Set the rest of the headers | ||||
| 		for key, value := range labels.Headers { | ||||
| @@ -310,6 +312,8 @@ func (h *Handlers) LoginHandler(c *gin.Context) { | ||||
| 		// Set totp pending cookie | ||||
| 		h.Auth.CreateSessionCookie(c, &types.SessionCookie{ | ||||
| 			Username:    login.Username, | ||||
| 			Name:        utils.Capitalize(login.Username), | ||||
| 			Email:       fmt.Sprintf("%s@%s", strings.ToLower(login.Username), h.Config.Domain), | ||||
| 			Provider:    "username", | ||||
| 			TotpPending: true, | ||||
| 		}) | ||||
| @@ -328,6 +332,8 @@ func (h *Handlers) LoginHandler(c *gin.Context) { | ||||
| 	// Create session cookie with username as provider | ||||
| 	h.Auth.CreateSessionCookie(c, &types.SessionCookie{ | ||||
| 		Username: login.Username, | ||||
| 		Name:     utils.Capitalize(login.Username), | ||||
| 		Email:    fmt.Sprintf("%s@%s", strings.ToLower(login.Username), h.Config.Domain), | ||||
| 		Provider: "username", | ||||
| 	}) | ||||
|  | ||||
| @@ -402,6 +408,8 @@ func (h *Handlers) TotpHandler(c *gin.Context) { | ||||
| 	// Create session cookie with username as provider | ||||
| 	h.Auth.CreateSessionCookie(c, &types.SessionCookie{ | ||||
| 		Username: user.Username, | ||||
| 		Name:     utils.Capitalize(user.Username), | ||||
| 		Email:    fmt.Sprintf("%s@%s", strings.ToLower(user.Username), h.Config.Domain), | ||||
| 		Provider: "username", | ||||
| 	}) | ||||
|  | ||||
| @@ -465,6 +473,8 @@ func (h *Handlers) UserHandler(c *gin.Context) { | ||||
| 		Status:      200, | ||||
| 		IsLoggedIn:  userContext.IsLoggedIn, | ||||
| 		Username:    userContext.Username, | ||||
| 		Name:        userContext.Name, | ||||
| 		Email:       userContext.Email, | ||||
| 		Provider:    userContext.Provider, | ||||
| 		Oauth:       userContext.OAuth, | ||||
| 		TotpPending: userContext.TotpPending, | ||||
| @@ -654,9 +664,29 @@ func (h *Handlers) OauthCallbackHandler(c *gin.Context) { | ||||
|  | ||||
| 	log.Debug().Msg("Email whitelisted") | ||||
|  | ||||
| 	// Get username | ||||
| 	var username string | ||||
|  | ||||
| 	if user.PreferredUsername != "" { | ||||
| 		username = user.PreferredUsername | ||||
| 	} else { | ||||
| 		username = fmt.Sprintf("%s_%s", strings.Split(user.Email, "@")[0], strings.Split(user.Email, "@")[1]) | ||||
| 	} | ||||
|  | ||||
| 	// Get name | ||||
| 	var name string | ||||
|  | ||||
| 	if user.Name != "" { | ||||
| 		name = user.Name | ||||
| 	} else { | ||||
| 		name = fmt.Sprintf("%s (%s)", utils.Capitalize(strings.Split(user.Email, "@")[0]), strings.Split(user.Email, "@")[1]) | ||||
| 	} | ||||
|  | ||||
| 	// Create session cookie (also cleans up redirect cookie) | ||||
| 	h.Auth.CreateSessionCookie(c, &types.SessionCookie{ | ||||
| 		Username: user.Email, | ||||
| 		Username: username, | ||||
| 		Name:     name, | ||||
| 		Email:    user.Email, | ||||
| 		Provider: providerName.Provider, | ||||
| 	}) | ||||
|  | ||||
|   | ||||
| @@ -1,22 +1,27 @@ | ||||
| package hooks | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 	"tinyauth/internal/auth" | ||||
| 	"tinyauth/internal/providers" | ||||
| 	"tinyauth/internal/types" | ||||
| 	"tinyauth/internal/utils" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/rs/zerolog/log" | ||||
| ) | ||||
|  | ||||
| func NewHooks(auth *auth.Auth, providers *providers.Providers) *Hooks { | ||||
| func NewHooks(config types.HooksConfig, auth *auth.Auth, providers *providers.Providers) *Hooks { | ||||
| 	return &Hooks{ | ||||
| 		Config:    config, | ||||
| 		Auth:      auth, | ||||
| 		Providers: providers, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| type Hooks struct { | ||||
| 	Config    types.HooksConfig | ||||
| 	Auth      *auth.Auth | ||||
| 	Providers *providers.Providers | ||||
| } | ||||
| @@ -36,11 +41,11 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext { | ||||
| 		if user != nil && hooks.Auth.CheckPassword(*user, basic.Password) { | ||||
| 			// Return user context since we are logged in with basic auth | ||||
| 			return types.UserContext{ | ||||
| 				Username:    basic.Username, | ||||
| 				IsLoggedIn:  true, | ||||
| 				OAuth:       false, | ||||
| 				Provider:    "basic", | ||||
| 				TotpPending: false, | ||||
| 				Username:   basic.Username, | ||||
| 				Name:       utils.Capitalize(basic.Username), | ||||
| 				Email:      fmt.Sprintf("%s@%s", strings.ToLower(basic.Username), hooks.Config.Domain), | ||||
| 				IsLoggedIn: true, | ||||
| 				Provider:   "basic", | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| @@ -50,13 +55,7 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext { | ||||
| 	if err != nil { | ||||
| 		log.Error().Err(err).Msg("Failed to get session cookie") | ||||
| 		// Return empty context | ||||
| 		return types.UserContext{ | ||||
| 			Username:    "", | ||||
| 			IsLoggedIn:  false, | ||||
| 			OAuth:       false, | ||||
| 			Provider:    "", | ||||
| 			TotpPending: false, | ||||
| 		} | ||||
| 		return types.UserContext{} | ||||
| 	} | ||||
|  | ||||
| 	// Check if session cookie has totp pending | ||||
| @@ -65,8 +64,8 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext { | ||||
| 		// Return empty context since we are pending totp | ||||
| 		return types.UserContext{ | ||||
| 			Username:    cookie.Username, | ||||
| 			IsLoggedIn:  false, | ||||
| 			OAuth:       false, | ||||
| 			Name:        cookie.Name, | ||||
| 			Email:       cookie.Email, | ||||
| 			Provider:    cookie.Provider, | ||||
| 			TotpPending: true, | ||||
| 		} | ||||
| @@ -82,11 +81,11 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext { | ||||
|  | ||||
| 			// It exists so we are logged in | ||||
| 			return types.UserContext{ | ||||
| 				Username:    cookie.Username, | ||||
| 				IsLoggedIn:  true, | ||||
| 				OAuth:       false, | ||||
| 				Provider:    "username", | ||||
| 				TotpPending: false, | ||||
| 				Username:   cookie.Username, | ||||
| 				Name:       cookie.Name, | ||||
| 				Email:      cookie.Email, | ||||
| 				IsLoggedIn: true, | ||||
| 				Provider:   "username", | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| @@ -108,33 +107,22 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext { | ||||
| 			hooks.Auth.DeleteSessionCookie(c) | ||||
|  | ||||
| 			// Return empty context | ||||
| 			return types.UserContext{ | ||||
| 				Username:    "", | ||||
| 				IsLoggedIn:  false, | ||||
| 				OAuth:       false, | ||||
| 				Provider:    "", | ||||
| 				TotpPending: false, | ||||
| 			} | ||||
| 			return types.UserContext{} | ||||
| 		} | ||||
|  | ||||
| 		log.Debug().Msg("Email is whitelisted") | ||||
|  | ||||
| 		// Return user context since we are logged in with oauth | ||||
| 		return types.UserContext{ | ||||
| 			Username:    cookie.Username, | ||||
| 			IsLoggedIn:  true, | ||||
| 			OAuth:       true, | ||||
| 			Provider:    cookie.Provider, | ||||
| 			TotpPending: false, | ||||
| 			Username:   cookie.Username, | ||||
| 			Name:       cookie.Name, | ||||
| 			Email:      cookie.Email, | ||||
| 			IsLoggedIn: true, | ||||
| 			OAuth:      true, | ||||
| 			Provider:   cookie.Provider, | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Neither basic auth or oauth is set so we return an empty context | ||||
| 	return types.UserContext{ | ||||
| 		Username:    "", | ||||
| 		IsLoggedIn:  false, | ||||
| 		OAuth:       false, | ||||
| 		Provider:    "", | ||||
| 		TotpPending: false, | ||||
| 	} | ||||
| 	return types.UserContext{} | ||||
| } | ||||
|   | ||||
| @@ -33,6 +33,8 @@ type UserContextResponse struct { | ||||
| 	Message     string `json:"message"` | ||||
| 	IsLoggedIn  bool   `json:"isLoggedIn"` | ||||
| 	Username    string `json:"username"` | ||||
| 	Name        string `json:"name"` | ||||
| 	Email       string `json:"email"` | ||||
| 	Provider    string `json:"provider"` | ||||
| 	Oauth       bool   `json:"oauth"` | ||||
| 	TotpPending bool   `json:"totpPending"` | ||||
|   | ||||
| @@ -78,3 +78,8 @@ type AuthConfig struct { | ||||
| 	LoginTimeout    int | ||||
| 	LoginMaxRetries int | ||||
| } | ||||
|  | ||||
| // HooksConfig is the configuration for the hooks service | ||||
| type HooksConfig struct { | ||||
| 	Domain string | ||||
| } | ||||
|   | ||||
| @@ -25,6 +25,8 @@ type OAuthProviders struct { | ||||
| // SessionCookie is the cookie for the session (exculding the expiry) | ||||
| type SessionCookie struct { | ||||
| 	Username    string | ||||
| 	Name        string | ||||
| 	Email       string | ||||
| 	Provider    string | ||||
| 	TotpPending bool | ||||
| } | ||||
| @@ -40,6 +42,8 @@ type TinyauthLabels struct { | ||||
| // UserContext is the context for the user | ||||
| type UserContext struct { | ||||
| 	Username    string | ||||
| 	Name        string | ||||
| 	Email       string | ||||
| 	IsLoggedIn  bool | ||||
| 	OAuth       bool | ||||
| 	Provider    string | ||||
|   | ||||
| @@ -323,3 +323,8 @@ func CheckWhitelist(whitelist string, str string) bool { | ||||
| 	// Return false if no match was found | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // Capitalize just the first letter of a string | ||||
| func Capitalize(str string) string { | ||||
| 	return strings.ToUpper(string([]rune(str)[0])) + string([]rune(str)[1:]) | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Stavros
					Stavros