Compare commits

...

8 Commits

Author SHA1 Message Date
Stavros
ec8121499c feat: add nonce claim support to oidc server 2026-03-03 23:13:09 +02:00
Stavros
0e6bcf9713 fix: lookup config file options correctly in file loader 2026-03-03 22:48:44 +02:00
Stavros
af5a8bc452 fix: handle empty client name in authorize page 2026-03-03 22:48:44 +02:00
Stavros
de980815ce fix: include kid in jwks response 2026-03-03 22:48:44 +02:00
Stavros
189ad7115a New translations en.json (Serbian (Cyrillic)) (#683) 2026-03-02 20:17:41 +02:00
Stavros
2f2556d480 fix: set correct paths in dockerfiles 2026-03-02 20:15:51 +02:00
Stavros
f1512f45b7 chore: update example env 2026-03-02 19:53:17 +02:00
Stavros
cd410b6cdf refactor: categorize leftover config options (#682)
* refactor: categorize leftover config options

* chore: update config description
2026-03-02 19:49:59 +02:00
28 changed files with 196 additions and 102 deletions

View File

@@ -4,14 +4,23 @@
# The base URL where the app is hosted.
TINYAUTH_APPURL=
# database config
# The path to the database, including file name.
TINYAUTH_DATABASE_PATH="./tinyauth.db"
# analytics config
# Enable periodic version information collection.
TINYAUTH_ANALYTICS_ENABLED=true
# resources config
# Enable the resources server.
TINYAUTH_RESOURCES_ENABLED=true
# The directory where resources are stored.
TINYAUTH_RESOURCESDIR="./resources"
# The path to the database file.
TINYAUTH_DATABASEPATH="./tinyauth.db"
# Disable analytics.
TINYAUTH_DISABLEANALYTICS=false
# Disable resources server.
TINYAUTH_DISABLERESOURCES=false
TINYAUTH_RESOURCES_PATH="./resources"
# server config
@@ -107,9 +116,9 @@ TINYAUTH_OAUTH_PROVIDERS_name_NAME=
# oidc config
# Path to the private key file.
# Path to the private key file, including file name.
TINYAUTH_OIDC_PRIVATEKEYPATH="./tinyauth_oidc_key"
# Path to the public key file.
# Path to the public key file, including file name.
TINYAUTH_OIDC_PUBLICKEYPATH="./tinyauth_oidc_key.pub"
# OIDC client ID.
TINYAUTH_OIDC_CLIENTS_name_CLIENTID=
@@ -130,8 +139,8 @@ TINYAUTH_UI_TITLE="Tinyauth"
TINYAUTH_UI_FORGOTPASSWORDMESSAGE="You can change your password by changing the configuration."
# Path to the background image.
TINYAUTH_UI_BACKGROUNDIMAGE="/background.jpg"
# Disable UI warnings.
TINYAUTH_UI_DISABLEWARNINGS=false
# Enable UI warnings.
TINYAUTH_UI_WARNINGSENABLED=true
# ldap config

3
.gitignore vendored
View File

@@ -45,3 +45,6 @@ __debug_*
# generated markdown (for docs)
/config.gen.md
# testing config
config.certify.yml

View File

@@ -57,9 +57,9 @@ EXPOSE 3000
VOLUME ["/data"]
ENV TINYAUTH_DATABASEPATH=/data/tinyauth.db
ENV TINYAUTH_DATABASE_PATH=/data/tinyauth.db
ENV TINYAUTH_RESOURCESDIR=/data/resources
ENV TINYAUTH_RESOURCES_PATH=/data/resources
ENV PATH=$PATH:/tinyauth

View File

@@ -18,8 +18,8 @@ COPY ./air.toml ./
EXPOSE 3000
ENV TINYAUTH_DATABASEPATH=/data/tinyauth.db
ENV TINYAUTH_DATABASE_PATH=/data/tinyauth.db
ENV TINYAUTH_RESOURCESDIR=/data/resources
ENV TINYAUTH_RESOURCES_PATH=/data/resources
ENTRYPOINT ["air", "-c", "air.toml"]

View File

@@ -60,9 +60,9 @@ EXPOSE 3000
VOLUME ["/data"]
ENV TINYAUTH_DATABASEPATH=/data/tinyauth.db
ENV TINYAUTH_DATABASE_PATH=/data/tinyauth.db
ENV TINYAUTH_RESOURCESDIR=/data/resources
ENV TINYAUTH_RESOURCES_PATH=/data/resources
ENV PATH=$PATH:/tinyauth

View File

@@ -31,7 +31,7 @@ const BaseLayout = ({ children }: { children: React.ReactNode }) => {
};
export const Layout = () => {
const { appUrl, disableUiWarnings } = useAppContext();
const { appUrl, warningsEnabled } = useAppContext();
const [ignoreDomainWarning, setIgnoreDomainWarning] = useState(() => {
return window.sessionStorage.getItem("ignoreDomainWarning") === "true";
});
@@ -42,7 +42,7 @@ export const Layout = () => {
setIgnoreDomainWarning(true);
}, [setIgnoreDomainWarning]);
if (!ignoreDomainWarning && !disableUiWarnings && appUrl !== currentUrl) {
if (!ignoreDomainWarning && warningsEnabled && appUrl !== currentUrl) {
return (
<BaseLayout>
<DomainWarning

View File

@@ -4,6 +4,7 @@ export type OIDCValues = {
client_id: string;
redirect_uri: string;
state: string;
nonce: string;
};
interface IuseOIDCParams {
@@ -13,7 +14,7 @@ interface IuseOIDCParams {
missingParams: string[];
}
const optionalParams: string[] = ["state"];
const optionalParams: string[] = ["state", "nonce"];
export function useOIDCParams(params: URLSearchParams): IuseOIDCParams {
let compiled: string = "";
@@ -26,6 +27,7 @@ export function useOIDCParams(params: URLSearchParams): IuseOIDCParams {
client_id: params.get("client_id") ?? "",
redirect_uri: params.get("redirect_uri") ?? "",
state: params.get("state") ?? "",
nonce: params.get("nonce") ?? "",
};
for (const key of Object.keys(values)) {

View File

@@ -58,8 +58,8 @@
"invalidInput": "Неисправан унос",
"domainWarningTitle": "Неисправан домен",
"domainWarningSubtitle": "Ова инстанца је подешена да јој се приступа са <code>{{appUrl}}</code>, али се користи <code>{{currentUrl}}</code>. Ако наставите, можете искусити проблеме са аутентификацијом.",
"domainWarningCurrent": "Current:",
"domainWarningExpected": "Expected:",
"domainWarningCurrent": "Тренутни:",
"domainWarningExpected": "Очекивани:",
"ignoreTitle": "Игнориши",
"goToCorrectDomainTitle": "Иди на исправан домен",
"authorizeTitle": "Ауторизуј",

View File

@@ -98,6 +98,7 @@ export const AuthorizePage = () => {
client_id: props.client_id,
redirect_uri: props.redirect_uri,
state: props.state,
nonce: props.nonce,
});
},
mutationKey: ["authorize", props.client_id],
@@ -155,8 +156,8 @@ export const AuthorizePage = () => {
<Card>
<CardHeader className="mb-2">
<div className="flex flex-col gap-3 items-center justify-center text-center">
<div className="bg-accent-foreground box-content text-muted text-xl font-bold font-sans rounded-lg size-10 p-2 flex items-center justify-center">
{getClientInfo.data?.name.slice(0, 1)}
<div className="bg-accent-foreground box-content text-muted text-xl font-bold font-sans rounded-lg size-8 p-2 flex items-center justify-center">
{getClientInfo.data?.name.slice(0, 1) || "U"}
</div>
<CardTitle className="text-xl">
{t("authorizeCardTitle", {

View File

@@ -14,7 +14,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { useRedirectUri } from "@/lib/hooks/redirect-uri";
export const ContinuePage = () => {
const { cookieDomain, disableUiWarnings } = useAppContext();
const { cookieDomain, warningsEnabled } = useAppContext();
const { isLoggedIn } = useUserContext();
const { search } = useLocation();
const { t } = useTranslation();
@@ -35,10 +35,9 @@ export const ContinuePage = () => {
const urlHref = url?.href;
const hasValidRedirect = valid && allowedProto;
const showUntrustedWarning =
hasValidRedirect && !trusted && !disableUiWarnings;
const showUntrustedWarning = hasValidRedirect && !trusted && warningsEnabled;
const showInsecureWarning =
hasValidRedirect && httpsDowngrade && !disableUiWarnings;
hasValidRedirect && httpsDowngrade && warningsEnabled;
const shouldAutoRedirect =
isLoggedIn &&
hasValidRedirect &&

View File

@@ -14,7 +14,7 @@ export const appContextSchema = z.object({
forgotPasswordMessage: z.string(),
backgroundImage: z.string(),
oauthAutoRedirect: z.string(),
disableUiWarnings: z.boolean(),
warningsEnabled: z.boolean(),
});
export type AppContextSchema = z.infer<typeof appContextSchema>;

View File

@@ -0,0 +1,2 @@
ALTER TABLE "oidc_codes" DROP COLUMN "nonce";
ALTER TABLE "oidc_tokens" DROP COLUMN "nonce";

View File

@@ -0,0 +1,2 @@
ALTER TABLE "oidc_codes" ADD COLUMN "nonce" TEXT DEFAULT "";
ALTER TABLE "oidc_tokens" ADD COLUMN "nonce" TEXT DEFAULT "";

View File

@@ -124,7 +124,7 @@ func (app *BootstrapApp) Setup() error {
tlog.App.Trace().Str("redirectCookieName", app.context.redirectCookieName).Msg("Redirect cookie name")
// Database
db, err := app.SetupDatabase(app.config.DatabasePath)
db, err := app.SetupDatabase(app.config.Database.Path)
if err != nil {
return fmt.Errorf("failed to setup database: %w", err)
@@ -193,7 +193,7 @@ func (app *BootstrapApp) Setup() error {
go app.dbCleanup(queries)
// If analytics are not disabled, start heartbeat
if !app.config.DisableAnalytics {
if app.config.Analytics.Enabled {
tlog.App.Debug().Msg("Starting heartbeat routine")
go app.heartbeat()
}

View File

@@ -71,7 +71,7 @@ func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
ForgotPasswordMessage: app.config.UI.ForgotPasswordMessage,
BackgroundImage: app.config.UI.BackgroundImage,
OAuthAutoRedirect: app.config.OAuth.AutoRedirect,
DisableUIWarnings: app.config.UI.DisableWarnings,
WarningsEnabled: app.config.UI.WarningsEnabled,
}, apiRouter)
contextController.SetupRoutes()
@@ -103,8 +103,8 @@ func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
userController.SetupRoutes()
resourcesController := controller.NewResourcesController(controller.ResourcesControllerConfig{
ResourcesDir: app.config.ResourcesDir,
ResourcesDisabled: app.config.DisableResources,
Path: app.config.Resources.Path,
Enabled: app.config.Resources.Enabled,
}, &engine.RouterGroup)
resourcesController.SetupRoutes()

View File

@@ -3,8 +3,16 @@ package config
// Default configuration
func NewDefaultConfiguration() *Config {
return &Config{
ResourcesDir: "./resources",
DatabasePath: "./tinyauth.db",
Database: DatabaseConfig{
Path: "./tinyauth.db",
},
Analytics: AnalyticsConfig{
Enabled: true,
},
Resources: ResourcesConfig{
Enabled: true,
Path: "./resources",
},
Server: ServerConfig{
Port: 3000,
Address: "0.0.0.0",
@@ -19,6 +27,7 @@ func NewDefaultConfiguration() *Config {
Title: "Tinyauth",
ForgotPasswordMessage: "You can change your password by changing the configuration.",
BackgroundImage: "/background.jpg",
WarningsEnabled: true,
},
Ldap: LdapConfig{
Insecure: false,
@@ -68,20 +77,32 @@ var RedirectCookieName = "tinyauth-redirect"
// Main app config
type Config struct {
AppURL string `description:"The base URL where the app is hosted." yaml:"appUrl"`
ResourcesDir string `description:"The directory where resources are stored." yaml:"resourcesDir"`
DatabasePath string `description:"The path to the database file." yaml:"databasePath"`
DisableAnalytics bool `description:"Disable analytics." yaml:"disableAnalytics"`
DisableResources bool `description:"Disable resources server." yaml:"disableResources"`
Server ServerConfig `description:"Server configuration." yaml:"server"`
Auth AuthConfig `description:"Authentication configuration." yaml:"auth"`
Apps map[string]App `description:"Application ACLs configuration." yaml:"apps"`
OAuth OAuthConfig `description:"OAuth configuration." yaml:"oauth"`
OIDC OIDCConfig `description:"OIDC configuration." yaml:"oidc"`
UI UIConfig `description:"UI customization." yaml:"ui"`
Ldap LdapConfig `description:"LDAP configuration." yaml:"ldap"`
Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental"`
Log LogConfig `description:"Logging configuration." yaml:"log"`
AppURL string `description:"The base URL where the app is hosted." yaml:"appUrl"`
Database DatabaseConfig `description:"Database configuration." yaml:"database"`
Analytics AnalyticsConfig `description:"Analytics configuration." yaml:"analytics"`
Resources ResourcesConfig `description:"Resources configuration." yaml:"resources"`
Server ServerConfig `description:"Server configuration." yaml:"server"`
Auth AuthConfig `description:"Authentication configuration." yaml:"auth"`
Apps map[string]App `description:"Application ACLs configuration." yaml:"apps"`
OAuth OAuthConfig `description:"OAuth configuration." yaml:"oauth"`
OIDC OIDCConfig `description:"OIDC configuration." yaml:"oidc"`
UI UIConfig `description:"UI customization." yaml:"ui"`
Ldap LdapConfig `description:"LDAP configuration." yaml:"ldap"`
Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental"`
Log LogConfig `description:"Logging configuration." yaml:"log"`
}
type DatabaseConfig struct {
Path string `description:"The path to the database, including file name." yaml:"path"`
}
type AnalyticsConfig struct {
Enabled bool `description:"Enable periodic version information collection." yaml:"enabled"`
}
type ResourcesConfig struct {
Enabled bool `description:"Enable the resources server." yaml:"enabled"`
Path string `description:"The directory where resources are stored." yaml:"path"`
}
type ServerConfig struct {
@@ -114,8 +135,8 @@ type OAuthConfig struct {
}
type OIDCConfig struct {
PrivateKeyPath string `description:"Path to the private key file." yaml:"privateKeyPath"`
PublicKeyPath string `description:"Path to the public key file." yaml:"publicKeyPath"`
PrivateKeyPath string `description:"Path to the private key file, including file name." yaml:"privateKeyPath"`
PublicKeyPath string `description:"Path to the public key file, including file name." yaml:"publicKeyPath"`
Clients map[string]OIDCClientConfig `description:"OIDC clients configuration." yaml:"clients"`
}
@@ -123,7 +144,7 @@ type UIConfig struct {
Title string `description:"The title of the UI." yaml:"title"`
ForgotPasswordMessage string `description:"Message displayed on the forgot password page." yaml:"forgotPasswordMessage"`
BackgroundImage string `description:"Path to the background image." yaml:"backgroundImage"`
DisableWarnings bool `description:"Disable UI warnings." yaml:"disableWarnings"`
WarningsEnabled bool `description:"Enable UI warnings." yaml:"warningsEnabled"`
}
type LdapConfig struct {

View File

@@ -33,7 +33,7 @@ type AppContextResponse struct {
ForgotPasswordMessage string `json:"forgotPasswordMessage"`
BackgroundImage string `json:"backgroundImage"`
OAuthAutoRedirect string `json:"oauthAutoRedirect"`
DisableUIWarnings bool `json:"disableUiWarnings"`
WarningsEnabled bool `json:"warningsEnabled"`
}
type Provider struct {
@@ -50,7 +50,7 @@ type ContextControllerConfig struct {
ForgotPasswordMessage string
BackgroundImage string
OAuthAutoRedirect string
DisableUIWarnings bool
WarningsEnabled bool
}
type ContextController struct {
@@ -59,7 +59,7 @@ type ContextController struct {
}
func NewContextController(config ContextControllerConfig, router *gin.RouterGroup) *ContextController {
if config.DisableUIWarnings {
if !config.WarningsEnabled {
tlog.App.Warn().Msg("UI warnings are disabled. This may expose users to security risks. Proceed with caution.")
}
@@ -124,6 +124,6 @@ func (controller *ContextController) appContextHandler(c *gin.Context) {
ForgotPasswordMessage: controller.config.ForgotPasswordMessage,
BackgroundImage: controller.config.BackgroundImage,
OAuthAutoRedirect: controller.config.OAuthAutoRedirect,
DisableUIWarnings: controller.config.DisableUIWarnings,
WarningsEnabled: controller.config.WarningsEnabled,
})
}

View File

@@ -32,7 +32,7 @@ var contextControllerCfg = controller.ContextControllerConfig{
ForgotPasswordMessage: "Contact admin to reset your password.",
BackgroundImage: "/assets/bg.jpg",
OAuthAutoRedirect: "google",
DisableUIWarnings: false,
WarningsEnabled: true,
}
var contextCtrlTestContext = config.UserContext{
@@ -82,7 +82,7 @@ func TestAppContextHandler(t *testing.T) {
ForgotPasswordMessage: contextControllerCfg.ForgotPasswordMessage,
BackgroundImage: contextControllerCfg.BackgroundImage,
OAuthAutoRedirect: contextControllerCfg.OAuthAutoRedirect,
DisableUIWarnings: contextControllerCfg.DisableUIWarnings,
WarningsEnabled: contextControllerCfg.WarningsEnabled,
}
router, recorder := setupContextController(nil)

View File

@@ -296,7 +296,7 @@ func (controller *OIDCController) Token(c *gin.Context) {
return
}
tokenRes, err := controller.oidc.GenerateAccessToken(c, client, entry.Sub, entry.Scope)
tokenRes, err := controller.oidc.GenerateAccessToken(c, client, entry)
if err != nil {
tlog.App.Error().Err(err).Msg("Failed to generate access token")

View File

@@ -7,8 +7,8 @@ import (
)
type ResourcesControllerConfig struct {
ResourcesDir string
ResourcesDisabled bool
Path string
Enabled bool
}
type ResourcesController struct {
@@ -18,7 +18,7 @@ type ResourcesController struct {
}
func NewResourcesController(config ResourcesControllerConfig, router *gin.RouterGroup) *ResourcesController {
fileServer := http.StripPrefix("/resources", http.FileServer(http.Dir(config.ResourcesDir)))
fileServer := http.StripPrefix("/resources", http.FileServer(http.Dir(config.Path)))
return &ResourcesController{
config: config,
@@ -32,14 +32,14 @@ func (controller *ResourcesController) SetupRoutes() {
}
func (controller *ResourcesController) resourcesHandler(c *gin.Context) {
if controller.config.ResourcesDir == "" {
if controller.config.Path == "" {
c.JSON(404, gin.H{
"status": 404,
"message": "Resources not found",
})
return
}
if controller.config.ResourcesDisabled {
if !controller.config.Enabled {
c.JSON(403, gin.H{
"status": 403,
"message": "Resources are disabled",

View File

@@ -18,7 +18,8 @@ func TestResourcesHandler(t *testing.T) {
group := router.Group("/")
ctrl := controller.NewResourcesController(controller.ResourcesControllerConfig{
ResourcesDir: "/tmp/tinyauth",
Path: "/tmp/tinyauth",
Enabled: true,
}, group)
ctrl.SetupRoutes()

View File

@@ -11,6 +11,7 @@ type OidcCode struct {
RedirectURI string
ClientID string
ExpiresAt int64
Nonce string
}
type OidcToken struct {
@@ -21,6 +22,7 @@ type OidcToken struct {
ClientID string
TokenExpiresAt int64
RefreshTokenExpiresAt int64
Nonce string
}
type OidcUserinfo struct {

View File

@@ -16,11 +16,12 @@ INSERT INTO "oidc_codes" (
"scope",
"redirect_uri",
"client_id",
"expires_at"
"expires_at",
"nonce"
) VALUES (
?, ?, ?, ?, ?, ?
?, ?, ?, ?, ?, ?, ?
)
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce
`
type CreateOidcCodeParams struct {
@@ -30,6 +31,7 @@ type CreateOidcCodeParams struct {
RedirectURI string
ClientID string
ExpiresAt int64
Nonce string
}
func (q *Queries) CreateOidcCode(ctx context.Context, arg CreateOidcCodeParams) (OidcCode, error) {
@@ -40,6 +42,7 @@ func (q *Queries) CreateOidcCode(ctx context.Context, arg CreateOidcCodeParams)
arg.RedirectURI,
arg.ClientID,
arg.ExpiresAt,
arg.Nonce,
)
var i OidcCode
err := row.Scan(
@@ -49,6 +52,7 @@ func (q *Queries) CreateOidcCode(ctx context.Context, arg CreateOidcCodeParams)
&i.RedirectURI,
&i.ClientID,
&i.ExpiresAt,
&i.Nonce,
)
return i, err
}
@@ -61,11 +65,12 @@ INSERT INTO "oidc_tokens" (
"scope",
"client_id",
"token_expires_at",
"refresh_token_expires_at"
"refresh_token_expires_at",
"nonce"
) VALUES (
?, ?, ?, ?, ?, ?, ?
?, ?, ?, ?, ?, ?, ?, ?
)
RETURNING sub, access_token_hash, refresh_token_hash, scope, client_id, token_expires_at, refresh_token_expires_at
RETURNING sub, access_token_hash, refresh_token_hash, scope, client_id, token_expires_at, refresh_token_expires_at, nonce
`
type CreateOidcTokenParams struct {
@@ -76,6 +81,7 @@ type CreateOidcTokenParams struct {
ClientID string
TokenExpiresAt int64
RefreshTokenExpiresAt int64
Nonce string
}
func (q *Queries) CreateOidcToken(ctx context.Context, arg CreateOidcTokenParams) (OidcToken, error) {
@@ -87,6 +93,7 @@ func (q *Queries) CreateOidcToken(ctx context.Context, arg CreateOidcTokenParams
arg.ClientID,
arg.TokenExpiresAt,
arg.RefreshTokenExpiresAt,
arg.Nonce,
)
var i OidcToken
err := row.Scan(
@@ -97,6 +104,7 @@ func (q *Queries) CreateOidcToken(ctx context.Context, arg CreateOidcTokenParams
&i.ClientID,
&i.TokenExpiresAt,
&i.RefreshTokenExpiresAt,
&i.Nonce,
)
return i, err
}
@@ -148,7 +156,7 @@ func (q *Queries) CreateOidcUserInfo(ctx context.Context, arg CreateOidcUserInfo
const deleteExpiredOidcCodes = `-- name: DeleteExpiredOidcCodes :many
DELETE FROM "oidc_codes"
WHERE "expires_at" < ?
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce
`
func (q *Queries) DeleteExpiredOidcCodes(ctx context.Context, expiresAt int64) ([]OidcCode, error) {
@@ -167,6 +175,7 @@ func (q *Queries) DeleteExpiredOidcCodes(ctx context.Context, expiresAt int64) (
&i.RedirectURI,
&i.ClientID,
&i.ExpiresAt,
&i.Nonce,
); err != nil {
return nil, err
}
@@ -184,7 +193,7 @@ func (q *Queries) DeleteExpiredOidcCodes(ctx context.Context, expiresAt int64) (
const deleteExpiredOidcTokens = `-- name: DeleteExpiredOidcTokens :many
DELETE FROM "oidc_tokens"
WHERE "token_expires_at" < ? AND "refresh_token_expires_at" < ?
RETURNING sub, access_token_hash, refresh_token_hash, scope, client_id, token_expires_at, refresh_token_expires_at
RETURNING sub, access_token_hash, refresh_token_hash, scope, client_id, token_expires_at, refresh_token_expires_at, nonce
`
type DeleteExpiredOidcTokensParams struct {
@@ -209,6 +218,7 @@ func (q *Queries) DeleteExpiredOidcTokens(ctx context.Context, arg DeleteExpired
&i.ClientID,
&i.TokenExpiresAt,
&i.RefreshTokenExpiresAt,
&i.Nonce,
); err != nil {
return nil, err
}
@@ -276,7 +286,7 @@ func (q *Queries) DeleteOidcUserInfo(ctx context.Context, sub string) error {
const getOidcCode = `-- name: GetOidcCode :one
DELETE FROM "oidc_codes"
WHERE "code_hash" = ?
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce
`
func (q *Queries) GetOidcCode(ctx context.Context, codeHash string) (OidcCode, error) {
@@ -289,6 +299,7 @@ func (q *Queries) GetOidcCode(ctx context.Context, codeHash string) (OidcCode, e
&i.RedirectURI,
&i.ClientID,
&i.ExpiresAt,
&i.Nonce,
)
return i, err
}
@@ -296,7 +307,7 @@ func (q *Queries) GetOidcCode(ctx context.Context, codeHash string) (OidcCode, e
const getOidcCodeBySub = `-- name: GetOidcCodeBySub :one
DELETE FROM "oidc_codes"
WHERE "sub" = ?
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce
`
func (q *Queries) GetOidcCodeBySub(ctx context.Context, sub string) (OidcCode, error) {
@@ -309,12 +320,13 @@ func (q *Queries) GetOidcCodeBySub(ctx context.Context, sub string) (OidcCode, e
&i.RedirectURI,
&i.ClientID,
&i.ExpiresAt,
&i.Nonce,
)
return i, err
}
const getOidcCodeBySubUnsafe = `-- name: GetOidcCodeBySubUnsafe :one
SELECT sub, code_hash, scope, redirect_uri, client_id, expires_at FROM "oidc_codes"
SELECT sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce FROM "oidc_codes"
WHERE "sub" = ?
`
@@ -328,12 +340,13 @@ func (q *Queries) GetOidcCodeBySubUnsafe(ctx context.Context, sub string) (OidcC
&i.RedirectURI,
&i.ClientID,
&i.ExpiresAt,
&i.Nonce,
)
return i, err
}
const getOidcCodeUnsafe = `-- name: GetOidcCodeUnsafe :one
SELECT sub, code_hash, scope, redirect_uri, client_id, expires_at FROM "oidc_codes"
SELECT sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce FROM "oidc_codes"
WHERE "code_hash" = ?
`
@@ -347,12 +360,13 @@ func (q *Queries) GetOidcCodeUnsafe(ctx context.Context, codeHash string) (OidcC
&i.RedirectURI,
&i.ClientID,
&i.ExpiresAt,
&i.Nonce,
)
return i, err
}
const getOidcToken = `-- name: GetOidcToken :one
SELECT sub, access_token_hash, refresh_token_hash, scope, client_id, token_expires_at, refresh_token_expires_at FROM "oidc_tokens"
SELECT sub, access_token_hash, refresh_token_hash, scope, client_id, token_expires_at, refresh_token_expires_at, nonce FROM "oidc_tokens"
WHERE "access_token_hash" = ?
`
@@ -367,12 +381,13 @@ func (q *Queries) GetOidcToken(ctx context.Context, accessTokenHash string) (Oid
&i.ClientID,
&i.TokenExpiresAt,
&i.RefreshTokenExpiresAt,
&i.Nonce,
)
return i, err
}
const getOidcTokenByRefreshToken = `-- name: GetOidcTokenByRefreshToken :one
SELECT sub, access_token_hash, refresh_token_hash, scope, client_id, token_expires_at, refresh_token_expires_at FROM "oidc_tokens"
SELECT sub, access_token_hash, refresh_token_hash, scope, client_id, token_expires_at, refresh_token_expires_at, nonce FROM "oidc_tokens"
WHERE "refresh_token_hash" = ?
`
@@ -387,12 +402,13 @@ func (q *Queries) GetOidcTokenByRefreshToken(ctx context.Context, refreshTokenHa
&i.ClientID,
&i.TokenExpiresAt,
&i.RefreshTokenExpiresAt,
&i.Nonce,
)
return i, err
}
const getOidcTokenBySub = `-- name: GetOidcTokenBySub :one
SELECT sub, access_token_hash, refresh_token_hash, scope, client_id, token_expires_at, refresh_token_expires_at FROM "oidc_tokens"
SELECT sub, access_token_hash, refresh_token_hash, scope, client_id, token_expires_at, refresh_token_expires_at, nonce FROM "oidc_tokens"
WHERE "sub" = ?
`
@@ -407,6 +423,7 @@ func (q *Queries) GetOidcTokenBySub(ctx context.Context, sub string) (OidcToken,
&i.ClientID,
&i.TokenExpiresAt,
&i.RefreshTokenExpiresAt,
&i.Nonce,
)
return i, err
}
@@ -437,7 +454,7 @@ UPDATE "oidc_tokens" SET
"token_expires_at" = ?,
"refresh_token_expires_at" = ?
WHERE "refresh_token_hash" = ?
RETURNING sub, access_token_hash, refresh_token_hash, scope, client_id, token_expires_at, refresh_token_expires_at
RETURNING sub, access_token_hash, refresh_token_hash, scope, client_id, token_expires_at, refresh_token_expires_at, nonce
`
type UpdateOidcTokenByRefreshTokenParams struct {
@@ -465,6 +482,7 @@ func (q *Queries) UpdateOidcTokenByRefreshToken(ctx context.Context, arg UpdateO
&i.ClientID,
&i.TokenExpiresAt,
&i.RefreshTokenExpiresAt,
&i.Nonce,
)
return i, err
}

View File

@@ -8,6 +8,7 @@ import (
"crypto/sha256"
"crypto/x509"
"database/sql"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
@@ -50,13 +51,14 @@ type ClaimSet struct {
Email string `json:"email,omitempty"`
PreferredUsername string `json:"preferred_username,omitempty"`
Groups []string `json:"groups,omitempty"`
Nonce string `json:"nonce,omitempty"`
}
type UserinfoResponse struct {
Sub string `json:"sub"`
Name string `json:"name"`
Email string `json:"email"`
PreferredUsername string `json:"preferred_username"`
Name string `json:"name,omitempty"`
Email string `json:"email,omitempty"`
PreferredUsername string `json:"preferred_username,omitempty"`
Groups []string `json:"groups,omitempty"`
UpdatedAt int64 `json:"updated_at"`
}
@@ -76,6 +78,7 @@ type AuthorizeRequest struct {
ClientID string `json:"client_id" binding:"required"`
RedirectURI string `json:"redirect_uri" binding:"required"`
State string `json:"state" binding:"required"`
Nonce string `json:"nonce"`
}
type OIDCServiceConfig struct {
@@ -211,6 +214,9 @@ func (service *OIDCService) Init() error {
for id, client := range service.config.Clients {
client.ID = id
if client.Name == "" {
client.Name = utils.Capitalize(client.ID)
}
service.clients[client.ClientID] = client
}
@@ -292,6 +298,7 @@ func (service *OIDCService) StoreCode(c *gin.Context, sub string, code string, r
RedirectURI: req.RedirectURI,
ClientID: req.ClientID,
ExpiresAt: expiresAt,
Nonce: req.Nonce,
})
return err
@@ -353,7 +360,7 @@ func (service *OIDCService) GetCodeEntry(c *gin.Context, codeHash string) (repos
return oidcCode, nil
}
func (service *OIDCService) generateIDToken(client config.OIDCClientConfig, user repository.OidcUserinfo, scope string) (string, error) {
func (service *OIDCService) generateIDToken(client config.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()
@@ -383,6 +390,7 @@ func (service *OIDCService) generateIDToken(client config.OIDCClientConfig, user
Email: userInfo.Email,
PreferredUsername: userInfo.PreferredUsername,
Groups: userInfo.Groups,
Nonce: nonce,
}
payload, err := json.Marshal(claims)
@@ -406,14 +414,14 @@ func (service *OIDCService) generateIDToken(client config.OIDCClientConfig, user
return token, nil
}
func (service *OIDCService) GenerateAccessToken(c *gin.Context, client config.OIDCClientConfig, sub string, scope string) (TokenResponse, error) {
user, err := service.GetUserinfo(c, sub)
func (service *OIDCService) GenerateAccessToken(c *gin.Context, client config.OIDCClientConfig, codeEntry repository.OidcCode) (TokenResponse, error) {
user, err := service.GetUserinfo(c, codeEntry.Sub)
if err != nil {
return TokenResponse{}, err
}
idToken, err := service.generateIDToken(client, user, scope)
idToken, err := service.generateIDToken(client, user, codeEntry.Sub, codeEntry.Nonce)
if err != nil {
return TokenResponse{}, err
@@ -433,15 +441,15 @@ func (service *OIDCService) GenerateAccessToken(c *gin.Context, client config.OI
TokenType: "Bearer",
ExpiresIn: int64(service.config.SessionExpiry),
IDToken: idToken,
Scope: strings.ReplaceAll(scope, ",", " "),
Scope: strings.ReplaceAll(codeEntry.Scope, ",", " "),
}
_, err = service.queries.CreateOidcToken(c, repository.CreateOidcTokenParams{
Sub: sub,
Sub: codeEntry.Sub,
AccessTokenHash: service.Hash(accessToken),
RefreshTokenHash: service.Hash(refreshToken),
ClientID: client.ClientID,
Scope: scope,
Scope: codeEntry.Scope,
TokenExpiresAt: tokenExpiresAt,
RefreshTokenExpiresAt: refrshTokenExpiresAt,
})
@@ -480,7 +488,7 @@ func (service *OIDCService) RefreshAccessToken(c *gin.Context, refreshToken stri
idToken, err := service.generateIDToken(config.OIDCClientConfig{
ClientID: entry.ClientID,
}, user, entry.Scope)
}, user, entry.Scope, entry.Nonce)
if err != nil {
return TokenResponse{}, err
@@ -665,10 +673,21 @@ func (service *OIDCService) Cleanup() {
}
func (service *OIDCService) GetJWK() ([]byte, error) {
hasher := sha256.New()
der := x509.MarshalPKCS1PublicKey(&service.privateKey.PublicKey)
if der == nil {
return nil, errors.New("failed to marshal public key")
}
hasher.Write(der)
jwk := jose.JSONWebKey{
Key: service.privateKey,
Algorithm: string(jose.RS256),
Use: "sig",
KeyID: base64.URLEncoding.EncodeToString(hasher.Sum(nil)),
}
return jwk.Public().MarshalJSON()

View File

@@ -1,6 +1,8 @@
package loaders
import (
"os"
"github.com/rs/zerolog/log"
"github.com/traefik/paerser/cli"
"github.com/traefik/paerser/file"
@@ -16,11 +18,16 @@ func (f *FileLoader) Load(args []string, cmd *cli.Command) (bool, error) {
return false, err
}
// I guess we are using traefik as the root name
configFileFlag := "traefik.experimental.configFile"
// I guess we are using traefik as the root name (we can't change it)
configFileFlag := "traefik.experimental.configfile"
envVar := "TINYAUTH_EXPERIMENTAL_CONFIGFILE"
if _, ok := flags[configFileFlag]; !ok {
return false, nil
if value := os.Getenv(envVar); value != "" {
flags[configFileFlag] = value
} else {
return false, nil
}
}
log.Warn().Msg("Using experimental file config loader, this feature is experimental and may change or be removed in future releases")

View File

@@ -5,9 +5,10 @@ INSERT INTO "oidc_codes" (
"scope",
"redirect_uri",
"client_id",
"expires_at"
"expires_at",
"nonce"
) VALUES (
?, ?, ?, ?, ?, ?
?, ?, ?, ?, ?, ?, ?
)
RETURNING *;
@@ -45,9 +46,10 @@ INSERT INTO "oidc_tokens" (
"scope",
"client_id",
"token_expires_at",
"refresh_token_expires_at"
"refresh_token_expires_at",
"nonce"
) VALUES (
?, ?, ?, ?, ?, ?, ?
?, ?, ?, ?, ?, ?, ?, ?
)
RETURNING *;

View File

@@ -4,7 +4,8 @@ CREATE TABLE IF NOT EXISTS "oidc_codes" (
"scope" TEXT NOT NULL,
"redirect_uri" TEXT NOT NULL,
"client_id" TEXT NOT NULL,
"expires_at" INTEGER NOT NULL
"expires_at" INTEGER NOT NULL,
"nonce" TEXT DEFAULT ""
);
CREATE TABLE IF NOT EXISTS "oidc_tokens" (
@@ -14,7 +15,8 @@ CREATE TABLE IF NOT EXISTS "oidc_tokens" (
"scope" TEXT NOT NULL,
"client_id" TEXT NOT NULL,
"token_expires_at" INTEGER NOT NULL,
"refresh_token_expires_at" INTEGER NOT NULL
"refresh_token_expires_at" INTEGER NOT NULL,
"nonce" TEXT DEFAULT ""
);
CREATE TABLE IF NOT EXISTS "oidc_userinfo" (

View File

@@ -22,3 +22,7 @@ sql:
go_type: "string"
- column: "sessions.ldap_groups"
go_type: "string"
- column: "oidc_codes.nonce"
go_type: "string"
- column: "oidc_tokens.nonce"
go_type: "string"