Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Password-less Auth in Go – Hands-on Part 1
Abhik Banerjee
Abhik Banerjee

Posted on • Originally published atabhik.hashnode.dev

Password-less Auth in Go – Hands-on Part 1

Picking up right where we left off, we will start with the packages before we come to themain package. This will mean taking a bottom-up approach to coding our Go server. So, without further ado…

Utility Package

Why are we choosing this one first? For the simple reason that theutils package in a Go project contains most of the independent code. Other packages typically use this package. In ourutils folder insidepkg folder, we will create autils.go which will look like the code below:

packageutilsimport("crypto/rand""encoding/json""fmt""log""net/http""os""regexp""time""github.com/go-playground/validator/v10""github.com/golang-jwt/jwt/v5""github.com/joho/godotenv""github.com/sendgrid/sendgrid-go""github.com/sendgrid/sendgrid-go/helpers/mail""github.com/twilio/twilio-go"api"github.com/twilio/twilio-go/rest/api/v2010""go.mongodb.org/mongo-driver/bson/primitive")varvalidate=validator.New()typeGenericJsonResponseDTOstruct{Messagestring`json:"message"`}// DecodeJSONRequest decodes the JSON request body into the provided interface and validates it.funcDecodeJSONRequest(r*http.Request,vinterface{})error{err:=json.NewDecoder(r.Body).Decode(v)iferr!=nil{returnerr}// Validate the decoded structreturnvalidate.Struct(v)}// EncodeJSONResponse encodes the provided interface as JSON and writes it to the response writer.funcEncodeJSONResponse(whttp.ResponseWriter,statusint,vinterface{})error{w.Header().Set("Content-Type","application/json")w.WriteHeader(status)returnjson.NewEncoder(w).Encode(v)}funcGetENV(namestring)string{err:=godotenv.Load(".env.local")iferr!=nil{log.Fatal("ERROR while loading the env file")log.Fatal(err)}returnos.Getenv(name)}funcIsValidPhoneNumber(phoneNostring)bool{e164Regex:=`^\+[1-9]\d{1,14}$`re:=regexp.MustCompile(e164Regex)returnre.Find([]byte(phoneNo))!=nil}funcGenerateJWT(idstring)(string,error){claims:=jwt.RegisteredClaims{// Also fixed dates can be used for the NumericDateExpiresAt:jwt.NewNumericDate(time.Now().Add(time.Hour*2)),Issuer:GetENV("JWT_ISSUER"),Subject:id,}token:=jwt.NewWithClaims(jwt.SigningMethodHS256,claims)returntoken.SignedString([]byte(GetENV("JWT_SECRET")))}funcValidateJWT(tokenStringstring)(jwt.MapClaims,error){token,err:=jwt.Parse(tokenString,func(token*jwt.Token)(interface{},error){if_,ok:=token.Method.(*jwt.SigningMethodHMAC);!ok{returnnil,fmt.Errorf("Invalid Token Signing Method: %v",token.Header["alg"])}return[]byte(GetENV("JWT_SECRET")),nil})iferr!=nil{returnnil,err}if!token.Valid{returnnil,fmt.Errorf("Invalid Token")}ifclaims,ok:=token.Claims.(jwt.MapClaims);ok{returnclaims,nil}else{returnnil,fmt.Errorf("Invalid Token Claim")}}funcOTPGenerator()(string,error){constotpChars="0123456789"buffer:=make([]byte,8)_,err:=rand.Read(buffer)iferr!=nil{return"",err}otpCharsLength:=len(otpChars)fori:=0;i<8;i++{buffer[i]=otpChars[int(buffer[i])%otpCharsLength]}returnstring(buffer),nil}funcSendOTPMail(receipientEmailstring,otpstring)error{from:=mail.NewEmail(GetENV("SENDER_NAME"),GetENV("SENDER_EMAIL"))// Change to your verified sendersubject:="Your Login OTP"to:=mail.NewEmail(receipientEmail,receipientEmail)plainTextContent:=fmt.Sprintf("Your OTP is %s",otp)htmlContent:=fmt.Sprintf("Your OTP is <strong>%s</string>",otp)message:=mail.NewSingleEmail(from,subject,to,plainTextContent,htmlContent)client:=sendgrid.NewSendClient(os.Getenv("SENDGRID_API_KEY"))if_,err:=client.Send(message);err!=nil{log.Println("[ERROR]",err)returnerr}returnnil}funcSendOTPSms(receipientNostring,otpstring)error{client:=twilio.NewRestClient()params:=&api.CreateMessageParams{}params.SetBody(fmt.Sprintf("Your OTP for Login is %s.",otp))params.SetFrom(GetENV("TWILLIO_PHONE_NO"))params.SetTo(receipientNo)_,err:=client.Api.CreateMessage(params)returnerr}funcIsValidObjectID(idstring)bool{// Use a regular expression to check for the correct format (hexadecimal, 24 characters).objectIDRegex:=regexp.MustCompile(`^[0-9a-fA-F]{24}$`)if!objectIDRegex.MatchString(id){returnfalse}// Use the MongoDB Go driver to try parsing the string as an ObjectID._,err:=primitive.ObjectIDFromHex(id)returnerr==nil}
Enter fullscreen modeExit fullscreen mode

