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

Develop - PR 1 #1

Open
wants to merge 40 commits into
base: main
Choose a base branch
from
Open

Develop - PR 1 #1

wants to merge 40 commits into from

Conversation

atburke
Copy link
Owner

@atburke atburke commented May 31, 2021

This part of the implementation should be feature complete and just needs feedback.

crypto.go Outdated
// hash are hex-encoded strings.
func GenHash(password, salt string) string {
passwordIn := []byte(password)
saltIn, _ := hex.DecodeString(salt)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't drop errors here (and everywhere else) return the error to the caller and let the caller make the decision.

return &Env{db}, nil
}

func main() {
Copy link

@russjones russjones Jun 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you want to put everything in your main package (main.go and database.go). Your main package (and file) should basically just parse flags and call some library to do the actual work. This will also make it easier for you to write tests because you can target just that functionality.

For example you may want to do something like below.

/pkg/server/server.go
/pkg/server/server_test.go
/pkg/database/database.go
/pkg/database/database_test.go
/cmd
/cmd/server.go

Then /cmd/server.go which will be your main package would look something like the following:

package main

import (
    "example.com/pkg/server"
)

func main() {
   var hostport := flag.String("hostport", ":8080", "help message for flag")

   server, err := server.New(&server.Config{
      HostPort: *hostport,
   })
   if err != nil {
     log.Fatal(err)
   }
   if err := server.Start(); err != nil {
     log.Fatal(err)
   }
}

You don't have to use this exact structure, but having everything in the main package isn't a good idea.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/golang-standards/project-layout
I didn't realize that there was a standard(ish) Go project structure. Thanks for introducing it to me!

Copy link

@alex-kovoy alex-kovoy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've reviewed only the front-end part, for the initial PR overall looks good except the error handling part, I left a few comments to address. Once addressed, consider the frontend part approved.

Comment on lines 49 to 51
} else {
this.setState({ errorMessage: 'Invalid email/password.' });
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets add a proper error handling, right now all API errors get swallowed by the api.tsx so I suggest to propagate an actual error and display it to the user.

Comment on lines 19 to 22
return response.status === 200;
} catch (error) {
return false;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

either return a promise as is and let the caller to handle it, or you can so something like:

return { result, error }

so it could be used like so:

const { result, error } = api.login(xxxx)
const { error } = api.login(xxxx)
if (error) {
 ///
}


func (env *Env) login(c *gin.Context) {
now := time.Now()
csrfToken := c.Request.Header.Get("CSRF")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check that it's not empty?

func (env *Env) logout(c *gin.Context) {
// TODO: move logged in check to its own function
now := time.Now()
csrfToken := c.Request.Header.Get("CSRF")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check that it's not empty?

}

func (env *Env) login(c *gin.Context) {
now := time.Now()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Move this closer to where it's used.


func (env *Env) logout(c *gin.Context) {
// TODO: move logged in check to its own function
now := time.Now()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Move this closer to where it's used.


func Run(env *Env) error {
go func() {
tick := time.NewTicker(time.Minute)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

defer tick.Close()


err = env.db.DeleteSession(sessionToken)
if err != nil {
log.Printf("Error deleting session: %v\n", err)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this return unsuccessful status here if we failed to delete the session?

}

func (env *Env) logout(c *gin.Context) {
// TODO: move logged in check to its own function
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think if you move this logic for checking csrf token, retrieving and checking the sesion etc, that would make it a bit cleaner.

DeleteExpiredSessions(t time.Time) error

// Close closes any underlying resources.
Close()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious why doesn't this return an error? If it did, it'd make it compatible with io.Closer.

}

func (db *MySqlDatabase) Close() {
db.driver.Close()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this return an error?

}

func (db *MySqlDatabase) DeleteExpiredSessions(t time.Time) error {
stmt := "DELETE FROM Sessions WHERE expire_idle < ? OR expire_abs < ?"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this always delete any session that's existed for longer than 30 minutes (current expire_idle duration) - even if it's still "active"? I couldn't see expire_idle field being updated anywhere.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expire_idle would be updated when a user makes an authenticated request. For the level 1 task, however, the only authenticated request is logout.

Comment on lines 224 to 234
go func() {
tick := time.NewTicker(time.Minute)
for {
now := <-tick.C
log.Println("Clearing expired sessions")
err := env.db.DeleteExpiredSessions(now)
if err != nil {
log.Printf("Error while cleaning up expired sessions: %v\n", err)
}
}
}()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic should be internal to your database and not exposed to the caller. So start the goroutine in NewMySqlDatabase.

return &MySqlDatabase{driver}, nil
}

func (db *MySqlDatabase) CreatePreAuthSession(csrfToken string, initTime time.Time) (*types.Session, error) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of passing in time.Now() to each function,. the common Go pattern is to create a fake clock, something like the following:

import (
   "github.com/jonboulle/clockwork"
)

type MySqlDatabase struct {
   driver *sql.DB
   clock clockwork.Clock
}

func NewMySqlDatabase(username, password, databaseName string, clock clockwork.Clock) (*MySqlDatabase, error) {
   [...]
   if clock == nil {
     clock = clockwork.NewRealClock()
   }
   return &MySqlDatabase{
       [...]
       clock: clock,
   }
}

Now the caller does not have to worry about time and you can also fake time in tests.

Comment on lines 18 to 21
// Expired checks if the session has expired.
func (session *Session) Expired(t time.Time) bool {
return t.After(session.ExpireIdle) || t.After(session.ExpireAbs)
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Three things:

  1. Do you need this function, or can you update your FetchSession query to not fetch expired sessions then let your cleanup loop remove it whenever it's expired.
  2. The way you are setting ExpireIdle and ExpireAbs I think this will always trigger on ExpireIdle because you are not doing any renewal as far as I can tell.
  3. Renewal is not in scope, so just set a maximum session life time for your session. Upon logout just remove it. No need for idle timeouts.

return
}

if session.Expired(now) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you update your query to not return expired sessions, you don't need this function.

Comment on lines 114 to 125
isCorrectPassword, err := crypto.IsCorrectPassword(account, password)
if err != nil {
log.Println("Salt stored in database is incorrectly encoded")
c.AbortWithStatus(http.StatusInternalServerError)
return
}

if !isCorrectPassword {
log.Println("Bad password")
c.AbortWithStatus(http.StatusUnauthorized)
return
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you need to split this out, just consolidate both checks and into crypto.IsCorrectPassword and return an error if either one fails.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants