How to generate unique random alphanumeric tokens

2020-07-11 07:32发布

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?

标签: go token
3条回答
【Aperson】
2楼-- · 2020-07-11 07:45

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("bob@webserver.com"))
}

$ 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("bob@webserver.com"))
}

$ 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

查看更多
疯言疯语
3楼-- · 2020-07-11 07:47

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.

查看更多
爷、活的狠高调
4楼-- · 2020-07-11 08:00

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.
查看更多
登录 后发表回答