Implementing authorization in Go with Cryptr
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.
๐ ๏ธ๏ธ 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
:
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
- form3tech-oss/jwt-go to verify incoming JWTs
- codegangsta/negroni for HTTP middleware
- gorilla/mux to handle our routes.
Create simple rendering coursesโ
๐ ๏ธ๏ธ Import encoding/json
, net/http
, and github.com/gorilla/mux
:
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
)
๐ ๏ธ๏ธ Add the courses function:
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()
:
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
:
JWT authentication typesโ
๐ ๏ธ๏ธAdd authentication types:
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.
๐ ๏ธ๏ธ You can then create your application by following the onboarding steps.
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.
๐ ๏ธ๏ธ 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.
Add your Cryptr credentialsโ
๐ ๏ธ๏ธ Define cryptr config structure type:
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
:
// ...
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)
}
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:
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:
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:
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:
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
:
// ...
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:
// ...
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
:
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.
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! ๐