Skip to content

Commit

Permalink
feature(auth) Local Auth and beta api token implementation (#463)
Browse files Browse the repository at this point in the history
* 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
mtmr0x and sandromello authored Sep 17, 2024
1 parent a6ecd72 commit 8aea06e
Show file tree
Hide file tree
Showing 34 changed files with 986 additions and 123 deletions.
12 changes: 12 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ LICENSE_SIGNING_KEY=

# auth configuration
API_URL=http://localhost:8009
# Set the API_KEY with the following format:
# <your-org-id>|<random-string>
# API_KEY is only available for self hosted installations
# with ORG_MULTI_TENANT set to false
API_KEY=
# If you don't set a IDP and want to go with AUTH_METHOD as local, you must provide a JWK_KEY.
AUTH_METHOD=
# Generate a random string and place it here. This key shall not be lost or changed.
# If so, all the tokens generated with it will be invalid and all users will need
# to login again. This value is only necessary if AUTH_METHOD is set to local.
JWT_SECRET_KEY=

# It takes preference over IDP_CLIENT, IDP_CLIENT_SECRET and IDP_ISSUER.
# Format: <scheme>://<client-id>:<client-secret>@<issuer-url>/?groupsclaim=<claim-name>&scopes=<scope1,scope2>&_userinfo=<0|1>
# IDP_URI=
Expand Down
58 changes: 58 additions & 0 deletions gateway/api/localauth/login.go
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"})
}
106 changes: 106 additions & 0 deletions gateway/api/localauth/register.go
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"})
}
10 changes: 10 additions & 0 deletions gateway/api/localauth/types.go
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
}
Loading

0 comments on commit 8aea06e

Please sign in to comment.