Detach config and auth/signin from oidc

- moved the shared auth layout to components (used by signin, oauth and oidc)
- moved the post login redirects to internal/server
- load OIDC configuration during app initialization
- added provisioning tests
This commit is contained in:
Olivier Meunier
2026-05-16 11:20:01 +02:00
parent b3349d2592
commit 8328f0df6d
15 changed files with 828 additions and 656 deletions
+10
View File
@@ -37,3 +37,13 @@ func NewLayout() (l *Layout) {
Head: templ.NopComponent,
}
}
// AuthLayout is the layout used on all the authentication pages.
func AuthLayout(title string) templ.Component {
return defaultLayout.AuthBase(title)
}
// AuthError is the component for the authentication error pages.
func AuthError(status int) templ.Component {
return defaultLayout.AuthError(status)
}
+35
View File
@@ -4,6 +4,8 @@
package components
import "net/http"
// BasePage renders the layout's base page with head elements.
templ (c *Layout) BasePage(title string) {
<!DOCTYPE html>
@@ -206,3 +208,36 @@ templ (c *Layout) SideMenuStdWrapper() {
{ children... }
</main>
}
templ (c *Layout) authHeader() {
<h1 class="max-w-sm mt-12 mx-auto px-8">
<div>
<svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 567 128" class="w-full fill-gray-900">
<use href={ Asset(ctx, "img/logo-text.svg") + "#main" }></use>
</svg>
</div>
</h1>
}
templ (c *Layout) AuthBase(title string) {
@BasePage(title) {
@c.authHeader()
<main class="w-full max-w-sm p-8 mt-6 mx-auto bg-gray-100 text-gray-dark rounded-md shadow-md" id="content">
{ children... }
</main>
@CustomTemplate("auth/footer.html.tmpl", nil)
}
}
templ (c *Layout) AuthError(status int) {
@BasePage(L(ctx).Gettext("Error")) {
@c.authHeader()
<main class="w-full max-w-sm p-8 mt-6 mx-auto text-center border border-red-800 bg-red-100 rounded-md" id="content">
<h2 class="title text-h3">{ L(ctx).Gettext("An error occurred") }</h2>
<p>{ http.StatusText(status) }</p>
<p class="mt-8">
<a class="link font-semibold" href={ URL(ctx, "/") }>{ L(ctx).Gettext("Go back to Readeck") }</a>
</p>
</main>
}
}
+236 -26
View File
@@ -14,6 +14,8 @@ package components
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "net/http"
// BasePage renders the layout's base page with head elements.
func (c *Layout) BasePage(title string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
@@ -43,7 +45,7 @@ func (c *Layout) BasePage(title string) templ.Component {
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(L(ctx).Tag.String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 11, Col: 28}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 13, Col: 28}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
@@ -56,7 +58,7 @@ func (c *Layout) BasePage(title string) templ.Component {
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 16, Col: 17}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 18, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@@ -69,7 +71,7 @@ func (c *Layout) BasePage(title string) templ.Component {
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(CSPNonce(ctx))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 19, Col: 49}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 21, Col: 49}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
if templ_7745c5c3_Err != nil {
@@ -82,7 +84,7 @@ func (c *Layout) BasePage(title string) templ.Component {
var templ_7745c5c3_Var5 templ.SafeURL
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs(Asset(ctx, "bundle.css"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 21, Col: 57}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 23, Col: 57}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
@@ -95,7 +97,7 @@ func (c *Layout) BasePage(title string) templ.Component {
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(CSPNonce(ctx))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 21, Col: 81}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 23, Col: 81}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil {
@@ -108,7 +110,7 @@ func (c *Layout) BasePage(title string) templ.Component {
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(Asset(ctx, "bundle.js"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 22, Col: 54}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 24, Col: 54}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
if templ_7745c5c3_Err != nil {
@@ -121,7 +123,7 @@ func (c *Layout) BasePage(title string) templ.Component {
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(CSPNonce(ctx))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 22, Col: 78}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 24, Col: 78}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
if templ_7745c5c3_Err != nil {
@@ -134,7 +136,7 @@ func (c *Layout) BasePage(title string) templ.Component {
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(CSPNonce(ctx))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 23, Col: 32}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 25, Col: 32}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
if templ_7745c5c3_Err != nil {
@@ -147,7 +149,7 @@ func (c *Layout) BasePage(title string) templ.Component {
var templ_7745c5c3_Var10 templ.SafeURL
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinURLErrs(Asset(ctx, "img/fi/favicon.ico"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 30, Col: 59}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 32, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
@@ -160,7 +162,7 @@ func (c *Layout) BasePage(title string) templ.Component {
var templ_7745c5c3_Var11 templ.SafeURL
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinURLErrs(Asset(ctx, `img/fi/favicon.svg`))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 31, Col: 59}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 33, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
@@ -173,7 +175,7 @@ func (c *Layout) BasePage(title string) templ.Component {
var templ_7745c5c3_Var12 templ.SafeURL
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinURLErrs(Asset(ctx, `img/fi/apple-touch-icon.png`))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 32, Col: 80}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 34, Col: 80}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
@@ -186,7 +188,7 @@ func (c *Layout) BasePage(title string) templ.Component {
var templ_7745c5c3_Var13 templ.SafeURL
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs(URL(ctx, "/manifest.webmanifest"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 33, Col: 64}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 35, Col: 64}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
@@ -211,7 +213,7 @@ func (c *Layout) BasePage(title string) templ.Component {
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(CSPNonce(ctx))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 50, Col: 32}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 52, Col: 32}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
if templ_7745c5c3_Err != nil {
@@ -297,7 +299,7 @@ func (c *Layout) Menu(menuBtn templ.Component) templ.Component {
var templ_7745c5c3_Var17 templ.SafeURL
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinURLErrs(URL(ctx, "/"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 70, Col: 27}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 72, Col: 27}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil {
@@ -310,7 +312,7 @@ func (c *Layout) Menu(menuBtn templ.Component) templ.Component {
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(L(ctx).Gettext("Home page"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 70, Col: 65}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 72, Col: 65}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
if templ_7745c5c3_Err != nil {
@@ -351,7 +353,7 @@ func (c *Layout) Menu(menuBtn templ.Component) templ.Component {
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(L(ctx).Gettext("Change color theme"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 88, Col: 51}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 90, Col: 51}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19)
if templ_7745c5c3_Err != nil {
@@ -372,7 +374,7 @@ func (c *Layout) Menu(menuBtn templ.Component) templ.Component {
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(L(ctx).Gettext("Change color theme"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 93, Col: 67}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 95, Col: 67}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
if templ_7745c5c3_Err != nil {
@@ -457,7 +459,7 @@ func (c *Layout) menuButton(name string) templ.Component {
var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.ResolveAttributeValue(name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 118, Col: 14}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 120, Col: 14}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var22)
if templ_7745c5c3_Err != nil {
@@ -478,7 +480,7 @@ func (c *Layout) menuButton(name string) templ.Component {
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 126, Col: 30}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 128, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
if templ_7745c5c3_Err != nil {
@@ -520,7 +522,7 @@ func (c *Layout) mainMenuItem(name, path, icon string, current bool) templ.Compo
var templ_7745c5c3_Var25 templ.SafeURL
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinURLErrs(URL(ctx, path))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 132, Col: 26}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 134, Col: 26}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
if templ_7745c5c3_Err != nil {
@@ -533,7 +535,7 @@ func (c *Layout) mainMenuItem(name, path, icon string, current bool) templ.Compo
var templ_7745c5c3_Var26 string
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.ResolveAttributeValue(name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 132, Col: 41}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 134, Col: 41}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var26)
if templ_7745c5c3_Err != nil {
@@ -546,7 +548,7 @@ func (c *Layout) mainMenuItem(name, path, icon string, current bool) templ.Compo
var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue(current)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 132, Col: 66}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 134, Col: 66}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27)
if templ_7745c5c3_Err != nil {
@@ -567,7 +569,7 @@ func (c *Layout) mainMenuItem(name, path, icon string, current bool) templ.Compo
var templ_7745c5c3_Var28 string
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 134, Col: 31}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 136, Col: 31}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
if templ_7745c5c3_Err != nil {
@@ -611,7 +613,7 @@ func QuickAccessMenu(items [][2]string) templ.Component {
var templ_7745c5c3_Var30 string
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.ResolveAttributeValue(L(ctx).Gettext("Quick access menu"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 142, Col: 49}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 144, Col: 49}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var30)
if templ_7745c5c3_Err != nil {
@@ -629,7 +631,7 @@ func QuickAccessMenu(items [][2]string) templ.Component {
var templ_7745c5c3_Var31 templ.SafeURL
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinURLErrs("#" + item[0])
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 144, Col: 31}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 146, Col: 31}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
if templ_7745c5c3_Err != nil {
@@ -642,7 +644,7 @@ func QuickAccessMenu(items [][2]string) templ.Component {
var templ_7745c5c3_Var32 string
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(item[1])
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 144, Col: 56}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 146, Col: 56}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
if templ_7745c5c3_Err != nil {
@@ -906,4 +908,212 @@ func (c *Layout) SideMenuStdWrapper() templ.Component {
})
}
func (c *Layout) authHeader() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var40 := templ.GetChildren(ctx)
if templ_7745c5c3_Var40 == nil {
templ_7745c5c3_Var40 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "<h1 class=\"max-w-sm mt-12 mx-auto px-8\"><div><svg xmlns=\"http://www.w3.org/2000/svg\" viewbox=\"0 0 567 128\" class=\"w-full fill-gray-900\"><use href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var41 string
templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.ResolveAttributeValue(Asset(ctx, "img/logo-text.svg") + "#main")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 216, Col: 57}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var41)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "\"></use></svg></div></h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func (c *Layout) AuthBase(title string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var42 := templ.GetChildren(ctx)
if templ_7745c5c3_Var42 == nil {
templ_7745c5c3_Var42 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var43 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = c.authHeader().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, " <main class=\"w-full max-w-sm p-8 mt-6 mx-auto bg-gray-100 text-gray-dark rounded-md shadow-md\" id=\"content\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ_7745c5c3_Var42.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "</main>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = CustomTemplate("auth/footer.html.tmpl", nil).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = BasePage(title).Render(templ.WithChildren(ctx, templ_7745c5c3_Var43), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func (c *Layout) AuthError(status int) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var44 := templ.GetChildren(ctx)
if templ_7745c5c3_Var44 == nil {
templ_7745c5c3_Var44 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var45 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = c.authHeader().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, " <main class=\"w-full max-w-sm p-8 mt-6 mx-auto text-center border border-red-800 bg-red-100 rounded-md\" id=\"content\"><h2 class=\"title text-h3\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var46 string
templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(L(ctx).Gettext("An error occurred"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 236, Col: 66}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "</h2><p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var47 string
templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(http.StatusText(status))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 237, Col: 31}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "</p><p class=\"mt-8\"><a class=\"link font-semibold\" href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var48 templ.SafeURL
templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinURLErrs(URL(ctx, "/"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 239, Col: 54}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var49 string
templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(L(ctx).Gettext("Go back to Readeck"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/layout.templ`, Line: 239, Col: 95}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "</a></p></main>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = BasePage(L(ctx).Gettext("Error")).Render(templ.WithChildren(ctx, templ_7745c5c3_Var45), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate
+7
View File
@@ -26,6 +26,7 @@ import (
"codeberg.org/readeck/readeck/configs"
"codeberg.org/readeck/readeck/internal/acls"
"codeberg.org/readeck/readeck/internal/auth/oidc"
"codeberg.org/readeck/readeck/internal/auth/users"
"codeberg.org/readeck/readeck/internal/bookmarks"
"codeberg.org/readeck/readeck/internal/db"
@@ -159,6 +160,12 @@ func InitApp() {
// Init ACLs
acls.Load(configs.Config.Customize.ExtraPermissions...)
// Init OIDC
oidc.InitCookieHandler(configs.Keys.OIDCKey())
for k, v := range configs.Config.Auth.OIDC.Providers {
oidc.Providers.Add(k, v.Name, v.URL, v.ClientID, v.ClientSecret, v.Groups, v.ProvisioningEnabled)
}
// Init email sending
email.InitSender()
if !email.CanSendEmail() {
+2 -3
View File
@@ -5,7 +5,6 @@
package oauth2
import (
"codeberg.org/readeck/readeck/internal/auth/signin"
"codeberg.org/readeck/readeck/internal/server"
. "codeberg.org/readeck/readeck/components"
@@ -15,7 +14,7 @@ import (
type Views struct{}
templ (v Views) authCode(client *oauthClient, scopes []string) {
@signin.SigninLayout(L(ctx).Gettext("Authorization required")) {
@AuthLayout(L(ctx).Gettext("Authorization required")) {
@v.form(client, scopes)
}
}
@@ -24,7 +23,7 @@ templ (v Views) deviceCode(
step deviceCodeStep, f *deviceAuthorizationForm,
client *oauthClient, req *deviceAuthorizationRequest, scopes []string,
) {
@signin.SigninLayout(L(ctx).Gettext("Authorization required")) {
@AuthLayout(L(ctx).Gettext("Authorization required")) {
switch step {
case stepInvalid:
<h2 class="font-semibold">{ L(ctx).Gettext("Error") }</h2>
+35 -36
View File
@@ -15,7 +15,6 @@ import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"codeberg.org/readeck/readeck/internal/auth/signin"
"codeberg.org/readeck/readeck/internal/server"
. "codeberg.org/readeck/readeck/components"
@@ -63,7 +62,7 @@ func (v Views) authCode(client *oauthClient, scopes []string) templ.Component {
}
return nil
})
templ_7745c5c3_Err = signin.SigninLayout(L(ctx).Gettext("Authorization required")).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
templ_7745c5c3_Err = AuthLayout(L(ctx).Gettext("Authorization required")).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -116,7 +115,7 @@ func (v Views) deviceCode(
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(L(ctx).Gettext("Error"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 30, Col: 55}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 29, Col: 55}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
@@ -129,7 +128,7 @@ func (v Views) deviceCode(
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(L(ctx).Gettext("This code has expired or is not valid"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 31, Col: 64}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 30, Col: 64}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
@@ -151,7 +150,7 @@ func (v Views) deviceCode(
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(L(ctx).Gettext("Connect a device"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 34, Col: 77}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 33, Col: 77}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
@@ -164,7 +163,7 @@ func (v Views) deviceCode(
var templ_7745c5c3_Var8 templ.SafeURL
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinURLErrs(URL(ctx))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 35, Col: 36}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 34, Col: 36}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
@@ -190,7 +189,7 @@ func (v Views) deviceCode(
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(L(ctx).Gettext("Next"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 43, Col: 30}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 42, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
@@ -220,7 +219,7 @@ func (v Views) deviceCode(
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.UserCode.Value())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 48, Col: 69}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 47, Col: 69}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
if templ_7745c5c3_Err != nil {
@@ -244,7 +243,7 @@ func (v Views) deviceCode(
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(L(ctx).Gettext("Connect a device"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 51, Col: 77}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 50, Col: 77}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
@@ -257,7 +256,7 @@ func (v Views) deviceCode(
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(L(ctx).Gettext("Authorization request was denied."))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 53, Col: 58}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 52, Col: 58}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
@@ -270,7 +269,7 @@ func (v Views) deviceCode(
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 54, Col: 10}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 53, Col: 10}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
@@ -283,7 +282,7 @@ func (v Views) deviceCode(
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(L(ctx).Gettext(" You can now close this page."))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 55, Col: 54}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 54, Col: 54}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
@@ -305,7 +304,7 @@ func (v Views) deviceCode(
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(L(ctx).Gettext("Connect a device"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 59, Col: 77}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 58, Col: 77}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
@@ -323,7 +322,7 @@ func (v Views) deviceCode(
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(L(ctx).Gettext("Authorization granted. Connecting device..."))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 61, Col: 71}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 60, Col: 71}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil {
@@ -360,7 +359,7 @@ func (v Views) deviceCode(
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(L(ctx).Gettext("Device is connected!"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 75, Col: 46}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 74, Col: 46}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
if templ_7745c5c3_Err != nil {
@@ -373,7 +372,7 @@ func (v Views) deviceCode(
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 76, Col: 11}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 75, Col: 11}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil {
@@ -386,7 +385,7 @@ func (v Views) deviceCode(
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(L(ctx).Gettext(" You can now close this page."))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 77, Col: 55}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 76, Col: 55}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
if templ_7745c5c3_Err != nil {
@@ -404,7 +403,7 @@ func (v Views) deviceCode(
}
return nil
})
templ_7745c5c3_Err = signin.SigninLayout(L(ctx).Gettext("Authorization required")).Render(templ.WithChildren(ctx, templ_7745c5c3_Var4), templ_7745c5c3_Buffer)
templ_7745c5c3_Err = AuthLayout(L(ctx).Gettext("Authorization required")).Render(templ.WithChildren(ctx, templ_7745c5c3_Var4), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -440,7 +439,7 @@ func (v Views) backBtn() templ.Component {
var templ_7745c5c3_Var22 templ.SafeURL
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinURLErrs(URL(ctx, "/"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 87, Col: 85}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 86, Col: 85}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
if templ_7745c5c3_Err != nil {
@@ -453,7 +452,7 @@ func (v Views) backBtn() templ.Component {
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(L(ctx).Gettext("Go back to Readeck"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 88, Col: 41}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 87, Col: 41}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
if templ_7745c5c3_Err != nil {
@@ -507,7 +506,7 @@ func (v Views) form(client *oauthClient, scopes []string) templ.Component {
var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(L(ctx).Gettext("Authorization required"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 101, Col: 80}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 100, Col: 80}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
if templ_7745c5c3_Err != nil {
@@ -525,7 +524,7 @@ func (v Views) form(client *oauthClient, scopes []string) templ.Component {
var templ_7745c5c3_Var26 string
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.ResolveAttributeValue(client.Logo)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 104, Col: 51}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 103, Col: 51}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var26)
if templ_7745c5c3_Err != nil {
@@ -554,7 +553,7 @@ func (v Views) form(client *oauthClient, scopes []string) templ.Component {
var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(L(ctx).Gettext("Review permissions"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 113, Col: 70}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 112, Col: 70}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
if templ_7745c5c3_Err != nil {
@@ -576,7 +575,7 @@ func (v Views) form(client *oauthClient, scopes []string) templ.Component {
var templ_7745c5c3_Var28 string
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 118, Col: 9}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 117, Col: 9}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
if templ_7745c5c3_Err != nil {
@@ -589,7 +588,7 @@ func (v Views) form(client *oauthClient, scopes []string) templ.Component {
var templ_7745c5c3_Var29 string
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(x)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 119, Col: 7}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 118, Col: 7}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
if templ_7745c5c3_Err != nil {
@@ -607,7 +606,7 @@ func (v Views) form(client *oauthClient, scopes []string) templ.Component {
var templ_7745c5c3_Var30 string
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(L(ctx).Gettext("Application information"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 123, Col: 75}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 122, Col: 75}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
if templ_7745c5c3_Err != nil {
@@ -620,7 +619,7 @@ func (v Views) form(client *oauthClient, scopes []string) templ.Component {
var templ_7745c5c3_Var31 string
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(L(ctx).Gettext("Name"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 125, Col: 30}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 124, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
if templ_7745c5c3_Err != nil {
@@ -633,7 +632,7 @@ func (v Views) form(client *oauthClient, scopes []string) templ.Component {
var templ_7745c5c3_Var32 string
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(client.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 125, Col: 55}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 124, Col: 55}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
if templ_7745c5c3_Err != nil {
@@ -646,7 +645,7 @@ func (v Views) form(client *oauthClient, scopes []string) templ.Component {
var templ_7745c5c3_Var33 string
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(L(ctx).Gettext("Website"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 126, Col: 33}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 125, Col: 33}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
if templ_7745c5c3_Err != nil {
@@ -659,7 +658,7 @@ func (v Views) form(client *oauthClient, scopes []string) templ.Component {
var templ_7745c5c3_Var34 templ.SafeURL
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinURLErrs(client.URI)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 126, Col: 70}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 125, Col: 70}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
if templ_7745c5c3_Err != nil {
@@ -672,7 +671,7 @@ func (v Views) form(client *oauthClient, scopes []string) templ.Component {
var templ_7745c5c3_Var35 string
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(client.URI)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 126, Col: 101}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 125, Col: 101}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
if templ_7745c5c3_Err != nil {
@@ -685,7 +684,7 @@ func (v Views) form(client *oauthClient, scopes []string) templ.Component {
var templ_7745c5c3_Var36 string
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(L(ctx).Gettext("Version"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 127, Col: 33}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 126, Col: 33}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
if templ_7745c5c3_Err != nil {
@@ -698,7 +697,7 @@ func (v Views) form(client *oauthClient, scopes []string) templ.Component {
var templ_7745c5c3_Var37 string
templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(client.SoftwareVersion)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 127, Col: 69}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 126, Col: 69}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
if templ_7745c5c3_Err != nil {
@@ -711,7 +710,7 @@ func (v Views) form(client *oauthClient, scopes []string) templ.Component {
var templ_7745c5c3_Var38 templ.SafeURL
templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinURLErrs(URL(ctx))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 129, Col: 43}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 128, Col: 43}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
if templ_7745c5c3_Err != nil {
@@ -724,7 +723,7 @@ func (v Views) form(client *oauthClient, scopes []string) templ.Component {
var templ_7745c5c3_Var39 string
templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(L(ctx).Gettext("Authorize"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 136, Col: 32}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 135, Col: 32}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))
if templ_7745c5c3_Err != nil {
@@ -737,7 +736,7 @@ func (v Views) form(client *oauthClient, scopes []string) templ.Component {
var templ_7745c5c3_Var40 string
templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(L(ctx).Gettext("Deny"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 144, Col: 27}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/auth/oauth2/x-oauth.templ`, Line: 143, Col: 27}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
if templ_7745c5c3_Err != nil {
+6 -8
View File
@@ -15,7 +15,7 @@ import (
"github.com/go-chi/chi/v5"
"golang.org/x/oauth2"
"codeberg.org/readeck/readeck/internal/auth/signin"
"codeberg.org/readeck/readeck/components"
"codeberg.org/readeck/readeck/internal/server"
"codeberg.org/readeck/readeck/internal/sessions"
"codeberg.org/readeck/readeck/pkg/forms"
@@ -23,8 +23,6 @@ import (
// SetupRoutes mounts the routes for the auth domain.
func SetupRoutes(srv *server.Server) {
initCookieHandler()
srv.AddRoute("/login/oidc", newOIDCHandler())
}
@@ -45,7 +43,7 @@ func newOIDCHandler() *oidcHandler {
r.Use(
server.Csrf,
server.WithSession(),
server.WithCustomErrorComponent(signin.Views{}.Error),
server.WithCustomErrorComponent(components.AuthError),
)
h := &oidcHandler{r}
@@ -155,7 +153,7 @@ func (h *oidcHandler) oidcCallback(w http.ResponseWriter, r *http.Request) {
tracker.Delete(w, r)
// Provisioning user
form := info.provisioningForm()
form := info.Form()
slog.Debug("OIDC user",
slog.Any("id", form.ID.Value()),
slog.Any("username", form.Username.Value()),
@@ -168,7 +166,7 @@ func (h *oidcHandler) oidcCallback(w http.ResponseWriter, r *http.Request) {
return
}
user, err := form.execute(provider)
user, err := form.Execute()
if err != nil {
server.Err(w, r, err)
return
@@ -182,9 +180,9 @@ func (h *oidcHandler) oidcCallback(w http.ResponseWriter, r *http.Request) {
sess.Save(w, r)
if sess.Payload.RequiresMFA {
signin.RedirectToMFA(w, r, redir)
server.RedirectToMFA(w, r, redir)
return
}
signin.Redirect(w, r, redir)
server.RedirectNoLogin(w, r, redir)
}
+1 -13
View File
@@ -19,7 +19,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"codeberg.org/readeck/readeck/configs"
"codeberg.org/readeck/readeck/internal/auth/oidc"
. "codeberg.org/readeck/readeck/internal/testing" //revive:disable:dot-imports
)
@@ -66,23 +65,12 @@ func (s *oidcServer) start(clientID string) func() {
s.clientID = clientID
s.nonce = ""
configs.Config.Auth.OIDC.Providers = map[string]configs.OIDCProvider{
"test": {
Name: "OIDC Test",
URL: s.url,
ClientID: s.clientID,
ClientSecret: "client-secret",
ProvisioningEnabled: true,
},
}
oidc.Providers.Add("test", "OIDC Test", s.url, s.clientID, "client-secret", [][2]string{}, true)
return s.stop
}
func (s *oidcServer) stop() {
s.ts.Close()
configs.Config.Auth.OIDC.Providers = map[string]configs.OIDCProvider{}
oidc.Providers.Clear()
}
+84 -56
View File
@@ -19,7 +19,6 @@ import (
"github.com/coreos/go-oidc/v3/oidc"
"codeberg.org/readeck/readeck/configs"
"codeberg.org/readeck/readeck/internal/server/urls"
"codeberg.org/readeck/readeck/pkg/http/securecookie"
)
@@ -31,13 +30,13 @@ const (
var cookieHandler *securecookie.Handler
// initCookieHandler prepares the [securecookie.Handler]
// InitCookieHandler prepares the [securecookie.Handler]
// for the OIDC tracking cookie.
func initCookieHandler() {
func InitCookieHandler(key []byte) {
cookieHandler = securecookie.NewHandler(
securecookie.Key(configs.Keys.OIDCKey()),
securecookie.Key(key),
securecookie.WithPath(path.Join(urls.Prefix())),
securecookie.WithMaxAge(configs.Config.Server.Session.MaxAge),
securecookie.WithMaxAge(3600),
securecookie.WithName(cookieName),
)
}
@@ -86,7 +85,14 @@ func (c *cookie) Delete(w http.ResponseWriter, r *http.Request) {
// providerInfo is wrapper around [configs.OIDCProvider] that contains
// a resolved [oidc.ProviderConfig] and a [oauth2.Config].
type providerInfo struct {
configs.OIDCProvider
loaded bool
Name string
URL string
ClientID string
ClientSecret string
Groups [][2]string
ProvisioningEnabled bool
Config providerConfig
OAuth oauth2.Config
@@ -112,47 +118,31 @@ var Providers = providerList{
items: map[string]providerInfo{},
}
func newProvider(ctx context.Context, cf *configs.OIDCProvider) (*providerInfo, error) {
// XXX: handle configuration for providers without discovery route
p, err := oidc.NewProvider(ctx, cf.URL)
if err != nil {
return nil, err
}
// Items returns the provider list.
func (pl *providerList) Items() map[string]providerInfo {
return pl.items
}
res := &providerInfo{OIDCProvider: *cf}
if err = p.Claims(&res.Config); err != nil {
return nil, err
// Add adds a new, unloaded, provider to the list.
func (pl *providerList) Add(id, name, url, clientID, clientSecret string, groups [][2]string, prov bool) *providerInfo {
pl.Lock()
defer pl.Unlock()
p := providerInfo{
Name: name,
URL: url,
ClientID: clientID,
ClientSecret: clientSecret,
Groups: groups,
ProvisioningEnabled: prov,
}
if len(res.Config.GrantTypesSupported) > 0 && !slices.Contains(res.Config.GrantTypesSupported, "authorization_code") {
return nil, newError(
errors.New("provider doesn't support authorization_code grant"),
withStatus(http.StatusNotImplemented),
withAttrs(slog.Any("grant_types", res.Config.GrantTypesSupported)),
)
}
if len(res.Config.ResponseTypesSupported) > 0 && !slices.Contains(res.Config.ResponseTypesSupported, "code") {
return nil, newError(
errors.New("provider doesn't support code response type"),
withStatus(http.StatusNotImplemented),
withAttrs(slog.Any("response_types", res.Config.ResponseTypesSupported)),
)
}
res.OAuth = oauth2.Config{
ClientID: cf.ClientID,
ClientSecret: cf.ClientSecret,
RedirectURL: urls.AbsoluteURLContext(ctx, "/login/oidc").String(),
Endpoint: p.Endpoint(),
Scopes: []string{oidc.ScopeOpenID, "profile", "email", "groups"},
}
return res, nil
pl.items[id] = p
return &p
}
// Clear removes all cached providers. Used by tests.
func (pl *providerList) Clear() {
pl.Lock()
defer pl.Unlock()
pl.items = map[string]providerInfo{}
}
@@ -163,22 +153,60 @@ func (pl *providerList) get(ctx context.Context, id string) (*providerInfo, erro
pl.Lock()
defer pl.Unlock()
if cf, ok := pl.items[id]; ok {
return &cf, nil
p, ok := pl.items[id]
if !ok {
return nil, errNoProvider
}
if cf, ok := configs.Config.Auth.OIDC.Providers[id]; ok {
provider, err := newProvider(ctx, &cf)
if err := p.load(ctx); err != nil {
return nil, err
}
return &p, nil
}
func (p *providerInfo) load(ctx context.Context) (err error) {
var oidcProvider *oidc.Provider
if !p.loaded {
// XXX: handle configuration for providers without discovery route
oidcProvider, err = oidc.NewProvider(ctx, p.URL)
if err != nil {
return nil, err
return err
}
provider.Groups = cf.Groups
pl.items[id] = *provider
return provider, nil
if err = oidcProvider.Claims(&p.Config); err != nil {
return err
}
p.loaded = true
} else {
oidcProvider = p.Config.NewProvider(ctx)
}
return nil, errNoProvider
if len(p.Config.GrantTypesSupported) > 0 && !slices.Contains(p.Config.GrantTypesSupported, "authorization_code") {
return newError(
errors.New("provider doesn't support authorization_code grant"),
withStatus(http.StatusNotImplemented),
withAttrs(slog.Any("grant_types", p.Config.GrantTypesSupported)),
)
}
if len(p.Config.ResponseTypesSupported) > 0 && !slices.Contains(p.Config.ResponseTypesSupported, "code") {
return newError(
errors.New("provider doesn't support code response type"),
withStatus(http.StatusNotImplemented),
withAttrs(slog.Any("response_types", p.Config.ResponseTypesSupported)),
)
}
p.OAuth = oauth2.Config{
ClientID: p.ClientID,
ClientSecret: p.ClientSecret,
RedirectURL: urls.AbsoluteURLContext(ctx, "/login/oidc").String(),
Endpoint: oidcProvider.Endpoint(),
Scopes: []string{oidc.ScopeOpenID, "profile", "email", "groups"},
}
return nil
}
// AuthCodeURL returns the auth code URL needed to start the OAuth dance.
@@ -189,7 +217,7 @@ func (p *providerInfo) AuthCodeURL(state, nonce, pkceVerifier string) string {
// Exchange exchanges a code for an [oauth2.Token].
// If the token contains an "id_token", it will check that it's
// properly signed and have a matching nonce.
func (p *providerInfo) Exchange(ctx context.Context, code, nonce, pkceVerifier string) (*oauth2.Token, *userInfo, error) {
func (p *providerInfo) Exchange(ctx context.Context, code, nonce, pkceVerifier string) (*oauth2.Token, *UserInfo, error) {
// Fetch token
var options []oauth2.AuthCodeOption
if pkceVerifier != "" {
@@ -204,7 +232,7 @@ func (p *providerInfo) Exchange(ctx context.Context, code, nonce, pkceVerifier s
// Verify id_token, if any
rawID, ok := token.Extra("id_token").(string)
if !ok {
return token, &userInfo{provider: p}, nil
return token, &UserInfo{Provider: p}, nil
}
verifier := p.Config.NewProvider(ctx).Verifier(&oidc.Config{
@@ -223,14 +251,14 @@ func (p *providerInfo) Exchange(ctx context.Context, code, nonce, pkceVerifier s
}
// Retrieve the claims, it's ok to fail here.
userInfo := &userInfo{provider: p}
userInfo := &UserInfo{Provider: p}
_ = tokenID.Claims(userInfo)
return token, userInfo, nil
}
// GetUser returns a [userInfo] from the user info endpoint.
func (p *providerInfo) GetUser(ctx context.Context, token *oauth2.Token) (*userInfo, error) {
func (p *providerInfo) GetUser(ctx context.Context, token *oauth2.Token) (*UserInfo, error) {
pr := p.Config.NewProvider(ctx)
info, err := pr.UserInfo(ctx, p.OAuth.TokenSource(ctx, token))
@@ -238,8 +266,8 @@ func (p *providerInfo) GetUser(ctx context.Context, token *oauth2.Token) (*userI
return nil, err
}
res := &userInfo{
provider: p,
res := &UserInfo{
Provider: p,
Email: info.Email,
}
if err = info.Claims(res); err != nil {
+31 -25
View File
@@ -21,33 +21,35 @@ import (
"codeberg.org/readeck/readeck/pkg/forms"
)
type provisioningForm struct {
// ProvisioningForm is the form used to provisioon a user.
type ProvisioningForm struct {
forms.Form
userInfo *UserInfo
ID forms.TextField `json:"id" validate:"trim required"`
Username forms.TextField `json:"username" validate:"trim is_valid_username"`
Email forms.TextField `json:"email" validate:"trim is_valid_email"`
Group forms.TextField `json:"group" validate:"required_or_nil group_choices"`
}
// userInfo contains the information gathered after the token exchange.
type userInfo struct {
provider *providerInfo
Issuer string `json:"iss"`
Subject string `json:"sub"`
Username string `json:"preferred_username"`
Email string `json:"email"`
ReadeckUser string `json:"readeck_user"`
ReadeckEmail string `json:"readeck_email"`
ReadeckGroup string `json:"readeck_group"`
Groups []string `json:"groups"`
Group string `json:"-"`
// UserInfo contains the information gathered after the token exchange.
type UserInfo struct {
Provider *providerInfo `json:"-"`
Issuer string `json:"iss"`
Subject string `json:"sub"`
Username string `json:"preferred_username"`
Email string `json:"email"`
Groups []string `json:"groups"`
ReadeckUser string `json:"readeck_user"`
ReadeckEmail string `json:"readeck_email"`
ReadeckGroup string `json:"readeck_group"`
}
func (ui *userInfo) id() uuid.UUID {
func (ui *UserInfo) id() uuid.UUID {
return uuid.NewSHA1(uuid.NameSpaceURL, []byte(ui.Issuer+"#"+ui.Subject))
}
func (ui *userInfo) username() string {
func (ui *UserInfo) username() string {
switch {
case ui.ReadeckUser != "":
return ui.ReadeckUser
@@ -58,7 +60,7 @@ func (ui *userInfo) username() string {
}
}
func (ui *userInfo) email() string {
func (ui *UserInfo) email() string {
switch {
case ui.ReadeckEmail != "":
return ui.ReadeckEmail
@@ -67,12 +69,12 @@ func (ui *userInfo) email() string {
}
}
func (ui *userInfo) group() string {
func (ui *UserInfo) group() string {
if ui.ReadeckGroup != "" {
return ui.ReadeckGroup
}
for _, g := range ui.provider.Groups {
for _, g := range ui.Provider.Groups {
if slices.Contains(ui.Groups, g[0]) {
return g[1]
}
@@ -80,7 +82,8 @@ func (ui *userInfo) group() string {
return "user"
}
func (ui *userInfo) provisioningForm() *provisioningForm {
// Form returns a bound [ProvisioningForm] using the struct values.
func (ui *UserInfo) Form() *ProvisioningForm {
// Get some corner cases out the way first
username := ui.username()
email := ui.email()
@@ -99,13 +102,14 @@ func (ui *userInfo) provisioningForm() *provisioningForm {
"group": {ui.group()},
}
f := forms.New[provisioningForm](context.Background())
f := forms.New[ProvisioningForm](context.Background())
f.userInfo = ui
forms.BindValues(q, f)
return f
}
func (f *provisioningForm) errors() error {
func (f *ProvisioningForm) errors() error {
if len(f.Errors()) > 0 {
return f.Errors()
}
@@ -119,7 +123,9 @@ func (f *provisioningForm) errors() error {
return nil
}
func (f *provisioningForm) execute(provider *providerInfo) (*users.User, error) {
// Execute performs the user provisioning.
// It creates or updates a user depending on whether it exists already.
func (f *ProvisioningForm) Execute() (*users.User, error) {
tx, err := db.Q().Begin()
if err != nil {
return nil, err
@@ -167,7 +173,7 @@ func (f *provisioningForm) execute(provider *providerInfo) (*users.User, error)
now := time.Now().UTC()
if user.IsAnonymous() {
// Create user
if !provider.ProvisioningEnabled {
if !f.userInfo.Provider.ProvisioningEnabled {
return errNoProvisioning
}
@@ -184,7 +190,7 @@ func (f *provisioningForm) execute(provider *providerInfo) (*users.User, error)
user.ExtID = new(f.ID.Value())
user.ExtInfo = &users.ExtInfo{
AddedOn: user.Created,
Issuer: provider.URL,
Issuer: f.userInfo.Issuer,
}
ds := tx.Insert(db.TableUser).Rows(user).Prepared(true)
@@ -207,7 +213,7 @@ func (f *provisioningForm) execute(provider *providerInfo) (*users.User, error)
AddedOn: now,
}
}
user.ExtInfo.Issuer = provider.URL
user.ExtInfo.Issuer = f.userInfo.Issuer
_, err = tx.Update(db.TableUser).
Set(user).Where(goqu.C("id").Eq(user.ID)).
+146
View File
@@ -0,0 +1,146 @@
// SPDX-FileCopyrightText: © 2026 Olivier Meunier <olivier@neokraft.net>
//
// SPDX-License-Identifier: AGPL-3.0-only
package oidc_test
import (
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"codeberg.org/readeck/readeck/internal/auth/oidc"
"codeberg.org/readeck/readeck/internal/auth/users"
. "codeberg.org/readeck/readeck/internal/testing" //revive:disable:dot-imports
)
func TestProvisioning(t *testing.T) {
app := NewTestApp(t)
defer app.Close(t)
providerInfo := oidc.Providers.Add(
"test", "OIDC Test", "https://example.org", "client-id", "client-secret",
[][2]string{
{"roles:admin", "admin"},
{"roles:user", "user"},
},
true,
)
defer oidc.Providers.Clear()
user1, err := NewTestUser("bob", "bob@example.org", "abcd", "user")
require.NoError(t, err)
user2, err := NewTestUser("bob-oidc", "bob-oidc@example.org", "abcd", "user")
require.NoError(t, err)
user2.User.ExtID = new(uuid.NewSHA1(uuid.NameSpaceURL, []byte("https://example.org#bob-oidc")).String())
require.NoError(t, user2.User.Save())
tests := []struct {
name string
info *oidc.UserInfo
assert func(t *testing.T, user *users.User, err error)
}{
{
name: "new user default group",
info: &oidc.UserInfo{
Subject: "alice1",
Username: "alice1",
Email: "alice1@example.org",
},
assert: func(t *testing.T, user *users.User, err error) {
require.NoError(t, err)
assert.Equal(t, "alice1", user.Username)
assert.Equal(t, "alice1@example.org", user.Email)
assert.Equal(t, "user", user.Group)
},
},
{
name: "new user with oidc group",
info: &oidc.UserInfo{
Subject: "alice2",
Username: "alice2",
Email: "alice2@example.org",
Groups: []string{"roles:user", "roles:admin"},
},
assert: func(t *testing.T, user *users.User, err error) {
require.NoError(t, err)
assert.Equal(t, "alice2", user.Username)
assert.Equal(t, "alice2@example.org", user.Email)
assert.Equal(t, "admin", user.Group)
},
},
{
name: "new user with custom claims",
info: &oidc.UserInfo{
Subject: app.Users["user"].User.Username,
Username: app.Users["user"].User.Username,
Email: app.Users["user"].User.Email,
ReadeckUser: "new-user",
ReadeckEmail: "user@example.net",
ReadeckGroup: "admin",
},
assert: func(t *testing.T, user *users.User, err error) {
require.NoError(t, err)
assert.Equal(t, "new-user", user.Username)
assert.Equal(t, "user@example.net", user.Email)
assert.Equal(t, "admin", user.Group)
},
},
{
name: "provision existing user",
info: &oidc.UserInfo{
Subject: "bob",
Username: "bob",
Email: "bob-new-email@example.org",
},
assert: func(t *testing.T, user *users.User, err error) {
require.NoError(t, err)
assert.Equal(t, "bob", user.Username)
assert.Equal(t, "bob-new-email@example.org", user.Email)
assert.Equal(t, "user", user.Group)
assert.Equal(t, user1.User.ID, user.ID)
},
},
{
name: "update existing user",
info: &oidc.UserInfo{
Subject: "bob-oidc",
Username: "new-bob",
Email: "bob@example.com",
},
assert: func(t *testing.T, user *users.User, err error) {
require.NoError(t, err)
assert.Equal(t, "new-bob", user.Username)
assert.Equal(t, "bob@example.com", user.Email)
assert.Equal(t, "user", user.Group)
assert.Equal(t, user2.User.ID, user.ID)
},
},
{
name: "conflicting information",
info: &oidc.UserInfo{
Subject: app.Users["admin"].User.Username,
Username: app.Users["admin"].User.Username,
Email: app.Users["user"].User.Email,
},
assert: func(t *testing.T, _ *users.User, err error) {
require.Error(t, err, "more than one user is")
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
test.info.Provider = providerInfo
test.info.Issuer = providerInfo.URL
f := test.info.Form()
require.True(t, f.IsValid())
user, err := f.Execute()
test.assert(t, user, err)
})
}
}
+6 -24
View File
@@ -7,13 +7,13 @@ package signin
import (
"net/http"
"net/url"
"strings"
"time"
"github.com/doug-martin/goqu/v9"
"github.com/go-chi/chi/v5"
"codeberg.org/readeck/readeck/components"
"codeberg.org/readeck/readeck/configs"
"codeberg.org/readeck/readeck/internal/auth/users"
"codeberg.org/readeck/readeck/internal/server"
@@ -36,7 +36,7 @@ func newAuthHandler(srv *server.Server) *authHandler {
r.Use(
server.Csrf,
server.WithSession(),
server.WithCustomErrorComponent(Views{}.Error),
server.WithCustomErrorComponent(components.AuthError),
)
h := &authHandler{r}
@@ -62,24 +62,6 @@ func newAuthHandler(srv *server.Server) *authHandler {
return h
}
// Redirect triggers a 303 response with a redirect location, taking
// care of not sending the user to the login page again.
// Since it goes to [server.Redirect], it will be sanitized there
// and can only stay within the app.
func Redirect(w http.ResponseWriter, r *http.Request, redir string) {
if redir == "" || strings.HasPrefix(redir, "/login") {
redir = "/"
}
server.Redirect(w, r, redir)
}
// RedirectToMFA triggers a 303 response to the MFA page, keeping
// the initial redirect value.
func RedirectToMFA(w http.ResponseWriter, r *http.Request, redir string) {
v := url.Values{"r": {redir}}
server.Redirect(w, r, "/login/mfa?"+v.Encode())
}
func (h *authHandler) login(w http.ResponseWriter, r *http.Request) {
f := forms.New[loginForm](r.Context())
@@ -90,10 +72,10 @@ func (h *authHandler) login(w http.ResponseWriter, r *http.Request) {
// Do we have a session already?
if sess := server.GetSession(r); sess.Payload.User != 0 {
if sess.Payload.RequiresMFA {
RedirectToMFA(w, r, f.Redirect.Value())
server.RedirectToMFA(w, r, f.Redirect.Value())
return
}
Redirect(w, r, f.Redirect.Value())
server.RedirectNoLogin(w, r, f.Redirect.Value())
return
}
}
@@ -113,7 +95,7 @@ func (h *authHandler) login(w http.ResponseWriter, r *http.Request) {
sess.Save(w, r)
if sess.Payload.RequiresMFA {
RedirectToMFA(w, r, f.Redirect.Value())
server.RedirectToMFA(w, r, f.Redirect.Value())
return
}
@@ -121,7 +103,7 @@ func (h *authHandler) login(w http.ResponseWriter, r *http.Request) {
"last_login": time.Now().UTC(),
})
Redirect(w, r, f.Redirect.Value())
server.RedirectNoLogin(w, r, f.Redirect.Value())
return
}
// we must set the content type to avoid the
+5 -44
View File
@@ -8,23 +8,17 @@ import (
"maps"
"slices"
"codeberg.org/readeck/readeck/configs"
. "codeberg.org/readeck/readeck/components"
F "codeberg.org/readeck/readeck/components/forms"
"net/http"
"codeberg.org/readeck/readeck/internal/auth/oidc"
)
type Views struct{}
func SigninLayout(title string) templ.Component {
return Views{}.base(title)
}
type Components struct{}
templ (_ Components) OIDC() {
if providers := configs.Config.Auth.OIDC.Providers; len(providers) > 0 {
if providers := oidc.Providers.Items(); len(providers) > 0 {
<form action={ URL(ctx, "/login/oidc") } method="post">
<fieldset class="mt-8 border-t">
<legend class="mx-auto px-2">{ L(ctx).Gettext("Or continue with") }</legend>
@@ -46,41 +40,8 @@ templ (_ Components) OIDC() {
}
}
templ (_ Views) header() {
<h1 class="max-w-sm mt-12 mx-auto px-8">
<div>
<svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 567 128" class="w-full fill-gray-900">
<use href={ Asset(ctx, "img/logo-text.svg") + "#main" }></use>
</svg>
</div>
</h1>
}
templ (v Views) base(title string) {
@BasePage(title) {
@v.header()
<main class="w-full max-w-sm p-8 mt-6 mx-auto bg-gray-100 text-gray-dark rounded-md shadow-md" id="content">
{ children... }
</main>
@CustomTemplate("auth/footer.html.tmpl", nil)
}
}
templ (v Views) Error(status int) {
@BasePage(L(ctx).Gettext("Error")) {
@v.header()
<main class="w-full max-w-sm p-8 mt-6 mx-auto text-center border border-red-800 bg-red-100 rounded-md" id="content">
<h2 class="title text-h3">{ L(ctx).Gettext("An error occurred") }</h2>
<p>{ http.StatusText(status) }</p>
<p class="mt-8">
<a class="link font-semibold" href={ URL(ctx, "/") }>{ L(ctx).Gettext("Go back to Readeck") }</a>
</p>
</main>
}
}
templ (v Views) signIn(f *loginForm) {
@v.base(L(ctx).Gettext("Sign in")) {
@AuthLayout(L(ctx).Gettext("Sign in")) {
<h2 class="text-h3 mb-8 text-center">{ L(ctx).Gettext("Sign in to Readeck") }</h2>
<form
action={ URL(ctx, "/login") }
@@ -124,7 +85,7 @@ templ (v Views) signIn(f *loginForm) {
templ (v Views) mfa(f *totpForm) {
{{ title := L(ctx).Gettext("Two-factor authentication") }}
@v.base(title) {
@AuthLayout(title) {
<h2 class="text-h3 mb-8 text-center">{ title }</h2>
<form action={ URL(ctx, "/login/mfa") } method="post">
@F.Errors(f)
@@ -159,7 +120,7 @@ templ (v Views) recover(f *recoverForm, err error) {
title := L(ctx).Gettext("Password recovery")
step := f.Step.Value()
}}
@v.base(title) {
@AuthLayout(title) {
<h2 class="text-h3 mb-8 text-center">{ title }</h2>
switch step {
case 0:
+207 -421
View File
File diff suppressed because it is too large Load Diff
+17
View File
@@ -11,6 +11,8 @@ import (
"fmt"
"log/slog"
"net/http"
"net/url"
"strings"
"codeberg.org/readeck/readeck/configs"
"codeberg.org/readeck/readeck/internal/server/urls"
@@ -147,3 +149,18 @@ func Redirect(w http.ResponseWriter, r *http.Request, ref ...string) {
w.Header().Set("Location", urls.AbsoluteURL(r, ref...).String())
w.WriteHeader(http.StatusSeeOther)
}
// RedirectNoLogin yields a 303 redirection to "redir", unless it's a
// redirection to the login page.
func RedirectNoLogin(w http.ResponseWriter, r *http.Request, redir string) {
if redir == "" || strings.HasPrefix(redir, "/login") {
redir = "/"
}
Redirect(w, r, redir)
}
// RedirectToMFA yields a 303 response to the MFA page, keeping
// the initial redirect value.
func RedirectToMFA(w http.ResponseWriter, r *http.Request, redir string) {
Redirect(w, r, "/login/mfa?"+url.Values{"r": {redir}}.Encode())
}