Compare commits

..

8 Commits

Author SHA1 Message Date
Stavros 1fcac1b2f7 feat: automatically generate context ignore paths 2026-06-30 17:56:48 +03:00
Stavros ffafb5bff5 feat: add a reconnect to the initial ldap connection (#928) 2026-06-30 15:57:41 +03:00
Stavros bb867ea5f4 docs: update readme with openid certification badge 2026-06-29 01:35:06 +03:00
dependabot[bot] fdd516edf1 chore(deps): bump the minor-patch group across 1 directory with 2 updates (#957)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-28 17:59:34 +03:00
dependabot[bot] 1b14b90ede chore(deps): bump node from 26.3-alpine3.23 to 26.4-alpine3.23 (#956)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-28 17:59:01 +03:00
dependabot[bot] 6ba55b3d9c chore(deps): bump actions/setup-go from 6.4.0 to 6.5.0 (#954)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-28 17:58:38 +03:00
Stavros 09ec40cb76 feat: show provider in quick actions (#955) 2026-06-28 17:58:11 +03:00
Stavros 08af4557fd fix: use client ip instead of remote addr in tailscale whois lookups 2026-06-23 21:06:55 +03:00
39 changed files with 356 additions and 89 deletions
+4 -4
View File
@@ -21,7 +21,7 @@ jobs:
package_json_file: ./frontend/package.json
- name: Setup go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
with:
go-version: "^1.26.4"
@@ -36,9 +36,9 @@ jobs:
- name: Check codegen is up to date
run: |
sqlc generate
go generate ./internal/repository/...
git diff --exit-code -- internal/repository/
git status --porcelain -- internal/repository/ | grep -q . && echo "untracked files in internal/repository/" && exit 1 || true
go generate ./...
git diff --exit-code
git status --porcelain | grep -q . && echo "untracked files in git diff" && exit 1 || true
- name: Install frontend dependencies
working-directory: ./frontend
+2 -2
View File
@@ -65,7 +65,7 @@ jobs:
package_json_file: ./frontend/package.json
- name: Install go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
with:
go-version: "^1.26.4"
@@ -110,7 +110,7 @@ jobs:
package_json_file: ./frontend/package.json
- name: Install go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
with:
go-version: "^1.26.4"
+2 -2
View File
@@ -41,7 +41,7 @@ jobs:
package_json_file: ./frontend/package.json
- name: Install go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
with:
go-version: "^1.26.4"
@@ -83,7 +83,7 @@ jobs:
package_json_file: ./frontend/package.json
- name: Install go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
with:
go-version: "^1.26.4"
+1 -1
View File
@@ -1,5 +1,5 @@
# Site builder
FROM node:26.3-alpine3.23 AS frontend-builder
FROM node:26.4-alpine3.23 AS frontend-builder
WORKDIR /frontend
+1 -1
View File
@@ -1,5 +1,5 @@
# Site builder
FROM node:26.3-alpine3.23 AS frontend-builder
FROM node:26.4-alpine3.23 AS frontend-builder
WORKDIR /frontend
+1 -2
View File
@@ -94,5 +94,4 @@ sql:
# Go gen
generate:
go run ./gen
go generate ./internal/repository/...
go generate ./...
+5 -1
View File
@@ -1,7 +1,7 @@
<div align="center">
<img alt="Tinyauth" title="Tinyauth" width="96" src="assets/logo-rounded.png">
<h1>Tinyauth</h1>
<p>The tiniest authentication and authorization server you have ever seen.</p>
<p>The tiniest OpenID Certified™ authorization and authentication server you have ever seen.</p>
</div>
<div align="center">
@@ -28,6 +28,10 @@ Tinyauth is the simplest and tiniest authentication and authorization server you
> [!NOTE]
> This is the main development branch. For the latest stable release, see the [documentation](https://tinyauth.app) or the latest stable tag.
As of 2026-06-25, Tinyauth v5.1.0 is OpenID Certified™ for Basic OP. You can find the certification details [here](https://openid.net/certification-old/certified-openid-providers-profiles/), test suite available [here](https://www.certification.openid.net/plan-detail.html?public=true&plan=H0qhpsOcQkxUE).
<img alt="OpenID Certified" width="200" src="https://openid.net/wordpress-content/uploads/2016/05/oid-l-certification-mark-l-cmyk-150dpi-90mm.jpg" />
## Getting Started
You can get started with Tinyauth by following the guide in the [documentation](https://tinyauth.app/docs/getting-started). There is also an available [docker-compose](./docker-compose.example.yml) file that has Traefik, Whoami and Tinyauth to demonstrate its capabilities (keep in mind that this file lives in the development branch so it may have updates that are not yet released).
@@ -0,0 +1,22 @@
import type { SVGProps } from "react";
export function LocalAuthIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
{...props}
>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7a4 4 0 1 0 8 0a4 4 0 0 0-8 0M6 21v-2a4 4 0 0 1 4-4h5m3.5 3.5L15 22l-1.5-1.5m5.054-2.086a2 2 0 1 1 2.828-2.828a2 2 0 0 1-2.828 2.828M16 19l1 1"
></path>
</svg>
);
}
@@ -25,6 +25,8 @@ import {
Palette,
Settings,
Sun,
UserRoundKey,
X,
} from "lucide-react";
import { useTranslation } from "react-i18next";
import { useLocation } from "react-router";
@@ -37,20 +39,26 @@ import { useMutation } from "@tanstack/react-query";
import axios from "axios";
import { toast } from "sonner";
import { useEffect } from "react";
import { GoogleIcon } from "../icons/google";
import { GithubIcon } from "../icons/github";
import { TailscaleIcon } from "../icons/tailscale";
import { MicrosoftIcon } from "../icons/microsoft";
import { PocketIDIcon } from "../icons/pocket-id";
import { OAuthIcon } from "../icons/oauth";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
function Avatar({ initial }: { initial: string }) {
return (
<span className="group relative grid size-10 place-items-center rounded-full">
<span className="absolute inset-0 overflow-hidden rounded-full bg-linear-to-b from-neutral-50 to-neutral-100 dark:from-neutral-700 dark:to-neutral-950 shadow-lg"></span>
<span className="relative text-sm font-semibold text-primary">
{initial}
</span>
</span>
);
}
const iconStyles = "size-4";
const iconMap: Record<string, React.ReactNode> = {
google: <GoogleIcon className={iconStyles} />,
github: <GithubIcon className={iconStyles} />,
tailscale: <TailscaleIcon className={iconStyles} />,
microsoft: <MicrosoftIcon className={iconStyles} />,
pocketid: <PocketIDIcon className={iconStyles} />,
};
export const QuickActions = () => {
const { auth } = useUserContext();
const { auth, oauth, tailscale } = useUserContext();
const { theme, setTheme } = useTheme();
const { t } = useTranslation();
const { search } = useLocation();
@@ -64,6 +72,49 @@ export const QuickActions = () => {
const screenParams = useScreenParams(searchParams);
const compiledParams = recompileScreenParams(screenParams);
const [isOpen, setIsOpen] = useState(false);
const providerDetails = (():
| { name: string; icon: React.ReactNode }
| undefined => {
if (!auth.authenticated) {
return undefined;
}
if (auth.providerId === "local" || auth.providerId === "ldap") {
return {
name: t(
auth.providerId === "ldap"
? "quickActionsProviderLDAP"
: "quickActionsProviderLocal",
),
icon: (
<UserRoundKey
strokeWidth={1.5}
size={16}
className="text-muted-foreground ml-0.5"
/>
),
};
}
if (oauth.active) {
return {
name: t("quickActionsProviderOAuth", { provider: oauth.displayName }),
icon: iconMap[auth.providerId] || <OAuthIcon className={iconStyles} />,
};
}
if (auth.providerId === "tailscale") {
return {
name: `Tailscale (${tailscale.nodeName})`,
icon: <TailscaleIcon className={iconStyles} />,
};
}
return undefined;
})();
const logoutMutation = useMutation({
mutationFn: () => axios.post("/api/user/logout"),
mutationKey: ["logout"],
@@ -107,17 +158,29 @@ export const QuickActions = () => {
] as const;
return (
<DropdownMenu>
<DropdownMenu onOpenChange={(open) => setIsOpen(open)} open={isOpen}>
<DropdownMenuTrigger asChild>
<button
aria-label={t("quickActionsTitle")}
className="rounded-full transition-transform duration-200 will-change-transform hover:scale-105 hover:cursor-pointer focus:ring-0 focus:outline-3 focus:outline-ring/50"
>
{auth.authenticated ? (
<Avatar initial={initial!} />
<div className="size-10 flex justify-center items-center p-2 rounded-full bg-card border border-border">
{isOpen ? (
<X className="size-4 text-primary rotate-0 transition-transform duration-200 starting:rotate-45" />
) : (
<span className="text-sm text-primary rotate-0 transition-transform duration-200 starting:-rotate-45">
{initial}
</span>
)}
</div>
) : (
<span className="bg-card text-primary border-border size-10 flex items-center justify-center rounded-full border shadow-lg">
<Settings className="size-4" />
<Settings
className={`size-4 transition-transform duration-200 ${
isOpen ? "rotate-45" : "rotate-0"
}`}
/>
</span>
)}
</button>
@@ -126,19 +189,22 @@ export const QuickActions = () => {
<DropdownMenuContent
align="end"
sideOffset={8}
className="rounded-xl p-1"
className="rounded-xl p-1 w-3xs"
>
{auth.authenticated && (
<>
<DropdownMenuLabel className="flex items-center gap-3 p-2">
<div className="bg-foreground text-background flex size-9 shrink-0 items-center justify-center rounded-full text-sm font-medium">
{initial}
</div>
<div className="flex min-w-0 flex-col">
<Tooltip>
<TooltipTrigger className="size-9 rounded-full p-2 bg-muted border-border border flex items-center justify-center">
{providerDetails!.icon}
</TooltipTrigger>
<TooltipContent>{providerDetails!.name}</TooltipContent>
</Tooltip>
<div className="flex min-w-0 flex-col gap-0.5">
<span className="truncate text-sm font-medium">
{auth.name}
</span>
<span className="text-muted-foreground truncate text-xs font-normal">
<span className="text-muted-foreground truncate text-xs">
{auth.email}
</span>
</div>
@@ -197,7 +263,7 @@ export const QuickActions = () => {
onSelect={() => logoutMutation.mutate()}
className="text-destructive"
>
<DoorOpenIcon className="size-4" />
<DoorOpenIcon className="size-4 text-destructive" />
{t("quickActionsLogout")}
</DropdownMenuItem>
</>
+4 -1
View File
@@ -99,5 +99,8 @@
"quickActionsThemeDark": "Dark",
"quickActionsThemeSystem": "System",
"quickActionsLogout": "Logout",
"quickActionsTitle": "Quick Actions"
"quickActionsTitle": "Quick Actions",
"quickActionsProviderLocal": "Local",
"quickActionsProviderLDAP": "LDAP",
"quickActionsProviderOAuth": "{{provider}} OAuth"
}
+4 -1
View File
@@ -99,5 +99,8 @@
"quickActionsThemeDark": "Dark",
"quickActionsThemeSystem": "System",
"quickActionsLogout": "Logout",
"quickActionsTitle": "Quick Actions"
"quickActionsTitle": "Quick Actions",
"quickActionsProviderLocal": "Local",
"quickActionsProviderLDAP": "LDAP",
"quickActionsProviderOAuth": "{{provider}} OAuth"
}
+1 -1
View File
@@ -137,7 +137,7 @@ function LogoutLayout({ children, logoutMutation }: LogoutLayoutProps) {
</CardHeader>
<CardFooter>
<Button
className="w-full"
className="w-full text-destructive"
variant="outline"
loading={logoutMutation.isPending}
onClick={() => logoutMutation.mutate()}
+131
View File
@@ -0,0 +1,131 @@
// gen/context_paths generates the ignore paths for the user context since
// gin will not less apply the middleware to only specific paths.
//
// The generator reads every controller and looks for the //context:ignore comment.
// The format for the context ignore comment is:
//
// //contxt:ignore /api/mypath GET,POST
package main
import (
"bytes"
"fmt"
"go/format"
"os"
"strings"
"text/template"
_ "embed"
"golang.org/x/tools/go/packages"
)
//go:embed paths.tmpl
var pathsTmplSrc string
var pathsTmpl = template.Must(template.New("paths").Parse(pathsTmplSrc))
func main() {
if err := run(); err != nil {
fmt.Printf("Failed to generate: %s", err.Error())
os.Exit(1)
}
}
func run() error {
// load pkg
pkgConfig := &packages.Config{
Mode: packages.NeedFiles,
}
pkgs, err := packages.Load(pkgConfig, "github.com/tinyauthapp/tinyauth/internal/controller")
if err != nil {
return fmt.Errorf("failed to load pkg: %w", err)
}
if len(pkgs) == 0 {
return fmt.Errorf("failed to get controllers package")
}
pkg := pkgs[0]
// for each file we check the comments and either add or remove the context
var contextIgnorePaths []string
for _, gofile := range pkg.GoFiles {
// read the file
file, err := os.ReadFile(gofile)
if err != nil {
fmt.Printf("Failed to read %s, ignoring", gofile)
continue
}
// get the comment lines
lines := strings.SplitSeq(string(file), "\n")
for line := range lines {
if !strings.HasPrefix(strings.TrimSpace(line), "//context:ignore") {
continue
}
path, methods, ok := parseContextIgnoreLine(line)
if !ok {
fmt.Printf("Failed to parse %s rule, ignore", line)
continue
}
for _, m := range methods {
contextIgnorePaths = append(contextIgnorePaths, m+" "+path)
}
}
}
// generate out
type tmplData struct {
IgnorePaths []string
}
var buf bytes.Buffer
if err := pathsTmpl.Execute(&buf, tmplData{
IgnorePaths: contextIgnorePaths,
}); err != nil {
return err
}
formatted, err := format.Source(buf.Bytes())
if err != nil {
return fmt.Errorf("gofmt failed: %w", err)
}
// write out
err = os.WriteFile("context_paths.go", formatted, 0666)
if err != nil {
return fmt.Errorf("failed to write out: %w", err)
}
return nil
}
func parseContextIgnoreLine(line string) (string, []string, bool) {
line = strings.TrimPrefix(line, "//context:ignore ")
path, methodStr, ok := strings.Cut(line, " ")
if !ok {
return "", []string{}, false
}
var methodsParsed []string
methodParts := strings.SplitSeq(methodStr, ",")
for m := range methodParts {
if strings.TrimSpace(m) == "" {
continue
}
m = strings.ToUpper(m)
methodsParsed = append(methodsParsed, m)
}
return path, methodsParsed, true
}
+6
View File
@@ -0,0 +1,6 @@
// Code generated by gen/context_paths. DO NOT EDIT.
package middleware
var contextSkipPathsPrefix = []string{
{{range .IgnorePaths}}"{{.}}",
{{end}}}
+6
View File
@@ -1,3 +1,9 @@
// gen/docs generates the .env.example and config.gen.md
// files for the configuration of Tinyauth. Run via:
//
// The generator reads the Tinyauth configuration package and using reflection it generates the
// example files. The .env.example is used in this repo while the config.gen.md is used in the
// documentaton alongside some warnings that are added later.
package main
import (
@@ -1,7 +1,5 @@
// gen/sqlc-wrapper generates store.go wrapper files for each sqlc driver package under
// internal/repository/<driver>/. Run via:
//
// go generate ./internal/repository/...
// gen/sqlc_wrapper generates store.go wrapper files for each sqlc driver package under
// internal/repository/<driver>/.
//
// The generator introspects *Queries methods and the model/params types in the
// driver package, then emits a store.go that wraps *Queries so it satisfies
@@ -1,4 +1,4 @@
// Code generated by cmd/gen/sqlc-wrapper. DO NOT EDIT.
// Code generated by cmd/gen/sqlc_wrapper. DO NOT EDIT.
package {{.PkgName}}
import (
+3
View File
@@ -0,0 +1,3 @@
package docs
//go:generate go run github.com/tinyauthapp/tinyauth/gen/docs
+3 -3
View File
@@ -24,10 +24,10 @@ require (
go.uber.org/dig v1.19.0
golang.org/x/crypto v0.53.0
golang.org/x/oauth2 v0.36.0
golang.org/x/tools v0.46.0
golang.org/x/tools v0.47.0
k8s.io/apimachinery v0.36.2
k8s.io/client-go v0.36.2
modernc.org/sqlite v1.52.0
modernc.org/sqlite v1.53.0
tailscale.com v1.100.0
)
@@ -175,7 +175,7 @@ require (
k8s.io/klog/v2 v2.140.0 // indirect
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect
modernc.org/libc v1.72.3 // indirect
modernc.org/libc v1.73.4 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
rsc.io/qr v0.2.0 // indirect
+12 -12
View File
@@ -526,8 +526,8 @@ golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk=
golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys=
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
@@ -571,20 +571,20 @@ k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hk
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0=
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU=
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ=
modernc.org/ccgo/v4 v4.34.0/go.mod h1:AS5WYMyBakQ+fhsHhtP8mWB82KTGPkNNJDGfGQCe0/A=
modernc.org/cc/v4 v4.28.4 h1:Hd/4Es+MBj+/7hSdZaisNyu6bv3V0Dp2MdllyfqaH+c=
modernc.org/cc/v4 v4.28.4/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.34.4 h1:OVnSOWQjVKOYkFxoHYB+qQmSHK5gqMqARM+K9DpR/Ws=
modernc.org/ccgo/v4 v4.34.4/go.mod h1:qdKqE8FNIYyysougB1RX9MxCzp5oJOcQXSobANJ4TuE=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc=
modernc.org/gc/v3 v3.1.3/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU=
modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs=
modernc.org/libc v1.73.4 h1:+ra4Ui8ngyt8HDcO1FTDPWlkAh6yOdaO2yAoh8MddQA=
modernc.org/libc v1.73.4/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
@@ -593,8 +593,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.52.0 h1:p4dhYh2tXZCiyaqHwRVJDjIGKWyXayiQpThxgDzJaxo=
modernc.org/sqlite v1.52.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
modernc.org/sqlite v1.53.0 h1:20WG8N9q4ji/dEqGk4uiI0c6OPjSeLTNYGFCc3+7c1M=
modernc.org/sqlite v1.53.0/go.mod h1:xoEpOIpGrgT48H5iiyt/YXPCZPEzlfmfFwtk8Lklw8s=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+5
View File
@@ -11,6 +11,7 @@ import (
"net/url"
"os"
"os/signal"
"slices"
"sort"
"strings"
"syscall"
@@ -131,6 +132,10 @@ func (app *BootstrapApp) Setup() error {
app.runtime.OAuthProviders = app.config.OAuth.Providers
for id, provider := range app.runtime.OAuthProviders {
if slices.Contains(model.ReservedProviderNames, id) {
return fmt.Errorf("provider id %s is reserved and cannot be used", id)
}
providerWhitelist, err := utils.GetStringList(provider.Whitelist, provider.WhitelistFile)
if err != nil {
return fmt.Errorf("failed to load oauth whitelist for provider %s: %w", id, err)
@@ -147,6 +147,7 @@ func (controller *ContextController) userContextHandler(c *gin.Context) {
c.JSON(200, userContext)
}
//context:ignore /api/context/app GET
func (controller *ContextController) appContextHandler(c *gin.Context) {
c.JSON(200, AppContextResponse{
Status: 200,
+1
View File
@@ -23,6 +23,7 @@ func NewHealthController(i HealthControllerInput) *HealthController {
return controller
}
//context:ignore /api/healthz GET,HEAD
func (controller *HealthController) healthHandler(c *gin.Context) {
c.JSON(200, gin.H{
"status": 200,
+2
View File
@@ -54,6 +54,7 @@ func NewOAuthController(i OAuthControllerInput) *OAuthController {
return controller
}
//context:ignore /api/oauth/url GET
func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
var req OAuthRequest
@@ -118,6 +119,7 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
})
}
//context:ignore /api/oauth/callback GET
func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
var req OAuthRequest
+2
View File
@@ -367,6 +367,7 @@ func (controller *OIDCController) authorizeComplete(c *gin.Context) {
})
}
//context:ignore /api/oidc/token POST
func (controller *OIDCController) Token(c *gin.Context) {
if controller.oidc == nil {
controller.log.App.Warn().Msg("Received OIDC request but OIDC server is not configured")
@@ -538,6 +539,7 @@ func (controller *OIDCController) Token(c *gin.Context) {
c.JSON(200, tokenResponse)
}
//context:ignore /api/oidc/userinfo GET,POST
func (controller *OIDCController) Userinfo(c *gin.Context) {
if controller.oidc == nil {
controller.log.App.Warn().Msg("Received OIDC userinfo request but OIDC server is not configured")
@@ -33,6 +33,7 @@ func NewResourcesController(i ResourcesControllerInput) *ResourcesController {
return controller
}
//context:ignore /resources GET
func (controller *ResourcesController) resourcesHandler(c *gin.Context) {
if controller.config.Resources.Path == "" {
c.JSON(404, gin.H{
+1
View File
@@ -57,6 +57,7 @@ func NewUserController(i UserControllerInput) *UserController {
return controller
}
//context:ignore /api/user/login POST
func (controller *UserController) loginHandler(c *gin.Context) {
var req LoginRequest
@@ -65,6 +65,7 @@ func NewWellKnownController(i WellKnownControllerInput) *WellKnownController {
return controller
}
//context:ignore /.well-known/openid-configuration GET
func (controller *WellKnownController) OpenIDConnectConfiguration(c *gin.Context) {
if controller.oidc == nil {
c.JSON(500, gin.H{
@@ -94,6 +95,7 @@ func (controller *WellKnownController) OpenIDConnectConfiguration(c *gin.Context
})
}
//context:ignore /.well-known/jwks.json GET
func (controller *WellKnownController) JWKS(c *gin.Context) {
if controller.oidc == nil {
c.JSON(500, gin.H{
@@ -122,6 +124,7 @@ func (controller *WellKnownController) JWKS(c *gin.Context) {
c.Status(http.StatusOK)
}
//context:ignore /.well-known/webfinger GET
func (controller *WellKnownController) WebFinger(c *gin.Context) {
c.Header("Content-Type", "application/jrd+json")
c.Header("Access-Control-Allow-Origin", "*")
+3 -23
View File
@@ -16,26 +16,6 @@ import (
"github.com/gin-gonic/gin"
)
// Gin won't let us set a middleware on a specific route (at least it doesn't work,
// see https://github.com/gin-gonic/gin/issues/531) so we have to do some hackery
var (
contextSkipPathsPrefix = []string{
"GET /api/context/app",
"GET /api/healthz",
"HEAD /api/healthz",
"GET /api/oauth/url",
"GET /api/oauth/callback",
"GET /api/oidc/clients",
"POST /api/oidc/token",
"GET /api/oidc/userinfo",
"POST /api/oidc/userinfo",
"GET /resources",
"POST /api/user/login",
"GET /.well-known/openid-configuration",
"GET /.well-known/jwks.json",
}
)
type ContextMiddleware struct {
log *logger.Logger
runtime *model.RuntimeConfig
@@ -74,7 +54,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
uuid, err := c.Cookie(m.runtime.SessionCookieName)
if err == nil {
userContext, cookie, err := m.cookieAuth(c.Request.Context(), uuid, c.RemoteIP())
userContext, cookie, err := m.cookieAuth(c.Request.Context(), uuid, c.ClientIP())
if err == nil {
if cookie != nil {
@@ -112,10 +92,10 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
// Lastly check if we have a tailscale session to add
if m.tailscale != nil {
tailscaleContext, err := m.tailscaleWhois(c.Request.Context(), c.RemoteIP())
tailscaleContext, err := m.tailscaleWhois(c.Request.Context(), c.ClientIP())
if err != nil {
m.log.App.Error().Err(err).Msgf("Error performing tailscale whois for IP %s: %v", c.RemoteIP(), err)
m.log.App.Error().Err(err).Msgf("Error performing tailscale whois for IP %s: %v", c.ClientIP(), err)
}
if tailscaleContext != nil {
+18
View File
@@ -0,0 +1,18 @@
// Code generated by gen/context_paths. DO NOT EDIT.
package middleware
var contextSkipPathsPrefix = []string{
"GET /api/context/app",
"GET /api/healthz",
"HEAD /api/healthz",
"GET /api/oauth/url",
"GET /api/oauth/callback",
"POST /api/oidc/token",
"GET /api/oidc/userinfo",
"POST /api/oidc/userinfo",
"GET /resources",
"POST /api/user/login",
"GET /.well-known/openid-configuration",
"GET /.well-known/jwks.json",
"GET /.well-known/webfinger",
}
+3
View File
@@ -0,0 +1,3 @@
package middleware
//go:generate go run github.com/tinyauthapp/tinyauth/gen/context_paths
+2
View File
@@ -17,6 +17,8 @@ var OverrideProviders = map[string]string{
"github": "GitHub",
}
var ReservedProviderNames = []string{"local", "ldap", "tailscale"}
const SessionCookieName = "tinyauth-session"
const CSRFCookieName = "tinyauth-csrf"
const RedirectCookieName = "tinyauth-redirect"
+1 -1
View File
@@ -1,3 +1,3 @@
package postgres
//go:generate go run github.com/tinyauthapp/tinyauth/gen/sqlc-wrapper -pkg github.com/tinyauthapp/tinyauth/internal/repository/postgres
//go:generate go run github.com/tinyauthapp/tinyauth/gen/sqlc_wrapper -pkg github.com/tinyauthapp/tinyauth/internal/repository/postgres
+1 -1
View File
@@ -1,4 +1,4 @@
// Code generated by cmd/gen/sqlc-wrapper. DO NOT EDIT.
// Code generated by cmd/gen/sqlc_wrapper. DO NOT EDIT.
package postgres
import (
+1 -1
View File
@@ -1,3 +1,3 @@
package sqlite
//go:generate go run github.com/tinyauthapp/tinyauth/gen/sqlc-wrapper -pkg github.com/tinyauthapp/tinyauth/internal/repository/sqlite
//go:generate go run github.com/tinyauthapp/tinyauth/gen/sqlc_wrapper -pkg github.com/tinyauthapp/tinyauth/internal/repository/sqlite
+1 -1
View File
@@ -1,4 +1,4 @@
// Code generated by cmd/gen/sqlc-wrapper. DO NOT EDIT.
// Code generated by cmd/gen/sqlc_wrapper. DO NOT EDIT.
package sqlite
import (
+12 -5
View File
@@ -18,6 +18,7 @@ import (
type LdapService struct {
log *logger.Logger
ctx context.Context
config *model.Config
conn *ldapgo.Conn
@@ -32,6 +33,7 @@ type LdapServiceInput struct {
Log *logger.Logger
Config *model.Config
Ding *ding.Ding
Ctx context.Context
}
func NewLdapService(i LdapServiceInput) (*LdapService, error) {
@@ -42,6 +44,7 @@ func NewLdapService(i LdapServiceInput) (*LdapService, error) {
ldap := &LdapService{
log: i.Log,
config: i.Config,
ctx: i.Ctx,
}
ldap.bindPw = utils.GetSecret(i.Config.LDAP.BindPassword, i.Config.LDAP.BindPasswordFile)
@@ -73,6 +76,8 @@ func NewLdapService(i LdapServiceInput) (*LdapService, error) {
_, err := ldap.connect()
if err != nil {
// 3s + 4.5s (3x1.5) = ~6.75-8.25s total wait time before giving up
err = ldap.reconnect(3 * time.Second)
return nil, fmt.Errorf("failed to connect to ldap server: %w", err)
}
@@ -88,7 +93,7 @@ func NewLdapService(i LdapServiceInput) (*LdapService, error) {
err := ldap.heartbeat()
if err != nil {
ldap.log.App.Warn().Err(err).Msg("LDAP connection heartbeat failed, attempting to reconnect")
if reconnectErr := ldap.reconnect(); reconnectErr != nil {
if reconnectErr := ldap.reconnect(1 * time.Second); reconnectErr != nil {
ldap.log.App.Error().Err(reconnectErr).Msg("Failed to reconnect to LDAP server")
continue
}
@@ -276,17 +281,19 @@ func (ldap *LdapService) heartbeat() error {
return nil
}
func (ldap *LdapService) reconnect() error {
func (ldap *LdapService) reconnect(interval time.Duration) error {
ldap.log.App.Info().Msg("Attempting to reconnect to LDAP server")
exp := backoff.NewExponentialBackOff()
exp.InitialInterval = 500 * time.Millisecond
exp.InitialInterval = interval
exp.RandomizationFactor = 0.1
exp.Multiplier = 1.5
exp.Reset()
operation := func() (*ldapgo.Conn, error) {
ldap.conn.Close()
if ldap.conn != nil {
ldap.conn.Close()
}
conn, err := ldap.connect()
if err != nil {
return nil, err
@@ -294,7 +301,7 @@ func (ldap *LdapService) reconnect() error {
return conn, nil
}
_, err := backoff.Retry(context.TODO(), operation, backoff.WithBackOff(exp), backoff.WithMaxTries(3))
_, err := backoff.Retry(ldap.ctx, operation, backoff.WithBackOff(exp), backoff.WithMaxTries(3))
if err != nil {
return err