Compare commits

...

5 Commits

Author SHA1 Message Date
Stavros
ff771c5c22 fix: use the href of the url object instead of the object iself as the
dep in the callback
2026-02-15 19:55:37 +02:00
Nicolas Meienberger
d7b00ffeea refactor(continue-page): simplify useEffect to avoid unnecessary dependencies 2026-02-11 18:43:48 +01:00
Stavros
22c4c262ea feat: add support for client secret post auth to oidc token endpoint 2026-02-07 21:04:58 +02:00
Stavros
baf4798665 fix: fix typo in oidc trusted redirect uris config 2026-02-07 12:59:25 +02:00
Stavros
bea680edec fix: healthcheck should not use public app url 2026-02-07 12:57:10 +02:00
6 changed files with 76 additions and 58 deletions

3
.gitignore vendored
View File

@@ -39,3 +39,6 @@ __debug_*
# infisical # infisical
/.infisical.json /.infisical.json
# traefik data
/traefik

View File

@@ -30,15 +30,9 @@ func healthcheckCmd() *cli.Command {
appUrl := "http://127.0.0.1:3000" appUrl := "http://127.0.0.1:3000"
appUrlEnv := os.Getenv("TINYAUTH_APPURL")
srvAddr := os.Getenv("TINYAUTH_SERVER_ADDRESS") srvAddr := os.Getenv("TINYAUTH_SERVER_ADDRESS")
srvPort := os.Getenv("TINYAUTH_SERVER_PORT") srvPort := os.Getenv("TINYAUTH_SERVER_PORT")
if appUrlEnv != "" {
appUrl = appUrlEnv
}
// Local-direct connection is preferred over the public app URL
if srvAddr != "" && srvPort != "" { if srvAddr != "" && srvPort != "" {
appUrl = fmt.Sprintf("http://%s:%s", srvAddr, srvPort) appUrl = fmt.Sprintf("http://%s:%s", srvAddr, srvPort)
} }
@@ -48,7 +42,7 @@ func healthcheckCmd() *cli.Command {
} }
if appUrl == "" { if appUrl == "" {
return errors.New("TINYAUTH_APPURL is not set and no argument was provided") return errors.New("Could not determine app URL")
} }
tlog.App.Info().Str("app_url", appUrl).Msg("Performing health check") tlog.App.Info().Str("app_url", appUrl).Msg("Performing health check")

View File

