Skip to content

Commit

Permalink
Add OIDC auth hooks (init, callback, session) and finish OIDC support.
Browse files Browse the repository at this point in the history
  • Loading branch information
knadh committed Oct 13, 2024
1 parent 7c92b65 commit 1e875af
Show file tree
Hide file tree
Showing 9 changed files with 144 additions and 144 deletions.
152 changes: 83 additions & 69 deletions cmd/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,84 @@ var oidcProviders = map[string]bool{

// handleLoginPage renders the login page and handles the login form.
func handleLoginPage(c echo.Context) error {
// Process POST login request.
var loginErr error
if c.Request().Method == http.MethodPost {
loginErr = doLogin(c)
if loginErr == nil {
return c.Redirect(http.StatusFound, utils.SanitizeURI(c.FormValue("next")))
}
}

return renderLoginPage(c, loginErr)
}

// handleLogout logs a user out.
func handleLogout(c echo.Context) error {
var (
sess = c.Get(auth.SessionKey).(*simplesessions.Session)
)

// Clear the session.
_ = sess.Destroy()

return c.JSON(http.StatusOK, okResp{true})
}

// handleOIDCLogin initializes an OIDC request and redirects to the OIDC provider for login.
func handleOIDCLogin(c echo.Context) error {
app := c.Get("app").(*App)

// Verify that the request came from the login page (CSRF).
nonce, err := c.Cookie("nonce")
if err != nil || nonce.Value == "" || nonce.Value != c.FormValue("nonce") {
return echo.NewHTTPError(http.StatusUnauthorized, app.i18n.T("users.invalidRequest"))
}

next := utils.SanitizeURI(c.FormValue("next"))
if next == "/" {
next = uriAdmin
}

return c.Redirect(http.StatusFound, app.auth.GetOIDCAuthURL(next, nonce.Value))
}

// handleOIDCFinish receives the redirect callback from the OIDC provider and completes the handshake.
func handleOIDCFinish(c echo.Context) error {
app := c.Get("app").(*App)

nonce, err := c.Cookie("nonce")
if err != nil || nonce.Value == "" {
return renderLoginPage(c, echo.NewHTTPError(http.StatusUnauthorized, app.i18n.T("users.invalidRequest")))
}

// Validate the OIDC token.
oidcToken, claims, err := app.auth.ExchangeOIDCToken(c.Request().URL.Query().Get("code"), nonce.Value)
if err != nil {
return renderLoginPage(c, err)
}

// Get the user by e-mail received from OIDC.
user, err := app.core.GetUser(0, "", claims.Email)
if err != nil {
return renderLoginPage(c, err)
}

// Update user login.
if err := app.core.UpdateUserLogin(user.ID, claims.Picture); err != nil {
return renderLoginPage(c, err)
}

// Set the session.
if err := app.auth.SetSession(user, oidcToken, c); err != nil {
return renderLoginPage(c, err)
}

return c.Redirect(http.StatusFound, utils.SanitizeURI(c.QueryParam("state")))
}

// renderLoginPage renders the login page and handles the login form.
func renderLoginPage(c echo.Context, loginErr error) error {
var (
app = c.Get("app").(*App)
next = utils.SanitizeURI(c.FormValue("next"))
Expand Down Expand Up @@ -71,17 +149,11 @@ func handleLoginPage(c echo.Context) error {
NextURI: next,
}

// Login request.
if c.Request().Method == http.MethodPost {
err := doLogin(c)
if err == nil {
return c.Redirect(http.StatusFound, utils.SanitizeURI(c.FormValue("next")))
}

if e, ok := err.(*echo.HTTPError); ok {
if loginErr != nil {
if e, ok := loginErr.(*echo.HTTPError); ok {
out.Error = e.Message.(string)
} else {
out.Error = err.Error()
out.Error = loginErr.Error()
}
}

Expand All @@ -94,27 +166,15 @@ func handleLoginPage(c echo.Context) error {
c.SetCookie(&http.Cookie{
Name: "nonce",
Value: nonce,
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
Path: "/",
SameSite: http.SameSiteLaxMode,
})
out.Nonce = nonce

return c.Render(http.StatusOK, "admin-login", out)
}

// handleLogout logs a user out.
func handleLogout(c echo.Context) error {
var (
sess = c.Get(auth.SessionKey).(*simplesessions.Session)
)

// Clear the session.
_ = sess.Destroy()

return c.JSON(http.StatusOK, okResp{true})
}

// doLogin logs a user in with a username and password.
func doLogin(c echo.Context) error {
var (
Expand Down Expand Up @@ -159,49 +219,3 @@ func doLogin(c echo.Context) error {

return nil
}

// handleOIDCLogin initializes an OIDC request and redirects to the OIDC provider for login.
func handleOIDCLogin(c echo.Context) error {
app := c.Get("app").(*App)

// Verify that the request came from the login page (CSRF).
nonce, err := c.Cookie("nonce")
if err != nil || nonce.Value == "" || nonce.Value != c.FormValue("nonce") {
return echo.NewHTTPError(http.StatusUnauthorized, app.i18n.T("users.invalidRequest"))
}

return c.Redirect(http.StatusFound, app.auth.GetOIDCAuthURL(c.Request().URL.RequestURI(), nonce.Value))
}

// handleOIDCFinish receives the redirect callback from the OIDC provider and completes the handshake.
func handleOIDCFinish(c echo.Context) error {
app := c.Get("app").(*App)

nonce, err := c.Cookie("nonce")
if err != nil || nonce.Value == "" {
return echo.NewHTTPError(http.StatusUnauthorized, app.i18n.T("users.invalidRequest"))
}

code := c.Request().URL.Query().Get("code")
if err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, app.i18n.T("users.invalidRequest"))
}

oidcToken, u, err := app.auth.ExchangeOIDCToken(code, nonce.Value)
if err != nil {
return err
}

// Get the user by e-mail received from OIDC.
user, err := app.core.GetUser(0, "", u.Email.String)
if err != nil {
return err
}

// Set the session.
if err := app.auth.SetSession(user, oidcToken, c); err != nil {
return err
}

return c.Redirect(http.StatusFound, utils.SanitizeURI(c.QueryParam("state")))
}
8 changes: 2 additions & 6 deletions cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -942,15 +942,11 @@ func initAuth(db *sql.DB, ko *koanf.Koanf, co *core.Core) *auth.Auth {

if ko.Bool("security.oidc.enabled") {
oidcCfg = auth.OIDCConfig{
Enabled: true,
ProviderURL: ko.String("security.oidc.provider_url"),
ClientID: ko.String("security.oidc.client_id"),
ClientSecret: ko.String("security.oidc.client_secret"),
RedirectURL: ko.String("security.oidc.redirect_url"),
Skipper: func(c echo.Context) bool {
// Skip OIDC check if the request is already BasicAuth'd.
// This context flag is set in basicAuth().
return c.Get(basicAuthd) != nil
},
RedirectURL: fmt.Sprintf("%s/auth/oidc", strings.TrimRight(ko.String("app.root_url"), "/")),
}
}

Expand Down
8 changes: 5 additions & 3 deletions cmd/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,10 +229,12 @@ func handleUpdateUserProfile(c echo.Context) error {
email := strings.TrimSpace(u.Email.String)

// Validate fields.
if !utils.ValidateEmail(email) {
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "email"))
if user.PasswordLogin {
if !utils.ValidateEmail(email) {
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "email"))
}
u.Email = null.String{String: email, Valid: true}
}
u.Email = null.String{String: email, Valid: true}

if u.PasswordLogin && u.Password.String != "" {
if !strHasLen(u.Password.String, 8, stdInputMaxLen) {
Expand Down
1 change: 1 addition & 0 deletions i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@
"settings.security.OIDCURL": "Provider URL",
"settings.security.OIDCClientID": "Client ID",
"settings.security.OIDCClientSecret": "Client secret",
"settings.security.OIDCRedirectURL": "Redirect URL for oAuth provider",
"settings.appearance.adminHelp": "Custom CSS to apply to the admin UI.",
"settings.appearance.adminName": "Admin",
"settings.appearance.customCSS": "Custom CSS",
Expand Down
Loading

0 comments on commit 1e875af

Please sign in to comment.