1
1
package application
2
2
3
3
import (
4
+ "crypto/sha256"
5
+ "crypto/subtle"
6
+ "encoding/base32"
7
+ "encoding/base64"
4
8
"encoding/json"
5
9
"errors"
6
10
"fmt"
@@ -11,6 +15,8 @@ import (
11
15
12
16
"github.com/go-chi/chi"
13
17
"github.com/go-chi/chi/middleware"
18
+ "github.com/pquerna/otp"
19
+ "github.com/pquerna/otp/totp"
14
20
)
15
21
16
22
// SuccessResponse is used to handle successful requests.
@@ -47,6 +53,12 @@ func NewFailureResponse(code int, message string) *FailureResponse {
47
53
}
48
54
}
49
55
56
+ // AuthRequestBody is to create the basic type of an incoming authentication request body.
57
+ type AuthRequestBody struct {
58
+ Username string `json:"username"`
59
+ Password string `json:"password"`
60
+ }
61
+
50
62
// Utility function to send succesful response.
51
63
func sendSuccessResponse (w http.ResponseWriter , successResponse * SuccessResponse ) {
52
64
w .Header ().Set ("Content-Type" , "application/json" )
@@ -153,6 +165,68 @@ func Configure() http.Handler {
153
165
sendSuccessResponse (w , res )
154
166
})
155
167
168
+ // Sample POST request that returns an OTP.
169
+ r .Post ("/" , func (w http.ResponseWriter , r * http.Request ) {
170
+ authRequestBody := & AuthRequestBody {}
171
+ failureResponse := decodeJSONBody (w , r , authRequestBody )
172
+ if failureResponse != nil {
173
+ sendFailureResponse (w , failureResponse )
174
+ return
175
+ }
176
+
177
+ // Calculate SHA256 hash to prevent 'ConstantTimeCompare' leaking the length of passwords / usernames.
178
+ // SHA256 is used to quickly generate and verify the hashes - SHA512 would take a bit longer.
179
+ usernameHash := sha256 .Sum256 ([]byte (authRequestBody .Username ))
180
+ passwordHash := sha256 .Sum256 ([]byte (authRequestBody .Password ))
181
+ expectedUsernameHash := sha256 .Sum256 ([]byte ("kaede" ))
182
+ expectedPasswordHash := sha256 .Sum256 ([]byte ("kaede" ))
183
+
184
+ // Compare if username and passwords match.
185
+ // Let's claim that the username and password are 'kaede' for now.
186
+ usernameMatch := subtle .ConstantTimeCompare (usernameHash [:], expectedUsernameHash [:]) == 1
187
+ passwordMatch := subtle .ConstantTimeCompare (passwordHash [:], expectedPasswordHash [:]) == 1
188
+ if ! usernameMatch || ! passwordMatch {
189
+ sendFailureResponse (w , NewFailureResponse (http .StatusUnauthorized , "Username or password do not match!" ))
190
+ return
191
+ }
192
+
193
+ // After this, we should check Redis and verify if there is a cache with this user.
194
+ // If not, simply send them an OTP. The secret, same as above, is 'kaedeKIMURA' for now.
195
+ // 'KIMURA' is the shared secret, 'kaede' is the username. We concatenate them together.
196
+ sharedSecret := base32 .StdEncoding .EncodeToString ([]byte (fmt .Sprintf ("%s%s" , authRequestBody .Username , "KIMURA" )))
197
+ otp , err := totp .GenerateCodeCustom (sharedSecret , time .Now (), totp.ValidateOpts {
198
+ Period : 30 ,
199
+ Skew : 1 ,
200
+ Digits : 10 ,
201
+ Algorithm : otp .AlgorithmSHA512 ,
202
+ })
203
+ if err != nil {
204
+ sendFailureResponse (w , NewFailureResponse (http .StatusInternalServerError , err .Error ()))
205
+ }
206
+
207
+ // Make a response body. This is for development only. Production will send the OTP via other methods.
208
+ basicAuthInformation := base64 .StdEncoding .EncodeToString ([]byte (fmt .Sprintf ("%s:%s" , authRequestBody .Username , otp )))
209
+ basicAuthContent := fmt .Sprintf ("%s%s" , "Basic " , basicAuthInformation )
210
+ decodedBasicAuth , err := base64 .StdEncoding .DecodeString (basicAuthInformation )
211
+ if err != nil {
212
+ sendFailureResponse (w , NewFailureResponse (http .StatusInternalServerError , err .Error ()))
213
+ }
214
+
215
+ // Anonymous struct.
216
+ responseData := struct {
217
+ OTP string `json:"otp"`
218
+ Username string `json:"user"`
219
+ BasicAuthContent string `json:"basicAuth"`
220
+ DecodedBasicAuth string `json:"decodedBasic"`
221
+ }{
222
+ OTP : otp ,
223
+ Username : authRequestBody .Username ,
224
+ BasicAuthContent : basicAuthContent ,
225
+ DecodedBasicAuth : string (decodedBasicAuth ),
226
+ }
227
+ sendSuccessResponse (w , NewSuccessResponse (http .StatusOK , "Sucessfully logged in!" , responseData ))
228
+ })
229
+
156
230
// Declare method not allowed as a fallback.
157
231
r .MethodNotAllowed (func (w http.ResponseWriter , r * http.Request ) {
158
232
errorMessage := fmt .Sprintf ("Method '%s' is not allowed in this route!" , r .Method )
0 commit comments