/
auth.go
178 lines (156 loc) · 6.04 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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
package controller
// auth.go
// Provides authorization closures and structures for tokens/claims
import (
"net/http"
"github.com/satori/go.uuid"
"github.com/patrickmn/go-cache"
"time"
"github.com/rraks/remocc/pkg/models"
"golang.org/x/crypto/bcrypt"
"log"
"github.com/dgrijalva/jwt-go"
"errors"
"encoding/json"
"github.com/mitchellh/mapstructure"
"strings"
"os"
)
// Memory cache for user session tokens
var usrAuthCache *cache.Cache
// JWT signing password
var jwtPassword string
// Claims structure for devices claiming a JWT
type DevClaims struct {
DevName string `json:"devName"`
Email string `json:"email"`
Pwd string `json:"pwd"`
}
// JWT response message structure
type JWToken struct {
Token string `json:"token"`
}
func init() {
usrAuthCache = cache.New(2*time.Hour,4*time.Hour)
jwtPassword = os.Getenv("JWT_PASSWORD")
}
// Simple BCrypt hash to store user and device passwords
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 7)
return string(bytes), err
}
// Check BCrypt hash by matching
func CheckPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
// Log a users web session via cookies
func LogSession(w http.ResponseWriter, usr *models.User) {
sessionToken := uuid.NewV4().String()
usrAuthCache.Set(usr.Email, sessionToken, cache.DefaultExpiration)
email_tbl := strings.Replace(usr.Email,"@","_",-1)
email_tbl = strings.Replace(email_tbl,".","_",-1)
http.SetCookie(w, &http.Cookie{
Name: "dev_table",
Value: "devices_" + email_tbl,
Path: "/",
})
http.SetCookie(w, &http.Cookie{
Name: "email",
Value: usr.Email,
Path: "/",
})
http.SetCookie(w, &http.Cookie{
Name: "session_token",
Value: sessionToken,
Path: "/",
})
}
// A test closure to wrap web functions for testing. A user "a" with email "a@a.com" password "a" is used
func Testprovidehandler(fn func(http.ResponseWriter, *http.Request, string, string)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fn(w, r, "a@a.com", "devices_a_a_com")
}
}
// Closure to provide web content. Checks for cookies and returns to the requester the requested page
// if user's session token is present in usrAuthCache. Also provides to the API function the users email and the device table name
// TODO: No need of device table. It can be constructed from user email
func ProvideWebHandler(fn func(http.ResponseWriter, *http.Request, string, string)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
tokenCook, err1 := r.Cookie("session_token")
emailCook, err2 := r.Cookie("email")
devTableCook, err3 := r.Cookie("dev_table")
if err1 != nil || err2 !=nil || err3 != nil {
if err1 == http.ErrNoCookie || err2 == http.ErrNoCookie {
http.Redirect(w, r, "/login/", http.StatusFound)
return
}
}
sessionEmail := emailCook.Value
sessionToken := tokenCook.Value
sessionDevTable := devTableCook.Value
if val, found := usrAuthCache.Get(sessionEmail); found {
if sessionToken == val {
fn(w, r, sessionEmail, sessionDevTable)
return
}
}
http.Redirect(w, r, "/login/", http.StatusFound)
}
}
// Provides JWT token when presented with valid device claims. Checks device name, user's email, and devices password
// to return back a JWT
// TODO: Make this agnostic to the user
// TODO: Add token validity period
func DeviceLoginHandler(w http.ResponseWriter, r *http.Request) {
var devClaims DevClaims
json.NewDecoder(r.Body).Decode(&devClaims)
email_tbl := strings.Replace(devClaims.Email,"@","_",-1)
email_tbl = strings.Replace(email_tbl,".","_",-1)
hash, err := devEnv.db.GetDevPwd("devices_"+email_tbl, devClaims.DevName)
if err != nil {
log.Println(err)
}
match := CheckPasswordHash(devClaims.Pwd, hash)
// Create token, TODO :check user policies
if match == true {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"email": devClaims.Email,
"devName": devClaims.DevName,
"pwd": devClaims.Pwd,
})
tokenString, err := token.SignedString([]byte(jwtPassword)) // TODO : replace in production through init
if err != nil {
log.Println(err)
}
json.NewEncoder(w).Encode(JWToken{Token: tokenString})
}
}
// Closure to provide requested API for devices. Checks for JWT token claims and services the requested api if
// token is valid. In addition, provides the devices claim to the underlying API
func ProvideApiHandler(fn func(http.ResponseWriter, *http.Request, *DevClaims)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
key := r.Header.Get("authToken")
token, _ := jwt.Parse(key, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("Failed to validate token")
}
return []byte(jwtPassword), nil // TODO : replace in production through init
})
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
var devClaims DevClaims
mapstructure.Decode(claims, &devClaims)
email_tbl := strings.Replace(devClaims.Email,"@","_",-1)
email_tbl = strings.Replace(email_tbl,".","_",-1)
passwordHash, err := devEnv.db.GetDevPwd("devices_"+email_tbl, devClaims.DevName)
if err != nil {
w.Write([]byte("Invalid authorization"))
}
if ok = CheckPasswordHash(devClaims.Pwd, passwordHash); ok {
fn(w, r, &devClaims)
return
}
}
w.Write([]byte("Invalid authorization"))
}
}