Add token creation (login)

This commit is contained in:
Howl 2016-04-05 22:22:13 +02:00
parent b3b4dde8f2
commit d02f3f9951
6 changed files with 215 additions and 2 deletions

View File

@ -18,6 +18,8 @@ func Start(conf common.Conf, db *sql.DB) {
{
gv1 := api.Group("/v1")
{
gv1.POST("/token/new", Method(v1.TokenNewPOST, db))
// Auth-free API endpoints
gv1.GET("/ping", Method(v1.PingGET, db))
gv1.GET("/surprise_me", Method(v1.SurpriseMeGET, db))

View File

@ -1,7 +1,9 @@
package app
import (
"crypto/md5"
"database/sql"
"fmt"
"github.com/osuripple/api/common"
)
@ -10,7 +12,7 @@ import (
func GetTokenFull(token string, db *sql.DB) (common.Token, bool) {
var uid int
var privs int
err := db.QueryRow("SELECT user, privileges FROM tokens WHERE token = ? LIMIT 1", token).Scan(&uid, &privs)
err := db.QueryRow("SELECT user, privileges FROM tokens WHERE token = ? LIMIT 1", fmt.Sprintf("%x", md5.Sum([]byte(token)))).Scan(&uid, &privs)
switch {
case err == sql.ErrNoRows:
return common.Token{}, false

View File

@ -1,13 +1,27 @@
package v1
import (
"strings"
"github.com/osuripple/api/common"
)
// Boilerplate errors
var (
Err500 = common.Response{
Code: 0,
Code: 500,
Message: "An error occurred. Try again, perhaps?",
}
ErrBadJSON = common.Response{
Code: 400,
Message: "There was an error processing your JSON data.",
}
)
// ErrMissingField generates a response to a request when some fields in the JSON are missing.
func ErrMissingField(missingFields ...string) common.Response {
return common.Response{
Code: 422, // http://stackoverflow.com/a/10323055/5328069
Message: "Missing fields: " + strings.Join(missingFields, ", ") + ".",
}
}

134
app/v1/token.go Normal file
View File

@ -0,0 +1,134 @@
package v1
import (
"crypto/md5"
"database/sql"
"encoding/json"
"fmt"
"github.com/osuripple/api/common"
"golang.org/x/crypto/bcrypt"
)
type tokenNewInData struct {
// either username or userid must be given in the request.
// if none is given, the request is trashed.
Username string `json:"username"`
UserID int `json:"id"`
Password string `json:"password"`
Privileges int `json:"privileges"`
Description string `json:"description"`
}
type tokenNewOutData struct {
Username string `json:"username"`
ID int `json:"id"`
Privileges int `json:"privileges"`
Token string `json:"token,omitempty"`
Banned bool `json:"banned"`
}
// TokenNewPOST is the handler for POST /token/new.
func TokenNewPOST(md common.MethodData) (r common.Response) {
data := tokenNewInData{}
err := json.Unmarshal(md.RequestData, &data)
if err != nil {
r = ErrBadJSON
return
}
var miss []string
if data.Username == "" && data.UserID == 0 {
miss = append(miss, "username|id")
}
if data.Password == "" {
miss = append(miss, "password")
}
if len(miss) != 0 {
r = ErrMissingField(miss...)
return
}
var q *sql.Row
const base = "SELECT id, username, rank, password_md5, password_version, allowed FROM users "
if data.UserID != 0 {
q = md.DB.QueryRow(base+"WHERE id = ? LIMIT 1", data.UserID)
} else {
q = md.DB.QueryRow(base+"WHERE username = ? LIMIT 1", data.Username)
}
ret := tokenNewOutData{}
var (
rank int
pw string
pwVersion int
allowed int
)
err = q.Scan(&ret.ID, &ret.Username, &rank, &pw, &pwVersion, &allowed)
switch {
case err == sql.ErrNoRows:
r.Code = 404
r.Message = "No user with that username/id was found."
return
case err != nil:
md.C.Error(err)
r = Err500
return
}
if pwVersion == 1 {
r.Code = 418 // Teapots!
r.Message = "That user still has a password in version 1. Unfortunately, in order for the API to check for the password to be OK, the user has to first log in through the website."
return
}
if err := bcrypt.CompareHashAndPassword([]byte(pw), []byte(fmt.Sprintf("%x", md5.Sum([]byte(data.Password))))); err != nil {
if err == bcrypt.ErrMismatchedHashAndPassword {
r.Code = 403
r.Message = "That password doesn't match!"
return
}
md.C.Error(err)
r = Err500
return
}
if allowed == 0 {
r.Code = 200
r.Message = "That user is banned."
ret.Banned = true
r.Data = ret
return
}
ret.Privileges = int(common.Privileges(data.Privileges).CanOnly(rank))
var (
tokenStr string
tokenMD5 string
)
for {
tokenStr = common.RandomString(32)
tokenMD5 = fmt.Sprintf("%x", md5.Sum([]byte(tokenStr)))
ret.Token = tokenStr
id := 0
err := md.DB.QueryRow("SELECT id FROM tokens WHERE token=?", tokenMD5).Scan(&id)
if err == sql.ErrNoRows {
break
}
if err != nil {
md.C.Error(err)
r = Err500
return
}
}
_, err = md.DB.Exec("INSERT INTO tokens(user, privileges, description, token) VALUES (?, ?, ?, ?)", ret.ID, ret.Privileges, data.Description, tokenMD5)
if err != nil {
md.C.Error(err)
r = Err500
return
}
r.Code = 200
r.Data = ret
return
}

View File

@ -98,3 +98,31 @@ func (p Privileges) String() string {
}
return strings.Join(pvs, ", ")
}
var privilegeMustBe = [...]int{
1,
1,
1,
3,
3,
4,
4,
4,
4,
4,
3,
}
// CanOnly removes any privilege that the user has requested to have, but cannot have due to their rank.
func (p Privileges) CanOnly(rank int) Privileges {
newPrivilege := 0
for i, v := range privilegeMustBe {
wants := p&1 == 1
can := rank >= v
if wants && can {
newPrivilege |= 1 << uint(i)
}
p >>= 1
}
return Privileges(newPrivilege)
}

33
common/random_string.go Normal file
View File

@ -0,0 +1,33 @@
package common
import (
"math/rand"
"time"
)
const letterBytes = "0123456789abcdef"
const (
letterIdxBits = 6 // 6 bits to represent a letter index
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
)
var randSrc = rand.NewSource(time.Now().UnixNano())
// RandomString generates a random string.
func RandomString(n int) string {
b := make([]byte, n)
// A randSrc.Int63() generates 63 random bits, enough for letterIdxMax characters!
for i, cache, remain := n-1, randSrc.Int63(), letterIdxMax; i >= 0; {
if remain == 0 {
cache, remain = randSrc.Int63(), letterIdxMax
}
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
b[i] = letterBytes[idx]
i--
}
cache >>= letterIdxBits
remain--
}
return string(b)
}