@@ -32,34 +32,43 @@ export const ContinuePage = () => {
cookieDomain, cookieDomain,
); );
const handleRedirect = useCallback(() => { const urlHref = url?.href;
const hasValidRedirect = valid && allowedProto;
const showUntrustedWarning =
hasValidRedirect && !trusted && !disableUiWarnings;
const showInsecureWarning =
hasValidRedirect && httpsDowngrade && !disableUiWarnings;
const shouldAutoRedirect =
isLoggedIn &&
hasValidRedirect &&
!showUntrustedWarning &&
!showInsecureWarning;
const redirectToTarget = useCallback(() => {
if (!urlHref || hasRedirected.current) {
return;
}
hasRedirected.current = true; hasRedirected.current = true;
window.location.assign(urlHref);
}, [urlHref]);
const handleRedirect = useCallback(() => {
setIsLoading(true); setIsLoading(true);
window.location.assign(url!); redirectToTarget();
}, [url]); }, [redirectToTarget]);
useEffect(() => { useEffect(() => {
if (!isLoggedIn) { if (!shouldAutoRedirect) {
return;
}
if (hasRedirected.current) {
return;
}
if (
(!valid || !allowedProto || !trusted || httpsDowngrade) &&
!disableUiWarnings
) {
return; return;
} }
const auto = setTimeout(() => { const auto = setTimeout(() => {
handleRedirect(); redirectToTarget();
}, 100); }, 100);
const reveal = setTimeout(() => { const reveal = setTimeout(() => {
setIsLoading(false);
setShowRedirectButton(true); setShowRedirectButton(true);
}, 5000); }, 5000);
@@ -67,18 +76,7 @@ export const ContinuePage = () => {
clearTimeout(auto); clearTimeout(auto);
clearTimeout(reveal); clearTimeout(reveal);
}; };
}, [ }, [shouldAutoRedirect, redirectToTarget]);
isLoggedIn,
hasRedirected,
valid,
allowedProto,
trusted,
httpsDowngrade,
disableUiWarnings,
setIsLoading,
handleRedirect,
setShowRedirectButton,
]);
if (!isLoggedIn) { if (!isLoggedIn) {
return ( return (
@@ -89,11 +87,11 @@ export const ContinuePage = () => {
); );
} }
if (!valid || !allowedProto) { if (!hasValidRedirect) {
return <Navigate to="/logout" replace />; return <Navigate to="/logout" replace />;
} }
if (!trusted && !disableUiWarnings) { if (showUntrustedWarning) {
return ( return (
<Card role="alert" aria-live="assertive" className="min-w-xs sm:min-w-sm"> <Card role="alert" aria-live="assertive" className="min-w-xs sm:min-w-sm">
<CardHeader> <CardHeader>
@@ -113,7 +111,7 @@ export const ContinuePage = () => {
</CardHeader> </CardHeader>
<CardFooter className="flex flex-col items-stretch gap-2"> <CardFooter className="flex flex-col items-stretch gap-2">
<Button <Button
onClick={() => handleRedirect()} onClick={handleRedirect}
loading={isLoading} loading={isLoading}
variant="destructive" variant="destructive"
> >
@@ -131,7 +129,7 @@ export const ContinuePage = () => {
); );
} }
if (httpsDowngrade && !disableUiWarnings) { if (showInsecureWarning) {
return ( return (
<Card role="alert" aria-live="assertive" className="min-w-xs sm:min-w-sm"> <Card role="alert" aria-live="assertive" className="min-w-xs sm:min-w-sm">
<CardHeader> <CardHeader>
@@ -150,7 +148,7 @@ export const ContinuePage = () => {
</CardHeader> </CardHeader>
<CardFooter className="flex flex-col items-stretch gap-2"> <CardFooter className="flex flex-col items-stretch gap-2">
<Button <Button
onClick={() => handleRedirect()} onClick={handleRedirect}
loading={isLoading} loading={isLoading}
variant="warning" variant="warning"
> >
@@ -178,7 +176,7 @@ export const ContinuePage = () => {
</CardHeader> </CardHeader>
{showRedirectButton && ( {showRedirectButton && (
<CardFooter className="flex flex-col items-stretch"> <CardFooter className="flex flex-col items-stretch">
<Button onClick={() => handleRedirect()}> <Button onClick={handleRedirect}>
{t("continueRedirectManually")} {t("continueRedirectManually")}
</Button> </Button>
</CardFooter> </CardFooter>

View File

@@ -138,7 +138,7 @@ type OIDCClientConfig struct {
ClientID string `description:"OIDC client ID." yaml:"clientId"` ClientID string `description:"OIDC client ID." yaml:"clientId"`
ClientSecret string `description:"OIDC client secret." yaml:"clientSecret"` ClientSecret string `description:"OIDC client secret." yaml:"clientSecret"`
ClientSecretFile string `description:"Path to the file containing the OIDC client secret." yaml:"clientSecretFile"` ClientSecretFile string `description:"Path to the file containing the OIDC client secret." yaml:"clientSecretFile"`
TrustedRedirectURIs []string `description:"List of trusted redirect URLs." yaml:"trustedRedirectUrls"` TrustedRedirectURIs []string `description:"List of trusted redirect URIs." yaml:"trustedRedirectUris"`
Name string `description:"Client name in UI." yaml:"name"` Name string `description:"Client name in UI." yaml:"name"`
} }

View File

@@ -33,6 +33,8 @@ type TokenRequest struct {
Code string `form:"code" url:"code"` Code string `form:"code" url:"code"`
RedirectURI string `form:"redirect_uri" url:"redirect_uri"` RedirectURI string `form:"redirect_uri" url:"redirect_uri"`
RefreshToken string `form:"refresh_token" url:"refresh_token"` RefreshToken string `form:"refresh_token" url:"refresh_token"`
ClientSecret string `form:"client_secret" url:"client_secret"`
ClientID string `form:"client_id" url:"client_id"`
} }
type CallbackError struct { type CallbackError struct {
@@ -49,6 +51,11 @@ type ClientRequest struct {
ClientID string `uri:"id" binding:"required"` ClientID string `uri:"id" binding:"required"`
} }
type ClientCredentials struct {
ClientID string
ClientSecret string
}
func NewOIDCController(config OIDCControllerConfig, oidcService *service.OIDCService, router *gin.RouterGroup) *OIDCController { func NewOIDCController(config OIDCControllerConfig, oidcService *service.OIDCService, router *gin.RouterGroup) *OIDCController {
return &OIDCController{ return &OIDCController{
config: config, config: config,
@@ -210,29 +217,45 @@ func (controller *OIDCController) Token(c *gin.Context) {
return return
} }
rclientId, rclientSecret, ok := c.Request.BasicAuth() // First we try form values
creds := ClientCredentials{
if !ok { ClientID: req.ClientID,
tlog.App.Error().Msg("Missing authorization header") ClientSecret: req.ClientSecret,
c.Header("www-authenticate", "basic")
c.JSON(401, gin.H{
"error": "invalid_client",
})
return
} }
client, ok := controller.oidc.GetClient(rclientId) // If it fails, we try basic auth
if creds.ClientID == "" || creds.ClientSecret == "" {
tlog.App.Debug().Msg("Tried form values and they are empty, trying basic auth")
clientId, clientSecret, ok := c.Request.BasicAuth()
if !ok {
tlog.App.Error().Msg("Missing authorization header")
c.Header("www-authenticate", "basic")
c.JSON(401, gin.H{
"error": "invalid_client",
})
return
}
creds.ClientID = clientId
creds.ClientSecret = clientSecret
}
// END - we don't support other authentication methods
client, ok := controller.oidc.GetClient(creds.ClientID)
if !ok { if !ok {
tlog.App.Warn().Str("client_id", rclientId).Msg("Client not found") tlog.App.Warn().Str("client_id", creds.ClientID).Msg("Client not found")
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"error": "invalid_client", "error": "invalid_client",
}) })
return return
} }
if client.ClientSecret != rclientSecret { if client.ClientSecret != creds.ClientSecret {
tlog.App.Warn().Str("client_id", rclientId).Msg("Invalid client secret") tlog.App.Warn().Str("client_id", creds.ClientID).Msg("Invalid client secret")
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"error": "invalid_client", "error": "invalid_client",
}) })
@@ -286,7 +309,7 @@ func (controller *OIDCController) Token(c *gin.Context) {
tokenResponse = tokenRes tokenResponse = tokenRes
case "refresh_token": case "refresh_token":
tokenRes, err := controller.oidc.RefreshAccessToken(c, req.RefreshToken, rclientId) tokenRes, err := controller.oidc.RefreshAccessToken(c, req.RefreshToken, creds.ClientID)
if err != nil { if err != nil {
if errors.Is(err, service.ErrTokenExpired) { if errors.Is(err, service.ErrTokenExpired) {

View File

@@ -58,7 +58,7 @@ func (controller *WellKnownController) OpenIDConnectConfiguration(c *gin.Context
GrantTypesSupported: service.SupportedGrantTypes, GrantTypesSupported: service.SupportedGrantTypes,
SubjectTypesSupported: []string{"pairwise"}, SubjectTypesSupported: []string{"pairwise"},
IDTokenSigningAlgValuesSupported: []string{"RS256"}, IDTokenSigningAlgValuesSupported: []string{"RS256"},
TokenEndpointAuthMethodsSupported: []string{"client_secret_basic"}, TokenEndpointAuthMethodsSupported: []string{"client_secret_basic", "client_secret_post"},
ClaimsSupported: []string{"sub", "updated_at", "name", "preferred_username", "email", "groups"}, ClaimsSupported: []string{"sub", "updated_at", "name", "preferred_username", "email", "groups"},
ServiceDocumentation: "https://tinyauth.app/docs/reference/openid", ServiceDocumentation: "https://tinyauth.app/docs/reference/openid",
}) })