forked from itsabot/itsabot
/
auth.go
163 lines (156 loc) · 4.37 KB
/
auth.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
// Package auth ensures that a user has sufficient permissions to access
// content.
package auth
import (
"crypto/hmac"
"crypto/sha512"
"database/sql"
"encoding/base64"
"encoding/json"
"net/http"
"os"
"strconv"
"strings"
"time"
"github.com/itsabot/abot/core"
"github.com/itsabot/abot/shared/log"
"github.com/itsabot/abot/shared/util"
"github.com/jmoiron/sqlx"
"github.com/labstack/echo"
)
const bearerAuthKey = "Bearer"
// Header represents an HTTP request's header from the front-end JS client. This
// is used to identify the logged in user in each web request and the
// permissions of that user.
type Header struct {
ID uint64
Email string
Scopes []string
IssuedAt int64
}
// LoggedIn determines if the user is currently logged in.
func LoggedIn() echo.HandlerFunc {
return func(c *echo.Context) error {
log.Debug("validating logged in")
c.Response().Header().Set(echo.WWWAuthenticate, bearerAuthKey+" realm=Restricted")
auth := c.Request().Header.Get(echo.Authorization)
l := len(bearerAuthKey)
// Ensure client sent the token
if len(auth) <= l+1 || auth[:l] != bearerAuthKey {
log.Debug("client did not send token")
return core.JSONError(echo.NewHTTPError(http.StatusUnauthorized))
}
// Ensure the token is still valid
tmp, err := util.CookieVal(c, "issuedAt")
if err != nil {
return core.JSONError(err)
}
issuedAt, err := strconv.ParseInt(tmp, 10, 64)
if err != nil {
return core.JSONError(err)
}
t := time.Unix(issuedAt, 0)
if t.Add(72 * time.Hour).Before(time.Now()) {
log.Debug("token expired")
return core.JSONError(echo.NewHTTPError(http.StatusUnauthorized))
}
// Ensure the token has not been tampered with
b, err := base64.StdEncoding.DecodeString(auth[l+1:])
if err != nil {
return core.JSONError(err)
}
tmp, err = util.CookieVal(c, "scopes")
if err != nil {
return core.JSONError(err)
}
scopes := strings.Fields(tmp)
tmp, err = util.CookieVal(c, "id")
if err != nil {
return core.JSONError(err)
}
userID, err := strconv.ParseUint(tmp, 10, 64)
if err != nil {
return core.JSONError(err)
}
email, err := util.CookieVal(c, "email")
if err != nil {
return core.JSONError(err)
}
a := Header{
ID: userID,
Email: email,
Scopes: scopes,
IssuedAt: issuedAt,
}
byt, err := json.Marshal(a)
if err != nil {
return core.JSONError(err)
}
known := hmac.New(sha512.New, []byte(os.Getenv("ABOT_SECRET")))
_, err = known.Write(byt)
if err != nil {
return core.JSONError(err)
}
ok := hmac.Equal(known.Sum(nil), b)
if !ok {
log.Debug("token tampered")
return core.JSONError(echo.NewHTTPError(http.StatusUnauthorized))
}
log.Debug("validated logged in")
return nil
}
}
// CSRF ensures that any forms posted to Abot are protected against Cross-Site
// Request Forgery. Without this function, Abot would be vulnerable to the
// attack because tokens are stored client-side in cookies.
func CSRF(db *sqlx.DB) echo.HandlerFunc {
return func(c *echo.Context) error {
// TODO look into other session-based temporary storage systems
// for these csrf tokens to prevent hitting the database.
// Whatever is selected must *not* introduce a dependency
// (memcached/Redis). Bolt might be an option.
if c.Request().Method == "GET" {
return nil
}
log.Debug("validating csrf")
var label string
q := `SELECT label FROM sessions
WHERE userid=$1 AND label='csrfToken' AND token=$2`
uid, err := util.CookieVal(c, "id")
if err != nil {
return core.JSONError(err)
}
token, err := util.CookieVal(c, "csrfToken")
if err != nil {
return core.JSONError(err)
}
err = db.Get(&label, q, uid, token)
if err == sql.ErrNoRows {
return echo.NewHTTPError(http.StatusUnauthorized)
}
if err != nil {
return core.JSONError(err)
}
log.Debug("validated csrf")
return nil
}
}
// Admin ensures that the current user is an admin. We trust the scopes
// presented by the client because they're validated through HMAC in LoggedIn().
func Admin() echo.HandlerFunc {
return func(c *echo.Context) error {
log.Debug("validating admin")
tmp, err := util.CookieVal(c, "scopes")
if err != nil {
return core.JSONError(err)
}
scopes := strings.Fields(tmp)
for _, scope := range scopes {
if scope == "admin" {
log.Debug("validated admin")
return nil
}
}
return core.JSONError(echo.NewHTTPError(http.StatusUnauthorized))
}
}