Skip to main content

Implementing authorization in Go with Cryptr

Cryptr - Go illustration

Go Authorization Getting Started: Secure an API

Go 1.8.24min

Learn how to use Cryptr to implement authorization in Go

In this tutorial, we are going to create an API that will allow you to read the data, i.e. see the list of courses in our example. Security will be enhanced so that only Cryptr users can access the list of courses by checking if the token is valid by following the Cryptr security procedure.

Let's get started! ๐Ÿ˜‰

1. Configurationโ€‹

Create a Go applicationโ€‹

๐Ÿ› ๏ธ๏ธ First, we create a new directory for our new go project:

mkdir cryptr-go-api-sample  
cd cryptr-go-api-sample

๐Ÿ› ๏ธ๏ธ Now that we're in our new project directory, we can run go mod init:

go mod init cryptr.com/sample

To tell Go modules what the name of our module is, we use go mod init, with the fully qualified path to our module. We have a new file, called go.mod, that includes our module and the Go version we used. When we add imports to our Go code later, they'll also be added to this file.

Rendering

๐Ÿ› ๏ธ๏ธ Next, create a file main.go:

touch main.go

๐Ÿ› ๏ธ๏ธ Open main.go file and add package main inside

Project Structureโ€‹

๐Ÿ› ๏ธ๏ธ Now copy paste this structure for the project inside the main.go:

main.go
package main

// Add the structure:
type Teacher struct {
Name string `json:"name"`
Picture string `json:"picture"`
}

type Course struct {
Id int `json:"id"`
User_id string `json:"user_id"`
Title string `json:"title"`
Tags []string `json:"tags"`
Img string `json:"img"`
Desc string `json:"desc"`
Date string `json:"date"`
Timestamp string `json:"timestamp"`
Teacher Teacher `json:"teacher"`
}

2. Valid access tokensโ€‹

Install dependenciesโ€‹

๐Ÿ› ๏ธ๏ธ Run the following commands in your terminal:

go get -d github.com/form3tech-oss/jwt-go
go get -d github.com/codegangsta/negroni
go get -d github.com/gorilla/mux
Note

Create simple rendering coursesโ€‹

๐Ÿ› ๏ธ๏ธ Import encoding/json, net/http, and github.com/gorilla/mux:

main.go
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
)

๐Ÿ› ๏ธ๏ธ Add the courses function:

main.go
func courses() []Course {
t := Teacher{"Max", "https://images.unsplash.com/photo-1558531304-a4773b7e3a9c?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=634&q=80"}
cTags := []string{"colaborate", "git", "cli", "commit", "versionning"}
c := Course{1, "eba25511-afce-4c8e-8cab-f82822434648", "learn git", cTags, "https://carlchenet.com/wp-content/uploads/2019/04/git-logo.png", "Learn how to create, manage, fork, and collaborate on a project. Git stays a major part of all companies projects. Learning git is learning how to make your project better everyday", "5 nov", "1604577600000", t}
return []Course{c}
}

๐Ÿ› ๏ธ๏ธ Add func main():

main.go
func main() {
r := mux.NewRouter()
r.Handle("/api/v1/courses", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
jsonResponse, err := json.Marshal(courses())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(jsonResponse)

}))

http.ListenAndServe(":8000", r)
}

The main function will manage the routes, accept requests on /api/v1/courses, and will return the Courses (in func courses()).

๐Ÿ› ๏ธ๏ธ Run the server with command go run . and open insomnia or postman to make a GET request which should end with 200:

Rendering

JWT authentication typesโ€‹

๐Ÿ› ๏ธ๏ธAdd authentication types:

main.go
type Jwks struct {
Keys []JSONWebKeys `json:"keys"`
}

type JSONWebKeys struct {
Kty string `json:"kty"`
Kid string `json:"kid"`
Use string `json:"use"`
N string `json:"n"`
E string `json:"e"`
X5c []string `json:"x5c"`
}

3. Application keysโ€‹

