DEV Community ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป

eliphosif
eliphosif

Posted on

Auth0 with Go - Gin backend with JWT middleware

Hi all, hope you are having a good day!
we will see how to validate the JWTs using Auth0 Golang JWT middleware using Gin Web Framework. Gin is a web framework written in Go (Golang). In deep-dive, we will see how to integrate the Auth0 Golang JWT middleware to verify JWTs

this blog is alter version of the @ksivamuthu blog
Thanks to @ksivamuthu, for writing an amazing blog, I am writing this blog as I could not find exact solution to my problem in the internet, hope it helps you today

if you follow the the above mentioned blog you would know about basics about Auth0, gin framework

basically
the user flow is like this
(refer CLI as CLIENT) and the black box as server
I am using a proxy in between (optional)

Image description

first follow this blog

you have to create an app first and I am using command line app so I have created a native app and we have to create users and user roles and assign to the users and for this example I have created a user and a role which has permission "crud:admin-settings" in Auth0 app and update your call back url in the app, I am using "http://localhost:4242/auth" as call back, make sure everything is setup before using Auth0

you call Auth0 API for login and gives a token as a response if you login successfully

basically we have to use this JWT token for our endpoints, but before actually calling our handlers we need to validate the token and permissions

example backed router go code

package api

import (
    "context"
    "errors"
    "fmt"
    "log"
    "net/http"
    "net/url"
    "strings"

    "github.com/gin-gonic/gin" 
    "github.com/lestrrat-go/jwx/jwk"
    "github.com/lestrrat-go/jwx/jwt"
)

func StartServer() {

    router := gin.Default() 


    ruoterGroup := router.Group("api/v1/product") //grouping the route end point for the handlers

    ruoterGroup.GET("", AuthorizeHandler(permission), GetHandler)
    ruoterGroup.POST("", AuthorizeHandler(permission), AddHandler)
    ruoterGroup.PUT("", AuthorizeHandler(permission), UpdateHandler)
    ruoterGroup.DELETE("", AuthorizeHandler(permission), DeleteHandler)

    baseURL := :"localhost:8080"
    router.Run(baseURL)
}
Enter fullscreen mode Exit fullscreen mode

and now you see we have a server created in gin with respective handlers, and the AuthorizeHandler shall validate the token and check permission, here you can have two separate functions to do it but I created only one

now at a common place I have this functions defined

const (
    authHeader = "Authorization"
    permClaim  = "permissions"
)

var permission = "crud:admin-settings" //your permissions here
var auth0Domain = "dev-******.us.auth0.com" //your auth0 domain here
var audience = "http://auth0-local.com"

func AuthorizeHandler(permission string) gin.HandlerFunc {
    return func(c *gin.Context) {
        token, err := extractToken(c, auth0Domain)
        if err != nil {
            fmt.Println(err.Error())
        }
        if token == nil {
            fmt.Printf("failed to find token in context\n")
            c.JSON(http.StatusUnauthorized, "user hasn't logged in yet")
            c.Abort()
            return
        }
        if !tokenHasPermission(token, permission) {
            fmt.Printf("permission check failed\n")
            c.AbortWithStatusJSON(500, "error occurred when authorizing user")
            return
        }
        c.Next()
    }
}

func tokenHasPermission(token jwt.Token, permission string) bool {
    claims := token.PrivateClaims()
    tkPermissions, ok := claims[permClaim]
    if !ok {
        return false
    }
    tkPermList, ok := tkPermissions.([]interface{})
    if !ok {
        return false
    }
    for _, perm := range tkPermList {
        if perm == permission {
            return true
        }
    }
    return false
}

func extractToken(c *gin.Context, auth0Domain string) (jwt.Token, error) {
    // fetchTenantKeys fetch and parse the tenant JSON Web Keys (JWK). The keys
    // are used for JWT token validation during requests authorization.
    tenantKeys, err := jwk.Fetch(context.Background(),
        fmt.Sprintf("https://%s/.well-known/jwks.json", auth0Domain))
    if err != nil {
        log.Fatalf("failed to parse tenant json web keys: %s\n", err)
    }
    authorization := c.GetHeader(authHeader)
    if authorization == "" {
        return nil, errors.New("authorization header missing")
    }
    bearerAndToken := strings.Split(authorization, " ")
    if len(bearerAndToken) < 2 {
        return nil, errors.New("malformed authorization header: " + authorization)
    }
    token, err := jwt.Parse([]byte(bearerAndToken[1]), jwt.WithKeySet(tenantKeys),
        jwt.WithValidate(true), jwt.WithAudience(audience))
    if err != nil {
        return nil, err
    }
    return token, nil
}

Enter fullscreen mode Exit fullscreen mode

and finally in my client I have this piece of code to get the token, basically it will open browser and goes to auth0 website and does the authentication and gives back the token which you can use for further rest API calls

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "io/ioutil"
    "net"
    "net/http"
    "net/url"
    "os"
    "strings"

    cv "github.com/nirasan/go-oauth-pkce-code-verifier"
    "github.com/skratchdot/open-golang/open"
)

