I've been working on a project recently that involves rebuilding one of my previous projects, which was written in Java (and Java 1.6 at that!), with the new code written in Go. I hadn't worked with Go previously, and though I'm spending time running to the library docs, and occasionally to ChatGPT or Google Gemini, I'm struck by the expressiveness of the language, and how easy it is to do many things that are very verbose in Java.
One task I just ported over was the code that generates an "auth string" that could be set into a cookie, to allow a user to log in without actually logging in. The cookie contains three pieces of data: the user ID, a randomly-generated "token" stored in the database, and a "checkvalue" combining the tool as a guard against tampering. The routine in question has to do the following:
- Make sure the user is logged in before we do this.
- Create a new random token value.
- Save that token off into the database.
- Create the full auth string from the components as described above.
Here's the original Java:
public String getAuthenticationToken() throws AccessError, DataException
{
if (!isLoggedIn())
{ // can't generate an authentication token if we're not authenticated!
logger.error("UserContext not authenticated, cannot generate auth token");
throw new AccessError("You cannot generate an authentication token without logging in.");
}
// Generate a random authentication string and poke it into the database for this user.
String tokenauth = Generator.get().generateRandomAuthString();
Connection conn = null;
PreparedStatement stmt = null;
try
{ // retrieve a connection from the data pool
conn = globalsite.getConnection(null);
stmt = conn.prepareStatement("UPDATE users SET tokenauth = ? WHERE uid = ?;");
stmt.setString(1,tokenauth);
stmt.setInt(2,uid);
stmt.executeUpdate();
}
catch (SQLException e)
{ // turn SQLException into data exception
logger.error("DB error setting token authentication string: " + e.getMessage(),e);
throw new DataException("Unable to set authentication token: " + e.getMessage(),e);
}
finally
{ // make sure the connection is released before we go
SQLUtil.shutdown(stmt);
SQLUtil.shutdown(conn);
}
// Build the full authentication token string value. int checkvalue = uid ^ tokenauth.hashCode();
StringBuffer buf = new StringBuffer(AUTH_TOKEN_PREFIX);
buf.append(uid).append(AUTH_TOKEN_SEP).append(tokenauth).append(AUTH_TOKEN_SEP).append(checkvalue);
buf.append(AUTH_TOKEN_SEP);
return buf.toString();
}
That's a mouthful! This method is implemented on the "user context," that tracks the current user for a session. The separate generateRandomAuthString method has to be part of a Generator singleton. Setting up the JDBC call involves a lot of boilerplate. (Those SQLUtil.shutdown() calls are wrappers around calling .close() on the statement and connection while catching any SQLException that gets thrown in the process.) Then we use the token's .hashCode() and the UID to make a checkvalue, before formatting the whole thing in a StringBuffer (StringBuilder didn't exist yet, much less a proper sprintf-like function).
Get ready, because here's the Go equivalent:
func (u *User) NewAuthToken() (string, error) {
if u.IsAnon {
return "", errors.New("cannot generate token for anonymous user")
}
u.Mutex.Lock()
defer u.Mutex.Unlock()
newToken := util.GenerateRandomAuthString()
if _, err := amdb.Exec("UPDATE users SET tokenauth = ? WHERE uid = ?", newToken, u.Uid); err != nil {
return "", err
}
u.Tokenauth = &newToken
checkValue := uint32(u.Uid) ^ crc32.ChecksumIEEE([]byte(newToken))
return fmt.Sprintf("AQAT:%d|%s|%d|", u.Uid, newToken, checkValue), nil
}
Now that's a lot better! Here's some of the improvements:
- This method is now on the
Userobject itself, which is fine because it'll usually be called after we've logged the user in anyway. But we still have a guard so you can't use it on an "anonymous user" (a user that isn't logged in). GenerateRandomAuthString()now lives on its own, in the separateutilpackage.- A single
Execcall on the global database object is all we need to throw the update to the server. - In lieu of a string-specific hash code, we use a CRC32, which is in the standard library.
- Finally, a simple
fmt.Sprintf()is all we need to generate the output auth string.
I'm sure all the Gophers out there will probably be telling me exactly what I did wrong. But I definitely think it's an improvement on the original!