Create an application with Cryptrโ€‹

๐Ÿ› ๏ธ๏ธ In order to start, you need to create a free Cryptr account if you don't have one yet.

Rendering Rendering

๐Ÿ› ๏ธ๏ธ You can then create your application by following the onboarding steps.

Rendering

Once you've completed all the funnel, after the processing steps, you arrive on the homepage of the back-office where you have access to various guides that will help you. They will depend on the choices of technology you have made in the funnel.

Rendering

๐Ÿ› ๏ธ๏ธ You can get your environment configuration by first clicking on ยซ applications ยป in the left menu, then by clicking on the application you will have created. A modal will appear and this is where you may copy/paste your environment variables.

Rendering Rendering

Add your Cryptr credentialsโ€‹

๐Ÿ› ๏ธ๏ธ Define cryptr config structure type:

main.go
type CryptrConfig struct {
AUDIENCE string
CRYPTR_BASE_URL string
TENANT_DOMAIN string
}

๐Ÿ› ๏ธ๏ธ Instantiate project config in the main function with the variables that you obtained previously (you can retrieve them in the Cryptr back-office). Don't forget to replace YOUR_DOMAIN:

main.go
// ...
func main() {
// Instantiate project config:
cryptrConfig := CryptrConfig{
"http://localhost:8081",
"https://auth.cryptr.eu",
"YOUR_DOMAIN",
}
r := mux.NewRouter()
r.Handle("/api/v1/courses", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
jsonResponse, err := json.Marshal(courses())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(jsonResponse)

}))

http.ListenAndServe(":8000", r)
}
NOTE

If you are from the EU, you must add https://auth.cryptr.eu/ in the CRYPTR_BASE_URL variable, and if you are from the US, you must add https://auth.cryptr.us/ in the same variable.

4. Protect API Endpointsโ€‹

Cryptr JWT middlewareโ€‹

๐Ÿ› ๏ธ๏ธ Import the following packages:

main.go
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"

// Add this packages:
"context"
"errors"
"fmt"
"log"
"strings"
"time"
"github.com/form3tech-oss/jwt-go"
)

๐Ÿ› ๏ธ๏ธ Next, add these types:

main.go
type JWTMiddleware struct {
Options Options
}

type errorHandler func(w http.ResponseWriter, r *http.Request, err string)

type TokenExtractor func(r *http.Request) (string, error)

type Options struct {
ValidationKeyGetter jwt.Keyfunc
UserProperty string
ErrorHandler errorHandler
CredentialsOptional bool
Extractor TokenExtractor
Debug bool
EnableAuthOnOptions bool
SigningMethod jwt.SigningMethod
}

๐Ÿ› ๏ธ ๏ธNext, add the following functions:

main.go
func OnError(w http.ResponseWriter, r *http.Request, err string) {
http.Error(w, err, http.StatusUnauthorized)
}

func CryptrJwtVerifier(token *jwt.Token, cryptrConfig CryptrConfig) (interface{}, error) {
// validate "exp"
checkExp := token.Claims.(jwt.MapClaims).VerifyExpiresAt(time.Now().Unix(), true)
if !checkExp {
return token, errors.New("token expired")
}
// validate "iat"
checkIat := token.Claims.(jwt.MapClaims).VerifyIssuedAt(time.Now().Unix(), true)
if !checkIat {
return token, errors.New("token issued at error")
}
// validate "iss"
iss := fmt.Sprintf("%v/t/%v", cryptrConfig.CRYPTR_BASE_URL, cryptrConfig.TENANT_DOMAIN)
checkIss := token.Claims.(jwt.MapClaims).VerifyIssuer(iss, false)
if !checkIss {
return token, errors.New("invalid issuer")
}
// validate "aud"
aud := cryptrConfig.AUDIENCE
checkAud := token.Claims.(jwt.MapClaims).VerifyAudience(aud, false)
if !checkAud {
return token, errors.New("invalid audience")
}

// validate Signature
cert, err := getPemCert(token, cryptrConfig)
if err != nil {
panic(err.Error())
}

result, _ := jwt.ParseRSAPublicKeyFromPEM([]byte(cert))
return result, nil
}

