gitlab.com/ignitionrobotics/web/ign-go@v1.0.0-rc4/access_tokens.go (about) 1 package ign 2 3 import ( 4 "crypto/rand" 5 "encoding/base64" 6 "github.com/jinzhu/gorm" 7 "golang.org/x/crypto/bcrypt" 8 "strings" 9 "time" 10 ) 11 12 // AccessToken is a single personal access token for a user. 13 type AccessToken struct { 14 // ID is the primary key for access tokens. 15 ID uint `gorm:"primary_key" json:"-"` 16 17 // CreatedAt is the time when the access token was created 18 CreatedAt time.Time `json:"created_at"` 19 20 // UserID is the user that owns this token. 21 UserID uint `json:"-"` 22 23 // Name is a string given to a token by the user. The name does not have to be unique. 24 Name string `json:"name"` 25 26 // Prefix is the first set of characters in the token. The prefix is used to identify the user 27 // and help a user keep track of their tokens. We use 'latin1_general_cs` collation to enforce 28 // case-senstive queries. 29 Prefix string `sql:"type:VARCHAR(64) CHARACTER SET latin1 COLLATE latin1_general_cs" json:"prefix"` 30 31 // Key is the second set of characters in the token, following the Prefix. The key is used to 32 // authenticate the user. We use 'latin1_general_cs` collation to enforce case-senstive queries. The key is omitted from json to prevent it from being transmitted over the wire. 33 Key string `sql:"type:VARCHAR(512) CHARACTER SET latin1 COLLATE latin1_general_cs" json:"-"` 34 35 // Last used time. 36 LastUsed *time.Time `json:"last_used"` 37 38 // For future use, when we add in the ability to expire tokens. 39 Expires *time.Time `json:"expires"` 40 } 41 42 // AccessTokens is an array of AccessToken 43 type AccessTokens []AccessToken 44 45 // AccessTokenCreateRequest contains information required to create a new access token. 46 type AccessTokenCreateRequest struct { 47 Name string `json:"name" validate:"required,min=3,alphanum"` 48 } 49 50 // AccessTokenCreateResponse contains information about a newly created access token. 51 type AccessTokenCreateResponse struct { 52 // Name is a string given to a token by the user. 53 Name string `json:"name"` 54 55 // Prefix is the first set of characters in the token. The prefix is used to identify the user 56 // and help a user keep track of their tokens. 57 Prefix string `json:"prefix"` 58 59 // Key is the second set of characters in the token, following the Prefix. The key is used to 60 // authenticate the user. 61 Key string `json:"key"` 62 } 63 64 // ValidateAccessToken checks a token string against tokens that exist in the 65 // provided database. If the access token is validated, then it is returned 66 // as the first return value. 67 func ValidateAccessToken(token string, tx *gorm.DB) (*AccessToken, *ErrMsg) { 68 // Split the token into the prefix and key parts. 69 parts := strings.Split(token, ".") 70 71 // Make sure that there are exactly two parts. 72 if len(parts) != 2 { 73 return nil, NewErrorMessage(ErrorUnauthorized) 74 } 75 76 // Get all the access tokens with the specified prefix. There should only 77 // be one, which we check for in the following `if` condition. 78 var accessTokens AccessTokens 79 if err := tx.Where("prefix = ?", parts[0]).Find(&accessTokens).Error; err != nil { 80 return nil, NewErrorMessage(ErrorUnauthorized) 81 } 82 83 // This should never happen, but it's better safe than sorry. 84 // If multiple prefixes are found, then we will assume the worse and 85 // deny authorization. 86 if len(accessTokens) != 1 { 87 return nil, NewErrorMessage(ErrorUnauthorized) 88 } 89 90 // At this point, we have a single user which can be authenticated by 91 // comparing the provided key with the salted key in the database. 92 if err := bcrypt.CompareHashAndPassword([]byte(accessTokens[0].Key), []byte(parts[1])); err != nil { 93 return nil, NewErrorMessage(ErrorUnauthorized) 94 } 95 96 return &accessTokens[0], nil 97 } 98 99 // Create instantiates a new unique random access token. The first return 100 // value is the full access token, which can be passed along to a user. 101 // Be careful with the full access token since it is a full-access key. 102 // The second return value is a salted token, which is suitable for storage 103 // in a database. The third return value is an error, or nil. 104 func (createReq *AccessTokenCreateRequest) Create(tx *gorm.DB) (*AccessTokenCreateResponse, *AccessToken, *ErrMsg) { 105 106 // Create the key 107 b := make([]byte, 32) 108 _, err := rand.Read(b) 109 if err != nil { 110 return nil, nil, NewErrorMessage(ErrorUnexpected) 111 } 112 key := base64.URLEncoding.EncodeToString(b) 113 114 var accessTokens AccessTokens 115 var prefixToken string 116 117 // An 8byte prefix would allow for 1.7e+19 tokens. 118 // Use a loop to make sure that we generate a unique prefix. 119 // Limit the loop iterations, just in case. We should always be able to 120 // generate a unique prefix. 121 for i := 0; i < 100; i++ { 122 prefix := make([]byte, 8) 123 _, err = rand.Read(prefix) 124 prefixToken = base64.URLEncoding.EncodeToString(prefix) 125 tx.Where("prefix = ?", prefixToken).Find(&accessTokens) 126 if len(accessTokens) <= 0 { 127 break 128 } 129 prefixToken = "" 130 } 131 132 // Return an error if we were not able to generate a unique prefix. 133 // This should never happen. 134 if prefixToken == "" { 135 return nil, nil, NewErrorMessage(ErrorUnexpected) 136 } 137 138 // Return the name, prefix, and key. 139 var newToken = AccessTokenCreateResponse{ 140 Name: createReq.Name, 141 Prefix: prefixToken, 142 Key: key, 143 } 144 145 // Generate the salted key, and return an error if the key could not be 146 // salted. 147 saltedKey, saltErr := bcrypt.GenerateFromPassword([]byte(key), 8) 148 if saltErr != nil { 149 return nil, nil, NewErrorMessage(ErrorUnexpected) 150 } 151 152 // Create the salted password, which is stored in the database. 153 var saltedToken = AccessToken{ 154 Name: createReq.Name, 155 Prefix: prefixToken, 156 Key: string(saltedKey), 157 } 158 159 return &newToken, &saltedToken, nil 160 }