diff --git a/app/method.go b/app/method.go index 9130359..41390d0 100644 --- a/app/method.go +++ b/app/method.go @@ -5,6 +5,7 @@ import ( "fmt" "reflect" "regexp" + "strings" "unsafe" "github.com/valyala/fasthttp" @@ -23,9 +24,13 @@ func initialCaretaker(c *fasthttp.RequestCtx, f func(md common.MethodData) commo qa := c.Request.URI().QueryArgs() var token string + var bearerToken bool switch { case len(c.Request.Header.Peek("X-Ripple-Token")) > 0: 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: token = string(qa.Peek("token")) case len(qa.Peek("k")) > 0: @@ -41,7 +46,15 @@ func initialCaretaker(c *fasthttp.RequestCtx, f func(md common.MethodData) commo R: red, } 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 { md.User = tokenReal doggoTags = append(doggoTags, "authorised") diff --git a/app/start.go b/app/start.go index 810f5ee..147ed3d 100644 --- a/app/start.go +++ b/app/start.go @@ -79,8 +79,10 @@ func Start(conf common.Conf, dbO *sqlx.DB) *fhr.Router { // v1 API { - r.POSTMethod("/api/v1/tokens", v1.TokenNewPOST) - r.POSTMethod("/api/v1/tokens/new", v1.TokenNewPOST) + // These require an user to pass the password in cleartext and are + // 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) // Auth-free API endpoints (public data) diff --git a/app/tokens.go b/app/tokens.go index 276fdd8..c2142db 100644 --- a/app/tokens.go +++ b/app/tokens.go @@ -2,12 +2,14 @@ package app import ( "crypto/md5" + "crypto/sha256" "database/sql" "fmt" + "strings" "time" - "zxq.co/ripple/rippleapi/common" "github.com/jmoiron/sqlx" + "zxq.co/ripple/rippleapi/common" ) // 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 priv8 bool ) - err := db.QueryRow(`SELECT + err := db.QueryRow(`SELECT t.id, t.user, t.privileges, t.private, u.privileges FROM tokens t 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 +} diff --git a/app/v1/token.go b/app/v1/token.go index 8ddb502..6fc1d67 100644 --- a/app/v1/token.go +++ b/app/v1/token.go @@ -2,8 +2,12 @@ package v1 import ( "crypto/md5" + "crypto/sha256" "database/sql" + "encoding/json" + "errors" "fmt" + "strconv" "time" "github.com/jmoiron/sqlx" @@ -138,8 +142,14 @@ func TokenSelfDeletePOST(md common.MethodData) common.CodeMessager { if md.ID() == 0 { 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", - fmt.Sprintf("%x", md5.Sum([]byte(md.User.Value)))) + var err error + 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 { md.Err(err) return Err500 @@ -160,7 +170,7 @@ type tokenResponse struct { // TokenGET retrieves a list listing all the user's public tokens. 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 { return Err500 } @@ -178,20 +188,66 @@ func TokenGET(md common.MethodData) common.CodeMessager { 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 { common.ResponseBase token } +type bearerTokenSingleResponse struct { + common.ResponseBase + bearerToken +} + // TokenSelfGET retrieves information about the token the user is connecting with. func TokenSelfGET(md common.MethodData) common.CodeMessager { 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?!") } + if md.IsBearer() { + return getBearerToken(md) + } var r tokenSingleResponse // 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 = ? "+ - common.Paginate(md.Query("p"), md.Query("l"), 50), md.User.ID).Scan( + err := md.DB.QueryRow("SELECT id, privileges, description, last_updated FROM tokens WHERE id = ?", md.User.ID).Scan( &r.ID, &r.Privileges, &r.Description, &r.LastUpdated, ) if err != nil { @@ -202,6 +258,25 @@ func TokenSelfGET(md common.MethodData) common.CodeMessager { 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, // or of all the users if no user is given. func TokenFixPrivilegesPOST(md common.MethodData) common.CodeMessager { @@ -223,7 +298,7 @@ func fixPrivileges(user int, db *sqlx.DB) { } rows, err := db.Query(` SELECT - tokens.id, tokens.privileges, users.privileges + tokens.id, tokens.privileges, users.privileges FROM tokens LEFT JOIN users ON users.id = tokens.user `+wc, params...) diff --git a/common/method_data.go b/common/method_data.go index c42125b..4c093c3 100644 --- a/common/method_data.go +++ b/common/method_data.go @@ -143,3 +143,8 @@ func (md MethodData) HasQuery(q string) bool { func (md MethodData) Unmarshal(into interface{}) error { 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 +}