Skip to content

Instantly share code, notes, and snippets.

@putrikarunia
Last active December 29, 2020 21:42
Show Gist options
  • Save putrikarunia/538a92913a60d1d48a8d99d305705f94 to your computer and use it in GitHub Desktop.
Save putrikarunia/538a92913a60d1d48a8d99d305705f94 to your computer and use it in GitHub Desktop.
Go cotter auth middleware
package middleware
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"strings"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
"gopkg.in/square/go-jose.v2"
"gopkg.in/square/go-jose.v2/jwt"
)
// 👇 Enter your API KEY ID here
const API_KEY_ID = "YOUR_API_KEY_ID"
const JWKSURL = "https://www.cotter.app/api/v0/token/jwks"
const JWKSLookupKeyID = "SPACE_JWT_PUBLIC:8028AAA3-EC2D-4BAA-BE7A-7C8359CCB9F9"
func getKey() ([]byte, error) {
// Fetch the JWT Public Key from the URL
resp, err := http.Get(JWKSURL)
if err != nil {
return nil, err
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
// Parse the response into our keys struct
keyset := make(map[string][]map[string]interface{})
err = json.Unmarshal(body, &keyset)
if err != nil {
return nil, err
}
// It's a Key Set = there might be multiple keys
// Find the key with kid = JWKSLookupKeyID
if len(keyset["keys"]) <= 0 {
return nil, errors.New("Key set is empty")
}
for _, k := range keyset["keys"] {
if k["kid"] == JWKSLookupKeyID {
key, err := json.Marshal(k)
if err != nil {
return nil, err
}
return key, nil
}
}
return nil, errors.New("Cannot find key with kid")
}
// validateClientAccessToken validates access token created above
func validateClientAccessToken(accessToken string) (map[string]interface{}, error) {
tok, err := jwt.ParseSigned(accessToken)
if err != nil {
return nil, errors.New("Fail parsing access token")
}
keys, err := getKey()
if err != nil {
return nil, err
}
key := jose.JSONWebKey{}
key.UnmarshalJSON(keys)
token := make(map[string]interface{})
if err := tok.Claims(key, &token); err != nil {
return nil, errors.New("Fail parsing access token to claims")
}
// Check that the aud is our API KEY ID
apiKeyID, ok := token["aud"].(string)
if !ok {
return nil, errors.New("fail asserting aud from jwt.MapClaims")
}
if apiKeyID != API_KEY_ID {
return nil, errors.New("Invalid aud, not meant for this api key id")
}
return token, nil
}
// CotterAuth is used to authenticate cotter access_tokens
func CotterAuth(next echo.HandlerFunc) echo.HandlerFunc {
return func(ctx echo.Context) error {
// get the access token from auth header
reqToken := ctx.Request().Header.Get("Authorization")
splitToken := strings.Split(reqToken, " ")
if len(splitToken) != 2 {
return errors.New("authorization header malformed")
}
tokenString := splitToken[1]
// Validate that the access token and signature is valid
token, err := validateClientAccessToken(tokenString)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
// Read other claims, such as sub = Cotter User ID
uIDStr, ok := token["sub"].(string)
if !ok {
return echo.NewHTTPError(http.StatusBadRequest, "fail asserting sub from jwt.MapClaims")
}
cotterUserID, err := uuid.Parse(uIDStr)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid user id format in jwt claims")
}
fmt.Println("User logging in = ", cotterUserID)
return next(ctx)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment