feat: allow custom app and generic oauth title

This commit is contained in:
Stavros
2025-02-23 20:51:56 +02:00
parent 7ee0b645e6
commit 30aab17f06
7 changed files with 69 additions and 34 deletions

View File

@@ -113,7 +113,9 @@ var rootCmd = &cobra.Command{
AppURL: config.AppURL, AppURL: config.AppURL,
CookieSecure: config.CookieSecure, CookieSecure: config.CookieSecure,
DisableContinue: config.DisableContinue, DisableContinue: config.DisableContinue,
CookieExpiry: config.SessionExpiry, SessionExpiry: config.SessionExpiry,
Title: config.Title,
GenericName: config.GenericName,
}, hooks, auth, providers) }, hooks, auth, providers)
// Setup routes // Setup routes
@@ -169,10 +171,12 @@ func init() {
rootCmd.Flags().String("generic-auth-url", "", "Generic OAuth auth URL.") rootCmd.Flags().String("generic-auth-url", "", "Generic OAuth auth URL.")
rootCmd.Flags().String("generic-token-url", "", "Generic OAuth token URL.") rootCmd.Flags().String("generic-token-url", "", "Generic OAuth token URL.")
rootCmd.Flags().String("generic-user-url", "", "Generic OAuth user info URL.") rootCmd.Flags().String("generic-user-url", "", "Generic OAuth user info URL.")
rootCmd.Flags().String("generic-name", "Generic", "Generic OAuth provider name.")
rootCmd.Flags().Bool("disable-continue", false, "Disable continue screen and redirect to app directly.") rootCmd.Flags().Bool("disable-continue", false, "Disable continue screen and redirect to app directly.")
rootCmd.Flags().String("oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth.") rootCmd.Flags().String("oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth.")
rootCmd.Flags().Int("session-expiry", 86400, "Session (cookie) expiration time in seconds.") rootCmd.Flags().Int("session-expiry", 86400, "Session (cookie) expiration time in seconds.")
rootCmd.Flags().Int("log-level", 1, "Log level.") rootCmd.Flags().Int("log-level", 1, "Log level.")
rootCmd.Flags().String("app-title", "Tinyauth", "Title of the app.")
// Bind flags to environment // Bind flags to environment
viper.BindEnv("port", "PORT") viper.BindEnv("port", "PORT")
@@ -199,10 +203,12 @@ func init() {
viper.BindEnv("generic-auth-url", "GENERIC_AUTH_URL") viper.BindEnv("generic-auth-url", "GENERIC_AUTH_URL")
viper.BindEnv("generic-token-url", "GENERIC_TOKEN_URL") viper.BindEnv("generic-token-url", "GENERIC_TOKEN_URL")
viper.BindEnv("generic-user-url", "GENERIC_USER_URL") viper.BindEnv("generic-user-url", "GENERIC_USER_URL")
viper.BindEnv("generic-name", "GENERIC_NAME")
viper.BindEnv("disable-continue", "DISABLE_CONTINUE") viper.BindEnv("disable-continue", "DISABLE_CONTINUE")
viper.BindEnv("oauth-whitelist", "OAUTH_WHITELIST") viper.BindEnv("oauth-whitelist", "OAUTH_WHITELIST")
viper.BindEnv("session-expiry", "SESSION_EXPIRY") viper.BindEnv("session-expiry", "SESSION_EXPIRY")
viper.BindEnv("log-level", "LOG_LEVEL") viper.BindEnv("log-level", "LOG_LEVEL")
viper.BindEnv("app-title", "APP_TITLE")
// Bind flags to viper // Bind flags to viper
viper.BindPFlags(rootCmd.Flags()) viper.BindPFlags(rootCmd.Flags())

View File

@@ -84,7 +84,7 @@ func (api *API) Init() {
Path: "/", Path: "/",
HttpOnly: true, HttpOnly: true,
Secure: api.Config.CookieSecure, Secure: api.Config.CookieSecure,
MaxAge: api.Config.CookieExpiry, MaxAge: api.Config.SessionExpiry,
}) })
router.Use(sessions.Sessions("tinyauth", store)) router.Use(sessions.Sessions("tinyauth", store))
@@ -332,36 +332,45 @@ func (api *API) SetupRoutes() {
configuredProviders = append(configuredProviders, "username") configuredProviders = append(configuredProviders, "username")
} }
// We are not logged in so return unauthorized // Fill status struct with data from user context and api config
status := types.Status{
Username: userContext.Username,
IsLoggedIn: userContext.IsLoggedIn,
Oauth: userContext.OAuth,
Provider: userContext.Provider,
ConfiguredProviders: configuredProviders,
DisableContinue: api.Config.DisableContinue,
Title: api.Config.Title,
GenericName: api.Config.GenericName,
}
// If we are not logged in we set the status to 401 and add the WWW-Authenticate header else we set it to 200
if !userContext.IsLoggedIn { if !userContext.IsLoggedIn {
log.Debug().Msg("Unauthorized") log.Debug().Msg("Unauthorized")
c.Header("WWW-Authenticate", "Basic realm=\"tinyauth\"") c.Header("WWW-Authenticate", "Basic realm=\"tinyauth\"")
c.JSON(200, gin.H{ status.Status = 401
"status": 200, status.Message = "Unauthorized"
"message": "Unauthorized", } else {
"username": "", log.Debug().Interface("userContext", userContext).Strs("configuredProviders", configuredProviders).Bool("disableContinue", api.Config.DisableContinue).Msg("Authenticated")
"isLoggedIn": false, status.Status = 200
"oauth": false, status.Message = "Authenticated"
"provider": "",
"configuredProviders": configuredProviders,
"disableContinue": api.Config.DisableContinue,
})
return
} }
log.Debug().Interface("userContext", userContext).Strs("configuredProviders", configuredProviders).Bool("disableContinue", api.Config.DisableContinue).Msg("Authenticated") // // Marshall status to JSON
// statusJson, marshalErr := json.Marshal(status)
// We are logged in so return our user context // // Handle error
c.JSON(200, gin.H{ // if marshalErr != nil {
"status": 200, // log.Error().Err(marshalErr).Msg("Failed to marshal status")
"message": "Authenticated", // c.JSON(500, gin.H{
"username": userContext.Username, // "status": 500,
"isLoggedIn": userContext.IsLoggedIn, // "message": "Internal Server Error",
"oauth": userContext.OAuth, // })
"provider": userContext.Provider, // return
"configuredProviders": configuredProviders, // }
"disableContinue": api.Config.DisableContinue,
}) // Return data
c.JSON(200, status)
}) })
api.Router.GET("/api/oauth/url/:provider", func(c *gin.Context) { api.Router.GET("/api/oauth/url/:provider", func(c *gin.Context) {

View File

@@ -23,7 +23,7 @@ var apiConfig = types.APIConfig{
Secret: "super-secret-api-thing-for-tests", // It is 32 chars long Secret: "super-secret-api-thing-for-tests", // It is 32 chars long
AppURL: "http://tinyauth.localhost", AppURL: "http://tinyauth.localhost",
CookieSecure: false, CookieSecure: false,
CookieExpiry: 3600, SessionExpiry: 3600,
DisableContinue: false, DisableContinue: false,
} }
@@ -55,7 +55,7 @@ func getAPI(t *testing.T) *api.API {
Username: user.Username, Username: user.Username,
Password: user.Password, Password: user.Password,
}, },
}, nil, apiConfig.CookieExpiry) }, nil, apiConfig.SessionExpiry)
// Create providers service // Create providers service
providers := providers.NewProviders(types.OAuthConfig{}) providers := providers.NewProviders(types.OAuthConfig{})