func NewCryptrJwtMiddleware(cryptrConfig CryptrConfig) *JWTMiddleware {
options := Options{
ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
return CryptrJwtVerifier(token, cryptrConfig)
},
SigningMethod: jwt.SigningMethodRS256,
UserProperty: "user",
ErrorHandler: OnError,
Extractor: FromAuthHeader,
}

return &JWTMiddleware{
Options: options,
}
}

func FromAuthHeader(r *http.Request) (string, error) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
return "", nil
}

authHeaderParts := strings.Fields(authHeader)
if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "bearer" {
return "", errors.New("authorization header format must be Bearer {token}")
}
return authHeaderParts[1], nil
}

func (m *JWTMiddleware) logf(format string, args ...interface{}) {
if m.Options.Debug {
log.Printf(format, args...)
}
}

func (m *JWTMiddleware) HandlerWithNext(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
err := m.CheckJWT(w, r)

if err == nil && next != nil {
next(w, r)
}
}

func (m *JWTMiddleware) Handler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := m.CheckJWT(w, r)

if err != nil {
return
}

h.ServeHTTP(w, r)
})
}

func (m *JWTMiddleware) CheckJWT(w http.ResponseWriter, r *http.Request) error {
if r.Method == "OPTIONS" && !m.Options.EnableAuthOnOptions {
return nil
}

token, err := m.Options.Extractor(r)

if err != nil {
m.logf("Error extracting JWT: %v", err)
m.Options.ErrorHandler(w, r, err.Error())
return fmt.Errorf("error extracting token: %w", err)
}

if token == "" {
if m.Options.CredentialsOptional {
m.logf(" No credentials found (CredentialsOptional=true)")
return nil
}

errorMsg := "required authorization token not found"
m.Options.ErrorHandler(w, r, errorMsg)
m.logf(" Error: No credentials found (CredentialsOptional=false)")
return fmt.Errorf(errorMsg)
}

parsedToken, err := jwt.Parse(token, m.Options.ValidationKeyGetter)

if err != nil {
m.logf("Error parsing token: %v", err)
m.Options.ErrorHandler(w, r, err.Error())
return fmt.Errorf("error parsing token: %w", err)
}

if m.Options.SigningMethod != nil && m.Options.SigningMethod.Alg() != parsedToken.Header["alg"] {
message := fmt.Sprintf("Expected %s signing method but token specified %s",
m.Options.SigningMethod.Alg(),
parsedToken.Header["alg"])
m.logf("Error validating token algorithm: %s", message)
m.Options.ErrorHandler(w, r, errors.New(message).Error())
return fmt.Errorf("error validating token algorithm: %s", message)
}

if !parsedToken.Valid {
m.logf("Token is invalid")
m.Options.ErrorHandler(w, r, "The token isn't valid")
return errors.New("token is invalid")
}

newRequest := r.WithContext(context.WithValue(r.Context(), m.Options.UserProperty, parsedToken))
*r = *newRequest
return nil
}

func getPemCert(token *jwt.Token, cryptrConfig CryptrConfig) (string, error) {
cert := ""
JwksUri := fmt.Sprintf("%v/t/%v/.well-known", cryptrConfig.CRYPTR_BASE_URL, cryptrConfig.TENANT_DOMAIN)
resp, err := http.Get(JwksUri)

if err != nil {
return cert, err
}
defer resp.Body.Close()

var jwks = Jwks{}
err = json.NewDecoder(resp.Body).Decode(&jwks)

if err != nil {
return cert, err
}

for k := range jwks.Keys {
if token.Header["kid"] == jwks.Keys[k].Kid {
cert = "-----BEGIN CERTIFICATE-----\n" + jwks.Keys[k].X5c[0] + "\n-----END CERTIFICATE-----"
}
}

if cert == "" {
err := errors.New("unable to find appropriate key")
return cert, err
}

return cert, nil
}

