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  }