View File

@@ -48,10 +48,12 @@ type Config struct {
GenericAuthURL string `mapstructure:"generic-auth-url"` GenericAuthURL string `mapstructure:"generic-auth-url"`
GenericTokenURL string `mapstructure:"generic-token-url"` GenericTokenURL string `mapstructure:"generic-token-url"`
GenericUserURL string `mapstructure:"generic-user-url"` GenericUserURL string `mapstructure:"generic-user-url"`
GenericName string `mapstructure:"generic-name"`
DisableContinue bool `mapstructure:"disable-continue"` DisableContinue bool `mapstructure:"disable-continue"`
OAuthWhitelist string `mapstructure:"oauth-whitelist"` OAuthWhitelist string `mapstructure:"oauth-whitelist"`
SessionExpiry int `mapstructure:"session-expiry"` SessionExpiry int `mapstructure:"session-expiry"`
LogLevel int8 `mapstructure:"log-level" validate:"min=-1,max=5"` LogLevel int8 `mapstructure:"log-level" validate:"min=-1,max=5"`
Title string `mapstructure:"app-title"`
} }
// UserContext is the context for the user // UserContext is the context for the user
@@ -69,8 +71,10 @@ type APIConfig struct {
Secret string Secret string
AppURL string AppURL string
CookieSecure bool CookieSecure bool
CookieExpiry int SessionExpiry int
DisableContinue bool DisableContinue bool
GenericName string
Title string
} }
// OAuthConfig is the configuration for the providers // OAuthConfig is the configuration for the providers
@@ -129,3 +133,17 @@ type TailscaleQuery struct {
type Proxy struct { type Proxy struct {
Proxy string `uri:"proxy" binding:"required"` Proxy string `uri:"proxy" binding:"required"`
} }
// Status response
type Status struct {
Status int `json:"status"`
Message string `json:"message"`
IsLoggedIn bool `json:"isLoggedIn"`
Username string `json:"username"`
Provider string `json:"provider"`
Oauth bool `json:"oauth"`
ConfiguredProviders []string `json:"configuredProviders"`
DisableContinue bool `json:"disableContinue"`
Title string `json:"title"`
GenericName string `json:"genericName"`
}

View File

@@ -27,7 +27,7 @@ export const LoginPage = () => {
const params = new URLSearchParams(queryString); const params = new URLSearchParams(queryString);
const redirectUri = params.get("redirect_uri") ?? ""; const redirectUri = params.get("redirect_uri") ?? "";
const { isLoggedIn, configuredProviders } = useUserContext(); const { isLoggedIn, configuredProviders, title, genericName } = useUserContext();
const oauthProviders = configuredProviders.filter( const oauthProviders = configuredProviders.filter(
(value) => value !== "username", (value) => value !== "username",
@@ -111,7 +111,7 @@ export const LoginPage = () => {
return ( return (
<Layout> <Layout>
<Title ta="center">Tinyauth</Title> <Title ta="center">{title}</Title>
<Paper shadow="md" p="xl" mt={30} radius="md" withBorder> <Paper shadow="md" p="xl" mt={30} radius="md" withBorder>
{oauthProviders.length > 0 && ( {oauthProviders.length > 0 && (
<> <>
@@ -175,7 +175,7 @@ export const LoginPage = () => {
onClick={() => loginOAuthMutation.mutate("generic")} onClick={() => loginOAuthMutation.mutate("generic")}
loading={loginOAuthMutation.isLoading} loading={loginOAuthMutation.isLoading}
> >
Generic {genericName}
</Button> </Button>
</Grid.Col> </Grid.Col>
)} )}

View File

@@ -8,7 +8,7 @@ import { Layout } from "../components/layouts/layout";
import { capitalize } from "../utils/utils"; import { capitalize } from "../utils/utils";
export const LogoutPage = () => { export const LogoutPage = () => {
const { isLoggedIn, username, oauth, provider } = useUserContext(); const { isLoggedIn, username, oauth, provider, genericName } = useUserContext();
if (!isLoggedIn) { if (!isLoggedIn) {
return <Navigate to="/login" />; return <Navigate to="/login" />;
@@ -45,7 +45,7 @@ export const LogoutPage = () => {
</Text> </Text>
<Text> <Text>
You are currently logged in as <Code>{username}</Code> You are currently logged in as <Code>{username}</Code>
{oauth && ` using ${capitalize(provider)} OAuth`}. Click the button {oauth && ` using ${capitalize(provider === "generic" ? genericName : provider)} OAuth`}. Click the button
below to log out. below to log out.
</Text> </Text>
<Button <Button

View File

@@ -7,6 +7,8 @@ export const userContextSchema = z.object({
provider: z.string(), provider: z.string(),
configuredProviders: z.array(z.string()), configuredProviders: z.array(z.string()),
disableContinue: z.boolean(), disableContinue: z.boolean(),
title: z.string(),
genericName: z.string(),
}); });
export type UserContextSchemaType = z.infer<typeof userContextSchema>; export type UserContextSchemaType = z.infer<typeof userContextSchema>;