Skip to content

Commit c5da2c0

Browse files
committed
feat: Implement basic sign-in
1 parent 705c307 commit c5da2c0

File tree

1 file changed

+74
-0
lines changed

1 file changed

+74
-0
lines changed

internal/application/nethttp.go

+74
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package application
22

33
import (
4+
"crypto/sha256"
5+
"crypto/subtle"
6+
"encoding/base32"
7+
"encoding/base64"
48
"encoding/json"
59
"errors"
610
"fmt"
@@ -11,6 +15,8 @@ import (
1115

1216
"github.com/go-chi/chi"
1317
"github.com/go-chi/chi/middleware"
18+
"github.com/pquerna/otp"
19+
"github.com/pquerna/otp/totp"
1420
)
1521

1622
// SuccessResponse is used to handle successful requests.
@@ -47,6 +53,12 @@ func NewFailureResponse(code int, message string) *FailureResponse {
4753
}
4854
}
4955

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+
5062
// Utility function to send succesful response.
5163
func sendSuccessResponse(w http.ResponseWriter, successResponse *SuccessResponse) {
5264
w.Header().Set("Content-Type", "application/json")
@@ -153,6 +165,68 @@ func Configure() http.Handler {
153165
sendSuccessResponse(w, res)
154166
})
155167

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+
156230
// Declare method not allowed as a fallback.
157231
r.MethodNotAllowed(func(w http.ResponseWriter, r *http.Request) {
158232
errorMessage := fmt.Sprintf("Method '%s' is not allowed in this route!", r.Method)

0 commit comments

Comments
 (0)