We will use the Go-Validator module to validate the type of our request body. We initialize the validator at line 23. TheGenericJsonResponseDTO contains a generic structure of a response body. We can use it by itself or by embedding it inside another DTO as we will see later. TheDecodeJSONRequest() function takes in the request and then unmarshals the request body into the struct passed in the second argument. Because we are going to use it with a variety of DTOs, we use the open-endedinterface{} type in Go. This will allow us to pass any struct.

Next, we have theEncodeJSONRequest() function. This function takes theResponseWriter interface that is used by a Go HTTP handler to construct an HTTP response, an HTTP response status code, and an interface (typically our response struct DTO) and marshals the data in the struct DTO to the response body.

TheGetENV() function at line 47 is a wrapper to help us get any environment variable in a single line using thegodotenv package. The point to note here is that we will pick our environment variables from the.env.local file as shown in line 48. After that we have a simple function which validates if a given string is a valid phone number using RegExp.

We need to generate JWT for access control. For this, we will use thegolang-jwt module. At line 63, we create a wrapper calledGenerateJWT() which takes the Mongo DB ID and then creates a JWT. We put the ID into thesubject field of the JWT. This helps us in getting back the user ID easily using thegetSubject() function ofgolang-jwt package. TheGenerateJWT() function returns a signed JWT and anerror. If everything is all right, then theerror isnil.

At line 74, we have theValidateJWT() function. This takes a JWT and then decodes the JWT usingjwt.Parse(). Thejwt.Parse() takes the encoded JWT token and a function that returns the secret used to sign the JWT. Inside the function, at line 77, we also check if the token is signed using the algorithm of our choice. If a token has expired, then even if the token is decoded for its data, it stands as invalid. At line 87, we check if the token is “valid”. We take out the JWT claims and return them with the error asnil at line 91. This completes the happy flow of JWT token validation.

TheOTPGenerator() function returns a random 8-digit OTP for our use. It’s theSendOTPMail() that’s more interesting. It is a wrapper around SendGrid’s Mail SDK. It takes the email of the person to send the email to and the OTP. It then formats a mail and sends it to the recipient. It uses the API key obtained from SendGrid’s Dashboard. You also need to make sure that you have verified sender to complete this step.

You can start by having aSingle Sender Verification done for SendGrid account by following the steps listedhere.

TheSendOTPSms() function uses Twilio’s REST Client for sending an SMS. This means you need to have a registered phone number beforehand on Twilio’s Dashboard. Note how, unlike the Mail SDK, this one does not need any API key. The helpers under the hood consume the API key you have kept in the .env file. It provides a better DevX.

Lastly, theIsValidObjectID() takes in a string and then returns if the string is a valid MongoDB object ID. This is used for request and JWT-related validation purposes. Because the subject of our JWT will have the user ID, this utility function will help us validate the JWT in more ways than one.

Config Package

Theconfig package in our Golang web server, as you might have guessed, will contain the configuration files. More specifically, database connections and contexts will be exported from this file. But it won’t function quite the same way as you might have guessed if you are coming from another language. The code given below sums up what we will have in ourapp.go in ourconfig package:

