/
server.go
222 lines (203 loc) · 9.26 KB
/
server.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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
// package auth provides a default authentication framework based on user name and password.
// To use it, call RegisterAuthenticationService and pass it a structure that will handle the various
// routines for authentication.
//
// On the client side, you send an auth operation to the auth endpoint by sending 2 form values:
// 1) An "op" value, which is the operation to perform. See OpHello, etc. for possible values
// 2) A "msg" value, which gets passed on the auth service you provide.
//
// You coordinate between your client and your service on how encode your messages. A common way would be
// to use json, but its up to you to do the encoding and decoding on either end.
//
// See the AuthI interface for details on what each message type should accomplish.
package auth
import (
"context"
"github.com/goradd/goradd/web/app"
"net/http"
)
// TODO: Implement Open ID with OAuth mechanisms too. Remember when doing this that you need both. OAuth is
// only for authorization and NOT authentication!
// The approach here:
// The framework here can be used in a few different ways. You can implement basic authentication, or use a bearer
// token as a refresh token. You can return these as headers, or in the body of the response. Its up to you.
//
// However, once the user successfully logs in, either after creating a new account, or using login credentials,
// you should save the user's id or some other kind of identity token in the session, and then from that point
// on the session essentially becomes the access token. This way, you do not have to pass an access token
// every time, but you could choose to do that approach if you wanted.
import (
"fmt"
"github.com/goradd/goradd/pkg/goradd"
"github.com/goradd/goradd/pkg/session"
"time"
)
func MakeAuthApiServer(a app.ApplicationI) http.Handler {
// the handler chain gets built in the reverse order of getting called
// These handlers are called in reverse order
h := serveAuthApi()
h = a.SessionHandler(h)
return h
}
// serveAuthHandler serves up the auth api.
func serveAuthApi() http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
authApiHandler(w, r)
}
return http.HandlerFunc(fn)
}
// These are the operations accepted in the op form variable
const (
OpHello = "hello" // Requires a session in order to create a new user. Helps us rate limit new user requests.
OpNewUser = "new"
OpLogin = "login"
OpTokenLogin = "token"
OpRevoke = "logout"
OpRecover = "recover" // When passing this one, you will need to specify your own recovery method in the message.
)
// authMessage is the message sent by the server. It comes through as JSON and gets unpacked into
// this structure. The keys that are reserved and that we use are listed below. You can send other
// keys in the message as well to meet your needs, and the message will get sent on to the auth service.
// The op keyword is always required. The others are required depending on the operation.
//type authMessage map[string]interface{}
const (
formOperation = "op" // The operation to perform. Possibilities are AuthOp* options above.
formMsg = "msg" // This is the message from your client to your service. Use any format you wish.
)
func authApiHandler(w http.ResponseWriter, r *http.Request) {
op := r.FormValue(formOperation)
msg := r.FormValue(formMsg)
ctx := r.Context()
switch op {
case OpHello:
// Hello is only for the first time connection in order to establish a session. We use it as a kind of authentication
// mechanism to frustrate mischief.
// If the session is already established, it means someone is behaving badly.
if !session.Has(ctx, goradd.SessionAuthTime) {
// Valid first time, so we set up a timestamp for rate limiting new account requests
// We subtract LoginRateLimit here because we expect the user to try to login once immediately after
// saying hello. We rate limit any subsequent attempts.
session.Set(ctx, goradd.SessionAuthTime, time.Now().Unix()-LoginRateLimit)
// TODO: Prevent a DoS attack here by checking for rapid hellos from the same IP address and rate limiting them
// If we do it, one article suggested returning a 200 in order to prevent an attacker from knowing they were being rate limited
} else {
// Someone is sending us a hello when we already have a session. Provided we build our apps to check, this
// would only be done by someone trying to abuse the endpoint. We simply frustrate them by
// delaying.
time.Sleep(HackerDelay)
}
case OpNewUser:
// Requesting a new user account.
if !session.Has(ctx, goradd.SessionAuthTime) {
// Session was not established. Ask them to say hello first.
authWriteError(ctx, "say hello", 404, w)
} else if session.Has(ctx, goradd.SessionAuthSuccess) {
// This session already has created a new user or has successfully logged in, do not allow this a second time
//time.Sleep(HackerDelay)
authWriteError(ctx, "session already established", 400, w)
} else if authNewUser(ctx, []byte(msg), w) {
// if the user was successfully created, we mark that so that the same session cannot create another user
session.Set(ctx, goradd.SessionAuthSuccess, true)
}
case OpLogin:
// Logging in using a user name and password
if !session.Has(ctx, goradd.SessionAuthTime) {
// Session was not established. Ask them to say hello first.
authWriteError(ctx, "say hello", 404, w)
} else if session.Has(ctx, goradd.SessionAuthSuccess) {
// This client is already logged in. If logging in again, we will assume the client tried to logout
// and something failed, like a bad connection at that moment. So, we will logout here and force the
// client to reestablish a session first
session.Clear(ctx)
session.Reset(ctx)
authWriteError(ctx, "say hello", 404, w)
} else {
lastLogin := session.Get(ctx, goradd.SessionAuthTime).(int64)
now := time.Now().Unix()
if now-lastLogin < LoginRateLimit {
authWriteError(ctx, fmt.Sprintf("%d", LoginRateLimit-(now-lastLogin)+1), 425, w)
} else {
if authLogin(ctx, []byte(msg), w) {
// if the user was successfully logged in, we mark that so that the same session cannot create another user or log in
session.Set(ctx, goradd.SessionAuthSuccess, true)
}
session.Set(ctx, goradd.SessionAuthTime, time.Now().Unix())
}
}
case OpTokenLogin:
// Logging in using a token
if !session.Has(ctx, goradd.SessionAuthTime) {
// Session was not established. Ask them to say hello first.
authWriteError(ctx, "say hello", 404, w)
} else if session.Has(ctx, goradd.SessionAuthSuccess) {
// This session already has created a new user or has successfully logged in, do not allow this a second time
//time.Sleep(HackerDelay)
authWriteError(ctx, "session already established", 400, w)
} else {
lastLogin := session.Get(ctx, goradd.SessionAuthTime).(int64)
now := time.Now().Unix()
if now-lastLogin < LoginRateLimit {
authWriteError(ctx, fmt.Sprintf("%d", LoginRateLimit-(now-lastLogin)+1), 425, w)
} else {
if authTokenLogin(ctx, []byte(msg), w) {
// if the user was successfully logged in, we mark that so that the same session cannot create another user or log in
session.Set(ctx, goradd.SessionAuthSuccess, true)
}
session.Set(ctx, goradd.SessionAuthTime, time.Now().Unix())
}
}
case OpRevoke:
if !session.Has(ctx, goradd.SessionAuthTime) {
// Session was not established. Ask them to say hello first.
authWriteError(ctx, "say hello", 404, w)
} else {
if authRevoke(ctx, []byte(msg), w) {
// kill the session
session.Clear(ctx)
session.Reset(ctx)
}
}
case OpRecover:
if !session.Has(ctx, goradd.SessionAuthTime) {
// Session was not established. Ask them to say hello first.
authWriteError(ctx, "say hello", 404, w)
} else if session.Has(ctx, goradd.SessionAuthSuccess) {
// Trying to recover when the person is already logged in. This makes no sense.
authWriteError(ctx, "session already established", 400, w)
} else {
// rate limit recovery attempts
lastLogin := session.Get(ctx, goradd.SessionAuthTime).(int64)
now := time.Now().Unix()
if now-lastLogin < LoginRateLimit {
authWriteError(ctx, fmt.Sprintf("%d", LoginRateLimit-(now-lastLogin)+1), 425, w)
} else {
authRecover(ctx, []byte(msg), w)
}
}
default:
if op == "" {
authWriteError(ctx, "No operation specified", 400, w)
} else {
authWriteError(ctx, "Invalid operation: "+op, 400, w)
}
}
}
func authNewUser(ctx context.Context, msg []byte, w http.ResponseWriter) bool {
return authService.NewUser(ctx, msg, w)
}
func authLogin(ctx context.Context, msg []byte, w http.ResponseWriter) bool {
return authService.Login(ctx, msg, w)
}
func authTokenLogin(ctx context.Context, msg []byte, w http.ResponseWriter) bool {
return authService.TokenLogin(ctx, msg, w)
}
func authRevoke(ctx context.Context, msg []byte, w http.ResponseWriter) bool {
// its important the authRevoke not write to the response writer unless there is an error, since we need to control that to close the session
return authService.RevokeToken(ctx, msg, w)
}
func authRecover(ctx context.Context, msg []byte, w http.ResponseWriter) bool {
return authService.Recover(ctx, msg, w)
}
func authWriteError(ctx context.Context, errorMessage string, errorCode int, w http.ResponseWriter) {
authService.WriteError(ctx, errorMessage, errorCode, w)
}