func main()  {
    //typically this values will come from config file
    AuthorizeUser("ghxc6OoNP**************HnLdqJN", "dev-*******.auth0.com","http://localhost:4242/auth")
}

// AuthorizeUser implements the PKCE OAuth2 flow.
func AuthorizeUser(clientID string, authDomain string, redirectURL string) {
    // initialize the code verifier
    var CodeVerifier, _ = cv.CreateCodeVerifier()

    // Create code_challenge with S256 method
    codeChallenge := CodeVerifier.CodeChallengeS256()

    // construct the authorization URL (with Auth0 as the authorization provider)
    authorizationURL := fmt.Sprintf(
        "https://%s/authorize?"+
            "&scope=openid"+
            "&response_type=code&client_id=%s"+
            "&code_challenge=%s"+
            "&code_challenge_method=S256&redirect_uri=%s",
        authDomain, clientID, codeChallenge, redirectURL)

    // start a web server to listen on a callback URL
    server := &http.Server{Addr: redirectURL}

    // define a handler that will get the authorization code, call the token endpoint, and close the HTTP server
    http.HandleFunc("/auth", func(w http.ResponseWriter, r *http.Request) {
        // get the authorization code
        code := r.URL.Query().Get("code")
        if code == "" {
            fmt.Println("snap: Url Param 'code' is missing")
            io.WriteString(w, "Error: could not find 'code' URL parameter\n")

            // close the HTTP server and return
            cleanup(server)
            return
        }

        // trade the authorization code and the code verifier for an access token
        codeVerifier := CodeVerifier.String()
        token, err := getAccessToken(clientID, codeVerifier, code, redirectURL)
        if err != nil {
            fmt.Println("snap: could not get access token")
            io.WriteString(w, "Error: could not retrieve access token\n")

            // close the HTTP server and return
            cleanup(server)
            return
        }

        //you can see the token
        fmt.Println(token) 

        io.WriteString(w, `
        <html>
            <body>
                <h1>Login successful!</h1>
                <h2>You can close this window</h2>
            </body>
        </html>`)

        fmt.Println("Successfully logged into snapmaster API.")

        // close the HTTP server
        cleanup(server)
    })

    // parse the redirect URL for the port number
    u, err := url.Parse(redirectURL)
    if err != nil {
        fmt.Printf("snap: bad redirect URL: %s\n", err)
        os.Exit(1)
    }

    // set up a listener on the redirect port
    port := fmt.Sprintf(":%s", u.Port())
    l, err := net.Listen("tcp", port)
    if err != nil {
        fmt.Printf("snap: can't listen to port %s: %s\n", port, err)
        os.Exit(1)
    }

    // open a browser window to the authorizationURL
    err = open.Start(authorizationURL)
    if err != nil {
        fmt.Printf("snap: can't open browser to URL %s: %s\n", authorizationURL, err)
        os.Exit(1)
    }

    // start the blocking web server loop
    // this will exit when the handler gets fired and calls server.Close()
    server.Serve(l)
}

// getAccessToken trades the authorization code retrieved from the first OAuth2 leg for an access token
func getAccessToken(clientID string, codeVerifier string, authorizationCode string, callbackURL string) (string, error) {
    // set the url and form-encoded data for the POST to the access token endpoint
    fmt.Println("authorizationCode:", authorizationCode)
    url := "https://dev-*******.us.auth0.com/oauth/token" //your auth0 domain
    data := fmt.Sprintf(
        "grant_type=authorization_code&client_id=%s"+
            "&code_verifier=%s"+
            "&code=%s"+
            "&redirect_uri=%s",
        clientID, codeVerifier, authorizationCode, callbackURL)
    payload := strings.NewReader(data)

    // create the request and execute it
    req, _ := http.NewRequest("POST", url, payload)
    req.Header.Add("content-type", "application/x-www-form-urlencoded")
    res, err := http.DefaultClient.Do(req)
    if err != nil {
        fmt.Printf("snap: HTTP error: %s", err)
        return "", err
    }

    // process the response
    defer res.Body.Close()
    var responseData map[string]interface{}
    body, _ := ioutil.ReadAll(res.Body)

    // unmarshal the json into a string map
    err = json.Unmarshal(body, &responseData)
    if err != nil {
        fmt.Printf("snap: JSON error: %s", err)
        return "", err
    } 
    // retrieve the access token out of the map, and return to caller
    //accessToken := responseData["access_token"].(string)
    return "", nil
}

// cleanup closes the HTTP server
func cleanup(server *http.Server) {
    // we run this as a goroutine so that this function falls through and
    // the socket to the browser gets flushed/closed before the server goes away
    go server.Close()
}
Enter fullscreen mode Exit fullscreen mode

hopefully this will help some one

thanks for reading, pls comment if you have any questions,
have a great day

Top comments (1)

We are hiring! Do you want to be our Senior Platform Engineer? We're hiring for a Senior Platform Engineer and would love for you to apply.

Head here to learn more about who we're looking for.