Repository navigation
Manage Authentication Sessions
The goauth package you connectors to databases to store user sessions and invalidate them after a fixed amount of time. The implementation provides a default database scheme that you can alter and methods to directly communicate with gorilla sessions. Currently MySQL, postgres, sqlite3, redis and a memcached wrapper are supported. But the use of sqlite3 may seem tempting, but it is really slow with storing unique key strings.
Contents:
- Short Example
- Supported Storages
- Deleting Invalid Keys
- Details on the Functions
- The User Identification Type
- Changing the Database Name
- Further Modifications
- Workflow
Here is a very small example that shows you the basic usage. However, you should continue reading the docs because this is very important stuff.
First you need an instance of a SessionController. This type is used to controll the sessions via functions. It requires a handler of type SessionHandler that is used to handle the actual storage (could be anything, something in place or a database storage). There are some function that put the correct handler directly inside a new controller, like NewPostgresSessionController.
db, openErr := sql.Open("postgres", "<YourConnectionInformation>")
if openErr != nil {
log.Fatal(openErr)
}
c := goauth.NewPostgresSessionController(db, "", "")
if initErr := c.Init(); initErr != nil {
log.Fatal(initErr)
}Call the Init method every time you start your app, this should not end in any errors. The storage information (for example the database table) gets only created if it does not exist already.
Let's ignore the arguments for a minute, we'll come back to that later.
If a user logged himself in via a login form (or whatever you do) you can create a new session for the user with the function CreateAuthSession:
data, key, session, err := controller.CreateAuthSession(r, store, 1, time.Duration(10*time.Minute))The required arguments for this method are:
-
r *http.RequestThe request object of the user. -
store sessions.Store: The session store from the gorilla sessions package. - Some object that identifies the user you use to identify the user in the sessions database. There is a special type UserKeyType that is simply an
interface{}. Usually you would use some kind of integer id. In the example above we created a session for the user with the id 1. - A time.Duration that specifies how long you consider the login session valid. Note that in cookies, that are used for golang secure sessions, the expiration time is meassured in an
int(seconds). So you should set this to a reasonable value, let's say a month.
It returns the following values:
-
data *goauth.SessionKeyData: This object contains the information that was saved to the storage. It contains: The user, the time the session was created and the time the session becomes invalid. -
key string: The string that was used to identify this auth session. This is a random string of a certain length, the default uses random strings of length 64. -
session *sessions.Session: The gorilla session that stores the key. -
err error: If an error occurred.
More details follow below. But note that this function does not save the session, you have to call the Save function yourself!
To validate a session use ValidateSession:
data, session, err := controller.ValidateSession(r, store)This method requires the following arguments:
-
r *http.RequestThe request object of the user. -
store sessions.Store: The session store from the gorilla sessions package.
It will lookup the session details and return the user information. data is again of type *goauth.SessionKeyData and contains the user that was used for lookup. The session is the session from the store.
More details below. It will update the MaxAge of the cookie, but again does not call Save.
To end a session, i.e. delete it entirely, you can use EndSession.
err := c.EndSession(r, store)This will set the cookie MaxAge to -1 and also remove the key from the database.
- MySQL via NewMySQLSessionController
- postgres via NewPostgresSessionController
- sqlite3 via NewSQLite3SessionController
- redis via NewRedisSessionHandler (then create a controller from the handler)
- memcached via NewMemcachedSessionHandler: Wrap another handler and then create a controller.
At some point a key becomes invalid, but it remains in the database. So you should periodically delete all invalid keys or your database will contain much useless data. You could write your own cron job and call DeleteInvalidKeys for that or simply use DeleteEntriesDaemon:
c.DeleteEntriesDaemon(time.Duration(24*time.Hour), nil, true)This will start a goroutine that clears the storage every day. The arguments are:
-
sleep time.Duration: The time after which the storage gets cleared again. -
ctx context.Context: A Context. Set to nil if you don't want to use it. Otherwise the function will listen on thectx.Donechannel and end the method if a parent context gets cancelled. See example below. -
reportErr bool: If set to true any error reported while removing the keys gets logged to stderr.
Here is an example of how to cancel this method (i.e. stop it):
b := context.Background()
ctx, cancelFunc := context.WithCancel(b)
c.DeleteEntriesDaemon(time.Duration(5*time.Second), ctx, true)
time.Sleep(15 * time.Second)
cancelFunc()We could have used a timeout of course, but this is just an example to show you how you could stop this method.
The functions CreateAuthSession, ValidateSession and EndSession all update the MaxAge of the session object but don't call session.Save in case you want to edit it by yourself.
A we've seen before this method returns data, key, session, err. Here a the causes that can produce an error:
- Something is wrong with your gorilla session store.
- Something is wrong with your random engine, i.e. we can't generate a suitable random key. If something like this happens something is really broken and you shouldn't continue.
- The underlying storage returned an error, for example an insert on your database failed.
This method returns nil, "", nil, err with err != nil if something is wrong (probably a session != nil, but this should simply be considered as a failure in any case).
If everything was fine it returns nil as an error.
This function is a bit more complex and you should make sure to understand it. As mentioned above the function returns data, session, err.
If everything is fine it returns data and session != nil. You can then consider the user to be logged in.
However there a certain errors you could choose to handle differently:
- If the session is not an auth-session, i.e. doesn't contain any information to authenticate a user it returns the error
ErrNotAuthSession. This can simply imply that the user never logged in and an auth session was created or that an old session was removed from the storage because the user didn't log in for a long time. - If the session is indeed an auth-session, i.e. stores a key but the key does not exist in the underlying storage (for example it got deleted because it became invalid) it returns the special error
ErrKeyNotFound. - If the session is an auth-session and the key still exists in the storage but is not valid any more it returns the special error
ErrInvalidKey. - In any case, if it returns an
err != nilyou should not consider the user logged in.
For more details on how we check if a session is an auth-session see below.
A small summary:
- If it returns a
*SessionKeyData != nileverything is ok, you can get the user information from the SessionKeyData element. In this case the it should also be the case thaterr != nil. - If it returns
nilas for the*SessionKeyDatasomething went wrong:- Something was wrong with the store.
-
err == ErrNotAuthSessionno authentication information was found, so probably the user has to log in and create a new sessionn. -
err == ErrKeyNotFoundauth information was provided, but the key was not found, so either someone tried a random key or the session of the user simply expired and was therefore deleted from storage. -
err == ErrInvalidKeythe key was still found in the database but is not valid any more, so probably the user hast to login again.
The function GetSession can be used to receive the auth session given the store and the request.
GetKey can be used to get the key out of the session.
Both functions are new in version v0.3
goauth creates a new gorilla session to store the auth information. This defaults to "user-auth", so you should never create a session with that name somewhere else! You can change this default name by setting the SessionName on your controller:
c.SessionName = "foo"Of course you should do that before you do anything else with that controller...
Inside this session it stores the key in session.Values["key"]. So you could add more information to the session for example the username the session was created for:
session.Values["username"] = "myuser"Don't forget that all the sessions that are returned by the functions are not saved! So call Save yourself.
As mentioned before user is identified by a unique key of type UserKeyType. Usually you identify a user by an unique integer in a database row. You could use usernames as well if you like. Simply pass the username of type string to CreateAuthSession. But in this case you have to change the database type in the user_sessions database (that's the default name for all user_sessions).
The third argument we passed to NewPostgresSessionController and it stand for userIDType. By passing the empty string we want to use a default value, which in the case of postgres is BIGINT NOT NULL. If you want to use something else use whatever type you want to use.
Note that we pass a value of type interface{} to the sql Row.Scan method. So you have to take care that the value we finally store in a SessionKeyData can be converted to a passing go type. Also we simply pass the interface{} directly to the sql INSERT. So the best thing to do: Adjust your database type and try if everything works.
There is a special case implemented in the SQLSessionHandler (a basic handler used for all kind of SQL based storages). It is called ForceUIDuint of type bool. If true value stored in the database is forced to be of type uint64. This is set to false by default, so if you wish to change that behaviour you should change the value. In this case you have to create a SQLSessionHandler by hand, set the value to true and then return a new controller. Otherwise the sql drivers will handle the integer types to be int. This should be fine, but if you want to force uint64 you could do so. Just take a look at the code of NewPostgresSessionController.
The database table that stores the sessions is by default called "user_sessions". You can change this name with the first parameter for NewPostgresSessionController. We always used "" to use the default. Simply change that name and everything should work fine.
In the example above we've always used postgres, but you could also use MySQL, sqlite (really slow) or an in memory controller (stores keys in a dictionary, not really good and also after you stop your app once it will be destroyed). See NewMySQLSessionController, NewSQLite3SessionController. They work nearly the same as postgres.
You can also change the database scheme or even more stuff. But this is far to much to document here, the above should be fine for 99% of the use cases. If you wish to do so here is a very short explanation. But you should certainly check the code for examples and the documentation for more details
-
SQLSessionTemplate: Write your own implementation of this interface and you can still use the
SQLSessionHandler. - There may be some problems with
time.Timeobjects in the database, there is a functionTimeFromScanTypethat you should look at.
This is not meant to be a complete documentation of what you should do, but it's a starting point. If you create a user, for example if users can create their own accounts, you should issue a new session.
- If you receive a request that requires authentication you can do the following: Call
ValidateSession. If the information returned by this method is valid consider the user logged in. You may want to write your own Handler that wraps other handlers. If the session is not valid consider the user as not logged in, for example forward the user to a log in page. - If you delete a user account or the user deletes his/her account delete all sessions for that account. Use the function
DeleteEntriesForUser(user UserKeyType). Also do this if you deactivate an account delete sessions. - If a user logs out (via webinterface for example) delete the key connected to this request. Use the function
DeleteKey. - If the user changes his/her password delete all sessions for that user. Also if you have a function to automatically create a new password and send it via email or something.
- Note that a single user can have multiple session (for example multiple devices).
- Use
CreateAuthSessionif a user logs in via a web interface. - Don't forget to clear the storage from time to time.
- If you think the security of your server was compromised delete all session keys! In SQL for example simply drop the table and restart the application.
This is not an exhaustive list and of course depends on how you manage your users.