How the middleware proceeds:

  • It checks the header, if there are errors the request is blocked
  • RS256 verifies the tokens against the public key for your Cryptr account
  • It accepts the request and processes it, otherwise it returns 401

Secure courses routeโ€‹

We can now instantiate the middleware and protect the course route

๐Ÿ› ๏ธ๏ธ First, import negroni package:

main.go
package main

import (
"encoding/json"
"net/http"

"github.com/gorilla/mux"

// Add this packages:
"context"
"errors"
"fmt"
"log"
"strings"
"time"

"github.com/form3tech-oss/jwt-go"

// 1. Import negroni package
"github.com/codegangsta/negroni"
)

// ...

๐Ÿ› ๏ธ ๏ธNext, instantiate the cryptr middleware with config. Don't forget to replace YOUR_DOMAIN:

main.go
// ...
func main() {
cryptrConfig := CryptrConfig{
"http://localhost:8081",
"https://auth.cryptr.eu",
"YOUR_DOMAIN",
}
// 2. Instantiate cryptr jwt middleware:
jwtMiddleware := NewCryptrJwtMiddleware(cryptrConfig)
r := mux.NewRouter()
// ...

๐Ÿ› ๏ธ๏ธ Then you can now protect the route:

main.go
// ...
func main() {
cryptrConfig := CryptrConfig{
"http://localhost:8081",
"https://auth.cryptr.eu",
"YOUR_DOMAIN",
}
// 2. Instantiate cryptr jwt middleware:
jwtMiddleware := NewCryptrJwtMiddleware(cryptrConfig)
r := mux.NewRouter()
// 3. Secure the courses route:
r.Handle("/api/v1/courses", negroni.New(
negroni.HandlerFunc(jwtMiddleware.HandlerWithNext),
negroni.Wrap(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
jsonResponse, err := json.Marshal(courses())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", cryptrConfig.AUDIENCE)
w.Header().Set("access-control-allow-headers", "authorization,content-type,sentry-trace")
w.WriteHeader(http.StatusOK)
w.Write(jsonResponse)
}))))

http.ListenAndServe(":8000", r)
}

Vue app exampleโ€‹

Letโ€™s try this on an application. For this purpose, we have an example app on Vue.

๐Ÿ›  Run your code with go run .

๐Ÿ› ๏ธ๏ธ Clone our cryptr-vue-sample:

git clone --branch 07-backend-courses-api https://github.com/cryptr-examples/cryptr-vue2-sample.git
cd cryptr-vue2-sample

๐Ÿ›  Install the Vue project dependencies with yarn

๐Ÿ› ๏ธ๏ธ Add .env.local file with your variables. Don't forget to replace YOUR_CLIENT_ID & YOUR_DOMAIN:

.env.local
VUE_APP_AUDIENCE=http://localhost:8081
VUE_APP_CLIENT_ID=YOUR_CLIENT_ID
VUE_APP_DEFAULT_LOCALE=en
VUE_APP_DEFAULT_REDIRECT_URI=http://localhost:8081
VUE_APP_TENANT_DOMAIN=YOUR_DOMAIN
VUE_APP_CRYPTR_TELEMETRY=FALSE
VUE_APP_CRYPTR_REGION=eu

๐Ÿ› ๏ธ๏ธ Run vue server with yarn serve and try to connect. Your Vue application redirects you to your sign form page, where you can sign in or sign up with an email.

SANDBOX EMAIL

You can log in with a sandbox email and we send you a magic link which should directly arrive in your personal inbox. Your sandbox email is based on your account's email. The email's structure is as follows: my-user-to-test@sandbox.my-admin-name. For example testemail@sandbox.lucas

Once you're connected, click on "Protected route". You can now view the list of the courses.

Itโ€™s done, congratulations if you made it to the end!

I hope this was helpful, and thanks for following this tutorial! ๐Ÿ™‚