Implement bearer tokens

This commit is contained in:
Morgan Bazalgette 2017-06-17 18:11:10 +02:00
parent 155750b746
commit 1136738111
No known key found for this signature in database
GPG Key ID: 40D328300D245DA5
5 changed files with 146 additions and 11 deletions

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"reflect" "reflect"
"regexp" "regexp"
"strings"
"unsafe" "unsafe"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
@ -23,9 +24,13 @@ func initialCaretaker(c *fasthttp.RequestCtx, f func(md common.MethodData) commo
qa := c.Request.URI().QueryArgs() qa := c.Request.URI().QueryArgs()
var token string var token string
var bearerToken bool
switch { switch {
case len(c.Request.Header.Peek("X-Ripple-Token")) > 0: case len(c.Request.Header.Peek("X-Ripple-Token")) > 0:
token = string(c.Request.Header.Peek("X-Ripple-Token")) token = string(c.Request.Header.Peek("X-Ripple-Token"))
case strings.HasPrefix(string(c.Request.Header.Peek("Authorization")), "Bearer "):
token = strings.TrimPrefix(string(c.Request.Header.Peek("Authorization")), "Bearer ")
bearerToken = true
case len(qa.Peek("token")) > 0: case len(qa.Peek("token")) > 0:
token = string(qa.Peek("token")) token = string(qa.Peek("token"))
case len(qa.Peek("k")) > 0: case len(qa.Peek("k")) > 0:
@ -41,7 +46,15 @@ func initialCaretaker(c *fasthttp.RequestCtx, f func(md common.MethodData) commo
R: red, R: red,
} }
if token != "" { if token != "" {
tokenReal, exists := GetTokenFull(token, db) var (
tokenReal common.Token
exists bool
)
if bearerToken {
tokenReal, exists = BearerToken(token, db)
} else {
tokenReal, exists = GetTokenFull(token, db)
}
if exists { if exists {
md.User = tokenReal md.User = tokenReal
doggoTags = append(doggoTags, "authorised") doggoTags = append(doggoTags, "authorised")

View File

@ -79,8 +79,10 @@ func Start(conf common.Conf, dbO *sqlx.DB) *fhr.Router {
// v1 API // v1 API
{ {
r.POSTMethod("/api/v1/tokens", v1.TokenNewPOST) // These require an user to pass the password in cleartext and are
r.POSTMethod("/api/v1/tokens/new", v1.TokenNewPOST) // as such really insecure.
//r.POSTMethod("/api/v1/tokens", v1.TokenNewPOST)
//r.POSTMethod("/api/v1/tokens/new", v1.TokenNewPOST)
r.POSTMethod("/api/v1/tokens/self/delete", v1.TokenSelfDeletePOST) r.POSTMethod("/api/v1/tokens/self/delete", v1.TokenSelfDeletePOST)
// Auth-free API endpoints (public data) // Auth-free API endpoints (public data)

View File

@ -2,12 +2,14 @@ package app
import ( import (
"crypto/md5" "crypto/md5"
"crypto/sha256"
"database/sql" "database/sql"
"fmt" "fmt"
"strings"
"time" "time"
"zxq.co/ripple/rippleapi/common"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"zxq.co/ripple/rippleapi/common"
) )
// GetTokenFull retrieves an user ID and their token privileges knowing their API token. // GetTokenFull retrieves an user ID and their token privileges knowing their API token.
@ -18,7 +20,7 @@ func GetTokenFull(token string, db *sqlx.DB) (common.Token, bool) {
userPrivsRaw uint64 userPrivsRaw uint64
priv8 bool priv8 bool
) )
err := db.QueryRow(`SELECT err := db.QueryRow(`SELECT
t.id, t.user, t.privileges, t.private, u.privileges t.id, t.user, t.privileges, t.private, u.privileges
FROM tokens t FROM tokens t
LEFT JOIN users u ON u.id = t.user LEFT JOIN users u ON u.id = t.user
@ -84,3 +86,41 @@ func tokenUpdater(db *sqlx.DB) {
} }
} }
} }
// BearerToken parses a Token guiven in the Authorization header, with the
// Bearer prefix.
func BearerToken(token string, db *sqlx.DB) (common.Token, bool) {
var x struct {
Scope string
Extra int
}
db.Get(&x, "SELECT scope, extra FROM osin_access WHERE access_token = ? LIMIT 1", fmt.Sprintf("%x", sha256.Sum256([]byte(token))))
if x.Extra == 0 {
return common.Token{}, false
}
var privs uint64
db.Get(&privs, "SELECT privileges FROM users WHERE id = ? LIMIT 1", x.Extra)
var t common.Token
t.ID = -1
t.UserID = x.Extra
t.Value = token
t.UserPrivileges = common.UserPrivileges(privs)
t.TokenPrivileges = oauthPrivileges(x.Scope).CanOnly(t.UserPrivileges)
return t, true
}
var privilegeMap = map[string]common.Privileges{
"read_confidential": common.PrivilegeReadConfidential,
"write": common.PrivilegeWrite,
}
func oauthPrivileges(scopes string) common.Privileges {
var p common.Privileges
for _, x := range strings.Split(scopes, " ") {
p |= privilegeMap[x]
}
return p
}

View File

@ -2,8 +2,12 @@ package v1
import ( import (
"crypto/md5" "crypto/md5"
"crypto/sha256"
"database/sql" "database/sql"
"encoding/json"
"errors"
"fmt" "fmt"
"strconv"
"time" "time"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
@ -138,8 +142,14 @@ func TokenSelfDeletePOST(md common.MethodData) common.CodeMessager {
if md.ID() == 0 { if md.ID() == 0 {
return common.SimpleResponse(400, "How should we delete your token if you haven't even given us one?!") return common.SimpleResponse(400, "How should we delete your token if you haven't even given us one?!")
} }
_, err := md.DB.Exec("DELETE FROM tokens WHERE token = ? LIMIT 1", var err error
fmt.Sprintf("%x", md5.Sum([]byte(md.User.Value)))) if md.IsBearer() {
_, err = md.DB.Exec("DELETE FROM osin_access WHERE access_token = ? LIMIT 1",
fmt.Sprintf("%x", sha256.Sum256([]byte(md.User.Value))))
} else {
_, err = md.DB.Exec("DELETE FROM tokens WHERE token = ? LIMIT 1",
fmt.Sprintf("%x", md5.Sum([]byte(md.User.Value))))
}
if err != nil { if err != nil {
md.Err(err) md.Err(err)
return Err500 return Err500
@ -160,7 +170,7 @@ type tokenResponse struct {
// TokenGET retrieves a list listing all the user's public tokens. // TokenGET retrieves a list listing all the user's public tokens.
func TokenGET(md common.MethodData) common.CodeMessager { func TokenGET(md common.MethodData) common.CodeMessager {
rows, err := md.DB.Query("SELECT id, privileges, description, last_updated FROM tokens WHERE user = ? AND private = '0'", md.ID()) rows, err := md.DB.Query("SELECT id, privileges, description, last_updated FROM tokens WHERE user = ? AND private = '0' "+common.Paginate(md.Query("p"), md.Query("l"), 50), md.ID())
if err != nil { if err != nil {
return Err500 return Err500
} }
@ -178,20 +188,66 @@ func TokenGET(md common.MethodData) common.CodeMessager {
return r return r
} }
type oauthClient struct {
ID string `json:"id"`
Name string `json:"name"`
OwnerID int `json:"owner_id"`
Avatar string `json:"avatar"`
}
// Scan scans the extra in the mysql table into Name, OwnerID and Avatar.
func (o *oauthClient) Scan(src interface{}) error {
var s []byte
switch x := src.(type) {
case string:
s = []byte(x)
case []byte:
s = x
default:
return errors.New("Can't scan non-string")
}
var vals [3]string
err := json.Unmarshal(s, &vals)
if err != nil {
return err
}
o.Name = vals[0]
o.OwnerID, _ = strconv.Atoi(vals[1])
o.Avatar = vals[2]
return nil
}
type bearerToken struct {
Client oauthClient `json:"client"`
Scope string `json:"scope"`
Privileges common.Privileges `json:"privileges"`
Created time.Time `json:"created"`
}
type tokenSingleResponse struct { type tokenSingleResponse struct {
common.ResponseBase common.ResponseBase
token token
} }
type bearerTokenSingleResponse struct {
common.ResponseBase
bearerToken
}
// TokenSelfGET retrieves information about the token the user is connecting with. // TokenSelfGET retrieves information about the token the user is connecting with.
func TokenSelfGET(md common.MethodData) common.CodeMessager { func TokenSelfGET(md common.MethodData) common.CodeMessager {
if md.ID() == 0 { if md.ID() == 0 {
return common.SimpleResponse(404, "How are we supposed to find the token you're using if you ain't even using one?!") return common.SimpleResponse(404, "How are we supposed to find the token you're using if you ain't even using one?!")
} }
if md.IsBearer() {
return getBearerToken(md)
}
var r tokenSingleResponse var r tokenSingleResponse
// md.User.ID = token id, userid would have been md.User.UserID. what a clusterfuck // md.User.ID = token id, userid would have been md.User.UserID. what a clusterfuck
err := md.DB.QueryRow("SELECT id, privileges, description, last_updated FROM tokens WHERE id = ? "+ err := md.DB.QueryRow("SELECT id, privileges, description, last_updated FROM tokens WHERE id = ?", md.User.ID).Scan(
common.Paginate(md.Query("p"), md.Query("l"), 50), md.User.ID).Scan(
&r.ID, &r.Privileges, &r.Description, &r.LastUpdated, &r.ID, &r.Privileges, &r.Description, &r.LastUpdated,
) )
if err != nil { if err != nil {
@ -202,6 +258,25 @@ func TokenSelfGET(md common.MethodData) common.CodeMessager {
return r return r
} }
func getBearerToken(md common.MethodData) common.CodeMessager {
var b bearerTokenSingleResponse
err := md.DB.
QueryRow(`
SELECT t.scope, t.created_at, c.id, c.extra
FROM osin_access t INNER JOIN osin_client c
WHERE t.access_token = ?
`, fmt.Sprintf("%x", sha256.Sum256([]byte(md.User.Value)))).Scan(
&b.Scope, &b.Created, &b.Client.ID, &b.Client,
)
if err != nil {
md.Err(err)
return Err500
}
b.Code = 200
b.Privileges = md.User.TokenPrivileges
return b
}
// TokenFixPrivilegesPOST fixes the privileges on the token of the given user, // TokenFixPrivilegesPOST fixes the privileges on the token of the given user,
// or of all the users if no user is given. // or of all the users if no user is given.
func TokenFixPrivilegesPOST(md common.MethodData) common.CodeMessager { func TokenFixPrivilegesPOST(md common.MethodData) common.CodeMessager {
@ -223,7 +298,7 @@ func fixPrivileges(user int, db *sqlx.DB) {
} }
rows, err := db.Query(` rows, err := db.Query(`
SELECT SELECT
tokens.id, tokens.privileges, users.privileges tokens.id, tokens.privileges, users.privileges
FROM tokens FROM tokens
LEFT JOIN users ON users.id = tokens.user LEFT JOIN users ON users.id = tokens.user
`+wc, params...) `+wc, params...)

View File

@ -143,3 +143,8 @@ func (md MethodData) HasQuery(q string) bool {
func (md MethodData) Unmarshal(into interface{}) error { func (md MethodData) Unmarshal(into interface{}) error {
return json.Unmarshal(md.Ctx.PostBody(), into) return json.Unmarshal(md.Ctx.PostBody(), into)
} }
// IsBearer tells whether the current token is a Bearer (oauth) token.
func (md MethodData) IsBearer() bool {
return md.User.ID == -1
}