Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Local Auth and beta api token implementation #463

Merged
merged 33 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
4fc11d2
wip(auth) new local auth feature
mtmr0x Sep 5, 2024
5d95b84
wip(local auth) overall endpoints and db migrations
mtmr0x Sep 5, 2024
8d4bcde
wip(pgrest) alter app state
mtmr0x Sep 6, 2024
b6c6e85
wip
mtmr0x Sep 10, 2024
9968c40
feature(localauth) middleware with context
mtmr0x Sep 12, 2024
40f91bf
feature(local auth) build context properly
mtmr0x Sep 12, 2024
278cadb
wip(auth) api key
mtmr0x Sep 12, 2024
8165ab4
feature(auth) api token for gateway requests
mtmr0x Sep 13, 2024
ae90d0f
feature(auth) allow api auth communication at grpc server
mtmr0x Sep 13, 2024
223f8fe
refactor(auth) make local auth work for default org
mtmr0x Sep 14, 2024
b1eeff8
feature(auth) local auth frontend login and register basic UI
mtmr0x Sep 14, 2024
fca27ed
feature(users) add local auth user creation with password
mtmr0x Sep 15, 2024
b135b98
fix(users) user id on creation for local users
mtmr0x Sep 15, 2024
eaa471c
fix(users) webapp console.log clean up
mtmr0x Sep 15, 2024
7ca8b7c
refactor(migration) remove reference
mtmr0x Sep 15, 2024
2fb68cb
fix(project) remove overall dirty code
mtmr0x Sep 16, 2024
d039e34
fix(middleware) remove prints
mtmr0x Sep 16, 2024
d087649
refactor(appconfig) adjust local auth definition logic to default to …
mtmr0x Sep 16, 2024
4930549
refactor(model) remove commented code from orgs model
mtmr0x Sep 16, 2024
a54ae73
refactor(auth) overall refactors and improvements
mtmr0x Sep 16, 2024
7abf6cd
refactor(local auth) remove need of storing jwt in our db
mtmr0x Sep 16, 2024
5e3701c
fix(auth) overall fixes and code styles
mtmr0x Sep 16, 2024
b17b95f
refactor(local auth) allow only the default organization to be used
mtmr0x Sep 17, 2024
49581ce
refactor(auth) make db call after checking api key
mtmr0x Sep 17, 2024
ccccfd9
refactor(orgs) return original behavior for the createdefaultorg method
mtmr0x Sep 17, 2024
e4ca560
refactor(auth) add allow-api-key context value to const
mtmr0x Sep 17, 2024
db29988
refactor(auth) remove else statement in favor of break in switch case
mtmr0x Sep 17, 2024
4ac323f
refactor(config) force jwt secret when auth method is set as local
mtmr0x Sep 17, 2024
e0f979f
feature(auth) create middleware to protect local auth routes
mtmr0x Sep 17, 2024
ffd88e9
refactor(auth) use radix buttons for local auth register and login forms
mtmr0x Sep 17, 2024
1db552e
refactor(auth) improve api key verifications
mtmr0x Sep 17, 2024
718754c
docs(env) better description for API KEY in .env.sample
mtmr0x Sep 17, 2024
aacaead
Add abort on middleware to stop processing further routes
sandromello Sep 17, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ LICENSE_SIGNING_KEY=

# auth configuration
API_URL=http://localhost:8009
API_KEY=<org-id>|<random-string>
# 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())
mtmr0x marked this conversation as resolved.
Show resolved Hide resolved
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) {
mtmr0x marked this conversation as resolved.
Show resolved Hide resolved
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 {
mtmr0x marked this conversation as resolved.
Show resolved Hide resolved
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
Loading