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

Update user route #14

Merged
merged 3 commits into from
Dec 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 9 additions & 4 deletions oauth2/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,20 @@ func main() {
r.POST("/auth/signin", api.SignInRoute())
r.GET("/auth/verify/:ref", api.VerifyEmailRoute())
r.POST("/auth/client", api.AuthClientRoute())
r.GET("/auth/authorize", authmw.AuthenticateUser(config.SECRET,
api.DB()),api.AuthorizationRoute())

r.GET("/auth/token", api.TokenRoute())

r.GET("/auth/authorize", authmw.AuthenticateUser(config.SECRET,
api.DB()), api.AuthorizationRoute())

// Resource API.
r.GET("/user", authmw.AuthenticateBearer(api.DB()), api.GetUserRoute())
r.GET("/client/:id", api.GetClientRoute())

r.GET("/user", authmw.AuthenticateBearer(config.SECRET, api.DB()),
api.GetUserRoute())

r.PUT("/user", authmw.AuthenticateBearer(config.SECRET, api.DB()),
api.UpdateUserRoute())

r.POST("/client", authmw.AuthenticateUser(config.SECRET, api.DB(),
"client.create"), api.CreateClientRoute())

Expand Down
44 changes: 22 additions & 22 deletions pkg/authapi/oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,10 @@ func (cntrl *DefaultAPIController) AuthorizationRoute() gin.HandlerFunc {
// Create implicit token.
if client.ResponseType == "token" {
token := authdb.TokenModel{
ClientID: client.ID,
UserID: user.ID,
Created: time.Now().Unix(),
TTL: 1200,
ClientID: client.ID,
UserID: user.ID,
CreatedAt: time.Now().Unix(),
TTL: 1200,
}

// Save to db.
Expand All @@ -118,10 +118,10 @@ func (cntrl *DefaultAPIController) AuthorizationRoute() gin.HandlerFunc {

// Create authorization code.
code := authdb.TokenModel{
ClientID: client.ID,
UserID: user.ID,
Created: time.Now().Unix(),
TTL: 600,
ClientID: client.ID,
UserID: user.ID,
CreatedAt: time.Now().Unix(),
TTL: 600,
}

// Save to DB.
Expand Down Expand Up @@ -216,7 +216,7 @@ func (cntrl *DefaultAPIController) handleAuthCode(c *gin.Context) {
}

// Ensure code has not expired.
if (codeExists.Created + codeExists.TTL) > time.Now().Unix() {
if (codeExists.CreatedAt + codeExists.TTL) > time.Now().Unix() {
cntrl.db.Tokens().DeleteAuthByID(codeExists.ID)
c.JSON(http.StatusUnauthorized, gin.H{
"error": "Token expired or could not be found",
Expand Down Expand Up @@ -263,10 +263,10 @@ func (cntrl *DefaultAPIController) handleAuthCode(c *gin.Context) {

// Create access token.
atoken := authdb.TokenModel{
ClientID: client.ID,
UserID: codeExists.UserID,
Created: time.Now().Unix(),
TTL: 1200,
ClientID: client.ID,
UserID: codeExists.UserID,
CreatedAt: time.Now().Unix(),
TTL: 1200,
}

aid, err := cntrl.db.Tokens().CreateAccess(atoken)
Expand All @@ -280,10 +280,10 @@ func (cntrl *DefaultAPIController) handleAuthCode(c *gin.Context) {

// Create refresh token.
rtoken := authdb.TokenModel{
ClientID: client.ID,
UserID: codeExists.UserID,
Created: time.Now().Unix(),
TTL: 5256000,
ClientID: client.ID,
UserID: codeExists.UserID,
CreatedAt: time.Now().Unix(),
TTL: 5256000,
}

rid, err := cntrl.db.Tokens().CreateRefresh(rtoken)
Expand Down Expand Up @@ -355,7 +355,7 @@ func (cntrl *DefaultAPIController) handleRefreshToken(c *gin.Context) {
}

// Ensure token is not expired.
if (token.Created + token.TTL) > time.Now().Unix() {
if (token.CreatedAt + token.TTL) > time.Now().Unix() {
cntrl.db.Tokens().DeleteRefreshByID(token.ID)
c.JSON(http.StatusBadRequest, gin.H{
"error": "Refresh token expired or could not be found",
Expand Down Expand Up @@ -383,10 +383,10 @@ func (cntrl *DefaultAPIController) handleRefreshToken(c *gin.Context) {

// Create new access token.
atoken := authdb.TokenModel{
ClientID: client.ID,
UserID: token.UserID,
Created: time.Now().Unix(),
TTL: 1200,
ClientID: client.ID,
UserID: token.UserID,
CreatedAt: time.Now().Unix(),
TTL: 1200,
}

// Save new access token to db.
Expand Down
65 changes: 62 additions & 3 deletions pkg/authapi/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,12 @@ func (cntrl *DefaultAPIController) GetUserRoute() gin.HandlerFunc {

if hasEmailScope {
c.JSON(http.StatusOK, gin.H{
"message": "success",
"user_id": user.ID,
"email": user.Email,
"first_name": user.FirstName,
"last_name": user.LastName,
"realms": user.Realms,
})
return
}
Expand All @@ -84,8 +86,65 @@ func (cntrl *DefaultAPIController) GetUserRoute() gin.HandlerFunc {
// UpdateUserRoute updates user information.
func (cntrl *DefaultAPIController) UpdateUserRoute() gin.HandlerFunc {
return func(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{
"error": "not implemented",

// Get user.
userAny, ok := c.Get("user")
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "User not found",
})
return
}

user, ok := userAny.(authdb.UserModel)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "User not found",
})
return
}

var req struct {
FirstName string `json:"first_name" binding:"required"`
LastName string `json:"last_name" binding:"required"`
}

// Extract JSON body.
if err := c.BindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Missing required fields",
})
return
}

if len(req.FirstName) > 20 || len(req.LastName) > 20 {
c.JSON(http.StatusBadRequest, gin.H{
"error": "first and/or last name are too long (> 20 chars)",
})
return
}

if len(req.FirstName) < 2 || len(req.LastName) < 2 {
c.JSON(http.StatusBadRequest, gin.H{
"error": "first and/or last name are too short",
})
return
}

user.FirstName = req.FirstName
user.LastName = req.LastName
if _, err := cntrl.db.Users().Update(user); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "an error occurred. please try again later",
})
return
}

c.JSON(http.StatusOK, gin.H{
"message": "success",
"user_id": user.ID,
"first_name": user.FirstName,
"last_name": user.LastName,
})
}
}
Expand Down Expand Up @@ -258,7 +317,7 @@ func (cntrl *DefaultAPIController) CreateClientRoute() gin.HandlerFunc {
Scope: req.Scope,
Owner: user.ID,
Key: string(pkeyHash),
Created: time.Now().Unix(),
CreatedAt: time.Now().Unix(),
TTL: 7890000, // 3 months.
}

Expand Down
11 changes: 9 additions & 2 deletions pkg/authapi/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ func (cntrl *DefaultAPIController) SignUpRoute() gin.HandlerFunc {
return
}

if len(req.FirstName) > 20 || len(req.LastName) > 20 {
c.JSON(http.StatusBadRequest, gin.H{
"error": "first and/or last name are too long (> 20 chars)",
})
return
}

// Validate Email.
if !common.ValidateEmail(req.Email) {
c.JSON(http.StatusBadRequest, gin.H{
Expand Down Expand Up @@ -92,7 +99,7 @@ func (cntrl *DefaultAPIController) SignUpRoute() gin.HandlerFunc {
LastName: req.LastName,
Realms: []string{},
LastVerified: 0,
Created: 0,
CreatedAt: 0,
},
TTL: 600, // 10 minutes.
}
Expand Down Expand Up @@ -196,7 +203,7 @@ func (cntrl *DefaultAPIController) VerifyEmailRoute() gin.HandlerFunc {
}

// Update user creation dates.
pending.User.Created = time.Now().Unix()
pending.User.CreatedAt = time.Now().Unix()
pending.User.LastVerified = time.Now().Unix()

// Sign up.
Expand Down
2 changes: 1 addition & 1 deletion pkg/authdb/clients.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type ClientModel struct {
Scope []string `bson:"scope"`
Owner string `bson:"owner"`
Key string `bson:"key"`
Created int64 `bson:"created"`
CreatedAt int64 `bson:"createdAt"`
TTL int64 `bson:"expireAfterSeconds"`
}

Expand Down
10 changes: 5 additions & 5 deletions pkg/authdb/tokens.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import (
// The Token schema is used for authentication codes, access tokens, and
// refresh tokens.
type TokenModel struct {
ID string `bson:"_id,omitempty"`
ClientID string `bson:"client_id"`
UserID string `bson:"user_id"`
Created int64 `bson:"created"`
TTL int64 `bson:"expireAfterSeconds"`
ID string `bson:"_id,omitempty"`
ClientID string `bson:"client_id"`
UserID string `bson:"user_id"`
CreatedAt int64 `bson:"createdAt"`
TTL int64 `bson:"expireAfterSeconds"`
}

// TokenController defines database operations for the OAuth2 token model.
Expand Down
2 changes: 1 addition & 1 deletion pkg/authdb/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type UserModel struct {
LastName string `bson:"last_name"`
Realms []string `bson:"realms"`
LastVerified int64 `bson:"last_verified"`
Created int64 `bson:"created"`
CreatedAt int64 `bson:"createdAt"`
}

// PendingUserModel is a sign up request that is awaiting email
Expand Down
46 changes: 42 additions & 4 deletions pkg/authmw/authmw.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ func AuthenticateClient(secret string, db authdb.Database) gin.HandlerFunc {
}

// Ensure client is not expired.
if clientExists.Created+clientExists.TTL < time.Now().Unix() {
if clientExists.CreatedAt+clientExists.TTL < time.Now().Unix() {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "client lease has expired",
})
Expand All @@ -129,7 +129,8 @@ func AuthenticateClient(secret string, db authdb.Database) gin.HandlerFunc {
}

// AuthenticateBearer is a middleware the verifies an access bearer token.
func AuthenticateBearer(db authdb.Database, scope ...string) gin.HandlerFunc {
func AuthenticateBearer(secret string, db authdb.Database,
scope ...string) gin.HandlerFunc {
return func(c *gin.Context) {
scopeStr := ""
for _, val := range scope {
Expand Down Expand Up @@ -157,6 +158,44 @@ func AuthenticateBearer(db authdb.Database, scope ...string) gin.HandlerFunc {
return
}

// Check if token is JWT.
if claims, ok := common.ValidateJWT(tkStr[1], secret); ok {
if claims.Type != "user" {
c.Header("WWW-Authenticate", "Bearer scope=\""+scopeStr+
"\" error=\"invalid_request\"")

c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "jwt must be of type 'user'",
})

return
}

// Check if subject exists.
userExists, err := db.Users().FindByID(claims.Sub)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "user not found",
})
return
}

// Ensure password hash not changed.
if userExists.Password != claims.PKey {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "user password has changed",
})
return
}

c.Set("user", userExists)
c.Set("client", authdb.ClientModel{
Scope: []string{"email", "public"},
})

return
}

// Verify key exists.
tkExists, err := db.Tokens().FindAccessByID(tkStr[1])
if err != nil {
Expand All @@ -170,7 +209,7 @@ func AuthenticateBearer(db authdb.Database, scope ...string) gin.HandlerFunc {
}

// Ensure key is not expired.
if (tkExists.Created + tkExists.TTL) > time.Now().Unix() {
if (tkExists.CreatedAt + tkExists.TTL) > time.Now().Unix() {
c.Header("WWW-Authenticate", "Bearer scope=\""+scopeStr+
"\" error=\"invalid_token\"")

Expand Down Expand Up @@ -226,7 +265,6 @@ func AuthenticateBearer(db authdb.Database, scope ...string) gin.HandlerFunc {
// Write client, user, token to context.
c.Set("user", userExists)
c.Set("client", clientExists)
c.Set("token", tkExists)
c.Next()
}
}
9 changes: 8 additions & 1 deletion pkg/common/validation.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
package common

import (
"fmt"
pwd "github.com/wagslane/go-password-validator"
"golang.org/x/crypto/bcrypt"
"net/mail"
"regexp"
pwd "github.com/wagslane/go-password-validator"
)

// ValidateEmail checks whether email is a valid email address.
func ValidateEmail(email string) bool {
if len(email) > 35 {
return false
}
if _, err := mail.ParseAddress(email); err != nil {
return false
}
Expand All @@ -17,6 +21,9 @@ func ValidateEmail(email string) bool {

// ValidatePassword checks whether password is strong enough.
func ValidatePassword(password string) error {
if len(password) > 35 {
return fmt.Errorf("password cannot be longer than 35 characters")
}
return pwd.Validate(password, 60)
}

Expand Down
Loading