packageconfigimport("context""fmt""log""github.com/abhik-99/passwordless-login/pkg/utils""github.com/go-redis/redis/v8""go.mongodb.org/mongo-driver/mongo""go.mongodb.org/mongo-driver/mongo/options""go.mongodb.org/mongo-driver/mongo/readpref")var(Db*mongo.DatabaseMongoCtxcontext.Contextclient*mongo.ClientRdb*redis.ClientRedisCtxcontext.Context)funcinit(){MongoCtx=context.Background()client,err:=mongo.Connect(MongoCtx,options.Client().ApplyURI(fmt.Sprintf("mongodb://%s:%s@localhost:27017/?retryWrites=true&w=majority",utils.GetENV("DBUSER"),utils.GetENV("DBPASS"))))iferr!=nil{// panic(err)log.Println(err)}iferr=client.Ping(MongoCtx,readpref.Primary());err==nil{log.Print("Connection to DB Successful")}else{log.Println("ERROR while pinging DB")log.Panic(err)return}Db=client.Database("passwordless-auth")Db.CreateCollection(MongoCtx,"user-collection")Rdb=redis.NewClient(&redis.Options{Addr:utils.GetENV("REDISADDR"),Password:utils.GetENV("REDISPASS"),DB:0,// use default DB})RedisCtx=context.Background()}funcDisconnect(){client.Disconnect(MongoCtx)Rdb.Close()}
Enter fullscreen modeExit fullscreen mode

Between lines 16 and 24, we define the variables we mean to export to other packages. As is apparent, we are exposing theDb andMongoCtx variables. The former is the connection to our specific Mongo database (which we will callpasswordless-auth). We are not going to export our Mongo Client though. The last two variables are connection to our Redis Database instance and the respective context.

Notice how we are using theGetENV() function from theutility package to get the DB username and password for the connection string.

Next, we have a special function calledinit(). What makes theinit() function special in Go? Well, theinit() function in Golang is a lifecycle function which is called even before themain() function is called. It initializes the application. You can have aninit() function in every package and be assured that those will be executed even before themain() function is. This assures that some states are set before the app runs.

Inside ourinit() function, we first create acontext and assign it toMongoCtx. We then create a client with the connection URL which helps us connect to our Docker Container where Mongo is running (refer to the previous article in the series). At line 42, we create a pointer to thepasswordless-auth database.A new DB will not be created in this case until we create a collection in it. So, we create a MongoDB collection calleduser-collection.

ThecreateCollection() function will create our collection if it does not exist. Otherwise, it will quietly send an error. We won’t concern ourselves with the error in this case as this step ensures that our collection is created before we start using it and we don’t end up with a null pointer error.

Lastly, we initialize the connection to our Redis instance running in a Docker container (again, refer to the previous article in the series). The Rdb and RedisCtx are Redis equivalents of the Mongo connection and context.

TheDisconnect() function being exported here is going to be invoked when the application is stopped. It is meant to close all the connections and gracefully shut down our application. It will be used in themain package.

Main Package

At this point, we have crossed the 1k-word limit. So, I am a bit wary. But I feel that this article will not be complete without a discussion of themain package. If you are new to Go, themain package is meant to house ourmain() function which is run right after anyinit() function by Go when the application starts execution. In our case, we will keep themain.go inside thecmd folder.

packagemainimport("fmt""log""net/http""github.com/abhik-99/passwordless-login/pkg/config""github.com/abhik-99/passwordless-login/pkg/routes""github.com/gorilla/mux")funcmain(){deferconfig.Disconnect()router:=mux.NewRouter()router.StrictSlash(true)authRouter:=router.PathPrefix("/auth").Subrouter()routes.RegisterAuthRoutes(authRouter)userRouter:=router.PathPrefix("/user").Subrouter()routes.RegisterUserRoutes(userRouter)fmt.Println("Started on PORT 3000")http.Handle("/",router)log.Fatal(http.ListenAndServe(":3000",router))}
Enter fullscreen modeExit fullscreen mode

As shown in the code above, the first thing we do is start bydeferring the execution of theDisconnect() function which we defined in theconfig package in the previous section. This is convenient as it ensures “we don’t forget about closing DB connections”.

Next, we create our main router. We setStrictSlash() totrue on our router. To understand the effect of this with an example, this means that the paths trailing with/user and/user/ are evaluated to the same controller.

At line 19, we create our authentication subrouter calledauthRouter. Any routes registered to this router will have the prefix/auth in them. We then pass this Gorilla Mux subrouter to theRegisterAuthRoutes() function in theroutes package.

We are yet to define theroutes andcontrollers package.

Similarly, we create our user subrouter and pass it toRegisterUserRoutes() function which we will define in the next article. This shows the finesse of Gorilla Mux. It allows easy grouping of routes through this system. One can similarly group our protected and public routes in their project when using Gorilla Mux.

After this, we use thenet/http module from the Standard Go library to start listening with our Gorilla Mux router which we created in line 16. This completes three of the packages of our application.

Conclusion

In this article, we took a hands-on approach to writing our Go web server in Gorilla Mux. I am way past the 1k-ish word limit so I am going to end this article at this point. In the next articles in this series, we will define our controllers and models for our specific routes. Until then, keep building awesome things and WAGMI!

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Web3 Dev, Foodie, Cats and Anime Lover rolled into one. Ping me up if you have any article topic recommendations ;)
  • Location
    Kolkata, West Bengal, India.
  • Joined

More fromAbhik Banerjee

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp