Skip to content

Commit

Permalink
Implement OIDC authentication
Browse files Browse the repository at this point in the history
Fixes: #27
  • Loading branch information
nemunaire committed May 23, 2024
1 parent 3d9aecf commit f0c24ea
Show file tree
Hide file tree
Showing 15 changed files with 544 additions and 63 deletions.
23 changes: 3 additions & 20 deletions api/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import (
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"

"git.happydns.org/happyDomain/actions"
"git.happydns.org/happyDomain/config"
"git.happydns.org/happyDomain/model"
"git.happydns.org/happyDomain/storage"
Expand Down Expand Up @@ -61,27 +60,11 @@ func retrieveUserFromClaims(claims *UserClaims) (user *happydns.User, err error)
user, err = storage.MainStore.GetUser(claims.Profile.UserId)
if err != nil {
// The user doesn't exists yet: create it!
user = &happydns.User{
Id: claims.Profile.UserId,
Email: claims.Profile.Email,
CreatedAt: time.Now(),
LastSeen: time.Now(),
Settings: *happydns.DefaultUserSettings(),
}

err = storage.MainStore.UpdateUser(user)
user, err = createUserFromProfile(claims.Profile)
if err != nil {
err = fmt.Errorf("has a correct JWT, but an error occured when trying to create the user: %w", err)
return
}

if claims.Profile.Newsletter {
err = actions.SubscribeToNewsletter(user)
if err != nil {
err = fmt.Errorf("something goes wrong during newsletter subscription: %w", err)
return
}
}
} else if time.Since(user.LastSeen) > time.Hour*12 {
// Update user's data when connected more than 12 hours
updateUserFromClaims(user, claims)
Expand Down Expand Up @@ -114,7 +97,7 @@ func authMiddleware(opts *config.Options, optional bool) gin.HandlerFunc {
session := sessions.Default(c)

var userid happydns.Identifier
if iu, ok := session.Get("iduser").([]uint8); ok {
if iu, ok := session.Get("iduser").([]byte); ok {
userid = happydns.Identifier(iu)
}

Expand Down Expand Up @@ -161,7 +144,7 @@ func authMiddleware(opts *config.Options, optional bool) gin.HandlerFunc {

if userid != nil {
if userid == nil || userid.IsEmpty() || !userid.Equals(user.Id) {
completeAuth(opts, c, claims.Profile)
CompleteAuth(opts, c, claims.Profile)
session.Clear()
session.Set("iduser", user.Id)
err = session.Save()
Expand Down
44 changes: 40 additions & 4 deletions api/user_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"

"git.happydns.org/happyDomain/actions"
"git.happydns.org/happyDomain/config"
"git.happydns.org/happyDomain/internal/session"
"git.happydns.org/happyDomain/model"
Expand Down Expand Up @@ -106,7 +107,7 @@ func displayNotAuthToken(opts *config.Options, c *gin.Context) *UserClaims {
return nil
}

claims, err := completeAuth(opts, c, UserProfile{
claims, err := CompleteAuth(opts, c, UserProfile{
UserId: []byte{0},
Email: NO_AUTH_ACCOUNT,
EmailVerified: true,
Expand Down Expand Up @@ -199,7 +200,7 @@ func checkAuth(opts *config.Options, c *gin.Context) {
return
}

claims, err := completeAuth(opts, c, UserProfile{
claims, err := CompleteAuth(opts, c, UserProfile{
UserId: user.Id,
Email: user.Email,
EmailVerified: user.EmailVerification != nil,
Expand All @@ -222,12 +223,47 @@ func checkAuth(opts *config.Options, c *gin.Context) {
}
}

func completeAuth(opts *config.Options, c *gin.Context, userprofile UserProfile) (*UserClaims, error) {
func createUserFromProfile(userprofile UserProfile) (*happydns.User, error) {
user := &happydns.User{
Id: userprofile.UserId,
Email: userprofile.Email,
CreatedAt: time.Now(),
LastSeen: time.Now(),
Settings: *happydns.DefaultUserSettings(),
}

err := storage.MainStore.UpdateUser(user)
if err != nil {
return user, err
}

if userprofile.Newsletter {
err = actions.SubscribeToNewsletter(user)
if err != nil {
err = fmt.Errorf("something goes wrong during newsletter subscription: %w", err)
return user, err
}
}

return user, nil
}

func CompleteAuth(opts *config.Options, c *gin.Context, userprofile UserProfile) (*UserClaims, error) {
session := sessions.Default(c)

// Check if the user already exists
_, err := storage.MainStore.GetUser(userprofile.UserId)
if err != nil {
// Create the user
_, err = createUserFromProfile(userprofile)
if err != nil {
return nil, fmt.Errorf("unable to create user account: %w", err)
}
}

session.Clear()
session.Set("iduser", userprofile.UserId)
err := session.Save()
err = session.Save()
if err != nil {
return nil, err
}
Expand Down
48 changes: 45 additions & 3 deletions api/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,17 @@ func declareUsersAuthRoutes(opts *config.Options, router *gin.RouterGroup) {
apiSameUserRoutes.Use(userHandler)
apiSameUserRoutes.Use(SameUserHandler)

apiSameUserRoutes.DELETE("", func(c *gin.Context) {
deleteMyUser(opts, c)
})
apiSameUserRoutes.GET("/settings", getUserSettings)
apiSameUserRoutes.POST("/settings", changeUserSettings)

apiUserAuthRoutes := router.Group("/users/:uid")
apiUserAuthRoutes.Use(userAuthHandler)
apiUserAuthRoutes.GET("/is_auth_user", func(c *gin.Context) {
c.Status(http.StatusNoContent)
})
apiUserAuthRoutes.POST("/delete", func(c *gin.Context) {
deleteUser(opts, c)
})
Expand Down Expand Up @@ -463,6 +469,42 @@ func changePassword(opts *config.Options, c *gin.Context) {
logout(opts, c)
}

func deleteMyUser(opts *config.Options, c *gin.Context) {
user := c.MustGet("user").(*happydns.User)

// Disallow route if user is authenticated through local service
if _, err := storage.MainStore.GetAuthUser(user.Id); err == nil {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "This route is for external account only. Please use the route ./delete instead."})
return
}

// Retrieve all user's sessions to disconnect them
sessions, err := storage.MainStore.GetUserSessions(user)
if err != nil {
log.Printf("%s: unable to GetUserSessions in deleteUser: %s", c.ClientIP(), err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Sorry, we are currently unable to delete your profile. Please try again later."})
return
}

err = storage.MainStore.DeleteUser(user)
if err != nil {
log.Printf("%s: unable to DeleteUser in deletemyuser: %s", c.ClientIP(), err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Sorry, we are currently unable to update your profile. Please try again later."})
return
}

log.Printf("%s: deletes user: %s", c.ClientIP(), user.Email)

for _, session := range sessions {
err = storage.MainStore.DeleteSession(session.Id)
if err != nil {
log.Printf("%s: unable to delete session (drop account): %s", c.ClientIP(), err.Error())
}
}

logout(opts, c)
}

// deleteUser delete the account related to the given user.
//
// @Summary Drop account
Expand Down Expand Up @@ -499,13 +541,13 @@ func deleteUser(opts *config.Options, c *gin.Context) {
sessions, err := storage.MainStore.GetAuthUserSessions(user)
if err != nil {
log.Printf("%s: unable to GetUserSessions in deleteUser: %s", c.ClientIP(), err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Sorry, we are currently unable to update your profile. Please try again later."})
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Sorry, we are currently unable to delete your profile. Please try again later."})
return
}

if err = storage.MainStore.DeleteAuthUser(user); err != nil {
log.Printf("%s: unable to DefinePassword in deleteuser: %s", c.ClientIP(), err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Sorry, we are currently unable to update your profile. Please try again later."})
log.Printf("%s: unable to DeleteAuthUser in deleteuser: %s", c.ClientIP(), err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Sorry, we are currently unable to delete your profile. Please try again later."})
return
}

Expand Down
28 changes: 28 additions & 0 deletions config/nooidc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2024 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

//go:build nooidc

package config

const (
OIDCProviderURL = ""
)
68 changes: 68 additions & 0 deletions config/oidc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2024 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

//go:build !nooidc

package config

import (
"context"
"flag"
"net/url"
"path"

"github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/oauth2"
)

var (
OIDCClientID string
oidcClientSecret string
OIDCProviderURL string
)

func init() {
flag.StringVar(&OIDCClientID, "oidc-client-id", OIDCClientID, "ClientID for OIDC")
flag.StringVar(&oidcClientSecret, "oidc-client-secret", oidcClientSecret, "Secret for OIDC")
flag.StringVar(&OIDCProviderURL, "oidc-provider-url", OIDCProviderURL, "Base URL of the OpenId Connect service")
}

func (o *Options) GetAuthURL() *url.URL {
redirecturl := *o.ExternalURL.URL
redirecturl.Path = path.Join(redirecturl.Path, o.BaseURL, "auth", "callback")
return &redirecturl
}

func (o *Options) GetOIDCProvider(ctx context.Context) (*oidc.Provider, error) {
return oidc.NewProvider(ctx, OIDCProviderURL)
}

func (o *Options) GetOAuth2Config(provider *oidc.Provider) *oauth2.Config {
oauth2Config := oauth2.Config{
ClientID: OIDCClientID,
ClientSecret: oidcClientSecret,
RedirectURL: o.GetAuthURL().String(),
Endpoint: provider.Endpoint(),
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
}

return &oauth2Config
}
5 changes: 5 additions & 0 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ func NewApp(cfg *config.Options) App {
api.DeclareRoutes(cfg, router)
ui.DeclareRoutes(cfg, router)

if config.OIDCProviderURL != "" {
authRoutes := router.Group("/auth")
InitializeOIDC(cfg, authRoutes)
}

app := App{
router: router,
cfg: cfg,
Expand Down
41 changes: 41 additions & 0 deletions internal/app/nooidc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2024 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

//go:build nooidc

package app

import (
"net/http"

"github.com/gin-gonic/gin"

"git.happydns.org/happyDomain/config"
)

func InitializeOIDC(cfg *config.Options, router *gin.RouterGroup) {
router.GET("oidc", func(c *gin.Context) {
c.Redirect(http.StatusFound, cfg.BaseURL+"/login")
})
router.GET("callback", func(c *gin.Context) {
c.Redirect(http.StatusFound, cfg.BaseURL+"/")
})
}
Loading

0 comments on commit f0c24ea

Please sign in to comment.