diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 0559b26f..f258eb98 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -2,9 +2,9 @@ import { Navigate } from "react-router";
import { useUserContext } from "./context/user-context";
export const App = () => {
- const { isLoggedIn } = useUserContext();
+ const { auth } = useUserContext();
- if (isLoggedIn) {
+ if (auth.authenticated) {
return ;
}
diff --git a/frontend/src/components/layout/layout.tsx b/frontend/src/components/layout/layout.tsx
index a71a1aa9..d59aadf3 100644
--- a/frontend/src/components/layout/layout.tsx
+++ b/frontend/src/components/layout/layout.tsx
@@ -6,17 +6,17 @@ import { DomainWarning } from "../domain-warning/domain-warning";
import { ThemeToggle } from "../theme-toggle/theme-toggle";
const BaseLayout = ({ children }: { children: React.ReactNode }) => {
- const { backgroundImage, title } = useAppContext();
+ const { ui } = useAppContext();
useEffect(() => {
- document.title = title;
- }, [title]);
+ document.title = ui.title;
+ }, [ui.title]);
return (
{
};
export const Layout = () => {
- const { appUrl, warningsEnabled } = useAppContext();
+ const { app, ui } = useAppContext();
const [ignoreDomainWarning, setIgnoreDomainWarning] = useState(() => {
return window.sessionStorage.getItem("ignoreDomainWarning") === "true";
});
@@ -42,11 +42,15 @@ export const Layout = () => {
setIgnoreDomainWarning(true);
}, [setIgnoreDomainWarning]);
- if (!ignoreDomainWarning && warningsEnabled && appUrl !== currentUrl) {
+ if (
+ !ignoreDomainWarning &&
+ ui.warningsEnabled &&
+ !app.trustedDomains.includes(currentUrl)
+ ) {
return (
handleIgnore()}
/>
diff --git a/frontend/src/pages/authorize-page.tsx b/frontend/src/pages/authorize-page.tsx
index f2b7c11d..91f8f9c9 100644
--- a/frontend/src/pages/authorize-page.tsx
+++ b/frontend/src/pages/authorize-page.tsx
@@ -77,7 +77,7 @@ const createScopeMap = (t: TFunction<"translation", undefined>): Scope[] => {
};
export const AuthorizePage = () => {
- const { isLoggedIn } = useUserContext();
+ const { auth } = useUserContext();
const { search } = useLocation();
const { t } = useTranslation();
const navigate = useNavigate();
@@ -127,7 +127,7 @@ export const AuthorizePage = () => {
);
}
- if (!isLoggedIn) {
+ if (!auth.authenticated) {
return ;
}
diff --git a/frontend/src/pages/continue-page.tsx b/frontend/src/pages/continue-page.tsx
index b7cdd743..82846c64 100644
--- a/frontend/src/pages/continue-page.tsx
+++ b/frontend/src/pages/continue-page.tsx
@@ -14,8 +14,8 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { useRedirectUri } from "@/lib/hooks/redirect-uri";
export const ContinuePage = () => {
- const { cookieDomain, warningsEnabled } = useAppContext();
- const { isLoggedIn } = useUserContext();
+ const { app, ui } = useAppContext();
+ const { auth } = useUserContext();
const { search } = useLocation();
const { t } = useTranslation();
const navigate = useNavigate();
@@ -29,17 +29,18 @@ export const ContinuePage = () => {
const { url, valid, trusted, allowedProto, httpsDowngrade } = useRedirectUri(
redirectUri,
- cookieDomain,
+ app.cookieDomain,
);
const urlHref = url?.href;
const hasValidRedirect = valid && allowedProto;
- const showUntrustedWarning = hasValidRedirect && !trusted && warningsEnabled;
+ const showUntrustedWarning =
+ hasValidRedirect && !trusted && ui.warningsEnabled;
const showInsecureWarning =
- hasValidRedirect && httpsDowngrade && warningsEnabled;
+ hasValidRedirect && httpsDowngrade && ui.warningsEnabled;
const shouldAutoRedirect =
- isLoggedIn &&
+ auth.authenticated &&
hasValidRedirect &&
!showUntrustedWarning &&
!showInsecureWarning;
@@ -77,7 +78,7 @@ export const ContinuePage = () => {
};
}, [shouldAutoRedirect, redirectToTarget]);
- if (!isLoggedIn) {
+ if (!auth.authenticated) {
return (
{
components={{
code: ,
}}
- values={{ cookieDomain }}
+ values={{ cookieDomain: app.cookieDomain }}
shouldUnescape={true}
/>
diff --git a/frontend/src/pages/forgot-password-page.tsx b/frontend/src/pages/forgot-password-page.tsx
index 7d47d02f..6438e353 100644
--- a/frontend/src/pages/forgot-password-page.tsx
+++ b/frontend/src/pages/forgot-password-page.tsx
@@ -13,7 +13,7 @@ import Markdown from "react-markdown";
import { useLocation } from "react-router";
export const ForgotPasswordPage = () => {
- const { forgotPasswordMessage } = useAppContext();
+ const { ui } = useAppContext();
const { t } = useTranslation();
const { search } = useLocation();
const searchParams = new URLSearchParams(search);
@@ -26,8 +26,8 @@ export const ForgotPasswordPage = () => {
- {forgotPasswordMessage !== ""
- ? forgotPasswordMessage
+ {ui.forgotPasswordMessage !== ""
+ ? ui.forgotPasswordMessage
: t("forgotPasswordMessage")}
diff --git a/frontend/src/pages/login-page.tsx b/frontend/src/pages/login-page.tsx
index c8c9f23d..91ad6411 100644
--- a/frontend/src/pages/login-page.tsx
+++ b/frontend/src/pages/login-page.tsx
@@ -36,13 +36,13 @@ const iconMap: Record = {
};
export const LoginPage = () => {
- const { isLoggedIn, tailscaleNodeName } = useUserContext();
- const { providers, title, oauthAutoRedirect } = useAppContext();
+ const { auth, tailscale } = useUserContext();
+ const { ui, oauth, auth: cauth } = useAppContext();
const { search } = useLocation();
const { t } = useTranslation();
const [showRedirectButton, setShowRedirectButton] = useState(false);
- const [useTailscale, setUseTailscale] = useState(tailscaleNodeName !== "");
+ const [useTailscale, setUseTailscale] = useState(tailscale.nodeName !== "");
const hasAutoRedirectedRef = useRef(false);
@@ -56,15 +56,15 @@ export const LoginPage = () => {
const oidcParams = useOIDCParams(searchParams);
const [isOauthAutoRedirect, setIsOauthAutoRedirect] = useState(
- providers.find((provider) => provider.id === oauthAutoRedirect) !==
+ cauth.providers.find((provider) => provider.id === oauth.autoRedirect) !==
undefined && redirectUri !== undefined,
);
- const oauthProviders = providers.filter(
+ const oauthProviders = cauth.providers.filter(
(provider) => provider.id !== "local" && provider.id !== "ldap",
);
const userAuthConfigured =
- providers.find(
+ cauth.providers.find(
(provider) => provider.id === "local" || provider.id === "ldap",
) !== undefined;
@@ -177,19 +177,19 @@ export const LoginPage = () => {
useEffect(() => {
if (
- !isLoggedIn &&
+ !auth.authenticated &&
isOauthAutoRedirect &&
!hasAutoRedirectedRef.current &&
redirectUri !== undefined
) {
hasAutoRedirectedRef.current = true;
- oauthMutate(oauthAutoRedirect);
+ oauthMutate(oauth.autoRedirect);
}
}, [
- isLoggedIn,
+ auth.authenticated,
oauthMutate,
hasAutoRedirectedRef,
- oauthAutoRedirect,
+ oauth.autoRedirect,
isOauthAutoRedirect,
redirectUri,
]);
@@ -206,11 +206,11 @@ export const LoginPage = () => {
};
}, [redirectTimer, redirectButtonTimer]);
- if (isLoggedIn && oidcParams.isOidc) {
+ if (auth.authenticated && oidcParams.isOidc) {
return ;
}
- if (isLoggedIn && redirectUri !== undefined) {
+ if (auth.authenticated && redirectUri !== undefined) {
return (
{
);
}
- if (isLoggedIn) {
+ if (auth.authenticated) {
return ;
}
@@ -272,7 +272,7 @@ export const LoginPage = () => {
credentials?
- Machine Name: {tailscaleNodeName}
+ Machine Name: {tailscale.nodeName}
@@ -299,8 +299,8 @@ export const LoginPage = () => {
return (
- {title}
- {providers.length > 0 && (
+ {ui.title}
+ {cauth.providers.length > 0 && (
{oauthProviders.length !== 0
? t("loginTitle")
@@ -338,7 +338,7 @@ export const LoginPage = () => {
})()}
/>
)}
- {providers.length == 0 && (
+ {cauth.providers.length == 0 && (
{t("failedToFetchProvidersTitle")}
diff --git a/frontend/src/pages/logout-page.tsx b/frontend/src/pages/logout-page.tsx
index f60bd656..7f500d7e 100644
--- a/frontend/src/pages/logout-page.tsx
+++ b/frontend/src/pages/logout-page.tsx
@@ -13,9 +13,11 @@ import { useEffect, useRef } from "react";
import { Trans, useTranslation } from "react-i18next";
import { Navigate } from "react-router";
import { toast } from "sonner";
+import { type UseMutationResult } from "@tanstack/react-query";
+import { type AxiosResponse } from "axios";
export const LogoutPage = () => {
- const { provider, username, isLoggedIn, email, oauthName } = useUserContext();
+ const { auth, oauth, tailscale } = useUserContext();
const { t } = useTranslation();
const redirectTimer = useRef(null);
@@ -47,42 +49,74 @@ export const LogoutPage = () => {
};
}, [redirectTimer]);
- if (!isLoggedIn) {
+ if (!auth.authenticated) {
return ;
}
+ if (oauth.active) {
+ return (
+
+ ,
+ }}
+ values={{
+ username: auth.email,
+ provider: oauth.displayName,
+ }}
+ shouldUnescape={true}
+ />
+
+ );
+ }
+
+ if (auth.providerId === "tailscale") {
+ return (
+
+ You are currently logged in with the Tailscale integration identified by
+ the {tailscale.nodeName} node. Click the button below to
+ log out.
+
+ );
+ }
+
+ return (
+
+ ,
+ }}
+ values={{
+ username: auth.username,
+ }}
+ shouldUnescape={true}
+ />
+
+ );
+};
+
+interface LogoutLayoutProps {
+ children: React.ReactNode;
+ logoutMutation: UseMutationResult<
+ //eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-empty-object-type
+ AxiosResponse,
+ Error,
+ void,
+ unknown
+ >;
+}
+
+function LogoutLayout({ children, logoutMutation }: LogoutLayoutProps) {
+ const { t } = useTranslation();
return (
{t("logoutTitle")}
-
- {provider !== "local" && provider !== "ldap" ? (
- ,
- }}
- values={{
- username: email,
- provider: oauthName,
- }}
- shouldUnescape={true}
- />
- ) : (
- ,
- }}
- values={{
- username,
- }}
- shouldUnescape={true}
- />
- )}
-
+ {children}
);
-};
+}
diff --git a/frontend/src/pages/totp-page.tsx b/frontend/src/pages/totp-page.tsx
index 4f1cb87b..984cb8db 100644
--- a/frontend/src/pages/totp-page.tsx
+++ b/frontend/src/pages/totp-page.tsx
@@ -19,7 +19,7 @@ import { toast } from "sonner";
import { useOIDCParams } from "@/lib/hooks/oidc";
export const TotpPage = () => {
- const { totpPending } = useUserContext();
+ const { totp } = useUserContext();
const { t } = useTranslation();
const { search } = useLocation();
const formId = useId();
@@ -64,7 +64,7 @@ export const TotpPage = () => {
};
}, [redirectTimer]);
- if (!totpPending) {
+ if (!totp.pending) {
return ;
}
diff --git a/frontend/src/schemas/app-context-schema.ts b/frontend/src/schemas/app-context-schema.ts
index a1dd4455..a91dda77 100644
--- a/frontend/src/schemas/app-context-schema.ts
+++ b/frontend/src/schemas/app-context-schema.ts
@@ -6,15 +6,32 @@ export const providerSchema = z.object({
oauth: z.boolean(),
});
-export const appContextSchema = z.object({
+const authSchema = z.object({
providers: z.array(providerSchema),
+});
+
+const oauthSchema = z.object({
+ autoRedirect: z.string(),
+});
+
+const uiSchema = z.object({
title: z.string(),
- appUrl: z.string(),
- cookieDomain: z.string(),
forgotPasswordMessage: z.string(),
backgroundImage: z.string(),
- oauthAutoRedirect: z.string(),
warningsEnabled: z.boolean(),
});
+const appSchema = z.object({
+ appUrl: z.string(),
+ cookieDomain: z.string(),
+ trustedDomains: z.array(z.string()),
+});
+
+export const appContextSchema = z.object({
+ auth: authSchema,
+ oauth: oauthSchema,
+ ui: uiSchema,
+ app: appSchema,
+});
+
export type AppContextSchema = z.infer;
diff --git a/frontend/src/schemas/user-context-schema.ts b/frontend/src/schemas/user-context-schema.ts
index 0c4b24c6..1a8b39e2 100644
--- a/frontend/src/schemas/user-context-schema.ts
+++ b/frontend/src/schemas/user-context-schema.ts
@@ -1,15 +1,31 @@
import { z } from "zod";
-export const userContextSchema = z.object({
- isLoggedIn: z.boolean(),
+const authSchema = z.object({
+ authenticated: z.boolean(),
username: z.string(),
name: z.string(),
email: z.string(),
- provider: z.string(),
- oauth: z.boolean(),
- totpPending: z.boolean(),
- oauthName: z.string(),
- tailscaleNodeName: z.string(),
+ providerId: z.string(),
+});
+
+const oauthSchema = z.object({
+ active: z.boolean(),
+ displayName: z.string(),
+});
+
+const totpSchema = z.object({
+ pending: z.boolean(),
+});
+
+const tailscaleSchema = z.object({
+ nodeName: z.string(),
+});
+
+export const userContextSchema = z.object({
+ auth: authSchema,
+ oauth: oauthSchema,
+ totp: totpSchema,
+ tailscale: tailscaleSchema,
});
export type UserContextSchema = z.infer;
diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go
index 1a932a28..630413a4 100644
--- a/internal/bootstrap/app_bootstrap.go
+++ b/internal/bootstrap/app_bootstrap.go
@@ -67,6 +67,8 @@ func (app *BootstrapApp) Setup() error {
log.Init()
app.log = log
+ app.log.App.Info().Msgf("Starting Tinyauth version: %s", model.Version)
+
// get app url
if app.config.AppURL == "" {
return errors.New("app url cannot be empty, perhaps config loading failed")
@@ -79,6 +81,7 @@ func (app *BootstrapApp) Setup() error {
}
app.runtime.AppURL = appUrl.Scheme + "://" + appUrl.Host
+ app.runtime.TrustedDomains = append(app.runtime.TrustedDomains, app.runtime.AppURL)
// validate session config
if app.config.Auth.SessionMaxLifetime != 0 && app.config.Auth.SessionMaxLifetime < app.config.Auth.SessionExpiry {
@@ -229,6 +232,11 @@ func (app *BootstrapApp) Setup() error {
app.runtime.ConfiguredProviders = configuredProviders
+ // throw in tailscale if it's configured just before setting up the controllers
+ if app.services.tailscaleService != nil {
+ app.runtime.TrustedDomains = append(app.runtime.TrustedDomains, "https://"+app.services.tailscaleService.GetHostname())
+ }
+
// setup router
err = app.setupRouter()
diff --git a/internal/controller/context_controller.go b/internal/controller/context_controller.go
index a63cf3c7..487dd94d 100644
--- a/internal/controller/context_controller.go
+++ b/internal/controller/context_controller.go
@@ -1,40 +1,74 @@
package controller
import (
- "fmt"
- "net/url"
-
"github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
"github.com/gin-gonic/gin"
)
+// UCR -> User Context Response
+
+type UCRAuth struct {
+ Authenticated bool `json:"authenticated"`
+ Username string `json:"username"`
+ Name string `json:"name"`
+ Email string `json:"email"`
+ ProviderID string `json:"providerId"`
+}
+
+type UCROAuth struct {
+ Active bool `json:"active"`
+ DisplayName string `json:"displayName"`
+}
+
+type UCRTOTP struct {
+ Pending bool `json:"pending"`
+}
+
+type UCRTailscale struct {
+ NodeName string `json:"nodeName,omitempty"`
+}
+
type UserContextResponse struct {
- Status int `json:"status"`
- 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"`
- OAuthName string `json:"oauthName"`
- TailscaleNodeName string `json:"tailscaleNodeName,omitempty"`
+ Status int `json:"status"`
+ Message string `json:"message"`
+ Auth UCRAuth `json:"auth"`
+ OAuth UCROAuth `json:"oauth"`
+ TOTP UCRTOTP `json:"totp"`
+ Tailscale UCRTailscale `json:"tailscale"`
+}
+
+// ACR -> App Context Response
+
+type ACRAuth struct {
+ Providers []model.Provider `json:"providers"`
+}
+
+type ACROAuth struct {
+ AutoRedirect string `json:"autoRedirect"`
+}
+
+type ACRUI struct {
+ Title string `json:"title"`
+ ForgotPasswordMessage string `json:"forgotPasswordMessage"`
+ BackgroundImage string `json:"backgroundImage"`
+ WarningsEnabled bool `json:"warningsEnabled"`
+}
+
+type ACRApp struct {
+ AppURL string `json:"appUrl"`
+ CookieDomain string `json:"cookieDomain"`
+ TrustedDomains []string `json:"trustedDomains"`
}
type AppContextResponse struct {
- Status int `json:"status"`
- Message string `json:"message"`
- Providers []model.Provider `json:"providers"`
- Title string `json:"title"`
- AppURL string `json:"appUrl"`
- CookieDomain string `json:"cookieDomain"`
- ForgotPasswordMessage string `json:"forgotPasswordMessage"`
- BackgroundImage string `json:"backgroundImage"`
- OAuthAutoRedirect string `json:"oauthAutoRedirect"`
- WarningsEnabled bool `json:"warningsEnabled"`
+ Status int `json:"status"`
+ Message string `json:"message"`
+ Auth ACRAuth `json:"auth"`
+ OAuth ACROAuth `json:"oauth"`
+ UI ACRUI `json:"ui"`
+ App ACRApp `json:"app"`
}
type ContextController struct {
@@ -72,52 +106,58 @@ func (controller *ContextController) userContextHandler(c *gin.Context) {
if err != nil {
controller.log.App.Error().Err(err).Msg("Failed to create user context from request")
c.JSON(200, UserContextResponse{
- Status: 401,
- Message: "Unauthorized",
- IsLoggedIn: false,
+ Status: 401,
+ Message: "Unauthorized",
+ Auth: UCRAuth{Authenticated: false},
})
return
}
userContext := UserContextResponse{
- Status: 200,
- Message: "Success",
- IsLoggedIn: context.Authenticated,
- Username: context.GetUsername(),
- Name: context.GetName(),
- Email: context.GetEmail(),
- Provider: context.GetProviderID(),
- OAuth: context.IsOAuth(),
- TOTPPending: context.TOTPPending(),
- OAuthName: context.OAuthName(),
- TailscaleNodeName: context.TailscaleNodeName(),
+ Status: 200,
+ Message: "Success",
+ Auth: UCRAuth{
+ Authenticated: context.Authenticated,
+ Username: context.GetUsername(),
+ Name: context.GetName(),
+ Email: context.GetEmail(),
+ ProviderID: context.GetProviderID(),
+ },
+ OAuth: UCROAuth{
+ Active: context.IsOAuth(),
+ DisplayName: context.OAuthName(),
+ },
+ TOTP: UCRTOTP{
+ Pending: context.TOTPPending(),
+ },
+ Tailscale: UCRTailscale{
+ NodeName: context.TailscaleNodeName(),
+ },
}
c.JSON(200, userContext)
}
func (controller *ContextController) appContextHandler(c *gin.Context) {
- appUrl, err := url.Parse(controller.runtime.AppURL)
-
- if err != nil {
- controller.log.App.Error().Err(err).Msg("Failed to parse app URL")
- c.JSON(500, gin.H{
- "status": 500,
- "message": "Internal Server Error",
- })
- return
- }
-
c.JSON(200, AppContextResponse{
- Status: 200,
- Message: "Success",
- Providers: controller.runtime.ConfiguredProviders,
- Title: controller.config.UI.Title,
- AppURL: fmt.Sprintf("%s://%s", appUrl.Scheme, appUrl.Host),
- CookieDomain: controller.runtime.CookieDomain,
- ForgotPasswordMessage: controller.config.UI.ForgotPasswordMessage,
- BackgroundImage: controller.config.UI.BackgroundImage,
- OAuthAutoRedirect: controller.config.OAuth.AutoRedirect,
- WarningsEnabled: controller.config.UI.WarningsEnabled,
+ Status: 200,
+ Message: "Success",
+ Auth: ACRAuth{
+ Providers: controller.runtime.ConfiguredProviders,
+ },
+ OAuth: ACROAuth{
+ AutoRedirect: controller.config.OAuth.AutoRedirect,
+ },
+ UI: ACRUI{
+ Title: controller.config.UI.Title,
+ ForgotPasswordMessage: controller.config.UI.ForgotPasswordMessage,
+ BackgroundImage: controller.config.UI.BackgroundImage,
+ WarningsEnabled: controller.config.UI.WarningsEnabled,
+ },
+ App: ACRApp{
+ AppURL: controller.runtime.AppURL,
+ CookieDomain: controller.runtime.CookieDomain,
+ TrustedDomains: controller.runtime.TrustedDomains,
+ },
})
}
diff --git a/internal/controller/context_controller_test.go b/internal/controller/context_controller_test.go
index 177f4744..cf879645 100644
--- a/internal/controller/context_controller_test.go
+++ b/internal/controller/context_controller_test.go
@@ -34,16 +34,25 @@ func TestContextController(t *testing.T) {
path: "/api/context/app",
expected: func() string {
expectedAppContextResponse := controller.AppContextResponse{
- Status: 200,
- Message: "Success",
- Providers: runtime.ConfiguredProviders,
- Title: cfg.UI.Title,
- AppURL: runtime.AppURL,
- CookieDomain: runtime.CookieDomain,
- ForgotPasswordMessage: cfg.UI.ForgotPasswordMessage,
- BackgroundImage: cfg.UI.BackgroundImage,
- OAuthAutoRedirect: cfg.OAuth.AutoRedirect,
- WarningsEnabled: cfg.UI.WarningsEnabled,
+ Status: 200,
+ Message: "Success",
+ Auth: controller.ACRAuth{
+ Providers: runtime.ConfiguredProviders,
+ },
+ OAuth: controller.ACROAuth{
+ AutoRedirect: cfg.OAuth.AutoRedirect,
+ },
+ UI: controller.ACRUI{
+ Title: cfg.UI.Title,
+ ForgotPasswordMessage: cfg.UI.ForgotPasswordMessage,
+ BackgroundImage: cfg.UI.BackgroundImage,
+ WarningsEnabled: cfg.UI.WarningsEnabled,
+ },
+ App: controller.ACRApp{
+ AppURL: runtime.AppURL,
+ CookieDomain: runtime.CookieDomain,
+ TrustedDomains: runtime.TrustedDomains,
+ },
}
bytes, err := json.Marshal(expectedAppContextResponse)
require.NoError(t, err)
@@ -84,13 +93,15 @@ func TestContextController(t *testing.T) {
path: "/api/context/user",
expected: func() string {
expectedUserContextResponse := controller.UserContextResponse{
- Status: 200,
- Message: "Success",
- IsLoggedIn: true,
- Username: "johndoe",
- Name: "John Doe",
- Email: utils.CompileUserEmail("johndoe", runtime.CookieDomain),
- Provider: "local",
+ Status: 200,
+ Message: "Success",
+ Auth: controller.UCRAuth{
+ Authenticated: true,
+ Username: "johndoe",
+ Name: "John Doe",
+ Email: utils.CompileUserEmail("johndoe", runtime.CookieDomain),
+ ProviderID: "local",
+ },
}
bytes, err := json.Marshal(expectedUserContextResponse)
require.NoError(t, err)
diff --git a/internal/middleware/context_middleware.go b/internal/middleware/context_middleware.go
index cdc4c273..174324ee 100644
--- a/internal/middleware/context_middleware.go
+++ b/internal/middleware/context_middleware.go
@@ -306,12 +306,18 @@ func (m *ContextMiddleware) tailscaleWhois(ctx context.Context, ip string) (*mod
return nil, nil
}
- return &model.TailscaleContext{
+ uctx := model.TailscaleContext{
BaseContext: model.BaseContext{
Username: whois.NodeName,
Email: whois.LoginName,
Name: whois.DisplayName,
},
UserID: whois.UserID,
- }, nil
+ }
+
+ if !strings.ContainsAny(uctx.Email, "@") {
+ uctx.Email = utils.CompileUserEmail(uctx.Email+"-tailscale", m.runtime.CookieDomain)
+ }
+
+ return &uctx, nil
}
diff --git a/internal/model/runtime.go b/internal/model/runtime.go
index 9bd81770..9df20b85 100644
--- a/internal/model/runtime.go
+++ b/internal/model/runtime.go
@@ -13,6 +13,7 @@ type RuntimeConfig struct {
OAuthWhitelist []string
ConfiguredProviders []Provider
OIDCClients []OIDCClientConfig
+ TrustedDomains []string
}
type Provider struct {
diff --git a/internal/service/tailscale_service.go b/internal/service/tailscale_service.go
index 1d4dc4eb..92b56012 100644
--- a/internal/service/tailscale_service.go
+++ b/internal/service/tailscale_service.go
@@ -7,6 +7,7 @@ import (
"net"
"strings"
"sync"
+ "time"
"github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
@@ -59,6 +60,15 @@ func NewTailscaleService(log *logger.Logger, config model.Config, ctx context.Co
lc: lc,
}
+ connectCtx, cancel := context.WithTimeout(ctx, 2*time.Minute)
+ defer cancel()
+
+ err = service.waitForConn(connectCtx)
+
+ if err != nil {
+ return nil, fmt.Errorf("failed to connect to tailscale network: %w", err)
+ }
+
wg.Go(service.watchAndClose)
return service, nil
@@ -89,7 +99,7 @@ func (ts *TailscaleService) Whois(ctx context.Context, addr string) (*model.Tail
UserID: who.UserProfile.ID.String(),
LoginName: who.UserProfile.LoginName,
DisplayName: who.UserProfile.DisplayName,
- NodeName: who.Node.Name,
+ NodeName: strings.TrimSuffix(who.Node.Name, "."),
}
return &res, nil
@@ -117,3 +127,19 @@ func (ts *TailscaleService) GetHostname() string {
return strings.TrimSuffix(status.Self.DNSName, ".")
}
+
+func (ts *TailscaleService) waitForConn(ctx context.Context) error {
+ for {
+ select {
+ case <-ctx.Done():
+ return fmt.Errorf("timed out waiting for tailscale connection")
+ default:
+ ip4, _ := ts.srv.TailscaleIPs()
+ if !ip4.IsValid() {
+ time.Sleep(1 * time.Second)
+ continue
+ }
+ return nil
+ }
+ }
+}