-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature(auth) Local Auth and beta api token implementation (#463)
* wip(auth) new local auth feature * wip(local auth) overall endpoints and db migrations * wip(pgrest) alter app state * wip * feature(localauth) middleware with context * feature(local auth) build context properly * wip(auth) api key * feature(auth) api token for gateway requests * feature(auth) allow api auth communication at grpc server * refactor(auth) make local auth work for default org * feature(auth) local auth frontend login and register basic UI * feature(users) add local auth user creation with password * fix(users) user id on creation for local users * fix(users) webapp console.log clean up * refactor(migration) remove reference * fix(project) remove overall dirty code * fix(middleware) remove prints * refactor(appconfig) adjust local auth definition logic to default to IDP if any is defined * refactor(model) remove commented code from orgs model * refactor(auth) overall refactors and improvements * refactor(local auth) remove need of storing jwt in our db * fix(auth) overall fixes and code styles * refactor(local auth) allow only the default organization to be used * refactor(auth) make db call after checking api key * refactor(orgs) return original behavior for the createdefaultorg method * refactor(auth) add allow-api-key context value to const * refactor(auth) remove else statement in favor of break in switch case * refactor(config) force jwt secret when auth method is set as local * feature(auth) create middleware to protect local auth routes * refactor(auth) use radix buttons for local auth register and login forms * refactor(auth) improve api key verifications * docs(env) better description for API KEY in .env.sample * Add abort on middleware to stop processing further routes Refactor localAuthMethod function --------- Co-authored-by: San <sandromll@gmail.com>
- Loading branch information
1 parent
a6ecd72
commit 8aea06e
Showing
34 changed files
with
986 additions
and
123 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
package localauthapi | ||
|
||
import ( | ||
"net/http" | ||
"time" | ||
|
||
"github.com/gin-gonic/gin" | ||
"github.com/golang-jwt/jwt/v5" | ||
"github.com/hoophq/hoop/common/log" | ||
"github.com/hoophq/hoop/gateway/api/openapi" | ||
"github.com/hoophq/hoop/gateway/appconfig" | ||
pgusers "github.com/hoophq/hoop/gateway/pgrest/users" | ||
"golang.org/x/crypto/bcrypt" | ||
) | ||
|
||
func Login(c *gin.Context) { | ||
var user openapi.User | ||
if err := c.BindJSON(&user); err != nil { | ||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||
return | ||
} | ||
|
||
dbUser, err := pgusers.GetOneByEmail(user.Email) | ||
if err != nil { | ||
log.Debugf("failed fetching user by email %s, %v", user.Email, err) | ||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"}) | ||
return | ||
} | ||
|
||
err = bcrypt.CompareHashAndPassword([]byte(dbUser.HashedPassword), []byte(user.HashedPassword)) | ||
if err != nil { | ||
log.Debugf("failed comparing password for user %s, %v", user.Email, err) | ||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"}) | ||
return | ||
} | ||
|
||
expirationTime := time.Now().Add(168 * time.Hour) // 7 days | ||
claims := &Claims{ | ||
UserID: dbUser.ID, | ||
UserEmail: dbUser.Email, | ||
UserSubject: dbUser.Subject, | ||
RegisteredClaims: jwt.RegisteredClaims{ | ||
ExpiresAt: jwt.NewNumericDate(expirationTime), | ||
}, | ||
} | ||
|
||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) | ||
tokenString, err := token.SignedString(appconfig.Get().JWTSecretKey()) | ||
if err != nil { | ||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"}) | ||
return | ||
} | ||
|
||
c.Header("Access-Control-Expose-Headers", "Token") | ||
c.Header("Token", tokenString) | ||
|
||
c.JSON(http.StatusOK, gin.H{"status": "ok"}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
package localauthapi | ||
|
||
import ( | ||
"fmt" | ||
"net/http" | ||
"time" | ||
|
||
"github.com/hoophq/hoop/common/log" | ||
|
||
"github.com/gin-gonic/gin" | ||
"github.com/golang-jwt/jwt/v5" | ||
"github.com/google/uuid" | ||
"github.com/hoophq/hoop/gateway/api/openapi" | ||
"github.com/hoophq/hoop/gateway/appconfig" | ||
"github.com/hoophq/hoop/gateway/pgrest" | ||
pgorgs "github.com/hoophq/hoop/gateway/pgrest/orgs" | ||
pgusers "github.com/hoophq/hoop/gateway/pgrest/users" | ||
"github.com/hoophq/hoop/gateway/storagev2/types" | ||
"golang.org/x/crypto/bcrypt" | ||
) | ||
|
||
func Register(c *gin.Context) { | ||
var user openapi.User | ||
if err := c.BindJSON(&user); err != nil { | ||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||
return | ||
} | ||
|
||
log.Debugf("looking for existing user %v", user.Email) | ||
// fetch user by email | ||
existingUser, err := pgusers.GetOneByEmail(user.Email) | ||
if err != nil { | ||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch user"}) | ||
return | ||
} | ||
if existingUser != nil { | ||
c.JSON(http.StatusConflict, gin.H{"error": "User already exists"}) | ||
return | ||
} | ||
|
||
// local auth creates the user at the default organization for now. | ||
// we plan to make it much more permissive, but at this moment this | ||
// limitation comes to make sure things are working as expected. | ||
org, totalUsers, err := pgorgs.New().FetchOrgByName("default") | ||
if err != nil { | ||
log.Debugf("failed fetching default organization, err=%v", err) | ||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch default organization"}) | ||
return | ||
} | ||
// if there is one user already, do not allow new users to be created | ||
// it avoids a security issue of anyone being able to add themselves to | ||
// the default organization. Instead, they should get an invitation | ||
if totalUsers > 0 { | ||
log.Debugf("default organization already has users") | ||
c.AbortWithStatus(http.StatusForbidden) | ||
return | ||
} | ||
|
||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.HashedPassword), bcrypt.DefaultCost) | ||
if err != nil { | ||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"}) | ||
return | ||
} | ||
|
||
adminGroupName := types.GroupAdmin | ||
userID := uuid.New().String() | ||
err = pgusers.New().Upsert(pgrest.User{ | ||
ID: userID, | ||
Subject: fmt.Sprintf("local|%v", userID), | ||
OrgID: org.ID, | ||
Email: user.Email, | ||
Name: user.Name, | ||
Status: "active", | ||
Verified: true, | ||
HashedPassword: string(hashedPassword), | ||
Groups: []string{adminGroupName}, | ||
}) | ||
|
||
if err != nil { | ||
log.Debugf("failed creating user, err=%v", err) | ||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"}) | ||
return | ||
} | ||
|
||
expirationTime := time.Now().Add(168 * time.Hour) // 7 days | ||
claims := &Claims{ | ||
UserID: userID, | ||
UserEmail: user.Email, | ||
UserSubject: fmt.Sprintf("local|%v", userID), | ||
RegisteredClaims: jwt.RegisteredClaims{ | ||
ExpiresAt: jwt.NewNumericDate(expirationTime), | ||
}, | ||
} | ||
|
||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) | ||
tokenString, err := token.SignedString(appconfig.Get().JWTSecretKey()) | ||
if err != nil { | ||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"}) | ||
return | ||
} | ||
|
||
c.Header("Access-Control-Expose-Headers", "Token") | ||
c.Header("Token", tokenString) | ||
|
||
c.JSON(http.StatusCreated, gin.H{"message": "User created successfully"}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package localauthapi | ||
|
||
import "github.com/golang-jwt/jwt/v5" | ||
|
||
type Claims struct { | ||
UserID string `json:"user_id"` | ||
UserEmail string `json:"user_email"` | ||
UserSubject string `json:"user_subject"` | ||
jwt.RegisteredClaims | ||
} |
Oops, something went wrong.