/** User package, authentication endpoint. * * @author Sonja Happ * @copyright 2014-2019, Institute for Automation of Complex Power Systems, EONERC * @license GNU General Public License (version 3) * * VILLASweb-backend-go * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . *********************************************************************************/ package user import ( "fmt" "log" "net/http" "strings" "time" "git.rwth-aachen.de/acs/public/villas/web-backend-go/configuration" "git.rwth-aachen.de/acs/public/villas/web-backend-go/database" "git.rwth-aachen.de/acs/public/villas/web-backend-go/helper" "github.com/dgrijalva/jwt-go" "github.com/gin-gonic/gin" ) var session *helper.AMQPsession func SetAMQPSession(s *helper.AMQPsession) { session = s } type tokenClaims struct { UserID uint `json:"id"` Role string `json:"role"` jwt.StandardClaims } func RegisterAuthenticate(r *gin.RouterGroup) { r.GET("", authenticated) r.POST("/:mechanism", authenticate) } // authenticated godoc // @Summary Check if user is authenticated and provide details on how the user can authenticate // @ID authenticated // @Accept json // @Produce json // @Tags authentication // @Success 200 {object} api.ResponseAuthenticate "JSON web token, success status, message and authenticated user object" // @Failure 401 {object} api.ResponseError "Unauthorized" // @Failure 500 {object} api.ResponseError "Internal server error." // @Router /authenticate [get] func authenticated(c *gin.Context) { ok, err := isAuthenticated(c) if err != nil { helper.InternalServerError(c, err.Error()) return } if ok { // ATTENTION: do not use c.GetInt (common.UserIDCtx) since userID is of type uint and not int userID, _ := c.Get(database.UserIDCtx) var user User err := user.byID(userID.(uint)) if helper.DBError(c, err) { return } c.JSON(http.StatusOK, gin.H{ "success": true, "authenticated": true, "user": user.User, }) } else { authExternal, err := configuration.GlobalConfig.Bool("auth.external.enabled") if err != nil { helper.UnauthorizedError(c, "Backend configuration error") return } if authExternal { c.JSON(http.StatusOK, gin.H{ "success": true, "authenticated": false, "external": "/oauth/start", }) } else { c.JSON(http.StatusOK, gin.H{ "success": true, "authenticated": false, }) } } } // authenticate godoc // @Summary Authentication for user // @ID authenticate // @Accept json // @Produce json // @Tags authentication // @Param inputUser body user.loginRequest true "loginRequest of user" // @Param mechanism path string true "Login mechanism" Enums(internal, external) // @Success 200 {object} api.ResponseAuthenticate "JSON web token, success status, message and authenticated user object" // @Failure 401 {object} api.ResponseError "Unauthorized" // @Failure 500 {object} api.ResponseError "Internal server error." // @Router /authenticate/{mechanism} [post] func authenticate(c *gin.Context) { var myUser User var err error switch c.Param("mechanism") { case "internal": myUser, err = authenticateInternal(c) if err != nil { log.Println("internal auth. failed with error: ", err.Error()) return } case "external": var authExternal bool authExternal, err = configuration.GlobalConfig.Bool("auth.external.enabled") if err == nil && authExternal { myUser, err = authenticateExternal(c) if err != nil { log.Println("external auth. failed with error: ", err) return } } else { helper.BadRequestError(c, "External authentication is not activated") return } default: helper.BadRequestError(c, "Invalid authentication mechanism") return } // Check if this is an active user if !myUser.Active { helper.UnauthorizedError(c, "User is not active") return } expiresStr, err := configuration.GlobalConfig.String("jwt.expires-after") if err != nil { helper.InternalServerError(c, "Invalid backend configuration: jwt.expires-after") return } expiresDuration, err := time.ParseDuration(expiresStr) if err != nil { helper.InternalServerError(c, "Invalid backend configuration: jwt.expires-after") return } secret, err := configuration.GlobalConfig.String("jwt.secret") if err != nil { helper.InternalServerError(c, "Invalid backend configuration: jwt.secret") return } // Create authentication token claims := tokenClaims{ myUser.ID, myUser.Role, jwt.StandardClaims{ ExpiresAt: time.Now().Add(expiresDuration).Unix(), IssuedAt: time.Now().Unix(), Issuer: "http://web.villas.fein-aachen.org/", }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenString, err := token.SignedString([]byte(secret)) if err != nil { helper.InternalServerError(c, "Invalid backend configuration: jwt.secret") return } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Authenticated", "token": tokenString, "user": myUser.User, }) } func authenticateInternal(c *gin.Context) (User, error) { // Bind the response (context) with the loginRequest struct var myUser User var credentials loginRequest if err := c.ShouldBindJSON(&credentials); err != nil { helper.UnauthorizedError(c, "Wrong username or password") return myUser, err } // Validate the login request if errs := credentials.validate(); errs != nil { helper.UnauthorizedError(c, "Failed to validate request") return myUser, errs } // Find the username in the database err := myUser.byUsername(credentials.Username) if err != nil { helper.UnauthorizedError(c, "Unknown username") return myUser, err } // Validate the password err = myUser.validatePassword(credentials.Password) if err != nil { helper.UnauthorizedError(c, "Invalid password") return myUser, err } return myUser, nil } func authenticateExternal(c *gin.Context) (User, error) { var myUser User username := c.Request.Header.Get("X-Forwarded-User") if username == "" { helper.UnauthorizedAbort(c, "Authentication failed (X-Forwarded-User headers)") return myUser, fmt.Errorf("no username") } email := c.Request.Header.Get("X-Forwarded-Email") if email == "" { helper.UnauthorizedAbort(c, "Authentication failed (X-Forwarded-Email headers)") return myUser, fmt.Errorf("no email") } groups := strings.Split(c.Request.Header.Get("X-Forwarded-Groups"), ",") // preferred_username := c.Request.Header.Get("X-Forwarded-Preferred-Username") // check if user already exists err := myUser.byUsername(username) if err != nil { // this is the first login, create new user role := "User" if _, found := helper.Find(groups, "admin"); found { role = "Admin" } myUser, err = NewUser(username, "", email, role, true) if err != nil { helper.UnauthorizedAbort(c, "Authentication failed (failed to create new user: "+err.Error()+")") return myUser, fmt.Errorf("failed to create new user") } log.Printf("Created new external user %s (id=%d)", myUser.Username, myUser.ID) } // Add users to scenarios based on static groups map db := database.GetDB() for _, group := range groups { if groupedArr, ok := configuration.ScenarioGroupMap[group]; ok { for _, groupedScenario := range groupedArr { var so database.Scenario err := db.Find(&so, groupedScenario.Scenario).Error if err != nil { log.Printf(`Cannot find scenario %s (id=%d) for adding/duplication. Affecting user %s (id=%d): %s\n`, so.Name, so.ID, myUser.Username, myUser.ID, err) continue } duplicateName := fmt.Sprintf("%s %s", so.Name, myUser.Username) alreadyDuplicated := isAlreadyDuplicated(duplicateName) if alreadyDuplicated { log.Printf("Scenario %d already duplicated for user %s", so.ID, myUser.Username) return myUser, nil } if groupedScenario.Duplicate { if err := <-duplicateScenarioForUser(so, &myUser.User, ""); err != nil { return User{}, err } } else { // add user to scenario err = db.Model(&so).Association("Users").Append(&(myUser.User)).Error if err != nil { log.Printf("Failed to add user %s (id=%d) to scenario %s (id=%d): %s\n", myUser.Username, myUser.ID, so.Name, so.ID, err) continue } log.Printf("Added user %s (id=%d) to scenario %s (id=%d)", myUser.Username, myUser.ID, so.Name, so.ID) } } } } return myUser, nil } func isAlreadyDuplicated(duplicatedName string) bool { db := database.GetDB() var scenarios []database.Scenario db.Find(&scenarios, "name = ?", duplicatedName) return (len(scenarios) > 0) }