package auth import ( "encoding/json" "fmt" "log" "net/http" "strings" "time" "git.acooldomain.co/server-manager/backend/dbhandler" "git.acooldomain.co/server-manager/backend/factories" "git.acooldomain.co/server-manager/backend/models" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt" ) type AuthApi struct { config models.GlobalConfig tokenHandler dbhandler.InviteTokenDbHandler userAuthDbHandler dbhandler.UserPassAuthanticationDbHandler serverAuthDbHandler dbhandler.ServersAuthorizationDbHandler OidcAuthDbHandler dbhandler.OidcAuthenticationDbHandler } type Claims struct { Username string `json:"username"` Email string `json:"email"` Permissions models.Permission `json:"permissions"` } type AuthClaims struct { *jwt.StandardClaims Claims } func (con *AuthApi) signToken(token Claims) (string, error) { t := jwt.New(jwt.GetSigningMethod(con.config.Signing.Algorithm)) t.Claims = &AuthClaims{ &jwt.StandardClaims{ ExpiresAt: time.Now().Add(time.Hour * 24 * 30).Unix(), }, token, } return t.SignedString(con.config.Signing.Key) } func AuthorizedTo(requiredPermissions models.Permission) gin.HandlerFunc { return func(ctx *gin.Context) { claimsPointer, exists := ctx.Get("claims") if !exists { log.Printf("LoggedIn was not called first") ctx.AbortWithError(500, fmt.Errorf("Misconfigured method")) return } claims, ok := claimsPointer.(*AuthClaims) if !ok { ctx.AbortWithStatus(500) return } if (requiredPermissions&claims.Permissions != requiredPermissions) && (models.Admin&claims.Permissions != models.Admin) { ctx.AbortWithStatusJSON(403, "matching permissions were not found") return } } } func (con *AuthApi) LoggedIn(ctx *gin.Context) { authCookie, err := ctx.Request.Cookie("auth") if err != nil { ctx.AbortWithError(403, err) return } token, err := jwt.ParseWithClaims(authCookie.Value, &AuthClaims{}, func(token *jwt.Token) (interface{}, error) { // Don't forget to validate the alg is what you expect: if token.Method.Alg() != con.config.Signing.Algorithm { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } // hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key") return con.config.Signing.Key, nil }) if err != nil { ctx.AbortWithError(403, err) return } if !token.Valid { ctx.AbortWithStatus(403) return } claims, ok := token.Claims.(*AuthClaims) if !ok { ctx.AbortWithStatus(500) return } ctx.Set("claims", claims) } type SignUpRequest struct { Token string Username string Password string } func (con AuthApi) signUp(ctx *gin.Context) { var request SignUpRequest err := json.NewDecoder(ctx.Request.Body).Decode(&request) if err != nil { ctx.AbortWithError(500, err) return } token, err := con.tokenHandler.GetInviteToken(ctx, request.Token) if err != nil { ctx.AbortWithError(500, err) return } if token.Token == "" { ctx.AbortWithStatusJSON(403, "PermissionDenied") return } err = con.userAuthDbHandler.CreateUser(ctx, request.Username, request.Password, token.Permissions, token.Email, con.config.Users.DefaultMaxOwnedServers) if err != nil { ctx.AbortWithError(500, err) return } con.signIn(ctx) } type SignInRequest struct { Username string Password string } func (con AuthApi) signIn(ctx *gin.Context) { var request SignInRequest err := json.NewDecoder(ctx.Request.Body).Decode(&request) if err != nil { ctx.AbortWithError(500, err) return } userItem, err := con.userAuthDbHandler.AuthenticateUser(ctx, request.Username, request.Password) if err != nil { ctx.AbortWithError(403, err) return } token := Claims{ Username: userItem.Username, Permissions: userItem.Permissions, } signedToken, err := con.signToken(token) if err != nil { ctx.AbortWithError(500, err) return } ctx.SetCookie("auth", signedToken, int(time.Hour)*24*30, "", "."+con.config.Domain, true, false) ctx.IndentedJSON(http.StatusOK, signedToken) } func (con AuthApi) Verify(ctx *gin.Context) { claimsPointer, exists := ctx.Get("claims") if !exists { ctx.Status(403) return } claims := claimsPointer.(*AuthClaims) forwarded_host := ctx.Request.Header.Get("x-forwarded-host") log.Printf("Checking auth of %s", forwarded_host) domainSegments := strings.Split(forwarded_host, ".") serverId, service := domainSegments[0], domainSegments[1] switch service { case "browsers": serverPermissions, err := con.serverAuthDbHandler.GetPermissions(ctx, claims.Username, serverId) if err != nil { ctx.AbortWithError(500, err) return } if (claims.Permissions|serverPermissions)&models.Admin == models.Admin { ctx.Header("X-Username", claims.Username) log.Printf("Set header X-Username %s", claims.Username) ctx.Status(200) return } case "cloud": if claims.Permissions&models.Cloud == models.Cloud || claims.Permissions&models.Admin == models.Admin { log.Printf("Set header X-Username %s", claims.Username) ctx.Header("X-Username", claims.Username) ctx.Status(200) return } } ctx.Redirect(303, fmt.Sprintf("http://%s/login", con.config.Domain)) } func LoadGroup(group *gin.RouterGroup, config models.GlobalConfig) gin.HandlerFunc { userAuthHandler, err := factories.GetUserPassAuthDbHandler(config.Authentication.UserPass) if err != nil { panic(err) } inviteHandler, err := factories.GetInviteTokenDbHandler(config.Authentication.UserPass.InviteTokenDatabase) if err != nil { panic(err) } connection := AuthApi{ userAuthDbHandler: userAuthHandler, tokenHandler: inviteHandler, config: config, } group.POST("/signin", connection.signIn) group.POST("/signup", connection.LoggedIn, AuthorizedTo(models.Admin), connection.signUp) group.Any("/verify", connection.Verify) return connection.LoggedIn }