14

For a RESTful backend API, I want to generate unique url tokens to be used to authenticate users.

The unique data provided at registration to generate tokens are email addresses. But after generating tokens and sending that to the users, I don't need to decrypt received tokens to get email or other information. So the encryption can be one-way.

Initially I used bcrypt to do so:

func GenerateToken(email string) string {
    hash, err := bcrypt.GenerateFromPassword([]byte(email), bcrypt.DefaultCost)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Hash to store:", string(hash))

    return string(hash)

}

But since the tokens come as a url parameter (like /api/path/to/{token}) I can not use bcrypt because it generates tokens containing / like this:

"$2a$10$NebCQ8BD7xOa82nkzRGA9OEh./zhBOPcuV98vpOKBKK6ZTFuHtqlK"

which will break the routing.

So I'm wondering what is the best way to generate some unique 16-32 character alphanumeric tokens based on emails in Golang?

6
  • 2
    Possible duplicate of How to generate a random string of a fixed length in golang? Commented Jul 23, 2017 at 16:05
  • @AlexK. The difference is that I want the token to be not only random, but also unique. Commented Jul 23, 2017 at 16:07
  • 3
    You may use URL-safe Base64 encoding on any random data. See the exported encoding/base64/URLEncoding variable. Using the output of that, you can safely include it in URLs. Commented Jul 23, 2017 at 16:28
  • If you can generate a unique number for each user, then you can use a URL safe version of base 64. Commented Jul 23, 2017 at 16:28
  • 1
    Shouldn't it be possible to simply urlencode that string? And shouldn't it be possible for anyone to generate a token for anyone else with that algorithm ? Commented Jul 23, 2017 at 16:29

3 Answers 3

28

As it was already mentioned you are doing it wrong and this is super insecure.

  1. Generate secure token using crypto package. This token completely random and not associated with any email.
func GenerateSecureToken(length int) string {
    b := make([]byte, length)
    if _, err := rand.Read(b); err != nil {
        return ""
    }
    return hex.EncodeToString(b)
}
  1. Create database table which maps this token to user identifier and during API request validate it.
Sign up to request clarification or add additional context in comments.

3 Comments

but this is super expensive as you need to store all that data....
Why is this super insecure? Predictability?
How long it should be?
8

Option 1: md5 hash the bcrypt output

Props to OP for mostly answering his own question :)

I think it'll satisfy everything you're looking for (32-character length):

package main

import (
    "crypto/md5"
    "encoding/hex"
    "fmt"
    "log"

    "golang.org/x/crypto/bcrypt"
)

// GenerateToken returns a unique token based on the provided email string
func GenerateToken(email string) string {
    hash, err := bcrypt.GenerateFromPassword([]byte(email), bcrypt.DefaultCost)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Hash to store:", string(hash))

    hasher := md5.New()
    hasher.Write(hash)
    return hex.EncodeToString(hasher.Sum(nil))
}

func main() {
    fmt.Println("token:", GenerateToken("[email protected]"))
}

$ go run main.go

Hash to store: $2a$10$B23cv7lDpbY3iVvfZ7GYE.e4691ow8i7l6CQXkmz315fbg4jLzoue

token: 90a514ab93e2c32fdd1072154b26a100


Below are 2 of my previous suggestions that I doubt will work better for you, but could be helpful for others to consider.


Option 2: base64

In the past, I've used base64 encoding to make tokens more portable after encryption/hashing. Here's a working example:

package main

import (
    "encoding/base64"
    "fmt"
    "log"

    "golang.org/x/crypto/bcrypt"
)

// GenerateToken returns a unique token based on the provided email string
func GenerateToken(email string) string {
    hash, err := bcrypt.GenerateFromPassword([]byte(email), bcrypt.DefaultCost)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Hash to store:", string(hash))

    return base64.StdEncoding.EncodeToString(hash)
}

func main() {
    fmt.Println("token:", GenerateToken("[email protected]"))
}

$ go run main.go

Hash to store: $2a$10$cbVMU9U665VSqpfwrNZWOeU5cIDOe5iBJ8ZVa2yJCTsnk9MEZHvRq

token: JDJhJDEwJGNiVk1VOVU2NjVWU3FwZndyTlpXT2VVNWNJRE9lNWlCSjhaVmEyeUpDVHNuazlNRVpIdlJx

As you can see, this unfortunately doesn't provide you with a 16-32 character length. If you're okay with the length being 80 long, then this might work for you.


Option 3: url path/query escapes

I also tried url.PathEscape and url.QueryEscape to be thorough. While they have the same problem as the base64 example (length, though a bit shorter), at least they "should" work in the path:

// GenerateToken returns a unique token based on the provided email string
func GenerateToken(email string) string {
    hash, err := bcrypt.GenerateFromPassword([]byte(email), bcrypt.DefaultCost)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Hash to store:", string(hash))

    fmt.Println("url.PathEscape:", url.PathEscape(string(hash)))
    fmt.Println("url.QueryEscape:", url.QueryEscape(string(hash)))

    return base64.StdEncoding.EncodeToString(hash)
}

url.PathEscape: $2a$10$wx1UL6%2F7RD6sFq7Bzpgcc.ibFSW114Tf6A521wRh9rVy8dp%2Fa82x2 url.QueryEscape: %242a%2410%24wx1UL6%2F7RD6sFq7Bzpgcc.ibFSW114Tf6A521wRh9rVy8dp%2Fa82x2

5 Comments

What about feeding the result into a MD5 to shorten the resulting token, like this: hasher := md5.New(); hasher.Write([]byte(token)); return hex.EncodeToString(hasher.Sum(nil)) ?
nice idea, @Karlom - I'll try that out also
consider adding time span validity github.com/anacrolix/dht/blob/master/tokens.go
Regarding Option 2: Is there any reason not to store the base64 encoded hash?
I like option 1, could you show us how i would decode the token to get the hased token that we stored?
8

TL;DR

Please don't do this, it's not secure!. Use an existing authentication library or design a better approach.

Explaination

Authentication mechanisms can be tricky to implement properly.

Since these tokens are for authentication purposes, you don't just want them to be unique, you also need them to be unguessable. An attacker should not be able to calculate a users authentication token.

Your current implementation uses the users email address as the secret input for bcrypt. bcrypt was designed as a secure password hashing algorithm and is hence quite computationally expensive to run. Hence you probably don't want to be doing this in every request.

More importantly, your tokens are not secure. If I know your algorithm then I can generate a token for anyone by simply knowing their email address!

Also, with this approach, you cannot revoke or change a compromised token as it is calculated from the users email address. This is also a major security concern.

There are a few different approaches you could take, depending on whether you need stateless authentication or the ability to revoke tokens.

Additionally, as a matter as good practice, authentication/session tokens should not be placed in a URL as it is much easier for these to accidentally leak (e.g. cached, available to proxy servers, accidentally stored in browser history etc).

Identifiers Only?

If you aren't using your tokens for authentication then simply use a hash function on a users email address. For example, Gravatar that calculate the MD5 of a the users lowercase email address and use this to uniquely identify a user. For example:

func GravatarMD5(email string) string {
    h := md5.New()
    h.Write([]byte(strings.ToLower(email)))
    return hex.EncodeToString(h.Sum(nil))
 }

There is an infinitesimal chance of a hash collision (and hence not guaranteed to be unique) but obviously in a real life implementation this hasn't been an issue.

2 Comments

Surely this is secure – it's just an expensive way to create a unique email verification token, no? Its not an authentication Bearer token sent on every request.
The original question asked how to generate tokens to authenticate users, not create email verification tokens.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.